Java中的各种锁

"锁概念详解"

Posted by ming on October 9, 2019

“If you think you can, you can. And if you think you can’t, you’re right.”

1. 公平锁/非公平锁

公平锁:排队,多个线程以请求锁的顺序来获取锁,有多个线程在等待一个锁时,等待时间最久的线程会获取锁。通过同步队列来实现多个线程按照申请锁的顺序来获取锁。

非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能会造成优先级反转或者饥饿现象。(先尝试插队,插队失败再排队,即多线程在获取锁时直接尝试获取锁,获取不到时才会排列到队列的队尾等待,其无法保证锁的获取是按照请求锁的顺序进行的)。

ReentrantLock和ReentrantReadWriteLock默认情况下是非公平锁,可以设置为公平锁。在构造两者的对象时,在构造函数中传入的是true则为公平锁,false则为非公平锁。非公平锁的优点在于吞吐量比公平锁大。对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有方法使其变成公平锁。

2. 可重入锁/非可重入锁

可重入性:表明了锁的分配机制,是基于线程的分配,不是基于方法调用的分配。是指在一个线程中可以多次获取同一把锁,避免了死锁

实现原理:为每个锁关联一个请求计数器和一个占有它的线程。计数器为0时,锁是未被占有的;线程占有锁时,计数器为1;之后线程继续再次请求时,计数器递增。当占用线程退出同步块,计数器递减,当计数器为0时,锁被释放。

例子:在一个线程中执行一个带锁的方法,在该方法中又调用了另一个需要相同锁的方法,线程可以直接执行调用的方法,不需要重新获得锁。ReentrantLock和Synchronized都是可重入锁。

不可重入锁:会出现死锁。其不能够多次获取同一把锁。

3. 乐观锁/悲观锁

乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。悲观锁在Java中的使用,就是利用各种锁。乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。

乐观锁:不锁住同步资源。对于同一个数据的并发操作,乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会加锁,只是在更新数据的时候再去判断之前有没有别的线程更新了这个数据。如果数据没有被更新,当前线程成功写入自己的数据;数据如果已经被其他线程修改,根据不同的实现方法执行不同的操作:报错或者自动重试。

乐观锁可以做到不锁定同步资源也可以正确的实现线程同步。乐观锁在Java中的实现:通过无锁编程来实现,最常采用CAS算法。

乐观锁适合读操作多的场景,不加锁的特点使读操作的性能提升很多。

悲观锁:锁住同步资源。对于同一个数据的并发操作,悲观锁认为自己在使用数据时一定有线程来修改数据,因此在获取数据时会先加锁,以确保数据不会被别的线程所修改。

synchronized和Lock都是悲观锁。悲观锁适合写操作多的场景,先加上锁可以保证写操作时数据的正确性。

4. 自旋锁/适应性自旋锁

获取锁失败时,线程不进行阻塞时,分为自旋锁适应性自旋锁

自旋锁:用于线程之间的同步,是一种非阻塞锁。在线程没有获取到锁时不进入阻塞状态,不放弃CPU时间片,在原地忙等,一直循环着,线程反复检查锁变量是否可以用,等待获取到锁的线程释放锁。线程在这一状态保持执行,是一种忙等待。避免了线程上下文的调度切换的开销,单核CPU不适合自旋锁。

适用于锁使用者保持锁时间比较短的情况。自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数还没有成功获得锁,就会挂起线程。

可能会产生死锁:同一线程两次调用lock(),会使得第二次调用lock的位置进行自旋,产生死锁。

递归程序不能再持有自旋锁时调用自己,也不能在递归调用时试图去获取相同的自旋锁。

自旋锁的实现原理同样也是CAS,在AtomicInteger中调用Unsafe进行自增操作时的while循环就是一个自旋操作,如果修改数值失败就通过循环来执行自旋,直到修改成功。

适应性自旋锁:自旋的时间(次数)不再固定,由前一次在同一个锁上的自旋时间以及锁的拥有着状态来决定自旋次数。

5. 无锁/偏向锁/轻量级锁/重量级锁

无锁、偏向锁、轻量级锁、重量级锁这四种锁是指锁的状态,专门针对Synchronized的。

5.1 无锁

不锁住资源,多个线程中只有一个能修改资源成功,其他线程会重试。

5.2 偏向锁

同一个线程执行同步资源时自动获取资源。

5.3 轻量级锁

多个线程竞争同步资源时,没有获取资源的线程自旋等待锁释放。

5.4 重量级锁

多个线程竞争同步资源时,没有获取资源的线程阻塞等待唤醒。

6. 共享锁/排他锁(独享锁)

共享锁:锁可以被多个线程所持有。一个线程对一个对象加上锁后,其他线程只能对这个对象再加共享锁,不能再加排他锁。获得共享锁的线程只能读数据,不能写、修改数据。

排它锁:锁一次只能被一个线程所拥有。一个线程对一个对象加上排他锁后,其他线程不能再对对象加上任何类型的锁。获得排它锁的线程既可以读数据又可以写数据。

ReentrantReadWriteLock中有两把锁:ReadLock(读锁)和WriteLock(写锁),合称为读写锁,读锁是共享锁,写锁是排他锁

读写锁将对一个资源文件的访问分为了读锁和写锁。读锁使得多个线程之间的读操作不会发生冲突,读锁的共享锁可以保证很高效的并发读。

7. 可中断锁

可以相应中断的锁。synchronized是不可中断的锁,Lock是可以中断的锁。

线程在等待获取锁时等待的时间过长,可以中断自己或在别的线程中中断它去处理其他的事情。

8. 互斥锁

上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。互斥锁在Java中的具体实现就是ReentrantLock,读写锁在Java中的具体实现就是ReentrantReadWriteLock。采用互斥的方式,从没有获得锁到获得锁的过程中,要有用户态和内核态调度和上下文切换的开销和损耗。

参考文章

Java中的各种锁

Java中的锁以及sychronized实现机制