learnJUC系列(四)Java的各种锁
日常工作学习中,经常有听到各种锁,如乐观锁/悲观锁、公平锁/非公平锁、可重入锁、自旋锁、死锁等等。此处通过一些具体的demo结合源码,来加深理解。
1、公平锁和非公平锁
1.1 是什么
并发包java.util.concurrent.locks包下,reentrantLock的创建,可以指定构造函数的boolean类型,来得到公平锁或非公平锁(默认)。如,Lock lock = new ReentrantLock();
公平锁是根据线程的先来后到,而非公平锁允许加塞,优先级不确定。
查看ReentrantLock源码中的构造函数。
1.2 两者区别
公平锁:Thread acquire a fair lock in the order in which they requested id.
在并发环境下,每个线程在获取锁时,会先查看此锁维护的等待队列,如果为空,或当前线程为第一个,就占有锁,否则加入到等待队列,按照FIFO的规则排队。
非公平锁:a nonfair lock permits barging: threads requesting a lock can jump ahead of the queue of waiting threads if the lock happens to be available when it is requested.
非公平锁会直接尝试占有资源,如果尝试失败,再采用类似公平锁的方式
1.3 其他
对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁。默认非公平锁,能够发挥吞吐量大的特点,无需维护顺序,能够充分发挥多线程抢占式的处理优势。
其中,synchronized是一种非公平锁,原因是,其底层使用的是mutex锁实现,而mutex锁不能保证公平性,目的也是为了提高性能。
非公平锁吞吐量大大原因分析:
比如 A 占用锁的时候,B 请求获取锁,发现被 A 占用之后,堵塞等待被唤醒,这个时候 C 同时来获取 A 占用的锁,如果是公平锁 C 后来者发现不可用之后一定排在 B 之后等待被唤醒,而非公平锁则可以让 C 先用,在 B 被唤醒之前 C 已经使用完成,从而节省了 C 等待和唤醒之间的性能消耗,这就是非公平锁比公平锁吞吐量大的原因。
2、可重入锁(即递归锁)
// 两个方法是同一锁,能够进入public sync void method01(){method02();}public sync void method02(){……}
2.1 是什么
同一线程外层函数获得锁之后,内层递归函数仍能获取该锁的代码,在同一个线程在外层方法获取锁的时候,再进入内层方法会自动获取锁,即线程可以进入任何一个它已经拥有的锁所同步的代码块。
ReentrantLock、Synchronized就是典型的可重入锁。
2.2 代码示例
package com.panda00hi.juc;import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;/*** 可重入锁示例** @author panda00hi* @date 2022/4/10*/public class ReenterLockDemo {public static void main(String[] args) {System.out.println("******Synchronized可重入锁示例******");Phone phone = new Phone();new Thread(() -> {try {phone.sendSMS();} catch (Exception e) {e.printStackTrace();}}, "t1").start();new Thread(() -> {try {phone.sendSMS();} catch (Exception e) {e.printStackTrace();}}, "t2").start();try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("******ReentrantLock可重入锁示例******");GetSetTest getSetTest = new GetSetTest();Thread t3 = new Thread(getSetTest, "t3");Thread t4 = new Thread(getSetTest, "t4");t3.start();t4.start();}}class GetSetTest implements Runnable {Lock lock = new ReentrantLock();@Overridepublic void run() {get();}public void get() {lock.lock();try {System.out.println(Thread.currentThread().getName() + " invoked get()");set();} finally {lock.unlock();}}public void set() {lock.lock();try {System.out.println(Thread.currentThread().getName() + " ####invoked set()");} finally {lock.unlock();}}}/*** 资源类*/class Phone {public synchronized void sendSMS() {System.out.println(Thread.currentThread().getName() + " invoked sendSMS()");sendEmail();}public synchronized void sendEmail() {System.out.println(Thread.currentThread().getName() + " ####invoked sendEmail()");}}
运行结果:
******Synchronized可重入锁示例******t1 invoked sendSMS()t1 ####invoked sendEmail()t2 invoked sendSMS()t2 ####invoked sendEmail()******ReentrantLock可重入锁示例******t3 invoked get()t3 ####invoked set()t4 invoked get()t4 ####invoked set()
3、自旋锁
3.1 是什么
尝试获取锁的线程不会立即阻塞,而是采用循环的方式取尝试获得锁,好处是减少线程上下文切换的消耗,没有类似wait的阻塞,缺点是循环会消耗CPU。
如,之前CAS部分有提到的比较交换操作
public final int getAndAddInt(Object var1, long var2, int var4) {int var5;do {var5 = this.getIntVolatile(var1, var2);} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));return var5;}
3.2 手写自旋锁
通过CAS完成自旋锁,A线程先进来调用mylock方法自己持有锁5秒钟,B随后进来发现当前线程持有锁,不为null,所以只能等待(自旋),直到A释放锁后,B抢到,结束自旋。
package com.panda00hi.juc;import java.util.concurrent.TimeUnit;import java.util.concurrent.atomic.AtomicReference;/*** 手写自旋锁示例** @author panda00hi* @date 2022/4/10*/public class SpinLockDemo {// 原子引用线程,初始值为nullAtomicReference<Thread> atomicReference = new AtomicReference<>();public void myLock() {Thread thread = Thread.currentThread();System.out.println(Thread.currentThread().getName() + " come in");// 如果为null,则将当前线程放入,返回true,取反,所以while不进入while (!atomicReference.compareAndSet(null, thread)) {// AA线程进入myLock方法,BB线程也进入,但是由于AA线程进入后,率先将atomicReference// 改为了自身线程对象,所以BB等其他线程将进入while循环内部,即自旋,直到AA线程通过// myUnlock方法,将atomicReference又置为null,BB线程才能跳出while循环,结束自旋try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " waiting...");}}public void myUnlock() {Thread thread = Thread.currentThread();// 若为当前线程对象,则置为nullatomicReference.compareAndSet(thread, null);System.out.println(Thread.currentThread().getName() + " invoked myUnlock()");}public static void main(String[] args) {SpinLockDemo spinLockDemo = new SpinLockDemo();new Thread(() -> {spinLockDemo.myLock();try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}spinLockDemo.myUnlock();}, "AA").start();new Thread(() -> {spinLockDemo.myLock();try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}spinLockDemo.myUnlock();}, "BB").start();new Thread(() -> {spinLockDemo.myLock();try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}spinLockDemo.myUnlock();}, "CC").start();}}
运行结果:
AA come inBB come inCC come inBB waiting...CC waiting...BB waiting...CC waiting...AA invoked myUnlock()CC waiting...BB waiting...CC invoked myUnlock()BB waiting...BB invoked myUnlock()
4、独占锁(写)、共享锁(读)、互斥锁
4.1 是什么
传统的Synchronized和Lock虽然保证了“读”和“写”的线程安全,但是也导致了“读”的并发性能大大降低,因此需要更细粒度的控制。
独占锁:指该锁一次只能被一个线程锁持有,对ReentrantLock和Synchronized而言都是独占锁。
共享锁:该锁可被多个线程锁持有,ReentrantReadWriteLock其读锁是共享锁,写锁是独占锁。
互斥锁:读锁的共享锁可以保证高并发读是高效的,读写、写读、写写的过程是互斥的。(有写的操作,就是互斥的)
多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该允许同时进行。但是,如果有一个线程想要写共享资源,就不应该再有其他线程对资源进行读或写。
总结:读读可共存、读写不可共存、写写不可共存
写操作:原子+独占,整个过程必须是一个完整的过程,不能被加塞、中断
4.2 代码示例
package com.panda00hi.juc;import java.util.HashMap;import java.util.Map;import java.util.concurrent.TimeUnit;/*** 读写锁示例* 多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该允许同时进行* 但是* 如果有一个线程想要写共享资源,就不应该再有其他线程对资源进行读或写* 总结* 读读可共存* 读写不可共存* 写写不可共存* 写操作:原子+独占,整个过程必须是一个完整的过程,不能被加塞、中断** @author panda00hi* @date 2022/4/10*/public class ReadWriteLockDemo {public static void main(String[] args) {MyCache myCache = new MyCache();// 5个线程读,5个线程写for (int i = 0; i < 5; i++) {// lambda表达式要求是final类型的,所以提前赋值final型的临时变量final int tempInt = i;new Thread(() -> {myCache.put(tempInt + "", tempInt);}, "Thread " + i).start();}for (int i = 0; i < 5; i++) {// lambda表达式要求是final类型的,所以提前赋值final型的临时变量final int tempInt = i;new Thread(() -> {myCache.get(tempInt + "");}, "Thread " + i).start();}}}/*** 缓存资源类* 同一时刻,可供多人读,但是仅供一人写*/class MyCache {private volatile Map<String, Object> map = new HashMap<>();// 可以保证一致性安全性,但是失去了多个线程读的并发性能// private Lock lock = new ReentrantLock();/*** 写操作必须保证原子性,独占不可分割。即,同一线程的"正在写入"、"写入完成"是连续的*/public void put(String key, Object value) {System.out.println(Thread.currentThread().getName() + " 正在写入:" + key);// 模拟网络时延500毫秒try {TimeUnit.MICROSECONDS.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " 写入完成:");}/*** 读是可以共享的,各个线程不一样*/public void get(String key) {System.out.println(Thread.currentThread().getName() + " 正在读取:");// 模拟网络时延500毫秒try {TimeUnit.MICROSECONDS.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}Object result = map.get(key);System.out.println(Thread.currentThread().getName() + " 正在读取:" + result);}}
运行结果:
Thread 0 正在写入:0Thread 2 正在写入:2Thread 1 正在写入:1Thread 3 正在写入:3Thread 4 正在写入:4Thread 0 正在读取:Thread 1 正在读取:Thread 2 正在读取:Thread 3 正在读取:Thread 4 正在读取:Thread 2 正在读取:nullThread 0 正在读取:nullThread 4 写入完成:Thread 0 写入完成:Thread 1 写入完成:Thread 3 写入完成:Thread 1 正在读取:nullThread 2 写入完成:Thread 3 正在读取:nullThread 4 正在读取:null
发现某个线程执行写入的操作会被打断,与预期不符。如果直接使用Synchronized或者Lock的话,会牺牲读的性能,不推荐。
解决方法:使用读写锁,针对读和写的不同操作进行精细控制。JDK中有提供具体的实现。ReadWriteLock接口的其中一个实现类ReentrantReadWriteLock。
4.3 使用读写锁ReentrantReadWriteLock解决问题
package com.panda00hi.juc;import java.util.HashMap;import java.util.Map;import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.ReentrantReadWriteLock;/*** 读写锁示例* 多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该允许同时进行* 但是* 如果有一个线程想要写共享资源,就不应该再有其他线程对资源进行读或写* 总结* 读读可共存* 读写不可共存* 写写不可共存* 写操作:原子+独占,整个过程必须是一个完整的过程,不能被加塞、中断** @author panda00hi* @date 2022/4/10*/public class ReadWriteLockDemo {public static void main(String[] args) {MyCache myCache = new MyCache();// 5个线程读,5个线程写for (int i = 0; i < 5; i++) {// lambda表达式要求是final类型的,所以提前赋值final型的临时变量final int tempInt = i;new Thread(() -> {myCache.put(tempInt + "", tempInt);}, "Thread " + i).start();}for (int i = 0; i < 5; i++) {// lambda表达式要求是final类型的,所以提前赋值final型的临时变量final int tempInt = i;new Thread(() -> {myCache.get(tempInt + "");}, "Thread " + i).start();}}}/*** 缓存资源类* 同一时刻,可供多人读,但是仅供一人写*/class MyCache {private volatile Map<String, Object> map = new HashMap<>();// 可以保证一致性安全性,但是失去了多个线程读的并发性能// private Lock lock = new ReentrantLock();private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();/*** 写操作必须保证原子性,独占不可分割。即,同一线程的"正在写入"、"写入完成"是连续的*/public void put(String key, Object value) {rwLock.writeLock().lock();try {System.out.println(Thread.currentThread().getName() + " 正在写入:" + key);// 模拟网络时延500毫秒try {TimeUnit.MICROSECONDS.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}map.put(key, value);System.out.println(Thread.currentThread().getName() + " 写入完成:" + value);} catch (Exception e) {e.printStackTrace();} finally {rwLock.writeLock().unlock();}}/*** 读是可以共享的,各个线程不一样*/public void get(String key) {rwLock.readLock().lock();try {System.out.println(Thread.currentThread().getName() + " 正在读取:");// 模拟网络时延500毫秒try {TimeUnit.MICROSECONDS.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}Object result = map.get(key);System.out.println(Thread.currentThread().getName() + " 读取完成:key " + key + " result " + result);} catch (Exception e) {e.printStackTrace();} finally {rwLock.readLock().unlock();}}}
运行结果:
Thread 0 正在写入:0Thread 0 写入完成:0Thread 1 正在写入:1Thread 1 写入完成:1Thread 2 正在写入:2Thread 2 写入完成:2Thread 3 正在写入:3Thread 3 写入完成:3Thread 4 正在写入:4Thread 4 写入完成:4Thread 0 正在读取:Thread 1 正在读取:Thread 2 正在读取:Thread 3 正在读取:Thread 4 正在读取:Thread 0 读取完成:key 0 result 0Thread 1 读取完成:key 1 result 1Thread 4 读取完成:key 4 result 4Thread 2 读取完成:key 2 result 2Thread 3 读取完成:key 3 result 3
5、死锁原因分析和问题排查
5.1 是什么
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法继续推进下去。
5.2 复现代码
package com.panda00hi.juc;import java.util.concurrent.TimeUnit;/*** 死锁复现示例** @author panda00hi* @date 2022/4/10*/public class DeadLockDemo {public static void main(String[] args) {String lockA = "lockA";String lockB = "lockB";new Thread(new HoldLockThread(lockA,lockB),"thread AAA").start();new Thread(new HoldLockThread(lockB,lockA),"thread BBB").start();}}class HoldLockThread implements Runnable {private String lockA;private String lockB;public HoldLockThread(String lockA, String lockB) {this.lockA = lockA;this.lockB = lockB;}public void run() {synchronized (lockA) {System.out.println(Thread.currentThread().getName() + " 自己持有:" + lockA + " 尝试获得:" + lockB);try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}synchronized (lockB) {System.out.println(Thread.currentThread().getName() + " 自己持有:" + lockB + " 尝试获得:" + lockA);}}}}
运行结果:
thread AAA 自己持有:lockA 尝试获得:lockBthread BBB 自己持有:lockB 尝试获得:lockA
程序进入阻塞状态(死锁)
5.3 死锁定位和解决
jps -l : 查看进程的进程号
➜ learnJUC git:(master) ✗ jps -l86929 org.jetbrains.jps.cmdline.Launcher54274 org.jetbrains.jps.cmdline.Launcher86930 com.panda00hi.juc.DeadLockDemo87016 sun.tools.jps.Jps50859
jstack+进程号:查看堆栈信息
➜ learnJUC git:(master) ✗ jstack 869302022-04-10 01:22:47Full thread dump OpenJDK 64-Bit Server VM (25.312-b07 mixed mode):"Attach Listener" #16 daemon prio=9 os_prio=31 tid=0x0000000130808800 nid=0x4a07 waiting on condition [0x0000000000000000]java.lang.Thread.State: RUNNABLE"DestroyJavaVM" #15 prio=5 os_prio=31 tid=0x000000013081a000 nid=0x1603 waiting on condition [0x0000000000000000]java.lang.Thread.State: RUNNABLE"thread BBB" #14 prio=5 os_prio=31 tid=0x0000000140064000 nid=0xa503 waiting for monitor entry [0x00000001703b2000]java.lang.Thread.State: BLOCKED (on object monitor)at com.panda00hi.juc.HoldLockThread.run(DeadLockDemo.java:41)- waiting to lock <0x0000000715719160> (a java.lang.String)- locked <0x0000000715719198> (a java.lang.String)at java.lang.Thread.run(Thread.java:748)"thread AAA" #13 prio=5 os_prio=31 tid=0x0000000130072000 nid=0xa703 waiting for monitor entry [0x00000001701a6000]java.lang.Thread.State: BLOCKED (on object monitor)at com.panda00hi.juc.HoldLockThread.run(DeadLockDemo.java:41)- waiting to lock <0x0000000715719198> (a java.lang.String)- locked <0x0000000715719160> (a java.lang.String)at java.lang.Thread.run(Thread.java:748)"Service Thread" #12 daemon prio=9 os_prio=31 tid=0x0000000136810800 nid=0xa903 runnable [0x0000000000000000]java.lang.Thread.State: RUNNABLE"C1 CompilerThread3" #11 daemon prio=9 os_prio=31 tid=0x0000000130816000 nid=0x5803 waiting on condition [0x0000000000000000]java.lang.Thread.State: RUNNABLE"C2 CompilerThread2" #10 daemon prio=9 os_prio=31 tid=0x0000000140844000 nid=0x5603 waiting on condition [0x0000000000000000]java.lang.Thread.State: RUNNABLE"C2 CompilerThread1" #9 daemon prio=9 os_prio=31 tid=0x000000013080d800 nid=0x4103 waiting on condition [0x0000000000000000]java.lang.Thread.State: RUNNABLE"C2 CompilerThread0" #8 daemon prio=9 os_prio=31 tid=0x000000014080a000 nid=0x4303 waiting on condition [0x0000000000000000]java.lang.Thread.State: RUNNABLE"JDWP Command Reader" #7 daemon prio=10 os_prio=31 tid=0x000000013080b800 nid=0x4503 runnable [0x0000000000000000]java.lang.Thread.State: RUNNABLE"JDWP Event Helper Thread" #6 daemon prio=10 os_prio=31 tid=0x000000013080a800 nid=0x3f03 runnable [0x0000000000000000]java.lang.Thread.State: RUNNABLE"JDWP Transport Listener: dt_socket" #5 daemon prio=10 os_prio=31 tid=0x000000013080a000 nid=0x4707 runnable [0x0000000000000000]java.lang.Thread.State: RUNNABLE"Signal Dispatcher" #4 daemon prio=9 os_prio=31 tid=0x000000014004e000 nid=0x3e03 runnable [0x0000000000000000]java.lang.Thread.State: RUNNABLE"Finalizer" #3 daemon prio=8 os_prio=31 tid=0x0000000130010000 nid=0x4d03 in Object.wait() [0x000000016ea0a000]java.lang.Thread.State: WAITING (on object monitor)at java.lang.Object.wait(Native Method)- waiting on <0x0000000715588ef0> (a java.lang.ref.ReferenceQueue$Lock)at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)- locked <0x0000000715588ef0> (a java.lang.ref.ReferenceQueue$Lock)at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:165)at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:216)"Reference Handler" #2 daemon prio=10 os_prio=31 tid=0x0000000140038000 nid=0x4f03 in Object.wait() [0x000000016e7fe000]java.lang.Thread.State: WAITING (on object monitor)at java.lang.Object.wait(Native Method)- waiting on <0x0000000715586c08> (a java.lang.ref.Reference$Lock)at java.lang.Object.wait(Object.java:502)at java.lang.ref.Reference.tryHandlePending(Reference.java:191)- locked <0x0000000715586c08> (a java.lang.ref.Reference$Lock)at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)"VM Thread" os_prio=31 tid=0x0000000140031000 nid=0x3403 runnable"ParGC Thread#0" os_prio=31 tid=0x0000000140815800 nid=0x2107 runnable"ParGC Thread#1" os_prio=31 tid=0x0000000140816000 nid=0x1f03 runnable"ParGC Thread#2" os_prio=31 tid=0x0000000140817000 nid=0x5403 runnable"ParGC Thread#3" os_prio=31 tid=0x0000000140817800 nid=0x5203 runnable"ParGC Thread#4" os_prio=31 tid=0x0000000140818800 nid=0x2c03 runnable"ParGC Thread#5" os_prio=31 tid=0x0000000140819000 nid=0x2e03 runnable"ParGC Thread#6" os_prio=31 tid=0x000000014081a000 nid=0x3003 runnable"ParGC Thread#7" os_prio=31 tid=0x000000014081a800 nid=0x3103 runnable"ParGC Thread#8" os_prio=31 tid=0x000000014081b800 nid=0x3203 runnable"VM Periodic Task Thread" os_prio=31 tid=0x0000000136811800 nid=0x5b03 waiting on conditionJNI global references: 1465Found one Java-level deadlock:============================="thread BBB":waiting to lock monitor 0x000000013000b0d0 (object 0x0000000715719160, a java.lang.String),which is held by "thread AAA""thread AAA":waiting to lock monitor 0x000000013000d5f0 (object 0x0000000715719198, a java.lang.String),which is held by "thread BBB"Java stack information for the threads listed above:==================================================="thread BBB":at com.panda00hi.juc.HoldLockThread.run(DeadLockDemo.java:41)- waiting to lock <0x0000000715719160> (a java.lang.String)- locked <0x0000000715719198> (a java.lang.String)at java.lang.Thread.run(Thread.java:748)"thread AAA":at com.panda00hi.juc.HldLockThread.run(DeadLockDemo.java:41)- waiting to lock <0x0000000715719198> (a java.lang.String)- locked <0x0000000715719160> (a java.lang.String)at java.lang.Thread.run(Thread.java:748)Found 1 deadlock.
可看到关于deadlock的信息,以及对应的代码位置。
