同步操作
两个线程操作都读取内存中的变量A,修改后写回到内存。实际上有两种情况
- 线程A与线程B之间有明确的串行顺序关系,则无需同步操作。
- 线程A与线程B不确定先后关系,则需要同步操作。
同步操作是对临界资源控制访问的一种手段。
锁分类
如果需要同步操作,可以进行加锁。根据不同阶段,可以按如下步骤对锁进行分类
- 锁的获取方式
- 锁的加锁方式
- 锁的预期方式
- 锁的等待方式
- 锁的状态形式
锁的获取方式
获取锁有两种方式:公平锁或者非公平锁。
公平锁,也就是说按线程们先到先得的方式进行锁分配。
非公平锁,也就是说就是不遵守线程们先到先得的原则进行锁分配。
实现方式
公平锁会涉及到先到先得的判断要求,需要引入一个先进先出队列(FIFO)进行控制。
在Java中java.utils.concurrent.locks(后续简称为JUC)中Doug Lea老爷子写的ReentrantLock就有一个实现案例
适用场景
默认情况下ReentrantLock是通过非公平锁来进行同步的,因为这样性能会更好。
在公平的锁上,线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO先进先出顺序,而非公平锁就是一种获取锁的抢占机制,是随机获得锁的,它允许插队:当一个线程请求非公平锁时,如果在发出请求的同时该锁变成可用状态,那么这个线程会跳过队列中所有的等待线程而获得锁。 非公平的ReentrantLock 并不提倡插队行为,但是无法防止某个线程在合适的时候进行插队。
在公平的锁中,如果有另一个线程持有锁或者有其他线程在等待队列中等待这个所,那么新发出的请求的线程将被放入到队列中。而非公平锁上,只有当锁被某个线程持有时,新发出请求的线程才会被放入队列中。
非公平锁性能高于公平锁性能的原因:
从线程进入了RUNNABLE状态,可以执行开始,到实际线程执行是要比较久的时间的。而且,在一个锁释放之后,其他的线程会需要重新来获取锁。其中经历了持有锁的线程释放锁,其他线程从挂起恢复到RUNNABLE状态,其他线程请求锁,获得锁,线程执行,这一系列步骤。如果这个时候,存在一个线程直接请求锁,可能就避开挂起到恢复RUNNABLE状态的这段消耗,所以性能更优化。
假设线程A持有一个锁,并且线程B请求这个锁。由于锁被A持有,因此B将被挂起。当A释放锁时,B将被唤醒,因此B会再次尝试获取这个锁。与此同时,如果线程C也请求这个锁,那么C很可能会在B被完全唤醒之前获得、使用以及释放这个锁。这样就是一种双赢的局面:B获得锁的时刻并没有推迟,C更早的获得了锁,并且吞吐量也提高了。
当持有锁的时间相对较长或者请求锁的平均时间间隔较长,应该使用公平锁。在这些情况下,插队带来的吞吐量提升(当锁处于可用状态时,线程却还处于被唤醒的过程中)可能不会出现。
锁的加锁方式
加锁有两种方式:独享锁及共享锁。
独享锁,也就是一旦某线程获取该锁后,其他线程只能等待。
共享锁,也就是说可以多个线程获取该锁对临界资源进行访问。
JUC中的ReentrantLock就是独享者的具体实现案例,只允许一个线程持有锁。而** ReadWriteLock**就是共享者的具体实现案例,分离读锁与写锁,允许多个线程持有读锁或者一个线程持有写锁。
实现方式
适用场景
锁的预期方式
锁有两种预期方式:悲观锁及乐观锁。
悲观锁,也就说预期临界资源一定会被修改,需要开始就加锁,而且是独享锁。
乐观锁,也就是预期临界资源不会被修改,开始并不进行加锁操作,后面如果真有修改才进行加锁操作。
实现方式
JAVA中的synchronized关键字所指向的内部锁就是悲观者的实现。
而乐观锁通常都是通过数据版本号加上递归方式实现,JUC中的atomic包下面的原子变量类就是使用乐观锁的具体实现案例。
适用场景
乐观锁适用于多读的应用场景,这样可以提高吞吐量,而悲观锁适用于多写的应用场景。
锁的等待方式
锁的等待方式是说当线程无法获取锁的时候的操作方式。这个跟线程状态息息相关,可以分为阻塞锁及自旋锁两类。
阻塞锁,也就是说当线程无法获取锁的时候,让线程进入阻塞状态进行等待,当获得相应的信号(唤醒,时间) 时,才可以进入线程的准备就绪状态,准备就绪状态的所有线程,通过竞争进入运行状态。
自旋锁,也就是让当前线程不停地的在循环体内尝试获取锁,当循环的条件被其他线程改变时就可以进入临界区,不需要改变线程状态。
实现方式
JAVA中,能够进入\退出、阻塞状态或包含阻塞锁的方法有 ,Object.wait()\notify(),synchronized 关键字,JUC中的ReentrantLock,,LockSupport.park()/unpart()等
适用场景
如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样情况下适合适用自旋锁。
锁的状态
锁有以下几种状态
- 偏向锁
- 轻量级锁
- 重量级锁
具体内容可以参考java 中的锁 -- 偏向锁、轻量级锁、自旋锁、重量级锁