在学习或者使用Java的过程中进程会遇到各种各样的锁的概念:公平锁、非公平锁、自旋锁、可重入锁、偏向锁、轻量级锁、重量级锁、读写锁、互斥锁等待。下边总结了对各种锁的解释
1、公平锁/非公平锁
公平锁是指多个线程在等待同一个锁时按照申请锁的先后顺序来获取锁。相反的非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。
- 公平锁的好处是等待锁的线程不会饿死,但是整体效率相对低一些;
- 非公平锁的好处是整体效率相对高一些,但是有些线程可能会饿死或者说很早就在等待锁,但要等很久才会获得锁。
其中的原因是公平锁是严格按照请求所的顺序来排队获得锁的,而非公平锁时可以抢占的,即如果在某个时刻有线程需要获取锁,而这个时候刚好锁可用,那么这个线程会直接抢占,而这时阻塞在等待队列的线程则不会被唤醒。对于Java ReentrantLock
而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。例:new ReentrantLock(true)是公平锁对于Synchronized
而言,也是一种非公平锁。由于其并不像ReentrantLock
是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。
2、可重入锁
也叫递归锁,是指在外层函数获得锁之后,内层递归函数仍然可以获取到该锁。即线程可以进入任何一个它已经拥有锁的代码块。在JAVA环境下 ReentrantLock 和synchronized 都是可重入锁。可重入锁最大的作用是避免死锁。具体区别下文阐述。
3、自旋锁
在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。JDK6中已经变为默认开启自旋锁,并且引入了自适应的自旋锁。自适应意味着自旋的时间不在固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。自旋是在轻量级锁中使用的,在重量级锁中,线程不使用自旋。
4、悲观锁和乐观锁
乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度
- 乐观锁认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。即假定不会发生并发冲突,只在提交操作时检测是否违反数据完整性。(使用版本号或者时间戳来配合实现)。在java中就是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。
- 悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。即假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。在java中就是各种锁编程。
- 从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。
5、共享锁和独占锁
- 共享锁:如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排它锁。获准共享锁的事务只能读数据,不能修改数据。
- 独占锁:如果事务T对数据A加上独占锁后,则其他事务不能再对A加任何类型的锁。获得独占锁的事务即能读数据又能修改数据。如Synchronized
6、互斥锁和读写锁
独占锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。
- 互斥锁:就是指一次最多只能有一个线程持有的锁。在JDK中synchronized和JUC的Lock就是互斥锁。
- 读写锁:读写锁是一个资源能够被多个读线程访问,或者被一个写线程访问但不能同时存在读线程。Java当中的读写锁通过ReentrantReadWriteLock实现。ReentrantReadWriteLock运行一个资源可以被多个读操作访问,或者一个写操作访问,但两者不能同时进行。