Java 中的锁有三类,一种是关键字 Synchronized,一种是对象 lock,还有一种 volatile 关键字。
公平锁就是获得锁的顺序按照先到先得的顺序。当一个线程或的锁并没有是否,接下来的线程就会进入阻塞队列等待,并按照队列的方式顺序的获取锁。
非公平锁就是新来的线程可以跟阻塞队列的队头争夺一把锁,争夺不过才会添加到队尾。这种情况下,后到的线程有可能无需进入等待队列直接获取锁。
Synchronized 和 lock 默认都是非公平锁。lock 可以通过构造函数的方式改为公平锁。
非公平锁性能高于公平锁。因为当一个线程执行完释放锁时,阻塞的线程需要被唤醒,这个过程有些漫长。在等待的时间如果有一个活跃的线程想争夺这把锁,就把锁让给他,减少等待的时间。
乐观锁和悲观锁是一种概念。
乐观锁:很乐观,每次拿数据时都认为数据没有被修改,所以先不加锁。通过判断其版本号来判断此数据是否发生改变。
悲观锁:很悲观,每次拿数据时都认为别人会修改数据,这时就要上锁来阻止别人进行修改。
乐观锁一般采用 CAS(compare and swapper)比较并交换的方式来实现。
实现思路:参考 AtomicInteger 的实现:
上述有种缺陷:因为该思路将自身值作为版本号,可以任意改变,而正常的版本号是不断增加的。造成的问题:
解决方法:不把自身作为版本号,而是再新建一个字段作为版本号,此版本只能增加,不能回溯。但此时只是版本号来进行 CAS,而需要同步的变量只是做普通的改变,这也会造成并发异常。解决方法还是得加锁。
if (version == UNSAFE.getObject()){ // 版本号相比较,比较成功修改
synchronized(对象){
// 对象赋值 // 赋值完再更改版本号
// 更改版本号
}
}
复制代码
悲观锁就比较暴力,直接加锁。
由于大部分时候,锁被占用的时间很短,共享变量的锁定时间也很短。所以当一个线程需要等到锁时没有必要挂起,因为用户态和内核态之间的切换十分影响性能。
自旋的利用 CAS 操作,比较版本号是否相同,如果相同则得到所,不相同就一直循环获取锁,让其处于活跃态,从而不用挂起线程。
由于一直循环也十分耗费资源。自旋的时间并不是固定的,于是采取了一种方法,当超过了时间就不再进行循环,而是直接将线程挂起。
jdk1.7 中 concurrentHashMap 添加就是采用此操作。
private HashEntry scanAndLockForPut(K key, int hash, V value) {
HashEntry first = entryForHash(this, hash); // 得到链表的第一个节点
HashEntry e = first;
HashEntry node = null;
int retries = -1; // 重复尝试
while (!tryLock()) { // 自旋操作,没有获取到锁
HashEntry f;
if (retries < 0) {
if (e == null) { // 首节点为 null
if (node == null)
node = new HashEntry(hash, key, value, null); // 给 node 创建对象
retries = 0; // 重复尝试
}
else if (key.equals(e.key)) // 添加的节点已存在
retries = 0;
else
e = e.next;
}
else if (++retries > MAX_SCAN_RETRIES) {
// 如果尝试次数大于默认的最大尝试次数,就使用 lock 阻塞。减少资源消耗,自适应自旋
lock();
break;
}
else if ((retries & 1) == 0 && (f = entryForHash(this, hash)) != first) {
// 判断首节点是否已经改变,已经改变
e = first = f; // 更换首节点
retries = -1; // 重新进行尝试,查看当前线程添加的节点是否是新添加的节点
}
}
return node; // 获得锁时退出循环,并返回此节点
}
复制代码
可重入锁就是当线程已获取到了 A 锁,当在执行阶段又需要获取 A 锁,并不会因为 A 锁被人拿走了而进行阻塞,而是因为自己有此锁继续执行。
Synchronized 和 ReentrantLock 都是可重入锁,只不过 Synchronized 自动获取和自动释放锁。ReentrantLock 得手动获取和释放,并且获取锁的次数必须和释放锁的次数相同,否则会造成死锁。
ReentrantLock 类具有完全互斥的效果,同一时间只有一个线程在执行,效率低下。
JDK 提供了一种读写锁 -- ReentrantReadWriteLock 类,使用它可以在进行一些操作时不需要同步执行,提高效率。
读锁之间不互斥,读锁和写锁互斥,写锁和写锁互斥(只要出现写锁就互斥)。
从 JDK1.6 版本后,Synchronized 本身也在不断优化锁的机制,有些情况下它并不是一个很重量级的锁。优化机制包括自适应锁,自旋锁,轻量级锁,重量级锁。
java 对象分为三部分:对象头,实体数据,对齐填充符。
在无锁的状态下,Mark Word 会记录:对象的 HashCode,分代年龄,是否是偏向锁,锁标志。
偏向锁的设计理念:
要保证锁是由一个线程来获取,就必须在锁的对象头上添加此线程的 ID。于是偏向锁状态下,Mark Word 会记录:线程对象的 HashCode,分代年龄,是否偏向锁,锁标志。
执行流程:
优点:在没有竞争或者只有一个线程使用锁的情况下,偏向锁节省了获取和释放锁对性能的损耗。
轻量级锁状态下,Mark Word 会记录:指向线程栈中锁记录的指针,锁标志位。
优点:避免在了线程的阻塞,当线程获取不到锁时,会进行自旋,而不会阻塞,造成系统调用内核态和用户态。
缺点:如果存在大量竞争,轻量锁采用的 CAS 和自旋操作会大量的消耗资源,程序的性能反而会下降。
适用场景:在没有多线程竞争uo少量线程竞争的前提下,使用轻量级锁会减少系统在用户态和内核态之间的转换,提高性能。
重量级锁是依赖对象内部的 monitor 锁来实现的,而 monitor 又依赖操作系统的 MutexLock(互斥锁)来实现,所以重量级锁也称为互斥锁。
重量级锁需要阻塞线程,唤醒线程,释放锁,消耗资源很大。