文章目录
声明:
本博客是本人在学习《Java 多线程编程核心技术》后整理的笔记,旨在方便复习和回顾,并非用作商业用途。
本博客已标明出处,如有侵权请告知,马上删除。
本章将要介绍使用 Java5 中 Lock 对象也能实现同步的效果,而且在使用上更加方便。本章着重掌握如下 2 个知识点:
- ReentrantLock 类的使用。
- ReentrantReadWriteLock 类的使用。
4.1 使用 ReentrantLock 类
在 Java 多线程中,可以使用 synchronized 关键字来实现线程之间同步互斥,但在 JDK1.5 中新增加了 ReentrantLock 类也能达到同样的效果,并且在扩展功能上也更加强大,比如具有嗅探锁定、多路分支通知等功能,而且在使用上也比 synchronized 更加的灵活。
4.1.1 使用 ReentrantLock 实现同步:测试1
既然 ReentrantLock 类在功能上相比 synchronized 更多,那么就以一个初步的程序示例来介绍一下 ReentrantLock 类的使用。
-
创建公共类
public class MyService { private Lock lock = new ReentrantLock(); public void testMethod() { lock.lock(); //获取锁,线程就持有了“对象监视器” for (int i = 0; i < 3; i++) { System.out.println("ThreadName=" + Thread.currentThread().getName() + (" " + (i + 1))); } lock.unlock(); //释放锁 } }
-
创建自定义线程类
public class MyThread extends Thread { private MyService myService; public MyThread(MyService myService) { this.myService = myService; } public void run() { myService.testMethod(); } }
-
测试类
public class Run { public static void main(String[] args) throws InterruptedException { MyService service = new MyService(); MyThread a1 = new MyThread(service); MyThread a2 = new MyThread(service); a1.start(); a2.start(); } }
运行结果:
ThreadName=Thread-1 1 ThreadName=Thread-1 2 ThreadName=Thread-1 3 ThreadName=Thread-0 1 ThreadName=Thread-0 2 ThreadName=Thread-0 3
从运行的结果来看,当前线程打印完毕之后将锁进行释放,其他线程才可以继续打印。线程打印的数据是分组打印,因为当前线程已经持有锁,但线程之间打印的顺序是随机的。
4.1.2 使用 ReentrantLock 实现同步:测试2
-
创建公共类
public class MyService { private Lock lock = new ReentrantLock(); public void methodA() { try { lock.lock(); System.out.println("method A begin Thread name = " + Thread.currentThread().getName() + " time = " + System.currentTimeMillis()); Thread.sleep(5000); System.out.println("method A end Thread name = " + Thread.currentThread().getName() + " time = " + System.currentTimeMillis()); } catch (InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); } } public void methodB() { try { lock.lock(); System.out.println("method B begin Thread name = " + Thread.currentThread().getName() + " time = " + System.currentTimeMillis()); Thread.sleep(5000); System.out.println("method B end Thread name = " + Thread.currentThread().getName() + " time = " + System.currentTimeMillis()); } catch (InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); } } }
-
创建 4 个自定义线程类
public class ThreadA extends Thread { private MyService service; public ThreadA(MyService service) { this.service = service; } @Override public void run() { super.run(); service.methodA(); } }
public class ThreadAA extends Thread { private MyService service; public ThreadAA(MyService service) { this.service = service; } @Override public void run() { service.methodA(); } }
public class ThreadB extends Thread { private MyService service; public ThreadB(MyService service) { this.service = service; } @Override public void run() { super.run(); service.methodB(); } }
public class ThreadBB extends Thread { private MyService service; public ThreadBB(MyService service) { this.service = service; } @Override public void run() { super.run(); service.methodB(); } }
-
测试类
public class Run { public static void main(String[] args) throws InterruptedException { MyService service = new MyService(); ThreadA a = new ThreadA(service); ThreadAA aa = new ThreadAA(service); ThreadB b = new ThreadB(service); ThreadBB bb = new ThreadBB(service); a.setName("A"); aa.setName("AA"); b.setName("B"); bb.setName("BB"); a.start(); aa.start(); Thread.sleep(100); b.start(); bb.start(); } }
运行结果:
method A begin Thread name = AA time = 1607926082523 method A end Thread name = AA time = 1607926087523 method A begin Thread name = A time = 1607926087523 method A end Thread name = A time = 1607926092524 method B begin Thread name = B time = 1607926092524 method B end Thread name = B time = 1607926097524 method B begin Thread name = BB time = 1607926097524 method B end Thread name = BB time = 1607926102524
此实验说明,调用 lock.lock() 代码的线程就持有了 “对象监视器”,其他线程只有等待锁被释放时再次争抢。效果和使用 synchronized 关键字一样,线程之间还是顺序执行的。
4.1.3 使用 Condition 实现等待/通知:错误用法与解决
关键字 synchronized 与 wait() 和 notify()/notifyAll() 方法相结合可以实现等待/通知模式,类 ReentrantLock 也可以实现同样的功能,但需要借助于 Condition 对象。Condition 类是在 JDK5 中出现的技术,使用它有更好的灵活性,比如可以实现多路通知功能,也就是在一个 Lock 对象里面可以创建多个 Condition(即对象监视器)实例,线程对象可以注册在指定的 Condition 中,从而可以有选择性地进行线程通知,在调度线程上更加灵活。
在使用 notify()/notifyAll() 方法进行通知时,被通知的线程却是由 JVM 随机选择的。但使用 ReentrantLock 结合 Condition 类是可以实现前面介绍过的 “选择性通知”,这个功能是非常重要的,而且在 Condition 类中是默认提供的。
而 synchronized 就相当于整个 Lock 对象中只有一个单一的 Condition 对象,所有的线程都注册在它一个对象的身上。线程开始 notifyAll() 时,需要通知所有的 WAITING 线程,没有选择权,会出现相当大的效率问题。
下面来看一个示例:
-
创建公共类
public class MyService { private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); public void await() { try { condition.await(); } catch (InterruptedException e) { e.printStackTrace(); } } }
-
创建自定义线程类
public class ThreadA extends Thread { private MyService service; public ThreadA(MyService service) { this.service = service; } @Override public void run() { super.run(); service.await(); } }
-
测试类
public class Run { public static void main(String[] args) { MyService service = new MyService(); ThreadA a = new ThreadA(service); a.start(); } }
运行结果:
Exception in thread "Thread-0" java.lang.IllegalMonitorStateException at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151) at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261) at java.util.concurrent.locks.AbstractQueuedSynchronizer.fullyRelease(AbstractQueuedSynchronizer.java:1723) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2036) at rt3.MyService.await(MyService.java:13) at rt3.ThreadA.run(ThreadA.java:13)
报错的异常信息是监视器出错,解决的办法是必须在 condition.await() 方法调用之前调用 lock.lock() 代码获得同步监视器。
正确使用 Condition 实现等待/通知,示例如下:
-
创建公共类
public class MyService { private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); public void waitMethod() { try { lock.lock(); System.out.println("A"); condition.await(); System.out.println("B"); } catch (InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); System.out.println("free lock!"); } } }
-
创建自定义线程类
public class ThreadA extends Thread { private MyService service; public ThreadA(MyService service) { this.service = service; } @Override public void run() { super.run(); service.waitMethod(); } }
-
测试类
public class Run { public static void main(String[] args) { MyService service = new MyService(); ThreadA a = new ThreadA(service); a.start(); } }
运行结果:
A
在控制台中只打印一个字母 A,原因是调用了 Condition 对象的 await() 方法,使当前执行任务的线程进入了等待 WAITING 状态。
4.1.4 正确使用 Condition 实现等待/通知
示例如下:
-
创建公共类
public class MyService { private Lock lock = new ReentrantLock(); public Condition condition = lock.newCondition(); public void await() { try { lock.lock(); System.out.println("await time = " + System.currentTimeMillis()); condition.await(); System.out.println("after await"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public void signal() { try { lock.lock(); System.out.println("signal time = " + System.currentTimeMillis()); condition.signal(); } finally { lock.unlock(); } } }
-
创建自定义线程类
public class ThreadA extends Thread { private MyService service; public ThreadA(MyService service) { this.service = service; } @Override public void run() { super.run(); service.await(); } }
-
测试类
public class Run { public static void main(String[] args) throws InterruptedException { MyService service = new MyService(); ThreadA a = new ThreadA(service); a.start(); Thread.sleep(3000); service.signal(); } }
运行结果:
await time = 1607931307284 signal time = 1607931310284 after await
成功实现等待/通知模式。
- Object 类中的 wait() 方法相当于 Condition 类中的 await() 方法。
- Object 类中的 wait(Iong timeout) 方法相当于 Condition 类中的 await(long time, TimeUnit unit) 方法。
- Object 类中的 notify() 方法相当于 Condition 类中的 signal() 方法。
- Object 类中的 notifyAll() 方法相当于 Condition 类中的 signalAll() 方法。
4.1.5 使用多个 Condition 实现通知部分线程:错误用法
前面章节使用一个 Condition 对象来实现等待/通知模式,其实 Condition 对象也可以创建多个。那么一个 Condition 对象和多个 Condition 对象在使用上有什么区别呢?
-
创建公共类
public class MyService { private Lock lock = new ReentrantLock(); public Condition condition = lock.newCondition(); public void awaitA() { try { lock.lock(); System.out.println("begin awaitA time = " + System.currentTimeMillis() + " thread name = " + Thread.currentThread().getName()); condition.await(); System.out.println(" end awaitA time = " + System.currentTimeMillis() + " thread name = " + Thread.currentThread().getName()); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public void awaitB() { try { lock.lock(); System.out.println("begin awaitB time = " + System.currentTimeMillis() + " thread name = " + Thread.currentThread().getName()); condition.await(); System.out.println(" end awaitA time = " + System.currentTimeMillis() + " thread name = " + Thread.currentThread().getName()); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public void signalAll() { try { lock.lock(); System.out.println("signalAll time = " + System.currentTimeMillis() + " thread name = " + Thread.currentThread().getName()); condition.signalAll(); } finally { lock.unlock(); } } }
-
创建 2 个自定义线程类
public class ThreadA extends Thread { private MyService service; public ThreadA(MyService service) { this.service = service; } @Override public void run() { super.run(); service.awaitA(); } }
public class ThreadB extends Thread { private MyService service; public ThreadB(MyService service) { this.service = service; } @Override public void run() { super.run(); service.awaitB(); } }
-
测试类
public class Run { public static void main(String[] args) throws InterruptedException { /** * A B 线程 都会被唤醒 */ MyService service = new MyService(); ThreadA a = new ThreadA(service); ThreadB b = new ThreadB(service); a.setName("A"); b.setName("B"); a.start(); b.start(); Thread.sleep(3000); service.signalAll(); } }
运行结果:
begin awaitA time = 1607933192977 thread name = A begin awaitB time = 1607933192978 thread name = B signalAll time = 1607933195969 thread name = main end awaitA time = 1607933195969 thread name = A end awaitA time = 1607933195969 thread name = B
如果想单独唤醒部分线程该怎么处理呢?这时就有必要使用多个 Condition 对象了,也就是 Condition 对象可以唤醒部分指定线程,有助于提升程序运行的效率。可以先对线程进行分组,然后再唤醒指定组中的线程。
4.1.6 使用多个 Condition 实现通知部分线程:正确用法
-
创建公共类
public class MyService { private Lock lock = new ReentrantLock(); public Condition conditionA = lock.newCondition(); public Condition conditionB = lock.newCondition(); public void awaitA() { try { lock.lock(); System.out.println("begin awaitA time = " + System.currentTimeMillis() + " thread name : " + Thread.currentThread().getName()); conditionA.await(); System.out.println(" end awaitA time = " + System.currentTimeMillis() + " thread name : " + Thread.currentThread().getName()); } catch (InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); } } public void awaitB() { try { lock.lock(); System.out.println("begin awaitB time = " + System.currentTimeMillis() + " thread name : " + Thread.currentThread().getName()); conditionB.await(); System.out.println(" end awaitB time = " + System.currentTimeMillis() + " thread name : " + Thread.currentThread().getName()); } catch (InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); } } public void signalAll_A() { try { lock.lock(); System.out.println("signalAll_A time = " + System.currentTimeMillis() + " thread name = " + Thread.currentThread().getName()); conditionA.signalAll(); }finally { lock.unlock(); } } public void signalAll_B(){ try { lock.lock(); System.out.println("signalAll_B time = " + System.currentTimeMillis() + " thread name = " + Thread.currentThread().getName()); conditionB.signalAll(); }finally { lock.unlock(); } } }
-
创建 2 个自定义线程类
public class ThreadA extends Thread { private MyService service; public ThreadA(MyService service) { this.service = service; } @Override public void run() { super.run(); service.awaitA(); } }
public class ThreadB extends Thread { private MyService service; public ThreadB(MyService service) { this.service = service; } @Override public void run() { super.run(); service.awaitB(); } }
-
测试类
public class Run { public static void main(String[] args) throws InterruptedException { /** * ReentrantLock对象可以唤醒指定种类的线程。 * 只唤醒A */ MyService service = new MyService(); ThreadA a = new ThreadA(service); ThreadB b = new ThreadB(service); a.setName("A"); b.setName("B"); a.start(); b.start(); Thread.sleep(3000); service.signalAll_A(); } }
运行结果:
begin awaitB time = 1607933799999 thread name : B begin awaitA time = 1607933800000 thread name : A signalAll_A time = 1607933802999 thread name = main end awaitA time = 1607933802999 thread name : A
程序运行后,只有线程 A 被唤醒了,线程 B 没有唤醒。
通过此实验可以得知,使用 ReentrantLock 对象可以唤醒指定种类的线程,这是控制部分线程行为的方便方式。
4.1.7 实现生产者/消费者模式:一对一交替打印
-
创建公共类
public class MyService { private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); private boolean hasValue = false; public void set() { try { lock.lock(); while (hasValue) { condition.await(); } System.out.println("打印★"); hasValue = true; condition.signal(); } catch (InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); } } public void get() { try { lock.lock(); while (!hasValue) { condition.await(); } System.out.println("打印☆"); hasValue = false; condition.signal(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } }
-
创建 2 个自定义线程类
public class ThreadA extends Thread { private MyService service; public ThreadA(MyService service) { this.service = service; } @Override public void run() { super.run(); for (int i = 0; i < Integer.MAX_VALUE; i++) { service.set(); } } }
public class ThreadB extends Thread { private MyService service; public ThreadB(MyService service) { this.service = service; } @Override public void run() { super.run(); for (int i = 0; i < Integer.MAX_VALUE; i++) { service.get(); } } }
-
测试类
public class Run { public static void main(String[] args) { MyService service = new MyService(); ThreadA a = new ThreadA(service); ThreadB b = new ThreadB(service); a.start(); b.start(); } }
运行结果:
打印★ 打印☆ 打印★ 打印☆ 打印★ 打印☆ 打印★ 打印☆ 打印★ 打印☆ 打印★ 打印☆ 打印★ 打印☆ 打印★ ...
通过使用 Condition 对象,成功实现交替打印的效果。
4.1.8 实现生产者/消费者模式:多对多交替打印
-
创建公共类
public class MyService { private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); private boolean hasValue = false; public void set() { try { lock.lock(); while (hasValue) { System.out.println("有可能★★连续"); condition.await(); } System.out.println("打印★"); hasValue = true; condition.signal(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public void get() { try { lock.lock(); while (!hasValue) { System.out.println("有可能☆☆连续"); condition.await(); } System.out.println("打印☆"); hasValue = false; condition.signal(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } }
-
创建 2 个自定义线程类
public class ThreadA extends Thread { private MyService service; public ThreadA(MyService service) { this.service = service; } @Override public void run() { super.run(); for (int i = 0; i < Integer.MAX_VALUE; i++) { service.set(); } } }
public class ThreadB extends Thread { private MyService service; public ThreadB(MyService service) { this.service = service; } @Override public void run() { super.run(); for (int i = 0; i < Integer.MAX_VALUE; i++) { service.get(); } } }
-
测试类
public class Run { public static void main(String[] args) { MyService service = new MyService(); ThreadA[] aThread = new ThreadA[10]; ThreadB[] bThread = new ThreadB[10]; for (int i = 0; i < 10; i++) { aThread[i] = new ThreadA(service); bThread[i] = new ThreadB(service); aThread[i].start(); bThread[i].start(); } } }
运行结果:
... 打印★ 有可能★★连续 有可能★★连续 打印☆ 有可能☆☆连续 有可能☆☆连续 打印★ 有可能★★连续 有可能★★连续
程序运行后又出现假死。
根据第 3 章中的 notifyAll() 解决方案,可以使用 signalAll() 方法来解决。将 MyService.java 类中两处 signal() 代码改成 signalAll() 后,程序得到正确运行,效果如下所示。
打印★
有可能★★连续
打印☆
打印★
有可能★★连续
有可能★★连续
打印☆
有可能☆☆连续
打印★
有可能★★连续
有可能★★连续
打印☆
有可能☆☆连续
...
从控制台打印的日志可以发现,运行后不再出现假死状态,假死问题被解决了。
控制台中 “打印★” 和 “打印☆” 是交替输出的,但是 “有可能★★连续” 和 “有可能☆☆连续” 却不是交替输出的,有时候出现连续打印的情况。原因是程序中使用了一个 Condition 对象,再结合 signalAll() 方法来唤醒所有的线程,那么唤醒的线程就有可能是同类,所以就出现连续打印 “有可能★★连续” 或 “有可能☆☆连续” 的情况了。
4.1.9 公平锁和非公平锁
锁 Lock 分为公平锁和非公平锁:
- 公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的 FIFO 先进先出顺序。
- 非公平锁就是一种获取锁的抢占机制,是随机获得锁的,和公平锁不一样的就是先来的不一定先得到锁,这个方式可能造成某些线程一直拿不到锁,结果也就是不公平的了。
-
公共类
public class Service { private Lock lock; public Service(boolean isFair) { lock = new ReentrantLock(isFair); } public void serviceMethod() { try { lock.lock(); System.out.println("Thread name = " + Thread.currentThread().getName() + " get locked"); } finally { lock.unlock(); } } }
-
创建公平锁测试的运行类
public class RunFair { public static void main(String[] args) { final Service service = new Service(true); Runnable runnable = new Runnable() { @Override public void run() { System.out.println("★process " + Thread.currentThread().getName() + "run"); service.serviceMethod(); } }; Thread[] threadArray = new Thread[10]; for (int i = 0; i < 10; i++) { threadArray[i] = new Thread(runnable); } for (int i = 0; i < 10; i++) { threadArray[i].start(); } } }
运行结果:
★process Thread-2run Thread name = Thread-2 get locked ★process Thread-0run Thread name = Thread-0 get locked ★process Thread-1run Thread name = Thread-1 get locked ★process Thread-3run Thread name = Thread-3 get locked ★process Thread-4run Thread name = Thread-4 get locked ★process Thread-5run Thread name = Thread-5 get locked ★process Thread-6run Thread name = Thread-6 get locked ★process Thread-8run Thread name = Thread-8 get locked ★process Thread-7run Thread name = Thread-7 get locked ★process Thread-9run Thread name = Thread-9 get locked
打印的结果基本是呈有序的状态,这就是公平锁的特点。
-
创建非公平锁测试的运行类
ublic class RunNotFair { public static void main(String[] args) { final Service service = new Service(false); Runnable runnable = new Runnable() { @Override public void run() { System.out.println("★process " + Thread.currentThread().getName() + "run"); service.serviceMethod(); } }; Thread[] threadArray = new Thread[10]; for (int i = 0; i < 10; i++) { threadArray[i] = new Thread(runnable); } for (int i = 0; i < 10; i++) { threadArray[i].start(); } }
运行结果:
★process Thread-0run ★process Thread-2run ★process Thread-1run Thread name = Thread-0 get locked ★process Thread-3run Thread name = Thread-3 get locked ★process Thread-4run Thread name = Thread-4 get locked Thread name = Thread-2 get locked Thread name = Thread-1 get locked ★process Thread-5run Thread name = Thread-5 get locked ★process Thread-6run Thread name = Thread-6 get locked ★process Thread-7run Thread name = Thread-7 get locked ★process Thread-8run Thread name = Thread-8 get locked ★process Thread-9run Thread name = Thread-9 get locked
非公平锁的运行结果基本上是乱序的,说明先 start() 启动的线程不代表先获得锁。