摘要:本文学习了并发场景下线程协作的新方式。
环境
Windows 10 企业版 LTSC 21H2 Java 1.8
1 背景 1.1 回顾 之前了解到,在涉及得到线程同步的问题时,可以使用synchronized关键字保证线程安全,还可以使用Object类提供的wait()
方法和notify()
方法进行线程通信。
在JDK1.5之后,可以使用Lock相关接口来实现类似的功能,以及其他新增的功能。在1.6之后,JDK对synchronized做了优化,两种方式在性能上的差距就很小了,可以根据场景灵活选择。
1.2 缺陷 在使用synchronized关键字时,如果一个线程获取了对应的锁,其他线程便只能一直等待释放锁,如果没有释放则需要无限的等待下去。
线程只会在两种情况下释放锁:
线程执行完了代码块,线程释放锁。
线程执行发生异常,JVM会让线程释放锁。
由此可以看出以往的方式在释放锁的时候比较笨重,不够灵活,不能满足更加灵活的使用场景。
如果想更加灵活的控制释放锁的时机,就需要使用新增的并发功能。
从JDK1.5之后,由Doug Lea大牛编写了JUC模块,将代码放在java.util.concurrent
包中,新增了许多在并发场景下使用的工具,用于解决并发场景下的线程安全问题。
2 Lock 2.1 简介 Lock是JUC新增的接口,ReentrantLock类实现了Lock接口,也是Lock接口的主要实现类。
常用方法:
java 1 2 3 4 5 6 7 8 9 10 11 12 void lock () ;void lockInterruptibly () throws InterruptedException;boolean tryLock () ;boolean tryLock (long time, TimeUnit unit) throws InterruptedException;void unlock () ;Condition newCondition () ;
2.2 比较 关于synchronized和Lock的比较:
原始构成:synchronized是关键字,属于JVM层面。Lock是接口,属于API层面。
是否公平:synchronized是非公平锁,不支持公平锁。Lock支持公平锁和非公平锁,可以在构造方法中指定。
异常处理:synchronized在发生异常时,自动释放锁,因此不会产生死锁。Lock在发生异常时,需要手动释放锁,否则会产生死锁,因此使用Lock时需要在finally块中释放锁。
中断处理:synchronized不能响应中断,除非抛出异常或者运行完成。Lock可以响应中断,通过设置超时方法tryLock()以及调用中断方法interrupt()。
互斥共享:synchronized是互斥锁,不支持共享锁。Lock的子类ReentrantReadWriteLock支持互斥锁WriteLock和共享锁ReadLock。
精确唤醒:synchronized不支持精确唤醒,只能唤醒一个或者唤醒全部。Lock通过Condition支持精确唤醒,能够对某个线程或者某一种线程进行唤醒。
2.3 使用 2.3.1 等待获取锁 使用lock()
方法等待获取锁,这是使用得最多的一个方法。
获取锁以后必须显示释放锁,即使出现异常时也不会自动释放,因此需要将业务逻辑写在try-catch
代码块中,并且将释放逻辑写在finally
代码块中,保证锁一定被被释放,防止死锁。
使用规范:
java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public void method () { Lock lock = new ReentrantLock (); lock.lock(); try { } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } }
2.3.2 尝试获取锁 使用tryLock()
方法尝试获取锁,无论是否获取成功都会立即返回结果。
使用规范:
java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public void method () { Lock lock = new ReentrantLock (); if (lock.tryLock()) { try { } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } else { } }
2.3.3 中断获取锁 使用lock()
方法在等待期间是不能被中断的,但使用lockInterruptibly()
方法在等待期间是可以被中断的。
如果当前线程在获取锁的等待期间被其他线程中断,当前线程会抛出InterruptedException异常,因此需要处理异常。
使用规范:
java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public void method () throws InterruptedException { Lock lock = new ReentrantLock (); lock.lockInterruptibly(); try { } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } }
调用interrupt()
方法不能中断正在运行的线程,只能中断等待的线程。
2.3.4 互斥锁 ReentrantLock类实现了Lock接口,并且ReentrantLock提供了更多的方法。
模拟窗口买票示例:
java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public class Demo { public static void main (String[] args) { Ticket ticket = new Ticket (); new Thread (() -> {ticket.sale();}, "窗口1" ).start(); new Thread (() -> {ticket.sale();}, "窗口2" ).start(); } } class Ticket { private int num = 5 ; Lock lock = new ReentrantLock (); public void sale () { while (num > 0 ) { try { Thread.sleep(1 ); } catch (InterruptedException e) { e.printStackTrace(); } lock.lock(); try { if (num > 0 ) { System.out.println(Thread.currentThread().getName() + " sale " + num--); } } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } } }
结果:
log 1 2 3 4 5 窗口1 sale 5 窗口1 sale 4 窗口2 sale 3 窗口1 sale 2 窗口2 sale 1
2.3.5 读写锁 使用ReadWriteLock接口定义读写锁,接口里包含读锁和写锁:
java 1 2 3 4 public interface ReadWriteLock { Lock readLock () ; Lock writeLock () ; }
ReentrantReadWriteLock类实现了ReadWriteLock接口,支持多个线程同时进行读操作。
模拟窗口买票,同一时间只能有一个窗口售卖,允许多个窗口查看:
java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 public class Demo { public static void main (String[] args) { Ticket ticket = new Ticket (); new Thread (() -> {ticket.show();}, "窗口1" ).start(); new Thread (() -> {ticket.show();}, "窗口2" ).start(); new Thread (() -> {ticket.show();}, "窗口3" ).start(); new Thread (() -> {ticket.sale();}, "窗口4" ).start(); } } class Ticket { ReadWriteLock lock = new ReentrantReadWriteLock (); private int num = 5 ; public void show () { while (num > 0 ) { try { Thread.sleep(1 ); } catch (InterruptedException e) { e.printStackTrace(); } lock.readLock().lock(); try { if (num > 0 ) { System.out.println(Thread.currentThread().getName() + " show " + num); } } catch (Exception e) { e.printStackTrace(); } finally { lock.readLock().unlock(); } } System.out.println(Thread.currentThread().getName() + " end" ); } public void sale () { while (num > 0 ) { try { Thread.sleep(1 ); } catch (InterruptedException e) { e.printStackTrace(); } lock.writeLock().lock(); try { if (num > 0 ) { System.out.println(Thread.currentThread().getName() + " sale " + num--); } } catch (Exception e) { e.printStackTrace(); } finally { lock.writeLock().unlock(); } } System.out.println(Thread.currentThread().getName() + " end" ); } }
结果:
log 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 窗口2 show 5 窗口3 show 5 窗口1 show 5 窗口4 sale 5 窗口2 show 4 窗口3 show 4 窗口1 show 4 窗口4 sale 4 窗口2 show 3 窗口1 show 3 窗口3 show 3 窗口4 sale 3 窗口1 show 2 窗口3 show 2 窗口4 sale 2 窗口2 show 1 窗口1 show 1 窗口2 show 1 窗口3 show 1 窗口4 sale 1 窗口4 end 窗口1 end 窗口2 end 窗口3 end
从运行的结果来看,最多有三个线程在同时读,提高了读操作的效率。
线程占用读写锁的规则:
当前线程占用读锁后,当前线程占用写锁需要等待,其他线程占用读锁无需等待,其他线程占用写锁需要等待。
当前线程占用写锁后,当前线程占用读锁无需等待,其他线程占用读锁和写锁需要等待。
对于当前线程来说,写锁可以降级为读锁,但是读锁不能升级为写锁。
写锁可以降级为读锁,程序可以正常执行:
java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public static void main (String[] args) { ReentrantReadWriteLock lock = new ReentrantReadWriteLock (); ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); ReentrantReadWriteLock.ReadLock readLock = lock.readLock(); try { writeLock.lock(); System.out.println("获取写锁" ); readLock.lock(); System.out.println("获取读锁" ); } catch (Exception e) { e.printStackTrace(); } finally { writeLock.unlock(); System.out.println("释放写锁" ); readLock.unlock(); System.out.println("释放读锁" ); } }
读锁不能升级为写锁,程序会等待获取写锁:
java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public static void main (String[] args) { ReentrantReadWriteLock lock = new ReentrantReadWriteLock (); ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); ReentrantReadWriteLock.ReadLock readLock = lock.readLock(); try { readLock.lock(); System.out.println("获取读锁" ); writeLock.lock(); System.out.println("获取写锁" ); } catch (Exception e) { e.printStackTrace(); } finally { readLock.unlock(); System.out.println("释放读锁" ); writeLock.unlock(); System.out.println("释放写锁" ); } }
3 Condition 3.1 简介 之前在使用synchronized关键字时,可以使用Object对象的wait()
方法和notify()
方法进行线程通信。
在使用Lock时,通过newCondition()
方法获取Condition对象,可以使用Condition对象的await()
方法和signal()
方法进行通信,对锁进行更精确的控制。
同一个Lock对象可以获取多个Condition对象,支持多路控制。
常用方法:
java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 void await () throws InterruptedException;void awaitUninterruptibly () ;boolean await (long time, TimeUnit unit) throws InterruptedException;long awaitNanos (long nanosTimeout) throws InterruptedException;boolean awaitUntil (Date deadline) throws InterruptedException;void signal () ;void signalAll () ;
3.2 比较 使用方式类似:
Condition中的await()
方法相当于Object的wait()
方法。
Condition中的signal()
方法相当于Object的notify()
方法。
Condition中的signalAll()
相当于Object的notifyAll()
方法。
Condition中的方法需要与Lock捆绑使用,Object中的方法需要与synchronized捆绑使用。
使用Condition的优势是能够更加精细的控制多线程的休眠与唤醒,同一个Lock可以创建多个Condition,使用不同的Condition管理不同线程的等待与唤醒,实现对线程的精细控制。
3.3 使用 3.3.1 使用synchronized实现两个线程交替打印 使用synchronized和Object类的方法实现两个线程交替打印:
java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class Demo { public static void main (String[] args) { Ticket ticket = new Ticket (); new Thread (() -> {ticket.sale();}, "线程1" ).start(); new Thread (() -> {ticket.sale();}, "线程2" ).start(); } } class Ticket { private int num = 8 ; public void sale () { while (num > 0 ) { synchronized (Ticket.class) { Ticket.class.notify(); if (num > 0 ) { System.out.println(Thread.currentThread().getName() + " >>> " + num--); } if (num > 0 ) { try { Ticket.class.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } } }
结果:
log 1 2 3 4 5 6 7 8 线程1 >>> 8 线程2 >>> 7 线程1 >>> 6 线程2 >>> 5 线程1 >>> 4 线程2 >>> 3 线程1 >>> 2 线程2 >>> 1
3.3.2 使用Lock实现两个线程交替打印 使用Lock和Condition类的方法实现两个线程交替打印:
java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public class Demo { public static void main (String[] args) { Ticket ticket = new Ticket (); new Thread (() -> {ticket.sale();}, "线程1" ).start(); new Thread (() -> {ticket.sale();}, "线程2" ).start(); } } class Ticket { private int num = 8 ; Lock lock = new ReentrantLock (); Condition condition = lock.newCondition(); public void sale () { while (num > 0 ) { lock.lock(); try { condition.signal(); if (num > 0 ) { System.out.println(Thread.currentThread().getName() + " >>> " + num--); } if (num > 0 ) { condition.await(); } } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } } }
结果:
log 1 2 3 4 5 6 7 8 线程1 >>> 8 线程2 >>> 7 线程1 >>> 6 线程2 >>> 5 线程1 >>> 4 线程2 >>> 3 线程1 >>> 2 线程2 >>> 1
3.3.3 使用Lock实现三个线程循环打印 使用Lock和Condition类的方法实现三个线程按循环打印:
java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 public class Demo { public static void main (String[] args) { Ticket ticket = new Ticket (); new Thread (() -> {ticket.sale1();}, "线程1" ).start(); new Thread (() -> {ticket.sale2();}, "线程2" ).start(); new Thread (() -> {ticket.sale3();}, "线程3" ).start(); } } class Ticket { private int num = 8 ; Lock lock = new ReentrantLock (); Condition c1 = lock.newCondition(); Condition c2 = lock.newCondition(); Condition c3 = lock.newCondition(); public void sale1 () { lock.lock(); try { while (num > 0 ) { c2.signal(); System.out.println(Thread.currentThread().getName() + " >>> " + num--); c1.await(); if (num <= 0 ) { c3.signal(); } } } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } public void sale2 () { lock.lock(); try { while (num > 0 ) { c3.signal(); System.out.println(Thread.currentThread().getName() + " >>> " + num--); c2.await(); if (num <= 0 ) { c1.signal(); } } } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } public void sale3 () { lock.lock(); try { while (num > 0 ) { c1.signal(); System.out.println(Thread.currentThread().getName() + " >>> " + num--); c3.await(); if (num <= 0 ) { c2.signal(); } } } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } }
结果:
log 1 2 3 4 5 6 7 8 线程1 >>> 8 线程2 >>> 7 线程3 >>> 6 线程1 >>> 5 线程2 >>> 4 线程3 >>> 3 线程1 >>> 2 线程2 >>> 1
3.3.4 使用Lock实现生产者消费者 生产者消费者交替执行:
java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 public class Demo { public static void main (String[] args) { Resource r = new Resource (); new Thread (()->{ for (int i = 0 ; i < 3 ; i++) { r.product(); } }, "生产者1" ).start(); new Thread (()->{ for (int i = 0 ; i < 3 ; i++) { r.product(); } }, "生产者2" ).start(); new Thread (()->{ for (int i = 0 ; i < 3 ; i++) { r.consume(); } }, "消费者1" ).start(); new Thread (()->{ for (int i = 0 ; i < 3 ; i++) { r.consume(); } }, "消费者2" ).start(); } } class Resource { private int count = 0 ; private Lock lock = new ReentrantLock (); Condition condition = lock.newCondition(); public void product () { lock.lock(); try { while (count != 0 ) { condition.await(); } count++; System.out.println(Thread.currentThread().getName() + " product ..." ); condition.signalAll(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } public void consume () { lock.lock(); try { while (count == 0 ) { condition.await(); } count--; System.out.println(Thread.currentThread().getName() + " consume ..." ); condition.signalAll(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } }
结果:
log 1 2 3 4 5 6 7 8 9 10 11 12 生产者1 product ... 消费者1 consume ... 生产者1 product ... 消费者1 consume ... 生产者1 product ... 消费者1 consume ... 生产者2 product ... 消费者2 consume ... 生产者2 product ... 消费者2 consume ... 生产者2 product ... 消费者2 consume ...
条