锁
📲

不可不说的Java"锁"事
Java提供了种类丰富的锁,每种锁因其特性的不同,在适当的场景下能够展现出非常高的效率。本文旨在对锁相关源码(本文中的源码来自JDK 8和Netty 3.10.6)、使用场景进行举例,为读者介绍主流锁的知识点,以及不同的锁的适用场景。 Java中往往是按照是否含有某一特性来定义锁,我们通过特性将锁进行分组归类,再使用对比的方式进行介绍,帮助大家更快捷的理解相关知识。下面给出本文内容的总体分类目录: 乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不同角度。在Java和数据库中都有此概念对应的实际应用。 先说概念。对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。 而乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。 乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。 根据从上面的概念描述我们可以发现: 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。 光说概念有些抽象,我们来看下乐观锁和悲观锁的调用方式示例: // ------------------------- 悲观锁的调用方式 ------------------------- // synchronized public synchronized void testMethod() { // 操作同步资源 } // ReentrantLock private ReentrantLock lock = new ReentrantLock(); // 需要保证多个线程使用的是同一个锁 public void modifyPublicResources() { lock.lock(); // 操作同步资源 lock.unlock(); } // ------------------------- 乐观锁的调用方式 ------------------------- private AtomicInteger atomicInteger = new AtomicInteger(); // 需要保证多个线程使用的是同一个AtomicInteger atomicInteger.incrementAndGet(); //执行自增1 通过调用方式示例,我们可以发现悲观锁基本都是在显式的锁定之后再操作同步资源,而乐观锁则直接去操作同步资源。那么,为何乐观锁能够做到不锁定同步资源也可以正确的实现线程同步呢?我们通过介绍乐观锁的主要实现方式 "CAS" 的技术原理来为大家解惑。 CAS全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。java.util.concurrent包中的原子类就是通过CAS来实现了乐观锁。 CAS算法涉及到三个操作数: 当且仅当 V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值("比较+更新"整体是一个原子操作),否则不会执行任何操作。一般情况下,"更新"是一个不断重试的操作。 之前提到java.util.concurrent包中的原子类,就是通过CAS来实现了乐观锁,那么我们进入原子类AtomicInteger的源码,看一下AtomicInteger的定义: 根据定义我们可以看出各属性的作用: unsafe: 获取并操作内存的数据。 valueOffset: 存储value在AtomicInteger中的偏移量。 value: 存储AtomicInteger的int值,该属性需要借助volatile关键字保证其在线程间是可见的。 接下来,我们查看AtomicInteger的自增函数incrementAndGet()的源码时,发现自增函数底层调用的是unsafe.getAndAddInt()。但是由于JDK本身只有Unsafe.class,只通过class文件中的参数名,并不能很好的了解方法的作用,所以我们通过OpenJDK 8 来查看Unsafe的源码: // ------------------------- JDK 8 ------------------------- // ...
不可不说的Java"锁"事
认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
synchronized关键字和Lock的实现类都是悲观锁

synchronized关键字

为了避免临界区的竞态条件发生,有多种手段可以达到目的。
  • 阻塞式的解决方案:synchronized,Lock
  • 非阻塞式的解决方案:原子变量
用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换,
synchronized实现使用的是monitorentermonitorexit指令 javap -c ***.class文件反编译
notion image

作用于代码块

