Java 中锁概览
Java 中锁概览
在读很多并发文章中,会提及各种各样锁如公平锁,乐观锁等等,这篇文章介绍各种锁的分类,介绍的内容如下:
- 公平锁
/ 非公平锁 - 可重入锁
/ 不可重入锁 - 独享锁
/ 共享锁 - 互斥锁
/ 读写锁 - 乐观锁
/ 悲观锁 - 分段锁
- 偏向锁
/ 轻量级锁/ 重量级锁 - 自旋锁
上面是很多锁的名词,这些分类并不是全是指锁的状态,有的指锁的特性,有的指锁的设计,下面总结的内容是对每个锁的名词进行一定的解释。
公平锁/ 非公平锁
- 公平锁
: 公平锁是指多个线程按照申请锁的顺序来获取锁。 - 非公平锁
: 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
对于
可重入锁/ 不可重入锁
可重入锁
广义上的可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或者
synchronized void setA() throws Exception {
Thread sLeep(1000)
setB();
}
synchronized void setB() throws Exception{
Thread.sleep(1000)
}
上面的代码就是一个可重入锁的一个特点,如果不是可重入锁的话,
不可重入锁
不可重入锁,与可重入锁相反,不可递归调用,递归调用就发生死锁。看到一个经典的讲解,使用自旋锁来模拟一个不可重入锁,代码如下:

代码也比较简单,使用原子引用来存放线程,同一线程两次调用

在执行每次操作之前,判断当前锁持有者是否是当前对象,采用

在
独享锁/ 共享锁
独享锁和共享锁在你去读
- 独享锁
: 该锁每一次只能被一个线程所持有。 - 共享锁
: 该锁可被多个线程共有,典型的就是ReentrantReadWriteLock 里的读锁,它的读锁是可以被共享的,但是它的写锁确每次只能被独占。
另外读锁的共享可保证并发读是非常高效的,但是读写和写写,写读都是互斥的。独享锁与共享锁也是通过
互斥锁/ 读写锁
互斥锁
在访问共享资源之前对进行加锁操作,在访问完成之后进行解锁操作。加锁后,任何其他试图再次加锁的线程会被阻塞,直到当前进程解锁。
如果解锁时有一个以上的线程阻塞,那么所有该锁上的线程都被编程就绪状态, 第一个变为就绪状态的线程又执行加锁操作,那么其他的线程又会进入等待。在这种方式下,只有一个线程能够访问被互斥锁保护的资源
读写锁
读写锁既是互斥锁,又是共享锁,
只有一个线程可以占有写状态的锁,但可以有多个线程同时占有读状态锁,这也是它可以实现高并发的原因。当其处于写状态锁下,任何想要尝试获得锁的线程都会被阻塞,直到写状态锁被释放;如果是处于读状态锁下,允许其它线程获得它的读状态锁,但是不允许获得它的写状态锁,直到所有线程的读状态锁被释放;为了避免想要尝试写操作的线程一直得不到写状态锁,当读写锁感知到有线程想要获得写状态锁时,便会阻塞其后所有想要获得读状态锁的线程。所以读写锁非常适合资源的读操作远多于写操作的情况。
乐观锁/ 悲观锁
悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程
乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和
分段锁
分段锁其实是一种锁的设计,并不是具体的一种锁,对于
我们一般有三种方式降低锁的竞争程度:
- 减少锁的持有时间
- 降低锁的请求频率
- 使用带有协调机制的独占锁,这些机制允许更高的并发性。
在某些情况下我们可以将锁分解技术进一步扩展为一组独立对象上的锁进行分解,这成为分段锁。其实说的简单一点就是:容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是
比如:在
偏向锁/ 轻量级锁/ 重量级锁
锁的状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。锁的状态是通过对象监视器在对象头中的字段来表明的。四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级。这四种状态都不是
- 偏向锁:偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
- 轻量级:轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
- 重量级锁:重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
自旋锁
我们知道
CAS 与自旋锁实现
- 需要读写的内存值
V - 进行比较的值
A - 拟写入的新值
B
更新一个变量的时候,只有当变量的预期值
自旋锁(spinlock)即是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁

自旋锁优劣
自旋锁存在的问题如下:
- 如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗
CPU 。使用不当会造成CPU 使用率极高。 - 上面
Java 实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。
自旋锁的优点如下:
- 自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是
active 的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快 - 非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换
。 (线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)