vlambda博客
学习文章列表

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(); @Override public 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 { // 原子引用线程,初始值为null AtomicReference<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(); // 若为当前线程对象,则置为null atomicReference.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; }
@Override 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 condition
JNI global references: 1465

Found 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的信息,以及对应的代码位置。