synchronized(锁对象){ //临界区代码 }
 
案例
@Slf4j(topic = "c.Test17") public class Test17 { public static void main(String[] args) throws InterruptedException { Room room = new Room(); Thread t1 = new Thread(() -> { for (int i = 0; i < 5000; i++) { room.increment(); } }, "t1"); Thread t2 = new Thread(() -> { for (int i = 0; i < 5000; i++) { room.decrement(); } }, "t2"); t1.start(); t2.start(); t1.join(); t2.join(); log.debug("{}", room.getCounter()); } } class Room { private int counter = 0; public synchronized void increment() { counter++; } public synchronized void decrement() { counter--; } public synchronized int getCounter() { return counter; } }
notion image
notion image
synchronized实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断
 

修饰成员方法,锁住this对象

class Test { public synchronized void increment() { } }等价于 class Test{ public void test(){ synchronized(this){ } } }
notion image
调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置。如果设置了,执行线程会将先持有monitor然后再执行方法, 最后在方法完成(无论是正常完成还是非正常完成)时释放 monitor

修饰静态方法,锁住方法所在类的class对象

class Test { public synchronized static void increment() { } }等价于 class Test{ public void test(){ synchronized(Test.class){ } } }
notion image
ACC_STATIC, ACC_SYNCHRONIZED访问标志区分该方法是否静态同步方法

案例’‘线程八锁’’

1
@Slf4j(topic = "c.Test8Locks") public class Test8Locks { public static void main(String[] args) { Number n1 = new Number(); new Thread(() -> { log.debug("begin"); n1.a(); }).start(); new Thread(() -> { log.debug("begin"); n1.b(); }).start(); } } @Slf4j(topic = "c.Number") class Number{ public synchronized void a() { log.debug("1"); } public synchronized void b() { log.debug("2"); } } //锁对象都为n1
2
@Slf4j(topic = "c.Test8Locks") public class Test8Locks { public static void main(String[] args) { Number n1 = new Number(); new Thread(() -> { log.debug("begin"); n1.a(); }).start(); new Thread(() -> { log.debug("begin"); n1.b(); }).start(); } } @Slf4j(topic = "c.Number") class Number{ public synchronized void a() { sleep(1); log.debug("1"); } public synchronized void b() { log.debug("2"); } } //output 1秒后1,2或者21秒后1 //sleep并不会释放锁对象
3
@Slf4j(topic = "c.Test8Locks") public class Test8Locks { public static void main(String[] args) { Number n1 = new Number(); new Thread(() -> { n1.a(); }).start(); new Thread(() -> { n1.b(); }).start(); new Thread(() -> { n1.c(); }).start(); } } @Slf4j(topic = "c.Number") class Number { public synchronized void a() { sleep(1); log.debug("1"); } public synchronized void b() { log.debug("2"); } public void c() {//c未上锁和,a,b互不影响,a,b同一对象锁互斥 log.debug("3"); } } //output:3,1秒后1,2或3,2,1秒后1或2,3,1秒,1或 1秒,1,3,2
4
@Slf4j(topic = "c.Test8Locks") public class Test8Locks { public static void main(String[] args) { Number n1 = new Number(); Number n2 = new Number(); new Thread(() -> { n1.a(); }).start(); new Thread(() -> { n2.b(); }).start(); } @Slf4j(topic = "c.Number") class Number { public synchronized void a() { sleep(1); log.debug("1"); } public synchronized void b() { log.debug("2"); } }//output:2,1秒后1
5
@Slf4j(topic = "c.Test8Locks") public class Test8Locks { public static void main(String[] args) { Number n1 = new Number(); new Thread(() -> { n1.a(); }).start(); new Thread(() -> { n1.b(); }).start(); } @Slf4j(topic = "c.Number") class Number { public static synchronized void a() { //锁对象为Number.class sleep(1); log.debug("1"); } public synchronized void b() { log.debug("2"); } }//output:2,1秒后1
6
@Slf4j(topic = "c.Test8Locks") public class Test8Locks { public static void main(String[] args) { Number n1 = new Number(); new Thread(() -> { n1.a(); }).start(); new Thread(() -> { n1.b(); }).start(); } @Slf4j(topic = "c.Number") class Number { public static synchronized void a() { //锁对象为Number.class sleep(1); log.debug("1"); } public static synchronized void b() { log.debug("2"); } }//output:2,1秒后1或者1秒后1,2
7
@Slf4j(topic = "c.Test8Locks") public class Test8Locks { public static void main(String[] args) { Number n1 = new Number(); Number n2 = new Number(); new Thread(() -> { n1.a(); }).start(); new Thread(() -> { n2.b(); }).start(); } @Slf4j(topic = "c.Number") class Number { public static synchronized void a() { //锁对象为Number.class sleep(1); log.debug("1"); } public synchronized void b() { log.debug("2"); } }//output:2,1秒后1
8
@Slf4j(topic = "c.Test8Locks") public class Test8Locks { public static void main(String[] args) { Number n1 = new Number(); Number n2 = new Number(); new Thread(() -> { n1.a(); }).start(); new Thread(() -> { n2.b(); }).start(); } @Slf4j(topic = "c.Number") class Number { public static synchronized void a() { //锁对象为Number.class sleep(1); log.debug("1"); } public static synchronized void b() { log.debug("2"); } }//output:2,1秒后1或者1秒后1,2
 
 

java对象头

Java对象的对象头由 mark wordklass pointer两部分组成,
mark word存储了同步状态、标识、hashcode、GC状态等等。
klass pointer存储对象的类型指针,该指针指向它的类元数据
值得注意的是,如果应用的对象过多,使用64位的指针将浪费大量内存。64位的JVM比32位的JVM多耗费50%的内存。
我们现在使用的64位 JVM会默认使用选项 +UseCompressedOops 开启指针压缩,将指针压缩至32位。
notion image
notion image
64位虚拟机
notion image
lock: 锁状态标记位,该标记的值不同,整个mark word表示的含义不同。
biased_lock:偏向锁标记,为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。
notion image
age:Java GC标记位对象年龄。
identity_hashcode:对象标识Hash码,采用延迟加载技术。当对象使用HashCode()计算后,并会将结果写到该对象头中。当对象被锁定时,该值会移动到线程Monitor中。
thread:持有偏向锁的线程ID和其他信息。这个线程ID并不是JVM分配的线程ID号,和Java Thread中的ID是两个概念。
epoch:偏向时间戳。
ptr_to_lock_record:指向栈中锁记录的指针。
ptr_to_heavyweight_monitor:指向线程Monitor的指针。

使用JOL工具类,打印对象头

<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.10</version> </dependency>
A a = new A(); System.out.println(ClassLayout.parseInstance(a).toPrintable());
notion image
输出的第一行内容和锁状态内容对应
unused:1 | age:4 | biased_lock:1 | lock:2
0 0000 0 01 代表A对象正处于无锁状态
第三行中表示的是被指针压缩为32位的klass pointer
第四行则是我们创建的A对象属性信息 1字节的boolean值
第五行则代表了对象的对齐字段 为了凑齐64位的对象,对齐字段占用了3个字节,24bit
简化JOL输出
SimpleClassLayout
public class SimpleClassLayout { private final ClassData classData; private final SortedSet<FieldLayout> fields; private final int headerSize; private final long size; public SimpleClassLayout(ClassData classData, SortedSet<FieldLayout> fields, int headerSize, long instanceSize, boolean check) { this.classData = classData; this.fields = fields; this.headerSize = headerSize; this.size = instanceSize; } public static SimpleClassLayout parseInstance(Object instance) { return parseInstance(instance, new SimpleLayouter()); } public static SimpleClassLayout parseInstance(Object instance, SimpleLayouter layouter) { return layouter.layout(ClassData.parseInstance(instance)); } public String toPrintSimple() { return toPrintSimple(classData.instance(), true); } public String toPrintSimple(Boolean isMoreInfo) { return toPrintSimple(classData.instance(), isMoreInfo); } public SortedSet<FieldLayout> fields() { return fields; } public String toPrintSimple(Object instance, Boolean isMoreInf) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); String MSG_GAP = "(alignment/padding gap)"; String MSG_NEXT_GAP = "(loss due to the next object alignment)"; int maxTypeLen = "TYPE".length(); for (FieldLayout f : fields()) { maxTypeLen = Math.max(f.typeClass().length(), maxTypeLen); } maxTypeLen += 2; int maxDescrLen = Math.max(MSG_GAP.length(), MSG_NEXT_GAP.length()); for (FieldLayout f : fields()) { maxDescrLen = Math.max(f.shortFieldName().length(), maxDescrLen); } maxDescrLen += 2; if (instance != null) { VirtualMachine vm = VM.current(); if (isMoreInf) { pw.print("\n64位Mark Word信息: "); } String lockSign = ""; for (long off = 4; off >= 0; off -= 4) { int word = vm.getInt(instance, off); pw.printf(toBinary((word >> 24) & 0xFF) + " " + toBinary((word >> 16) & 0xFF) + " " + toBinary((word >> 8) & 0xFF) + " " + toBinary((word >> 0) & 0xFF) + " " ); if (off == 0) { String last8 = toBinary((word >> 0) & 0xFF); lockSign = last8.substring(last8.length() - 3); } } if (isMoreInf) { pw.print("\n锁标识位为: " + lockSign); String lockName = getLockName(lockSign); pw.print("\n锁状态为: " + lockName); } } else { pw.printf(" %6d %5d %" + maxTypeLen + "s %-" + maxDescrLen + "s %s%n", 0, headerSize(), "", "(object header)", "N/A"); } pw.close(); return sw.toString(); } /** * 获取锁状态 * * @param lockSign * @return */ private String getLockName(String lockSign) { if ("000".equals(lockSign)) { return "轻量级锁、自旋锁"; } if ("010".equals(lockSign)) { return "重量级锁"; } if ("011".equals(lockSign)) { return "GC标记信息"; } if ("101".equals(lockSign)) { return "偏向锁"; } if ("001".equals(lockSign)) { return "无锁"; } return ""; } /** v * Answer header size * * @return header size */ public int headerSize() { return headerSize; } // very ineffective, so what? private static String toHex(int x) { StringBuilder s = new StringBuilder(Integer.toHexString(x)); int deficit = 2 - s.length(); for (int c = 0; c < deficit; c++) { s.insert(0, "0"); } return s.toString(); } // very ineffective, so what? private static String toBinary(int x) { StringBuilder s = new StringBuilder(Integer.toBinaryString(x)); int deficit = 8 - s.length(); for (int c = 0; c < deficit; c++) { s.insert(0, "0"); } return s.toString(); } }
SimpleLayouter
public class SimpleLayouter { public SimpleClassLayout layout(ClassData data) { VirtualMachine vm = VM.current(); if (data.isArray()) { // special case of arrays int base = vm.arrayBaseOffset(data.arrayComponentType()); int scale = vm.arrayIndexScale(data.arrayComponentType()); long instanceSize = MathUtil.align(base + data.arrayLength() * scale, vm.objectAlignment()); SortedSet<FieldLayout> result = new TreeSet<>(); result.add(new FieldLayout(FieldData.create(data.arrayClass(), "<elements>", data.arrayComponentType()), base, scale * data.arrayLength())); return new SimpleClassLayout(data, result, vm.arrayHeaderSize(), instanceSize, false); } Collection<FieldData> fields = data.fields(); SortedSet<FieldLayout> result = new TreeSet<>(); for (FieldData f : fields) { result.add(new FieldLayout(f, f.vmOffset(), vm.sizeOfField(f.typeClass()))); } long instanceSize; if (result.isEmpty()) { instanceSize = vm.objectHeaderSize(); } else { FieldLayout f = result.last(); instanceSize = f.offset() + f.size(); // TODO: This calculation is incorrect if there is a trailing @Contended field, or the instance is @Contended } instanceSize = MathUtil.align(instanceSize, vm.objectAlignment()); return new SimpleClassLayout(data, result, vm.objectHeaderSize(), instanceSize, true); } @Override public String toString() { return "Current VM Layout"; } }
使用
notion image

monitor锁

Monitor被翻译为监视器或管程
在HotSpot虚拟机中,monitor采用ObjectMonitor实现 ObjectMonitor.java→ObjectMonitor.cpp→objectMonitor.hpp
notion image
 
每个Java对象都可以关联一个Monitor对象(操作系统实现),如果使用synchronized给对象上锁(重量级)之后,该对象头的MarkWord中就可以被设置指向Monitor对象的指针
演示
notion image
entryList为链表结构
Monitor结构如下
notion image
  • 刚开始Monitor中Owner为null
  • 当Thread-2执行synchronized(obj)就会将Monitor的所有者Owner置为Thread-2,Monitor中只能有一个Owner
  • 在Thread-2上锁过程中,如果Thread-3,Thread-4,Thread-5 也来执行synchronized(obj),就会进入EntryList BLOCKED
  • Thread-2执行完同步代码块的内容,然后唤醒EntryList中等待的线程来竞争锁,竞争是非公平的
  • WaitSet中的Thread-0,Thread-1是之前获得过锁,但条件不满足进入WAITING状态的线程
notion image
 

synchronized优化

synchronized优化
 
 
 
 
 
 
 

wait和notify

notion image
 
  • Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
  • BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
  • BLOCKED 线程会在 Owner 线程释放锁时唤醒
  • WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入 EntryList 重新竞争
 
 

API 使用

  • obj.wait() 让进入 object 监视器的线程到 waitSet 等待
  • obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒
  • obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒
它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法
@Slf4j(topic = "c.TestWaitNotify") public class TestWaitNotify { final static Object obj = new Object(); public static void main(String[] args) { new Thread(() -> { synchronized (obj) { log.debug("执行...."); try { obj.wait(); // 让线程在obj上一直等待下去 } catch (InterruptedException e) { e.printStackTrace(); } log.debug("其它代码...."); } },"t1").start(); new Thread(() -> { synchronized (obj) { log.debug("执行...."); try { obj.wait(); // 让线程在obj上一直等待下去 } catch (InterruptedException e) { e.printStackTrace(); } log.debug("其它代码...."); } },"t2").start(); // 主线程两秒后执行 sleep(0.5); log.debug("唤醒 obj 上其它线程"); synchronized (obj) { // obj.notify(); // 唤醒obj上一个线程 obj.notifyAll(); // 唤醒obj上所有等待线程 } } }
 
notifyAll结果
20:19:04.789 c.TestWaitNotify [t1] - 执行.... 20:19:04.792 c.TestWaitNotify [t2] - 执行.... 20:19:05.286 c.TestWaitNotify [main] - 唤醒 obj 上其它线程 20:19:05.286 c.TestWaitNotify [t2] - 其它代码.... 20:19:05.286 c.TestWaitNotify [t1] - 其它代码....
 
notify结果
20:22:28.710 c.TestWaitNotify [t1] - 执行.... 20:22:28.712 c.TestWaitNotify [t2] - 执行.... 20:22:29.210 c.TestWaitNotify [main] - 唤醒 obj 上其它线程 20:22:29.210 c.TestWaitNotify [t1] - 其它代码....
 

sleep(long n)和wait(long n)的区别

 
  • sleep 是 Thread 方法,而 wait 是 Object 的方法
  • sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用
  • sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
  • 它们的线程状态都为 TIMED_WAITING
static final Object lock = new Object(); public static void main(String[] args) { new Thread(() -> { synchronized (lock) { log.debug("获得锁"); try { // Thread.sleep(20000); lock.wait(20000); } catch (InterruptedException e) { e.printStackTrace(); } } }, "t1").start(); Sleeper.sleep(1); synchronized (lock) { log.debug("获得锁"); } }

wait,notify的正确使用方式

new Thread(() -> { synchronized (room) { log.debug("有烟没?[{}]", hasCigarette); if (!hasCigarette) { log.debug("没烟,先歇会!"); try { room.wait(); } catch (InterruptedException e) } log.debug("有烟没?[{}]", hasCigarette); if (hasCigarette) { log.debug("可以开始干活了"); } else { log.debug("没干成活..."); } } }, "小南").start(); new Thread(() -> { synchronized (room) { Thread thread = Thread.currentThread(); log.debug("外卖送到没?[{}]", hasTakeout); if (!hasTakeout) { log.debug("没外卖,先歇会!"); try { room.wait(); } catch (InterruptedException e){ e.printStackTrace() } } log.debug("外卖送到没?[{}]", hasTakeout); if (hasTakeout) { log.debug("可以开始干活了"); } else { log.debug("没干成活..."); } } }, "小女").start(); sleep(1); new Thread(() -> { synchronized (room) { hasTakeout = true; log.debug("外卖到了噢!"); room.notify(); } }, "送外卖的").start(); //输出 //20:53:12.173 [小南] c.TestCorrectPosture - 有烟没?[false] //20:53:12.176 [小南] c.TestCorrectPosture - 没烟,先歇会! //20:53:12.176 [小女] c.TestCorrectPosture - 外卖送到没?[false] //20:53:12.176 [小女] c.TestCorrectPosture - 没外卖,先歇会! //20:53:13.174 [送外卖的] c.TestCorrectPosture - 外卖到了噢! //20:53:13.174 [小南] c.TestCorrectPosture - 有烟没?[false] //20:53:13.174 [小南] c.TestCorrectPosture - 没干成活...
 
notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程,称之为虚假唤醒 解决方法,改为 notifyAll
new Thread(() -> {         synchronized (room) {             hasTakeout = true; log.debug("外卖到了噢!");             room.notifyAll();       } }, "送外卖的").start(); //输出 //20:55:23.978 [小南] c.TestCorrectPosture - 有烟没?[false] //20:55:23.982 [小南] c.TestCorrectPosture - 没烟,先歇会! //20:55:23.982 [小女] c.TestCorrectPosture - 外卖送到没?[false] //20:55:23.982 [小女] c.TestCorrectPosture - 没外卖,先歇会! //20:55:24.979 [送外卖的] c.TestCorrectPosture - 外卖到了噢! //20:55:24.979 [小女] c.TestCorrectPosture - 外卖送到没?[true] //20:55:24.980 [小女] c.TestCorrectPosture - 可以开始干活了 //20:55:24.980 [小南] c.TestCorrectPosture - 有烟没?[false] //20:55:24.980 [小南] c.TestCorrectPosture - 没干成活...
 
 
唤醒了正确的线程的同时也唤醒了无关的线程
用 notifyAll 仅解决某个线程的唤醒问题,但使用 if + wait 判断仅有一次机会,一旦条件不成立,就没有重新判断的机会了 解决方法,用 while + wait,当条件不成立,再次 wait
while (!hasCigarette) { //业务代码 }
wait,notify的正确使用模式
synchronized(lock) { while(条件不成立) { lock.wait(); } // 条件成立的业务代码 } //另一个线程 synchronized(lock) { lock.notifyAll(); }
 

设计模式之保护性暂停

原理之 join

t1.join()等价于下面的代码
synchronized (t1) { // 调用者线程进入 t1 的 waitSet 等待, 直到 t1 运行结束 while (t1.isAlive()) { t1.wait(0); } }
jdk源码
public final synchronized void join(long millis) throws InterruptedException { long base = System.currentTimeMillis(); long now = 0; if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); } if (millis == 0) { while (isAlive()) { wait(0); } } else { while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } wait(delay); now = System.currentTimeMillis() - base; } } }

生产者消费者模式

 
 
 

多把锁(细粒度锁)

 
多把不相干的锁 一间大屋子有两个功能:睡觉、学习,互不相干。 现在小南要学习,小女要睡觉,但如果只用一间屋子(一个对象锁)的话,那么并发度很低
解决方法是准备多个房间(多个对象锁) 例如
@Slf4j(topic = "c.BigRoom") class BigRoom { private final Object studyRoom = new Object(); private final Object bedRoom = new Object(); public void sleep() { synchronized (bedRoom) { log.debug("sleeping 2 小时"); Sleeper.sleep(2); } } public void study() { synchronized (studyRoom) { log.debug("study 1 小时"); Sleeper.sleep(1); } } }
public class TestMultiLock { public static void main(String[] args) { BigRoom bigRoom = new BigRoom(); new Thread(() -> { bigRoom.study(); },"小南").start(); new Thread(() -> { bigRoom.sleep(); },"小女").start(); } }
将锁的粒度细分好处,是可以增强并发度 .坏处,如果一个线程需要同时获得多把锁,就容易发生死锁
 

锁的活跃性

死锁

有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁
线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。
当线程进入对象的synchronized代码块时,便占有了资源,直到它退出该代码块或者调用wait方法,才释放资源,在此期间,其他线程将不能进入该代码块。当线程互相持有对方所需要的资源时,会互相等待对方释放资源,如果线程都不主动释放所占有的资源,将产生死锁。
 
下面是一个简单的死锁案例
@Slf4j(topic = "c.TestDeadLock") public class TestDeadLock { public static void main(String[] args) { test1(); } private static void test1() { Object A = new Object(); Object B = new Object(); Thread t1 = new Thread(() -> { synchronized (A) { log.debug("lock A"); sleep(1); synchronized (B) { log.debug("lock B"); log.debug("操作..."); } } }, "t1"); Thread t2 = new Thread(() -> { synchronized (B) { log.debug("lock B"); sleep(0.5); synchronized (A) { log.debug("lock A"); log.debug("操作..."); } } }, "t2"); t1.start(); t2.start(); } }

定位死锁

使用jps命令查询Java进程,再用jstack + 进程 查询对应进程
notion image
Found one Java-level deadlock: ============================= "t2": waiting to lock monitor (object 0x0000000719fa4a10, a java.lang.Object), which is held by "t1" "t1": waiting to lock monitor 0x0000020030ee3208 (object 0x0000000719fa4a20, a java.lang.Object), which is held by "t2" Java stack information for the threads listed above: =================================================== "t2": at cn.itcast.n4.deadlock.TestDeadLock.lambda$test1$1(TestDeadLock.java:32) - waiting to lock <0x0000000719fa4a10> (a java.lang.Object) - locked <0x0000000719fa4a20> (a java.lang.Object) at cn.itcast.n4.deadlock.TestDeadLock$$Lambda$2/1792845110.run(Unknown Source) at java.lang.Thread.run(Thread.java:748) "t1": at cn.itcast.n4.deadlock.TestDeadLock.lambda$test1$0(TestDeadLock.java:21) - waiting to lock <0x0000000719fa4a20> (a java.lang.Object) - locked <0x0000000719fa4a10> (a java.lang.Object) at cn.itcast.n4.deadlock.TestDeadLock$$Lambda$1/897913732.run(Unknown Source) at java.lang.Thread.run(Thread.java:748) Found 1 deadlock.
或者使用jconsole定位
notion image

哲学家就餐问题

有五位哲学家,围坐在圆桌旁。
  • 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
  • 吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。
  • 如果筷子被身边的人拿着,自己就得等待
 
筷子类
class Chopstick { String name; public Chopstick(String name) { this.name = name; } @Override public String toString() { return "筷子{" + name + '}'; } }
哲学家类
@Slf4j(topic = "c.Philosopher") class Philosopher extends Thread { Chopstick left; Chopstick right; public Philosopher(String name, Chopstick left, Chopstick right) { super(name); this.left = left; this.right = right; } @Override public void run() { while (true) { // 尝试获得左手筷子 synchronized (left) { // 尝试获得右手筷子 synchronized (right) { eat(); } } } } Random random = new Random(); private void eat() { log.debug("eating..."); Sleeper.sleep(0.5); } }
就餐
public class TestDeadLock { public static void main(String[] args) { Chopstick c1 = new Chopstick("1"); Chopstick c2 = new Chopstick("2"); Chopstick c3 = new Chopstick("3"); Chopstick c4 = new Chopstick("4"); Chopstick c5 = new Chopstick("5"); new Philosopher("苏格拉底", c1, c2).start(); new Philosopher("柏拉图", c2, c3).start(); new Philosopher("亚里士多德", c3, c4).start(); new Philosopher("赫拉克利特", c4, c5).start(); new Philosopher("阿基米德", c5, c1).start(); } } //输出 //14:17:49.395 c.Philosopher [亚里士多德] - eating... //14:17:49.395 c.Philosopher [苏格拉底] - eating... //14:17:51.405 c.Philosopher [柏拉图] - eating... //14:17:53.415 c.Philosopher [柏拉图] - eating... //14:17:55.418 c.Philosopher [柏拉图] - eating... //14:17:57.424 c.Philosopher [柏拉图] - eating... 执行不多会,就执行不下去了
定位死锁
名称: 阿基米德 状态: cn.itcast.n4.deadlock.v1.Chopstick@4aaba93e上的BLOCKED, 拥有者: 苏格拉底 总阻止数: 1, 总等待数: 0 堆栈跟踪: cn.itcast.n4.deadlock.v1.Philosopher.run(TestDeadLock.java:41) - 已锁定 cn.itcast.n4.deadlock.v1.Chopstick@540277b5 名称: 苏格拉底 状态: cn.itcast.n4.deadlock.v1.Chopstick@7b0663b6上的BLOCKED, 拥有者: 柏拉图 总阻止数: 7, 总等待数: 2 堆栈跟踪: cn.itcast.n4.deadlock.v1.Philosopher.run(TestDeadLock.java:41) - 已锁定 cn.itcast.n4.deadlock.v1.Chopstick@4aaba93e 名称: 柏拉图 状态: cn.itcast.n4.deadlock.v1.Chopstick@11d11cbf上的BLOCKED, 拥有者: 亚里士多德 总阻止数: 2, 总等待数: 4 堆栈跟踪: cn.itcast.n4.deadlock.v1.Philosopher.run(TestDeadLock.java:41) - 已锁定 cn.itcast.n4.deadlock.v1.Chopstick@7b0663b6 名称: 亚里士多德 状态: cn.itcast.n4.deadlock.v1.Chopstick@60468775上的BLOCKED, 拥有者: 赫拉克利特 总阻止数: 7, 总等待数: 1 堆栈跟踪: cn.itcast.n4.deadlock.v1.Philosopher.run(TestDeadLock.java:41) - 已锁定 cn.itcast.n4.deadlock.v1.Chopstick@11d11cbf 名称: 赫拉克利特 状态: cn.itcast.n4.deadlock.v1.Chopstick@540277b5上的BLOCKED, 拥有者: 阿基米德 总阻止数: 2, 总等待数: 0 堆栈跟踪: cn.itcast.n4.deadlock.v1.Philosopher.run(TestDeadLock.java:41) - 已锁定 cn.itcast.n4.deadlock.v1.Chopstick@604687
苏格拉底 获得c1, 柏拉图 获得c2, 亚里士多德获得c3, 赫拉克利特 获得c4, 阿基米德获得c5
苏格拉底尝试获取c2 ,c2被柏拉图占有,因此阻塞, 柏拉图尝试获取c3 ,c3被亚历山多德占有,阻塞,同理,最后一个阿基米德尝试获取c1,也阻塞,出现死锁
 
使用顺序加锁的方式解决死锁问题
出现死锁的加锁顺序
线程1 获取锁顺序 A ,B
线程2 获取锁顺序B,A
notion image
顺序加锁方案
线程 1 获取锁顺序 A ,B
线程2 获取顺序 A,B
这样线程1获得A锁,线程2阻塞,不会持有B锁,线程可获取B锁,
notion image
 

活锁

活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束,例如。
@Slf4j(topic = "c.TestLiveLock") public class TestLiveLock { static volatile int count = 10; static final Object lock = new Object(); public static void main(String[] args) { new Thread(() -> { // 期望减到 0 退出循环 while (count > 0) { sleep(0.2); count--; log.debug("count: {}", count); } }, "t1").start(); new Thread(() -> { // 期望超过 20 退出循环 while (count < 20) { sleep(0.2); count++; log.debug("count: {}", count); } }, "t2").start(); } }
 

饥饿

指那些优先级比较低的线程在阻塞等待临界区资源时,长时间得不到资源而不能执行的现象。
更换哲学家就餐的获取锁顺序
public class TestDeadLock { public static void main(String[] args) { Chopstick c1 = new Chopstick("1"); Chopstick c2 = new Chopstick("2"); Chopstick c3 = new Chopstick("3"); Chopstick c4 = new Chopstick("4"); Chopstick c5 = new Chopstick("5"); new Philosopher("苏格拉底", c1, c2).start(); new Philosopher("柏拉图", c2, c3).start(); new Philosopher("亚里士多德", c3, c4).start(); new Philosopher("赫拉克利特", c4, c5).start(); new Philosopher("阿基米德", c1, c5).start(); } } //线程不再阻塞 //阿基米德获取锁的概率比较低,处于饥饿状态

ReentrantLock