线程
![](https://img2020.cnblogs.com/blog/2097679/202107/2097679-20210712095803574-422915042.png)
![](https://img2020.cnblogs.com/blog/2097679/202107/2097679-20210712095834738-1939047339.png)
![](https://img2020.cnblogs.com/blog/2097679/202107/2097679-20210712095842164-1489167853.png)
![](https://img2020.cnblogs.com/blog/2097679/202107/2097679-20210712095915037-1570205370.png)
![](https://img2020.cnblogs.com/blog/2097679/202107/2097679-20210712095920966-1445326808.png)
![](https://img2020.cnblogs.com/blog/2097679/202107/2097679-20210712095945766-1412382260.png)
![](https://img2020.cnblogs.com/blog/2097679/202107/2097679-20210712095951911-1293195294.png)
![](https://img2020.cnblogs.com/blog/2097679/202107/2097679-20210712100030794-461640017.png)
![](https://img2020.cnblogs.com/blog/2097679/202107/2097679-20210712100037329-1722654182.png)
![](https://img2020.cnblogs.com/blog/2097679/202107/2097679-20210712100043814-1713478346.png)
![](https://img2020.cnblogs.com/blog/2097679/202107/2097679-20210712170450049-649382957.png)
如果涉及到了synchronized的同步代码块或者是同步方法,获取锁资源之后,会将内部涉及到的变量从CPU缓存中移除,必须去主内存中重新拿数据,而且在释放锁之后,会立即将CPU缓存中的数据同步到主内存。
Lock锁保证可见性的方式和synchronized完全不同,synchronized基于他的内存语义,在获取锁和释放锁时,对CPU缓存做一个同步到主内存的操作。
![](https://img2020.cnblogs.com/blog/2097679/202107/2097679-20210712170509415-1042163223.png)
![](https://img2020.cnblogs.com/blog/2097679/202107/2097679-20210712170524088-1833987816.png)
![](https://img2020.cnblogs.com/blog/2097679/202107/2097679-20210712170537208-36276084.png)
![](https://img2020.cnblogs.com/blog/2097679/202107/2097679-20210712170559418-1904565152.png)
compare and swap也就是比较和交换,他是一条CPU的并发原语。
他在替换内存的某个位置的值时,首先查看内存中的值与预期值是否一致,如果一致,执行替换操作。这个操作是一个原子性操作。
Java中基于Unsafe的类提供了对CAS的操作的方法,JVM会帮助我们将方法实现CAS汇编指令。
在CAS的基础上实现了一些原子类,如AtomicInteger
cas的缺点:CAS只能保证对一个变量的操作是原子性的,无法实现对多行代码实现原子性。
ABA问题:如下图:
可以引入版本号的方式,来解决ABA的问题。Java中提供了一个类在CAS时,针对各个版本追加版本号的操作。 AtomicStampeReference,代码示例:
自旋时间过长问题:
- 可以指定CAS一共循环多少次,如果超过这个次数,直接失败/或者挂起线程。(自旋锁、自适应自旋锁)
- 可以在CAS一次失败后,将这个操作暂存起来,后面需要获取结果时,将暂存的操作全部执行,再返回最后的结果。
- volatile属性被写:当写一个volatile变量,JMM会将当前线程对应的CPU缓存及时的刷新到主内存中
- volatile属性被读:当读一个volatile变量,JMM会将对应的CPU缓存中的内存设置为无效,必须去主内存中重新读取共享变量
其实加了volatile就是告知CPU,对当前属性的读写操作,不允许使用CPU缓存,加了volatile修饰的属性,会在转为汇编之后,追加一个lock的前缀,CPU执行这个指令时,如果带有lock前缀会做两个事情:
- 将当前处理器缓存行的数据立即写回到主内存
- 这个写回的数据,在其他的CPU内核的缓存中,直接无效,要求必须重新从主内存中拉取。
Lock手动锁也是基于volatile实现的,因为本质也是在进行加锁和释放锁时去对一个被volatile修改是变量进行加减操作。
并不能保证类的线程安全性,只能保证类的可见性,最适合一个线程写,多个线程读的情景。volatile如何实现的禁止指令重排?
内存屏障概念。将内存屏障看成一条指令。会在两个操作之间,添加上一道指令,这个指令就可以避免上下执行的其他指令进行重排序。
final修饰的属性,在运行期间是不允许修改的,这样一来,就间接的保证了可见性,所有多线程读取final属性,值肯定是一样。并不是同其他锁机制一样,从主内存重新读取从而达到可见性的目的
synchronized的使用一般就是同步方法和同步代码块。
synchronized的锁是基于对象实现的。
如果使用同步方法
-
static:此时使用的是当前类.class作为锁(类锁)
-
非static:此时使用的是当前对象做为锁(对象锁)
public synchronized void method(){
// 没有操作临界资源
// 此时这个方法的synchronized你可以认为木有~~
}
synchronized就在JDK1.6做了锁升级的优化
-
无锁、匿名偏向:当前对象没有作为锁存在。
-
偏向锁:如果当前锁资源,只有一个线程在频繁的获取和释放,那么这个线程过来,只需要判断,当前指向的线程是否是当前线程 。
-
如果是,直接拿着锁资源走。
-
如果当前线程不是我,基于CAS的方式,尝试将偏向锁指向当前线程。如果获取不到,触发锁升级,升级为轻量级锁。(偏向锁状态出现了锁竞争的情况)
-
-
轻量级锁:会采用自旋锁的方式去频繁的以CAS的形式获取锁资源(采用的是自适应自旋锁)
-
如果成功获取到,拿着锁资源走
-
如果自旋了一定次数,没拿到锁资源,锁升级。
-
-
重量级锁
锁默认情况下,开启了偏向锁延迟。
如果没开启偏向锁或者设置偏向锁延迟开启,就会出现无锁状态(00);如果正常开启偏向锁了,那么不会出现无锁状态,对象会直接变为匿名偏向(01)
整个锁升级状态的转变:
重量锁底层ObjectMonitor
-
ReentrantLock是个类,synchronized是关键字,当然都是在JVM层面实现互斥锁的方式
效率区别:
-
如果竞争比较激烈,推荐ReentrantLock去实现,不存在锁升级概念。而synchronized是存在锁升级概念的,如果升级到重量级锁,是不存在锁降级的。
底层实现区别:
-
实现原理是不一样,ReentrantLock基于AQS实现的,synchronized是基于ObjectMonitor
功能向的区别:
-
ReentrantLock的功能比synchronized更全面。
-
ReentrantLock支持公平锁和非公平锁
-
加锁流程:
公平锁与非公平锁:
公平锁是指多个线程按照请求锁的顺序获取锁,即先到先得的原则。在公平锁中,如果有多个线程等待获取锁,那么锁会依次分配给等待时间最长的线程,这样可以避免线程饥饿的情况。
公平锁的实现比较复杂,需要维护一个双向线程等待队列,因此性能会比较低。
非公平锁是指多个线程按照竞争获取锁的顺序获取锁,即先到不一定先得的原则。在非公平锁中,如果有多个线程等待获取锁,那么锁可能会直接分配给等待时间较短的线程,这样可能会导致一些线程一直无法获取锁,出现线程饥饿的情况。
非公平锁的实现比较简单,不需要维护一个线程等待队列,因此性能会比较高。
ReentrantLock中实现公平与非公平锁的lock源码:
1 // 非公平锁 2 final void lock() { 3 // 上来就先基于CAS的方式,尝试将state从0改为1 4 if (compareAndSetState(0, 1)) 5 // 获取锁资源成功,会将当前线程设置到exclusiveOwnerThread属性,代表是当前线程持有着锁资源 6 setExclusiveOwnerThread(Thread.currentThread()); 7 else 8 // 执行acquire,尝试获取锁资源 9 acquire(1); 10 } 11 // 公平锁 12 final void lock() { 13 // 执行acquire,尝试获取锁资源 14 acquire(1); 15 }
acquire方法,公平锁和非公平锁的逻辑一样,源码如下:
1 public final void acquire(int arg) { 2 // tryAcquire:当前线程是否可以尝试获取锁资源 3 if (!tryAcquire(arg) && 4 // 没有拿到锁资源 5 // addWaiter(Node.EXCLUSIVE):将当前线程封装为Node节点,插入到AQS的双向链表的结尾 6 // acquireQueued:查看我是否是第一个排队的节点,如果是可以再次尝试获取锁资源,如果长时间拿不到,挂起线程 7 // 如果不是第一个排队的额节点,就尝试挂起线程即可 8 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 9 // 中断线程的操作 10 selfInterrupt(); 11 }
tryAcquire方法是竞争锁资源的逻辑,分为公平锁和非公平锁,具体的实现为:非公平锁实现nonfairTryAcquire、公平锁实现tryAcquire
1 // 非公平锁实现 2 final boolean nonfairTryAcquire(int acquires) { 3 // 获取当前线程 4 final Thread current = Thread.currentThread(); 5 // 获取了state属性 6 int c = getState(); 7 // 判断state当前是否为0,之前持有锁的线程释放了锁资源 8 if (c == 0) { 9 // 再次抢一波锁资源 10 if (compareAndSetState(0, acquires)) { 11 //拿锁成功,将当前线程设置到属性exclusiveOwnerThread中 12 setExclusiveOwnerThread(current); 13 //拿锁成功返回true 14 return true; 15 } 16 } 17 // 不是0,代表有线程持有着锁资源,判断持有锁的线程是否是当前线程,如果是,证明是锁重入操作 18 else if (current == getExclusiveOwnerThread()) { 19 // 将state + 1 20 int nextc = c + acquires; 21 if (nextc < 0) // 说明对重入次数+1后,超过了int正数的取值范围 22 // 01111111 11111111 11111111 11111111 23 // 10000000 00000000 00000000 00000000 24 // 说明重入的次数超过界限了。 25 throw new Error("Maximum lock count exceeded"); 26 // 正常的将计算结果,复制给state 27 setState(nextc); 28 // 锁重入成功 29 return true; 30 } 31 // 返回false 32 return false; 33 }
1 // 公平锁实现 2 protected final boolean tryAcquire(int acquires) { 3 // 获取当前线程 4 final Thread current = Thread.currentThread(); 5 // 获取state属性 6 int c = getState(); 7 if (c == 0) { 8 // 查看AQS中是否有排队的Node,没人排队抢一手 。有人排队,如果我是第一个,也抢一手 10 if (!hasQueuedPredecessors() && 11 // 抢一手 12 compareAndSetState(0, acquires)) { 13 setExclusiveOwnerThread(current); 14 return true; 15 } 16 } 17 // 锁重入(当前线程等于持有锁的线程) 18 else if (current == getExclusiveOwnerThread()) { 19 int nextc = c + acquires; 20 if (nextc < 0) 21 throw new Error("Maximum lock count exceeded"); 22 setState(nextc); 23 return true; 24 } 25 return false; 26 }
1 public final boolean hasQueuedPredecessors() { 2 //头尾节点 3 Node t = tail; 4 Node h = head; 5 //s为头结点的next节点 6 Node s; 7 //如果头尾节点相等,证明没有线程排队,直接去抢占锁资源 8 return h != t && 9 //// s节点不为null,并且s节点的线程为当前线程(排在第一名的是不是我) 10 ((s = h.next) == null || s.thread != Thread.currentThread()); 11 }
在非公平锁实现中,当线程没有获取到锁资源时,会将当前线程放入到一个双向队列中,具体的实现方法是addWaiter
1 // 没有拿到锁资源,过来排队, mode:代表互斥锁 2 private Node addWaiter(Node mode) { 3 // 将当前线程封装为Node, 4 Node node = new Node(Thread.currentThread(), mode); 5 // 拿到尾结点 6 Node pred = tail; 7 // 如果尾结点不为null 8 if (pred != null) { 9 // 1.当前节点的prev指向尾结点 10 node.prev = pred; 11 // 2.以CAS的方式,将当前线程设置为tail节点 12 if (compareAndSetTail(pred, node)) { 13 // 3.将之前的尾结点的next指向当前节点 14 pred.next = node; 15 return node; 16 } 17 } 18 // 如果CAS失败,以死循环的方式,保证当前线程的Node一定可以放到AQS队列的末尾 19 enq(node); 20 return node; 21 } 22 23 private Node enq(final Node node) { 24 for (;;) { 25 // 拿到尾结点 26 Node t = tail; 27 // 如果尾结点为空,AQS中一个节点都没有,构建一个伪节点,作为head和tail 28 if (t == null) { 29 if (compareAndSetHead(new Node())) 30 tail = head; 31 } else { 32 // 比较熟悉了,以CAS的方式,在AQS中有节点后,插入到AQS队列的末尾 33 node.prev = t; 34 if (compareAndSetTail(t, node)) { 35 t.next = node; 36 return t; 37 } 38 } 39 } 40 }
当线程没有拿到锁资源后,并且到AQS排队了之后触发的方法acquireQueued
// 当前没有拿到锁资源后,并且到AQS排队了之后触发的方法。 中断操作这里不用考虑 final boolean acquireQueued(final Node node, int arg) { // 不考虑中断 // failed:获取锁资源是否失败(这里简单掌握落地,真正触发的,还是tryLock和lockInterruptibly) boolean failed = true; try { boolean interrupted = false; // 死循环………… for (;;) { // 拿到当前节点的前继节点 final Node p = node.predecessor(); // 前继节点是否是head,如果是head,再次执行tryAcquire尝试获取锁资源。 if (p == head && tryAcquire(arg)) { // 获取锁资源成功 // 设置头结点为当前获取锁资源成功Node,并且取消thread信息 setHead(node); // help GC p.next = null; // 获取锁失败标识为false failed = false; return interrupted; } // 没拿到锁资源…… // shouldParkAfterFailedAcquire:基于上一个节点转改来判断当前节点是否能够挂起线程,如果可以返回true, // 如果不能,就返回false,继续下次循环 if (shouldParkAfterFailedAcquire(p, node) && // 这里基于Unsafe类的park方法,将当前线程挂起 parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) // 在lock方法中,基本不会执行。 cancelAcquire(node); } } // 获取锁资源成功后,先执行setHead private void setHead(Node node) { // 当前节点作为头结点 伪 head = node; // 头结点不需要线程信息 node.thread = null; node.prev = null; } // 当前Node没有拿到锁资源,或者没有资格竞争锁资源,看一下能否挂起当前线程 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { // -1,SIGNAL状态:代表当前节点的后继节点,可以挂起线程,后续我会唤醒我的后继节点 // 1,CANCELLED状态:代表当前节点以及取消了 int ws = pred.waitStatus; if (ws == Node.SIGNAL) // 上一个节点为-1之后,当前节点才可以安心的挂起线程 return true; if (ws > 0) { // 如果当前节点的上一个节点是取消状态,我需要往前找到一个状态不为1的Node,作为他的next节点 // 找到状态不为1的节点后,设置一下next和prev do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { // 上一个节点的状态不是1或者-1,那就代表节点状态正常,将上一个节点的状态改为-1 compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }
释放锁流程:
释放锁流程概述
释放锁源码:
public void unlock() { // 释放锁资源不分为公平锁和非公平锁,都是一个sync对象 sync.release(1); } // 释放锁的核心流程 public final boolean release(int arg) { // 核心释放锁资源的操作之一 if (tryRelease(arg)) { // 如果锁已经释放掉了,走这个逻辑 Node h = head; // h不为null,说明有排队的 // 如果h的状态不为0(为-1),说明后面有排队的Node,并且线程已经挂起了。 if (h != null && h.waitStatus != 0) // 唤醒排队的线程 unparkSuccessor(h); return true; } return false; } // ReentrantLock释放锁资源操作 protected final boolean tryRelease(int releases) { // 拿到state - 1(并没有赋值给state) int c = getState() - releases; // 判断当前持有锁的线程是否是当前线程,如果不是,直接抛出异常 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); // free,代表当前锁资源是否释放干净了。 boolean free = false; if (c == 0) { // 如果state - 1后的值为0,代表释放干净了。 free = true; // 将持有锁的线程置位null setExclusiveOwnerThread(null); } // 将c设置给state setState(c); // 锁资源释放干净返回true,否则返回false return free; } // 唤醒后面排队的Node private void unparkSuccessor(Node node) { // 拿到头节点状态 int ws = node.waitStatus; if (ws < 0) // 先基于CAS,将节点状态从-1,改为0 compareAndSetWaitStatus(node, ws, 0); // 拿到头节点的后续节点。 Node s = node.next; // 如果后续节点为null或者后续节点的状态为1,代表节点取消了。 if (s == null || s.waitStatus > 0) { s = null; // 如果后续节点为null,或者后续节点状态为取消状态,从后往前找到一个有效节点环境 for (Node t = tail; t != null && t != node; t = t.prev) // 从后往前找到状态小于等于0的节点 // 找到离head最新的有效节点,并赋值给s if (t.waitStatus <= 0) s = t; } // 只要找到了这个需要被唤醒的节点,执行unpark唤醒 if (s != null) LockSupport.unpark(s.thread); }
synchronized和ReentrantLock都是互斥锁。
如果说有一个操作是读多写少的,还要保证线程安全的话。如果采用上述的两种互斥锁,效率方面很定是很低的。
在这种情况下,咱们就可以使用ReentrantReadWriteLock读写锁去实现。
读读之间是不互斥的,可以读和读操作并发执行。
但是如果涉及到了写操作(读写、写写),那么还得是互斥的操作。
读写锁的实现原理
读锁操作:基于state的高16位进行操作。
写锁操作:基于state的低16为进行操作。
ReentrantReadWriteLock依然是可重入锁。
写锁重入:读写锁中的写锁的重入方式,基本和ReentrantLock一致,没有什么区别,依然是对state进行+1操作即可,只要确认持有锁资源的线程,是当前写锁线程即可。只不过之前ReentrantLock的重入次数是state的正数取值范围,但是读写锁中写锁范围就变小了。
读锁重入:因为读锁是共享锁。读锁在获取锁资源操作时,是要对state的高16位进行 + 1操作。因为读锁是共享锁,所以同一时间会有多个读线程持有读锁资源。这样一来,多个读操作在持有读锁时,无法确认每个线程读锁重入的次数。为了去记录读锁重入的次数,每个读操作的线程,都会有一个ThreadLocal记录锁重入的次数。
写锁的饥饿问题:读锁是共享锁,当有线程持有读锁资源时,再来一个线程想要获取读锁,直接对state修改即可。在读锁资源先被占用后,来了一个写锁资源,此时,大量的需要获取读锁的线程来请求锁资源,如果可以绕过写锁,直接拿资源,会造成写锁长时间无法获取到写锁资源。