Java并发:线程死锁,Java并发:线程协作,Java并发:临界资源,线程同步

关于死锁的问题,是非常麻烦的。对一般程序而言,如果马上出问题,你就可以立即跟踪下去。但是线程死锁不会马上出问题,看起来工作良好的程序却具有潜在的死锁风险。因此,在编写并发程序的时候,进行仔细的程序设计以防止死锁是关键部分。

先解释一下线程死锁吧:

某个任务在等待另一个任务,而后者又等待别的任务,这样一直下去,直到这条链条上的任务又在等待第一个任务释放锁。这得到了一个任务之间相互等待的连续循环,没有哪个线程能继续运行,这就被称为线程死锁。

哲学家进餐是一个典型的线程死锁例子,我就不多描述场景了,直接上程序模拟(省略了一些 import 语句,请自行添加):

  1. class Chopstick {
  2. private boolean taken = false;
  3. public synchronized void take() throws InterruptedException {
  4. while(taken) {
  5. wait();
  6. }
  7. taken = true;
  8. }
  9. public synchronized void drop() {
  10. taken = false;
  11. notifyAll();
  12. }
  13. }
  14. /*
  15. * 每个哲学家都是一个 Runnable 任务
  16. */
  17. class Philosopher implements Runnable {
  18. private Chopstick left;
  19. private Chopstick right;
  20. private final int id;
  21. private final int ponderFctor;
  22. private Random rand = new Random(47);
  23. public Philosopher(Chopstick left, Chopstick right, int id, int ponder) {
  24. this.left = left;
  25. this.right = right;
  26. this.id = id;
  27. this.ponderFctor = ponder;
  28. }
  29. // 思考的时间
  30. private void pause() throws InterruptedException {
  31. //不思考
  32. if (ponderFctor == 0) {
  33. return;
  34. }
  35. TimeUnit.MILLISECONDS.sleep(rand.nextInt(ponderFctor * 250));
  36. }
  37. @Override
  38. public void run() {
  39. try {
  40. while (!Thread.interrupted()) {
  41. // 思考
  42. System.out.print(this + " thinking...");
  43. pause();
  44. // 饿了
  45. System.out.println(this + " grabbing right");
  46. right.take();
  47. System.out.println(this + " garbbing left");
  48. left.take();
  49. System.out.println(this + " eating...");
  50. pause();
  51. //吃完了
  52. right.drop();
  53. left.drop();
  54. }
  55. } catch (InterruptedException e) {
  56. System.out.println(this + " exiting via interrupt");
  57. }
  58. }
  59. @Override
  60. public String toString() {
  61. return "Philosopher " + id + "号";
  62. }
  63. }
  64. /*
  65. * Args: 0 5 timeout
  66. *
  67. * 0:是思考时间的因子
  68. * 5:筷子数目
  69. * timeout:停止时间
  70. */
  71. public class DeadlockingDiningPhilosophers {
  72. public static void main(String[] args) throws Exception {
  73. int ponder = 5;
  74. if (args.length > 0) {
  75. ponder = Integer.parseInt(args[0]);
  76. }
  77. int size = 5;
  78. if (args.length > 1) {
  79. size = Integer.parseInt(args[1]);
  80. }
  81. ExecutorService exec = Executors.newCachedThreadPool();
  82. Chopstick[] chopsticks = new Chopstick[size];
  83. for (int i = 0; i < size; i++) {
  84. chopsticks[i] = new Chopstick();
  85. }
  86. for (int i = 0; i < size; i++) {
  87. exec.execute(new Philosopher(chopsticks[i], chopsticks[(i + 1) % size], i, ponder));
  88. }
  89. if (args.length == 3 && args[2].equals("timeout")) {
  90. TimeUnit.SECONDS.sleep(5);
  91. } else {
  92. System.out.println("Press 'Enter' to quit");
  93. System.in.read();
  94. }
  95. exec.shutdownNow();
  96. }
  97. }

这个程序不指定参数的话,就是0 5,然后按 Enter 停止;如果指定0 5 timeout,就在5s 后自动停止。不过悲催的是,我运行了几十次,一共就产生了1次死锁。。。其中有次运行了3个小时也没死锁 T_T 可见死锁问题的隐蔽性啊。

那么,死锁问题是不可能避免的吗?Note:其实线程死锁必须同时满足以下四个条件(一定是同时)

  • 互斥条件/临界资源:各个任务使用的资源至少有一个是不共享的,属于临界资源,不能共享。比如上面的筷子
  • 请求和保持资源:至少有一个任务必须占有资源的同时想获取另外一个或可用或被其他任务占用的资源
  • 资源不能被抢占:意思是大家都平等,不能强行抢夺别人的资源
  • 循环等待:就像开头描述的那样,大家要形成一个闭环

因为发生死锁要同时满足四个条件,那么解除死锁只需要破坏其中的一个即可。一般情况下,最容易防止死锁的方法是破坏第四个条件,破坏循环等待

比如上面的哲学家就餐问题,第四个条件是因为哲学家都先拿右边,然后拿左边。因为是圆桌,所以最后一个哲学家和第一个哲学家就会抢占它们中间的筷子形成闭环。解决方法很简单:如果最后一个哲学家被初始化为先拿左边的筷子,然后再拿右边的筷子,那么这个哲学家将永远不会阻止他右边的哲学家拿起筷子。

这个小节有一个习题,说的是把筷子集中放到一个容器中,每个哲学家要就餐的话,就取出两根筷子,用完就放进去。这样能防止死锁吗?

关于死锁的问题,一定要往四个条件上靠拢。所以,我们看看他能破坏哪个条件,如果破坏了任意一个,就一定能解决死锁。那我们逐一分析:

  • 不共享临界资源:满足。两根筷子要么在容器中,要么在哲学家手中。不能共享
  • 请求和保持资源:占着一个,想要另外一个:Bingo!因为我只要拿到筷子,就一定是两根,而且不会再去拿容器中的筷子。所以这个条件不满足
  • 资源不能被抢占:满足。
  • 循环等待资源:没有循环等待,因为两根筷子捆绑一会,只有一种情况,拿或者没拿。不会产生循环

综上,从第二条我们就知道,这种方法不会产生死锁。官方答案是这样的:

We use ChopstickQueue to create the ChopstickBin, so it gains all of LinkedBlockingQueue’s control logic. Instead of picking up the left and right chopsticks, philosophers take them from a common bin, an activity we suspend when too few chopsticks remain. The results are the same, with the flaw that an equal number of chopsticks and philosophers can quickly deadlock, but a single extra chopstick prevents this.

很悲催,我们想错了。那么错在哪里了?

其实是理解错题意了。囧 rz。人家没说一次拿两根,而是拿完一根再拿一根。。。。。。。那么,5个哲学家、5根筷子,启动时,所有哲学家都饿了,同时去拿1根。。。然后就悲剧了。。。同时4条件也满足了。所以,如果是拿两根筷子作为一个原子操作,就不会产生死锁了。

Java并发:线程协作

前面对线程进行了讨论,但是比较简单,要么是每个线程独享自己线程内部的资源,要么是用锁机制串行访问共享资源。而本小节做了一点点升级:使用共享资源不再是盲目的阻塞了,而是使用新的握手机制,其实握手类似事件驱动。不过我对事件驱动也只是了解个皮毛,知道大概的 select/kqueue/epoll 原理,具体的还请自行 man 或者 google。我简单举个例子吧:

  • 首先要理解阻塞:阻塞是什么呢?假如现在有个快递(共享资源),但是你不知道什么时候能送到自己手里,接下来的事要等快递来了才能做。那么你什么也不能去做,只能等待快递;
  • 然后是非阻塞忙轮询:如果用忙轮询的方法,那么你需要知道快递员的手机号,然后不停地给他打电话:“你到了没?”

这样一看,我们在前面学习的就是阻塞(synchronized 锁)或者非阻塞忙轮询(使用 while(资源不可用) {sleep…})。这样做不仅效率低,还会大大降低效率,因为对于线程来说,不仅需要保存线程上下文,还要频繁的切换用户态和内核态。而这里解决方法就类似 epoll 那样的回调。

线程协作简单来说就是对共享资源的使用还是阻塞,但是一旦共享资源释放,会主动给阻塞线程发送信号。这样就不用傻傻等待或者不停轮询了。而协作的关键就是如何传递这个信号,在 Java 中我们可以使用 Object 中的 wait()和 notify()/notifyAll()函数,也可以使用 concurrent 类库中提供的 await()和 signal()/signalAll()函数。

下面我们就来具体学习线程协作的相关知识。如果先大致浏览这一小节的话,会发现节奏非常紧凑:

  1. 第一小节首先介绍线程协作,以及相关的3个函数: wait()、notify()、notifyAll(),然后用一个凃蜡、喷漆程序演示如何进行线程协作(重点是明白线程为什么可以协作呢?因为 wait()不会一直占有锁,在挂起期间会允许进入其他 synchronized 方法改变条件,从而 notify()后再醒来继续工作)
  2. notify()和 notifyAll()的区别(个人感觉这一节讲的很迷糊,可以自己去 stackoverflow 或者其他地方查资料)
  3. 用厨师做菜、服务员上菜演示生产者消费者模型,其实还包括一个用 Lock 和 Condition 实现的凃蜡、喷漆程序,可以和第一小节再对比一下
  4. 使用同步队列的方式改变第三小节的生产者消费者模型,用一个队列解耦生产者和消费者
  5. 使用管道来进行输入/输出,本质上是生产者消费者模型的变体,不过它是存在于引入 BlockingQueue 之前的 Java 版本,所以能用 BlockingQueue 的地方就可以忘掉管道了

大概了解本节内容之后,就可以进行有的放矢的学习了:)

话说被书上这个例子坑的很惨,这个例子前前后后看过10遍左右吧,每次看到这都要纠结很久。这次也是弄了好久才算没那么迷糊。。。。。。(智商急需充值啊。。。)嗯,我先来描述这个“简单”的程序吧:

现在有一辆车,需要对它进行凃蜡和抛光操作。因为存在先后顺序,所以必须先凃蜡,然后在抛光。

场景说完了,show code:

  1. package concurrency;
  2. import java.util.concurrent.ExecutorService;
  3. import java.util.concurrent.Executors;
  4. import java.util.concurrent.TimeUnit;
  5. class Car {
  6. /*
  7. * true: 凃蜡
  8. * false:抛光
  9. */
  10. private boolean waxOn = false;
  11. // 完成凃蜡,可以进行抛光了
  12. public synchronized void waxed() {
  13. waxOn = true;
  14. notifyAll();
  15. }
  16. // 完成抛光,可以进行下一层凃蜡了
  17. public synchronized void buffed() {
  18. waxOn = false;
  19. notifyAll();
  20. }
  21. // 这里为什么要用 while()不断监测?为什么不用 if()?
  22. public synchronized void waitForWaxing() throws InterruptedException {
  23. while (waxOn == false) {
  24. wait();
  25. }
  26. }
  27. public synchronized void waitForBuffing() throws InterruptedException {
  28. while (waxOn == true) {
  29. wait();
  30. }
  31. }
  32. }
  33. // 凃蜡任务。因为凃蜡肯定先发生,所以步骤为凃蜡-等待抛光-凃蜡
  34. class WaxOn implements Runnable {
  35. private Car car;
  36. public WaxOn(Car c) {
  37. this.car = c;
  38. }
  39. public void run() {
  40. try {
  41. while(!Thread.interrupted()) {
  42. System.out.println("开始凃蜡...");
  43. TimeUnit.MILLISECONDS.sleep(200);
  44. car.waxed();
  45. car.waitForBuffing();
  46. }
  47. } catch(InterruptedException e) {
  48. System.out.println("Exit via interrupt");
  49. }
  50. System.out.println("结束凃蜡任务");
  51. }
  52. }
  53. // 抛光任务。先行任务为凃蜡,所以步骤为等待凃蜡-抛光-等待凃蜡
  54. class WaxOff implements Runnable {
  55. private Car car;
  56. public WaxOff(Car car) {
  57. this.car = car;
  58. }
  59. public void run() {
  60. try {
  61. while(!Thread.interrupted()) {
  62. car.waitForWaxing();
  63. System.out.println("开始抛光...");
  64. TimeUnit.MICROSECONDS.sleep(200);
  65. car.buffed();
  66. }
  67. } catch(InterruptedException e) {
  68. System.out.println("Exit via interupt");
  69. }
  70. System.out.println("结束抛光任务");
  71. }
  72. }
  73. /*
  74. * 特意先开始抛光任务,再开始凃蜡任务
  75. */
  76. public class WaxOMatic {
  77. public static void main(String[] args) throws InterruptedException {
  78. Car car = new Car();
  79. ExecutorService exec = Executors.newCachedThreadPool();
  80. exec.execute(new WaxOff(car));
  81. exec.execute(new WaxOn(car));
  82. TimeUnit.SECONDS.sleep(5);
  83. exec.shutdownNow();
  84. } }

整个程序的逻辑很简单,但是一定要理解 boolean 类型的 waxOn 变量所代表的含义。其实我到现在也没明白。。。。。。。。不过不影响看懂整个程序的逻辑:

  1. 首先定义一个车,waxed()代表凃蜡完成,buffed()代表抛光完成。waitForWaxing()等待凃蜡,如果 waxOn = false 就说明还在抛光;waitForBuffing()等待抛光,如果 waxOn = true 就说明还在凃蜡。(但是waxOn 究竟代表完成,还是进行中呢?如果代表完成,那么 waitForWaxing()的 waxOn = false 就代表抛光完成,可以进行凃蜡了。明明可以工作了好吗!!!竟然是 wait()。正好和我理解的相反;如果理解成进行中,那么 waxed()中又立即调用了 notifyAll(),明显是完成了凃蜡的意思。)
  2. WaxOn 就是凃蜡了,因为这个肯定是起始动作。所以 run()中先进行凃蜡,然后等待抛光
  3. WaxOff 就是抛光了,因为它肯定在凃蜡操作后执行,所以 run()中先等凃蜡完成后才能进行抛光操作,抛光完成后就再等待下一次凃蜡完成。
  4. main()为了突出这个逻辑,特意先调用了抛光过程,这个希望你能注意到(感觉作者对每个例子都好用心的T_T)

一定要多看看其中是如何使用 wait()和 nofityAll(),如果能提出问题就更好了。一个非常值得思考的问题是:为什么 wait()要使用 while()去监测,既然 notifyAll()发送了资源可用的信号,那么 wait()收到这个消息,用 if()就足够了呀。这是为什么呢?原因如下:

  • 可能有多个任务出于相同的原因在等待同一个锁,而第一个唤醒任务可能会改变这种状况(即使你没有这么做,有人也会通过继承你的类去这么做)。如果属于这种情况,那么这个任务应该被再次挂起,直至其感兴趣的条件发生变化
  • 在这里任务从其 wait()中被唤醒的时刻,有可能会有某个其他的任务已经做出了改变,从而使得这个任务在此时不能执行,或者执行其操作已经显得无关紧要。此时,应该通过再次调用 wait()来将其重新挂起。
  • 也有可能某些任务出于不用的原因在等待你的对象上的锁(在这种情况下必须使用 notifyAll())。在这种情况下,你需要检查是否已经由正确的原因唤醒,如果不是,就再次调用 wait()。

因此,总结一下上面的要点:

其本质就是要检查所感兴趣的特定条件,并在条件不满足的情况下返回到 wait()中。惯用的方法就是使用 while 来编写 wait()的代码。而且 wait()有两种使用,一种是指定到期时间,一种是无限等待。一般情况下我们都会使用无限等待,因为条件很多情况下是无法得知改变的大概时间的。 然后还有一个有趣的问题:wait()/notify()/notifyAll()既然都是关于线程协作方面的,为什么它们是在基类 Object 中实现而不是 Thread 中实现呢?

尽管乍一想有点奇怪,但是我们来分析一下。锁存在于所有对象的对象头中,所以任何同步控制的地方都用到了锁,而用到锁的地方当然也可以进行线程协作。如果把这3个方法实现在 Thread 中,那么使用线程协作的范围就会缩小到继承了 Thread 或者实现了 Runnable 接口的类的对象中,而不是所有对象。实际上,也只能在同步控制块中调用 wait()、notify()、notifyAll(),因为它们都和锁关联,而 sleep()因为不用操作锁,所以可以在非同步控制方法中调用,如果在非同步控制方法中调用了这3个方法,程序能够通过编译,不过在运行的时候,将得到 IllegalMonitorStateException 异常,并伴随着一些含糊的信息,比如“当前线程不是拥有者”。消息的意思是,调用 wait()、notify()、notifyAll()的任务在调用这些方法前必须“拥有”(获取)对象的锁。

当线程使用 wait()/notify()或者 wait()/notifyAll()时,均可能发生错失信号的问题。想想这是什么原因呢?比如下面这段代码你能看出问题吗?

  1. T1:
  2. synchronized(sharedMonitor) {
  3. <setup condition for T2>
  4. shareMonitor.notify();
  5. }
  6. T2:
  7. while(someCondition) {
  8. //Point 1
  9. synchronized(sharedMonitor) {
  10. sharedMonitor.wait();
  11. }
  12. }

两个线程 T1和 T2的协作正确吗?如果不正确,会发生什么问题呢?

其实代码中已经有了提示,在 Point 1处可能会发生错误。假如 T2执行到 Point 1的时候,说明someCondition 为 true,而这时线程调度器将时间片分给 T1,T1检查 sharedMonitor 对象锁没有占用,就拿到锁进入到同步控制块中,改变T2 线程的 while 条件,然后发送一个信号。但是因为 T2已经执行过 someCondition 的判断,所以就错失了 notify()的通知,在同步控制块中 wait()导致死锁(一直等待)。

那么,解决办法是防止在 someCondition 上产生变量竞争条件。不竞争不就是串行的意思嘛,所以 T2的代码将 while()放在同步控制块中即可:

  1. synchronized(sharedMonitor) {
  2. while(someCondition) {
  3. sharedMonitor.wait();
  4. }
  5. }

如果仔细观察上面的程序,会发现我们一会用 notifyAll()一会用 notify(),那么它们各自的使用场景是什么?

其实从名字可以猜个大概,notify()是对于单个线程来说的,notifyAll()是对于所有线程而言的。举个例子,现在大家都在教室自习,有个家长来找自己的孩子,那么 notify()就是去那个家长的孩子座位上单独告诉他,notifyAll()就是在班里大吼一声某某的家长在外面,让所有学生都知道,然后某某出去,其他孩子继续干自己的事情。

实际上,notify()和 notifyAll()的区别还是非常值得研究的,stackoverflow 上也有这个问题的讨论:Java: notify() vs. notifyAll() all over again

notify()和notifyAll()都是Object对象用于通知处在等待该对象的线程的方法。两者的最大区别在于:

notifyAll使所有原来在该对象上等待被notify的线程统统退出wait的状态,变成等待该对象上的锁,一旦该对象被解锁,他们就会去竞争。 notify则文明得多他只是选择一个wait状态线程进行通知,并使它获得该对象上的锁,但不惊动其他同样在等待被该对象notify的线程们,当第一个线程运行完毕以后释放对象上的锁此时如果该对象没有再次使用notify语句,则即便该对象已经空闲,其他wait状态等待的线程由于没有得到该对象的通知,继续处在wait状态,直到这个对象发出一个notify或notifyAll,它们等待的是被notify或notifyAll,而不是锁。

  1. package concurrency;
  2. import java.util.concurrent.ExecutorService;
  3. import java.util.concurrent.Executors;
  4. import java.util.concurrent.TimeUnit;
  5. class Meal {
  6. private final int orderNum;
  7. public Meal(int orderNum) {
  8. this.orderNum = orderNum;
  9. }
  10. public String toString() {
  11. return "Meal " + orderNum;
  12. }
  13. }
  14. class Waiter implements Runnable {
  15. private Restaurant restaurant;
  16. public Waiter(Restaurant restaurant) {
  17. this.restaurant = restaurant;
  18. }
  19. @Override
  20. public void run() {
  21. try {
  22. while (!Thread.interrupted()) {
  23. synchronized (this) {
  24. while (restaurant.meal == null) {
  25. wait();
  26. }
  27. }
  28. System.out.println("Waiter got " + restaurant.meal);
  29. // 为什么要选择 chef 作为同步控制块的锁呢?
  30. // 废话,想通知 chef,肯定要调用 chef.notifyAll()。因为 notifyAll()必须在
  31. // 同步控制块中调用,而且释放的是 chef 的锁,肯定需要先获取 chef 的锁了。。。
  32. synchronized (restaurant.chef) {
  33. restaurant.meal = null;
  34. restaurant.chef.notifyAll(); // 准备下一道菜
  35. }
  36. }
  37. } catch (InterruptedException e) {
  38. System.out.println("Waiter interrupted");
  39. }
  40. }
  41. }
  42. class Chef implements Runnable {
  43. private Restaurant restaurant;
  44. private int count = 0;
  45. public Chef(Restaurant restaurant) {
  46. this.restaurant = restaurant;
  47. }
  48. @Override
  49. public void run() {
  50. try {
  51. while (!Thread.interrupted()) {
  52. synchronized (this) {
  53. while (restaurant.meal != null) {
  54. wait();
  55. }
  56. }
  57. if (++count == 11) {
  58. System.out.println("菜上齐了");
  59. //这块只是向 chef 和 waiter 发送一个 interrupt 信号
  60. //但是因为 synchronized 和 IO 是不能被中断的,所以这里会通过可中断的
  61. //sleep()抛出 InterruptedException。
  62. //而 waiter 只能通过 while(Thread.interrupted())抛出的 InterruptedException返回
  63. //而且我们会发现,多做了一个菜!本来做了10个就够了。11个本意想关闭程序,但是因为
  64. //synchronized 无法中断,只好又做了一个菜(厨师也饿了)。但是因为服务员在 wait(),可以被中断
  65. //所以做好的菜没有被服务员上去。。。。
  66. restaurant.exec.shutdownNow();
  67. }
  68. System.out.print("做菜ing...");
  69. synchronized (restaurant.waiter) {
  70. restaurant.meal = new Meal(count);
  71. restaurant.waiter.notifyAll();
  72. }
  73. TimeUnit.MILLISECONDS.sleep(100);
  74. }
  75. } catch (InterruptedException e) {
  76. System.out.println("chef interrupted");
  77. }
  78. }
  79. }
  80. public class Restaurant {
  81. Meal meal;
  82. ExecutorService exec = Executors.newCachedThreadPool();
  83. Waiter waiter = new Waiter(this);
  84. Chef chef = new Chef(this);
  85. public Restaurant() {
  86. exec.execute(chef);
  87. exec.execute(waiter);
  88. }
  89. public static void main(String[] args) {
  90. new Restaurant();
  91. }
  92. }/*output:
  93. 做菜ing...Waiter got Meal 1
  94. 做菜ing...Waiter got Meal 2
  95. 做菜ing...Waiter got Meal 3
  96. 做菜ing...Waiter got Meal 4
  97. 做菜ing...Waiter got Meal 5
  98. 做菜ing...Waiter got Meal 6
  99. 做菜ing...Waiter got Meal 7
  100. 做菜ing...Waiter got Meal 8
  101. 做菜ing...Waiter got Meal 9
  102. 做菜ing...Waiter got Meal 10
  103. 菜上齐了
  104. Waiter interrupted
  105. 做菜ing...chef interrupted
  106. */

这个程序的输出会发现,最后一道菜已经做了,但是没有上。写完迷糊了好久才想起来,synchronized 不可被中断,但是 wait()可以被中断啊(同时中断状态被清除,抛出一个 InterruptedException)!!!!

上面是生产者消费者模型的最基本实现——厨师做完一道菜后通知服务员取菜,服务员取菜之后通知厨师做菜。这样的做法太低效,因为每次交互都需要握手。在更高效的程序中,可以使用同步队列来解决任务协作问题,同步队列在任何时刻都只允许一个任务插入或移除元素。在 java.util.concurrent.BlockingQueue 接口中提供了这种队列,这个接口有大量的标准实现。通常可以使用 LinkedBlockingQueue,它是一个无界队列,还可以使用 ArrayBlockingQueue,它又固定的大小,因此可以在它被阻塞之前向其中放置有限数量的元素。

并且,使用同步队列可以简化上面繁琐的握手方式。如果消费者任务试图从队列中获取元素,而该队列为空,那么这些队列还可以挂起消费者任务,当有更多的元素可用时,又会恢复消费者任务。阻塞队列可以解决非常大量的问题,而方式与 wait()和 notifyAll()相比,则简单可靠的多。

下面我们写一个简单的程序说明一下 BlockingQueue 的使用方法,以及它带来的便利。

  1. package concurrency;
  2. import java.io.BufferedReader;
  3. import java.io.IOException;
  4. import java.io.InputStreamReader;
  5. import java.util.concurrent.ArrayBlockingQueue;
  6. import java.util.concurrent.BlockingQueue;
  7. import java.util.concurrent.LinkedBlockingDeque;
  8. import java.util.concurrent.SynchronousQueue;
  9. class LiftOffRunner implements Runnable {
  10. private BlockingQueue<LiftOff> rockets;
  11. public LiftOffRunner(BlockingQueue<LiftOff> rockets) {
  12. this.rockets = rockets;
  13. }
  14. //生产者
  15. public void add(LiftOff lo) {
  16. try {
  17. rockets.put(lo);
  18. } catch(InterruptedException e) {
  19. System.out.println("Interrupted during put()");
  20. }
  21. }
  22. //消费者——注意后面的程序先启动了消费者。
  23. public void run() {
  24. try {
  25. while(!Thread.interrupted()) {
  26. LiftOff rocket = rockets.take();
  27. rocket.run();
  28. }
  29. } catch(InterruptedException e) {
  30. System.out.println("waking from take()");
  31. }
  32. System.out.println("Exiting LiftOffRunner");
  33. }
  34. }
  35. public class TestBlockingQueues {
  36. /*
  37. * 其实getkey()仅仅是为了隔开 BlockingQueue 的不同实现类。
  38. */
  39. static void getkey() {
  40. try {
  41. new BufferedReader(new InputStreamReader(System.in)).readLine();
  42. } catch(IOException e) {
  43. throw new RuntimeException(e);
  44. }
  45. }
  46. static void getkey(String message) {
  47. System.out.println(message);
  48. getkey();
  49. }
  50. /*
  51. * 每次测试一种 BlockingQueue 的实现。其中先调用t.start()是为了启动消费者。
  52. * 因为没有启动生产者,所以 BlockingQueue 会自动挂起。然后使用 for 循环生产 rockets 的元素。
  53. *
  54. * 所以不仅实例了 BlockingQueue 作为一个 Queue 的使用,也演示了当生产者或者消费者阻塞时,BlockingQueue
  55. * 会自动帮我们处理,使我们可以专注于业务逻辑。
  56. */
  57. static void test(String msg, BlockingQueue<LiftOff> queue) {
  58. System.out.println(msg);
  59. LiftOffRunner runner = new LiftOffRunner(queue);
  60. Thread t = new Thread(runner);
  61. t.start();
  62. for(int i = 0; i < 5; i++) {
  63. runner.add(new LiftOff(5));
  64. }
  65. getkey("Press 'Enter' (" + msg + ")");
  66. t.interrupt();
  67. System.out.println("Finished " + msg + " test");
  68. }
  69. public static void main(String[] args) {
  70. test("LinkedBlockingQueue", new LinkedBlockingDeque<LiftOff>());
  71. test("ArrayBlockingQueue", new ArrayBlockingQueue<LiftOff>(3));
  72. test("SynchronousQueue", new SynchronousQueue<LiftOff>());
  73. }
  74. }

程序的输出需要 System.in,所以自己去运行。运行之后,你的任务是再写一个程序。将厨师、服务员的例子改写成使用 BlockingQueue 的。我也来一发:

  1. package concurrency;
  2. import java.util.concurrent.ExecutorService;
  3. import java.util.concurrent.Executors;
  4. import java.util.concurrent.LinkedBlockingQueue;
  5. import java.util.concurrent.TimeUnit;
  6. /*
  7. * 这个例子的一个收获是:
  8. *
  9. * 想要抛出异常必须得有载体。比如:
  10. *
  11. * while(!Thread.interrupted()) {
  12. * }
  13. *
  14. * 是不会抛出异常的。
  15. *
  16. * 只有当里面有 sleep()/wait()/join()在运行(让线程处于阻塞状态),然后才能从阻塞状态退出,
  17. * 并抛出一个 InterruptedException。
  18. *
  19. */
  20. class NewMeal {
  21. private final int orderNum;
  22. public NewMeal(int orderNum) {
  23. this.orderNum = orderNum;
  24. }
  25. public String toString() {
  26. return "Meal " + orderNum;
  27. }
  28. }
  29. class NewWaiter implements Runnable {
  30. private RestaurantWithBlockingQueue restaurant;
  31. public NewWaiter(RestaurantWithBlockingQueue restaurant) {
  32. this.restaurant = restaurant;
  33. }
  34. @Override
  35. public void run() {
  36. try {
  37. while (!Thread.interrupted()) {
  38. while (!restaurant.meal.isEmpty()) {
  39. NewMeal meal = restaurant.meal.take();
  40. System.out.println("Waiter got " + meal);
  41. }
  42. }
  43. } catch (InterruptedException e) {
  44. System.out.println("Interrupted waiter");
  45. }
  46. }
  47. }
  48. class NewChef implements Runnable {
  49. private RestaurantWithBlockingQueue restaurant;
  50. public NewChef(RestaurantWithBlockingQueue restaurant) {
  51. this.restaurant = restaurant;
  52. }
  53. @Override
  54. public void run() {
  55. try {
  56. while (!Thread.interrupted()) {
  57. for (int i = 1; i <= 11; i++) {
  58. if (i == 11) {
  59. restaurant.exec.shutdownNow();
  60. continue;
  61. }
  62. System.out.println("做菜...");
  63. restaurant.meal.add(new NewMeal(i));
  64. TimeUnit.MILLISECONDS.sleep(100);
  65. }
  66. }
  67. } catch (InterruptedException e) {
  68. System.out.println("Interrupted chef");
  69. }
  70. }
  71. }
  72. public class RestaurantWithBlockingQueue {
  73. LinkedBlockingQueue<NewMeal> meal = new LinkedBlockingQueue<NewMeal>();
  74. ExecutorService exec = Executors.newCachedThreadPool();
  75. NewWaiter waiter = new NewWaiter(this);
  76. NewChef chef = new NewChef(this);
  77. public RestaurantWithBlockingQueue() {
  78. exec.execute(waiter);
  79. exec.execute(chef);
  80. }
  81. public static void main(String[] args) {
  82. // while(!Thread.interrupted()) {
  83. // System.out.println("ehl");
  84. // }
  85. new RestaurantWithBlockingQueue();
  86. }
  87. }/*output:
  88. 做菜...
  89. Waiter got Meal 1
  90. 做菜...
  91. Waiter got Meal 2
  92. 做菜...
  93. Waiter got Meal 3
  94. 做菜...
  95. Waiter got Meal 4
  96. 做菜...
  97. Waiter got Meal 5
  98. 做菜...
  99. Waiter got Meal 6
  100. 做菜...
  101. Waiter got Meal 7
  102. 做菜...
  103. Waiter got Meal 8
  104. 做菜...
  105. Waiter got Meal 9
  106. 做菜...
  107. Waiter got Meal 10
  108. */

通过这个程序得出的结论是:

  1. 如果线程没有被阻塞,调用 interrupt()将不起作用;若线程处于阻塞状态,就将得到异常(该线程必须事先预备好处理此状况),接着退出阻塞状态。
  2. 线程 A 在执行 sleep(),wait(),join()时,线程 B 调用 A 的 interrupt 方法,A会 catch 一个 InterruptedException异常.但这其实是在 sleep,wait,join 这些方法内部不断检查中断状态的值后抛出的 InterruptedException。
  3. 如果线程 A 正在执行一些指定的操作时,如赋值、for、while等,线程本身是不会去检查中断状态标志的,所以线程 A 自身不会抛出 InterruptedException 而是一直执行自己的操作。
  4. 当线程 A 终于执行到 wait(),sleep(),join()时,这些方法本身会抛出 InterruptedException
  5. 若没有调用 sleep(),wait(),join()这些方法,或是没有在线程里自己检查中断状态并抛出 InterruptedException 的话,那么上游是无法感知这个异常的(还记得异常不能跨线程传递吗?)

然后书上还有一个使用 BlockingQueue 的例子,非常简单。本质来说,BlockingQueue 可以当成是一个任务队列,它会自动的搞定同步操作,所以在处理生产者消费者模型时,可以作为首选。当然,使用具体哪种 BlockingQueue 就需要自己选择了。

  1. package concurrency;
  2. import java.util.Random;
  3. import java.util.concurrent.ExecutorService;
  4. import java.util.concurrent.Executors;
  5. import java.util.concurrent.LinkedBlockingQueue;
  6. import java.util.concurrent.TimeUnit;
  7. class Toast {
  8. public enum Status {
  9. DRY, BUTTERED, JAMMED
  10. };
  11. private Status status = Status.DRY;
  12. private final int id;
  13. public Toast(int id) {
  14. this.id = id;
  15. }
  16. public void butter() {
  17. status = Status.BUTTERED;
  18. }
  19. public void jam() {
  20. status = Status.JAMMED;
  21. }
  22. public Status getStatus() {
  23. return status;
  24. }
  25. public int getId() {
  26. return id;
  27. }
  28. public String toString() {
  29. return "Toast " + id + ": " + status;
  30. }
  31. }
  32. /*
  33. * ToastQueue 充当别名的作用。就好像 typedef
  34. *
  35. */
  36. class ToastQueue extends LinkedBlockingQueue<Toast> {
  37. }
  38. //制造吐司
  39. class Toaster implements Runnable {
  40. private ToastQueue toastQueue;
  41. private int count = 0;
  42. private Random rand = new Random(47);
  43. public Toaster(ToastQueue toastQueue) {
  44. this.toastQueue = toastQueue;
  45. }
  46. @Override
  47. public void run() {
  48. try {
  49. while(!Thread.interrupted()) {
  50. TimeUnit.MILLISECONDS.sleep(100 + rand.nextInt(500));
  51. Toast toast = new Toast(count++);
  52. System.out.println(toast);
  53. toastQueue.add(toast);
  54. }
  55. } catch(InterruptedException e) {
  56. System.out.println("制造吐司 is interrupted!");
  57. }
  58. System.out.println("Toaster off");
  59. }
  60. }
  61. //抹黄油
  62. class Butterer implements Runnable {
  63. private ToastQueue dryQueue, butteredQueue;
  64. public Butterer(ToastQueue dryQueue, ToastQueue butteredQueue) {
  65. this.dryQueue = dryQueue;
  66. this.butteredQueue = butteredQueue;
  67. }
  68. @Override
  69. public void run() {
  70. try {
  71. while(!Thread.interrupted()) {
  72. Toast toast = dryQueue.take();
  73. toast.butter();
  74. System.out.println(toast);
  75. butteredQueue.put(toast);
  76. }
  77. } catch(InterruptedException e) {
  78. System.out.println("抹黄油 is interrupted!");
  79. }
  80. System.out.println("Butterer off");
  81. }
  82. }
  83. //抹果酱
  84. class Jammer implements Runnable {
  85. private ToastQueue butteredQueue, finishedQueue;
  86. public Jammer(ToastQueue butteredQueue, ToastQueue finishedQueue) {
  87. this.butteredQueue = butteredQueue;
  88. this.finishedQueue = finishedQueue;
  89. }
  90. @Override
  91. public void run() {
  92. try {
  93. while(!Thread.interrupted()) {
  94. Toast toast = butteredQueue.take();
  95. toast.jam();
  96. System.out.println(toast);
  97. finishedQueue.put(toast);
  98. }
  99. } catch(InterruptedException e) {
  100. System.out.println("抹果酱 is interrupted!");
  101. }
  102. System.out.println("Jammer off");
  103. }
  104. }
  105. //吃吃吃
  106. class Eater implements Runnable {
  107. private ToastQueue finishedQueue;
  108. private int count = 0;
  109. public Eater(ToastQueue finishedQueue) {
  110. this.finishedQueue = finishedQueue;
  111. }
  112. @Override
  113. public void run() {
  114. try {
  115. while(!Thread.interrupted()) {
  116. Toast toast = finishedQueue.take();
  117. //检查吐司是否按照 order 送来,而且所有都是经过黄油、果酱加工
  118. if(toast.getId() != count++ || toast.getStatus() != Toast.Status.JAMMED) {
  119. System.err.println("Error: " + toast);
  120. System.exit(1);
  121. } else {
  122. System.out.println("真好吃啊!!!");
  123. }
  124. }
  125. } catch(InterruptedException e) {
  126. System.out.println("吃吃吃 is interrupted!");
  127. }
  128. System.out.println("Eater off");
  129. }
  130. }
  131. public class ToastMatic {
  132. public static void main(String[] args) throws Exception {
  133. ToastQueue dryQueue = new ToastQueue(),
  134. butteredQueue = new ToastQueue(),
  135. finishedQueue = new ToastQueue();
  136. ExecutorService exec = Executors.newCachedThreadPool();
  137. exec.execute(new Toaster(dryQueue));
  138. exec.execute(new Butterer(dryQueue, butteredQueue));
  139. exec.execute(new Jammer(butteredQueue, finishedQueue));
  140. exec.execute(new Eater(finishedQueue));
  141. TimeUnit.SECONDS.sleep(5);
  142. exec.shutdownNow();
  143. }
  144. }

这个程序虽然简单,但是有几个亮点值得关注:

  • Toast 是一个使用 enum 的优秀示例
  • 程序中没有显式的 Lock 或者 synchronized 关键字,就显得很简洁。同步全部由同步队列隐式管理了——每个 Toast 在任何时刻都只由一个任务在操作。
  • 因为队列自动进行阻塞、挂起、恢复,就使得程非常简洁,而且省略了 wait()/notifyAll()在类与类之间的耦合,因为每个类都只和它自己的 BlockingQueue 进行通信

首先需要声明:

这个模型可以看成是生产者-消费者问题的变体,这里的管道就是一个封装好的解决方案。管道基本上是一个阻塞队列,存在于多个引入 BlockingQueue 之前的 Java 版本中。意思很明显,有了 BlockingQueue 之后还是用 BlockingQueue 吧。目测公司的 jdk 都是1.6+吧,所以这个小节基本就是有个印象就好,重点还是掌握 BlockingQueue。

下面这个程序虽然简单,但是最好自己多调试。看看 PipedReader 和 PipedWriter 能不能中断,是 IOException 还是 InterruptedException(其实是java.io.InterruptedIOException)。

  1. package concurrency;
  2. import java.io.IOException;
  3. import java.io.PipedReader;
  4. import java.io.PipedWriter;
  5. import java.util.Random;
  6. import java.util.concurrent.ExecutorService;
  7. import java.util.concurrent.Executors;
  8. import java.util.concurrent.TimeUnit;
  9. /*
  10. * PipedWriter.write()和 PipedReader.read() 都可以中断,这是和普通 IO 之间最重要的区别了。
  11. */
  12. class Sender implements Runnable {
  13. private Random rand = new Random(47);
  14. private PipedWriter out = new PipedWriter();
  15. public PipedWriter getPipedWriter() {
  16. return out;
  17. }
  18. @Override
  19. public void run() {
  20. try {
  21. //while (true) {
  22. for(Integer i = 0; i < 10000000; i++) {
  23. out.write(i);
  24. //TimeUnit.MILLISECONDS.sleep(rand.nextInt(500));
  25. }
  26. //}
  27. } catch (IOException e) {
  28. System.out.println(e + " Sender write exception");
  29. }
  30. // } catch (InterruptedException e) {
  31. // System.out.println(e + " Sender sleep interrupted");
  32. // }
  33. }
  34. }
  35. class Receiver implements Runnable {
  36. private PipedReader in;
  37. //必须和一个 PipedWriter 相关联
  38. public Receiver(Sender sender) throws IOException {
  39. in = new PipedReader(sender.getPipedWriter());
  40. }
  41. @Override
  42. public void run() {
  43. try {
  44. while (true) {
  45. //调用 P ipedReader.read(),如果管道没有数据会自动阻塞
  46. System.out.print("Read: " + (char) in.read() + ", ");
  47. }
  48. } catch (IOException e) {
  49. System.out.println(e + " Receiver read exception");
  50. }
  51. }
  52. }
  53. public class PipedIO {
  54. public static void main(String[] args) throws Exception {
  55. Sender sender = new Sender();
  56. Receiver receiver = new Receiver(sender);
  57. ExecutorService exec = Executors.newCachedThreadPool();
  58. exec.execute(sender);
  59. exec.execute(receiver);
  60. TimeUnit.SECONDS.sleep(1);
  61. exec.shutdownNow();
  62. }
  63. }

开头就说了,现在 PipedWriter 和 PipedReader 已经被 BlockingQueue 取代,所以了解即可。记住一点,PipedWriter 和 PipedReader 是可以被中断的。

使用wait方法和使用synchornized来分配cpu时间是有本质区别的。wait会释放锁,synchornized不释放锁。 还有:(wait/notify/notifyAll)只能在取得对象锁的时候才能调用。

调用notifyAll通知所有线程继续执行,只能有一个线程执行,其余的线程在等待(因为在所有线程被唤醒的时候在synchornized块中)。这时的等待和调用notifyAll前的等待是不一样的。

  • notifyAll前:在对象上休息区内休息
  • notifyAll后:在排队等待获得对象锁。

notify和notifyAll都是把某个对象上休息区内的线程唤醒,notify只能唤醒一个,但究竟是哪一个不能确定,而notifyAll则唤醒这个对象上的休息室中所有的线程.

一般有为了安全性,我们在绝对多数时候应该使用notifyAll(),除非你明确知道只唤醒其中的一个线程.

至于有些书上说“notify:唤醒同一对象监视器中调用wait的第一个线程”我认为是没有根据的因为sun公司是这样说的“The choice is arbitrary and occurs at the discretion of the implementation.”

同类文章:

Java并发:临界资源,线程同步

这一小节其实也是基础知识,核心是处理临界资源,方法就是加锁。下面我们就简单说说。

2 种方式:

  1. synchronized:对象的同步锁
  2. ReentrantLock:更灵活,支持更多特性
    1. 公平锁(支持)
    2. 等待中断(支持)

现在有这样一个例子,简单的生产者-消费者模型,生产者生产数字,消费者检查数字(多个消费者),如何某个消费者发现数字不是偶数,那么全部消费者就停工。

看一下生产者的代码:

  1. public abstract class IntGenerator {
  2. private volatile boolean canceled = false;
  3. public abstract int next();
  4. // Allow this to be canceled;
  5. public void cancel() {
  6. canceled = true;
  7. }
  8. public boolean isCanceled() {
  9. return canceled;
  10. }
  11. }

我们定义一个通用的生产者模型,后续可以有具体的实现。下面是一个实现:

  1. public class EvenGenerator extends IntGenerator {
  2. private int currentEvenValue = 0;
  3. @Override
  4. public int next() {
  5. ++currentEvenValue; //Danger point here!
  6. ++currentEvenValue;
  7. return currentEvenValue;
  8. }
  9. public static void main(String[] args) {
  10. EvenChecker.test(new EvenGenerator());
  11. }
  12. }

其中的 EvenChecker 就是消费者了,下面是消费者的代码:

  1. import java.util.concurrent.ExecutorService;
  2. import java.util.concurrent.Executors;
  3. public class EvenChecker implements Runnable {
  4. private IntGenerator generator;
  5. private final int id;
  6. public EvenChecker(IntGenerator g, int ident) {
  7. generator = g;
  8. this.id = ident;
  9. }
  10. @Override
  11. public void run() {
  12. while(!generator.isCanceled()) {
  13. int val = generator.next();
  14. if(val % 2 != 0) {
  15. System.out.println(val + " not even!");
  16. generator.cancel(); // Cancels all EvenCheckers
  17. }
  18. }
  19. }
  20. public static void test(IntGenerator gp, int count) {
  21. System.out.println("Press Control-C to exit!");
  22. ExecutorService exec = Executors.newCachedThreadPool();
  23. for(int i = 0; i < count; i++) {
  24. exec.execute(new EvenChecker(gp, i));
  25. }
  26. exec.shutdown();
  27. }
  28. public static void test(IntGenerator gp) {
  29. test(gp, 10);
  30. }
  31. }

先整体看看代码,非常简单。一个生产者去生产数字(两次自加操作),N 个消费者(默认是10)去消费数字,发现非偶数就全部停工(通过 volatile 关键字)。

这个程序很快就会失败,因为 Java 的自加操作不是原子性的,所以如果一个线程在两次自加操作中间调用了 next(),那么就会产生非偶数了。那么,解决方案就是加锁基本上所有的并发模式在解决线程冲突问题的时候,都是采用序列化访问共享资源的方案。这意味着在给定时刻只允许一个任务访问共享资源。通常这是通过在代码前面加上一条锁语句来实现的,这种机制就是所谓的互斥量(mutex)。

首先定义一下临界资源和临界区

  • 临界资源:共享资源一般是以对象形式存在的内存片段,也可以是文件、输入/输出端口,或者是打印机之类的东西
  • 临界区:有时,你只是希望防止多个线程同时访问方法内部的内部代码而不是整个方法,通过这种方式分离出来的代码段被称为临界区(critical section),它也使用 synchronized 关键字建立。这里,synchronized 被用来指定某个对象,此对象的锁被用来对花括号内的代码进行同步控制

方法有两个:

  • 使用 synchronized 关键字
  • 使用 Lock 类

这里提供了2个方法,那么我们就要给自己留一个疑问:

为什么提供两种?什么情况下使用第一种,什么情况下使用第二种?

1)synchronized 的使用

要控制对共享资源的访问,得包装成一个对象。然后把所有要访问这个资源的方法标记为 synchronized。一般做法是使用 private 修饰这个临界资源的对象。注意,在使用并发时,将域设置为 private 是非常重要的,否则,synchronized 关键字就不能防止其他任务直接访问域,这样会产生冲突。基本的使用原则是:

如果你正在写一个变量,它可能接下来将被另一个线程读取,或者正在读取一个上一次已经被另一个线程写过的变量,那么你必须使用同步,并且,读写线程都必须用相同的监视器锁同步。总之,每个访问临界资源的方法都必须被同步,否则它们就不会正确工作。

比如用 synchronized 改进上面的例子:

  1. public class EvenGenerator extends IntGenerator {
  2. private int currentEvenValue = 0;
  3. @Override
  4. public synchronized int next() {
  5. ++currentEvenValue; //Danger point here!
  6. ++currentEvenValue;
  7. return currentEvenValue;
  8. }
  9. public static void main(String[] args) {
  10. EvenChecker.test(new EvenGenerator());
  11. }
  12. }

用法说完了,也需要大概了解一下 synchronized 的工作原理(更具体的实现可以参考《深入理解 Java 虚拟机》):

  • synchronized 相当于一个标志,每个对象都有对象头,对象头中含有锁信息(也称为监视器)。每次使用某个对象的时候,会去检查是否被 synchronized 修饰,如果修饰了,就去看看当前的对象是否获取了锁,如果没有,就阻塞;如果获取了,就可以进行对应的逻辑。等执行完之后,就释放当前对象使用的锁。
  • 还有一个比较好玩的是对象头中的锁不是 boolean 类型的,意思就是锁可以计数(应该是整型)。一个获取当前对象的锁的方法,可以调用另一个 synchronized 的方法,这时锁计数就是2,以此类推。当锁的计数变为0时候,就该释放锁了。
  • 还有一点是,针对每个类,也有一个锁(作为类的 Class 对象的一部分),所以 synchronized static 方法可以在类的范围内防止对 static 数据的并发访问

2) Lock 的使用

Java SE5的 java.util.concurrent 类库还包含有定义在 java.util.concurrent.locks 中的显式的互斥机制。Lock 对象必须显式的创建、锁定和释放。因此,它与 synchronized 提供的锁机制相比,代码缺少优雅性。但是对于有些场景,使用 Lock 会更加灵活。

使用 Lock 来改进上面的例子:

  1. import java.util.concurrent.locks.Lock;
  2. import java.util.concurrent.locks.ReentrantLock;
  3. public class MutexEvenGenerator extends IntGenerator {
  4. private int currentValue = 0;
  5. private Lock lock = new ReentrantLock();
  6. public int next() {
  7. lock.lock();
  8. try {
  9. ++currentValue;
  10. Thread.yield();
  11. ++currentValue;
  12. return currentValue;
  13. } finally {
  14. lock.unlock();
  15. }
  16. }
  17. public static void main(String[] args) {
  18. EvenChecker.test(new MutexEvenGenerator());
  19. }
  20. }

Notice:

return 语句必须在 try 子句中出现,以确保 unlock()不会过早发生,从而将数据暴露给第二个任务。即,释放锁之前,一定要return数据。

3) 总结一下吧:)

大体上,使用 synchronized 关键字时,需要写的代码量更少,并且用户错误出现的可能性也会降低,因此通常只有在解决特殊问题时,才使用显式的 Lock 对象。例如,用 synchronized 关键字不能尝试着获取锁且最终获取锁会失败,或者尝试着获取锁一段时间,然后放弃它,要实现这些,你必须使用 concurrent 类库:

  1. import java.util.concurrent.TimeUnit;
  2. import java.util.concurrent.locks.ReentrantLock;
  3. public class AttemptLocking {
  4. private ReentrantLock lock = new ReentrantLock();
  5. // 直接去获取锁,然后输出状态。
  6. public void untimed() {
  7. boolean captured = lock.tryLock();
  8. try {
  9. System.out.println("tryLock(): " + captured);
  10. } finally {
  11. if (captured) {
  12. lock.unlock();
  13. }
  14. }
  15. }
  16. //尝试获取2s,如果失败就返回。
  17. public void timed() {
  18. boolean captured = false;
  19. try {
  20. captured = lock.tryLock(2, TimeUnit.SECONDS);
  21. } catch (InterruptedException e) {
  22. throw new RuntimeException(e);
  23. }
  24. try {
  25. System.out.println("tryLock(2, TimeUnit.SECONDS): " + captured);
  26. } finally {
  27. if (captured) {
  28. lock.unlock();
  29. }
  30. }
  31. }
  32. public static void main(String[] args) throws InterruptedException {
  33. final AttemptLocking al = new AttemptLocking();
  34. al.untimed();
  35. al.timed();
  36. new Thread() {
  37. {
  38. setDaemon(true);
  39. }
  40. public void run() {
  41. al.lock.lock();
  42. System.out.println("acquired");
  43. }
  44. }.start();
  45. TimeUnit.MILLISECONDS.sleep(1000);
  46. al.untimed();
  47. al.timed();
  48. }
  49. }/*output:
  50. tryLock(): true
  51. tryLock(2, TimeUnit.SECONDS): true
  52. acquired
  53. tryLock(): false
  54. tryLock(2, TimeUnit.SECONDS): false
  55. */

结果很明显,没有被锁定的时候。tryLock1和2都可以获取锁,但是后面我们用一个后台线程去占有锁,这时如果使用 synchronized 就会一直阻塞,但是这里使用 Lock 的话,就可以尝试获取 N 秒,如果不能获取我就干别的事情。所以可以想到这个功能可以使用的场景:

  1. while(true) {
  2. 1. 首先使用boolean captured = lock.tryLock(),如果是 true 的话就走正常的逻辑
  3. 2. 如果 false 的话,使用captured = lock.tryLock(2, TimeUnit.SECONDS)尝试获取锁,如果2秒内不断轮询并且获得了锁,就走正常的逻辑
  4. 3. 如果超过2s还是不能获取,我就干点其他的事情。
  5. }

显式的 Lock 对象在加锁和释放锁方面,相对于内建的 synchronized 锁来说,还赋予了你更细粒度的控制力。这对于实现专有同步结构是很有用的,例如用于遍历链接列表中的节点的节节传递的加锁机制(也称为锁耦合),这种遍历代码必须在释放当前节点的锁之前捕获下一个节点的锁。如果使用 synchronized 是做不到的。

这一节比较浅,大致的知识点如下所示,想要深入了解还是去看《深入理解 Java 虚拟机》,很好很强大:

  • long 和 double 是2字节的,所以很多操作不具有原子性,会产生字撕裂;(但具体JVM实现,都被作为原子性?)
  • volatile 的使用以及2种使用场景
  • volatile 没有使用副本,而是直接作用于主内存。每个使用的线程都必须先从主内存刷新,所以不存在可视性问题
  • Java SE5引入了 AtomicInteger、AtomicLong、AtomicReference 等原子类(应该强调的是,Atomic 类被设计用来构建 java.util.concurrent 中的类,因此只有在特殊情况下才在自己的代码中使用它们,即便使用了也不能认为万无一失。通常依赖于锁会更安全)。它们提供下面形式的跟新操作: boolean compareAndSet(expectedValue, updateValue);

上面说过临界区的概念,简单举个例子:

  1. synchronized(syncObject) {
  2. //This code can be accessd
  3. //by only one task at a time
  4. }

这也被称为同步控制块,在进入这段代码前,必须获得 syncObject 对象的锁,如果其他线程已经得到这个锁,那么就得等到锁释放以后,才能进入临界区。那么,问题来了:

为什么不是对整个方法进行同步,而是选择部分代码呢?这样有什么好处呢?

其实答案很简单,大概想一下就知道了。如果对方法使用 synchronized,那么这个对象只能被一个线程独占,而且这个方法可能只有1/10涉及到并发问题,在执行其他9/10的时候完全没有危险,但是其他线程就是没法并发执行,极大的限制了程序的性能。为了解决这点问题,就有了临界区。我们通过一个程序来看看临界区的优势:

  1. import java.util.ArrayList;
  2. import java.util.Collections;
  3. import java.util.List;
  4. import java.util.concurrent.ExecutorService;
  5. import java.util.concurrent.Executors;
  6. import java.util.concurrent.TimeUnit;
  7. import java.util.concurrent.atomic.AtomicInteger;
  8. /**
  9. * 定义一个坐标,重点在于 x 和 y 都有自加1操作.如果 x != y,会抛出一个自定义的运行时异常
  10. * @author niushuai
  11. *
  12. */
  13. class Pair {
  14. private int x, y;
  15. public Pair(int x, int y) {
  16. this.x = x;
  17. this.y = y;
  18. }
  19. public Pair() {
  20. this(0, 0);
  21. }
  22. public int getX() {
  23. return x;
  24. }
  25. public int getY() {
  26. return y;
  27. }
  28. public void incrementX() {
  29. x++;
  30. }
  31. public void incrementY() {
  32. y++;
  33. }
  34. public String toString() {
  35. return "x: " + x + ", y: " + y;
  36. }
  37. /**
  38. * 如果 x != y 则抛出异常
  39. * @author niushuai
  40. *
  41. */
  42. public class PairValuesNotEqualException extends RuntimeException {
  43. /**
  44. *
  45. */
  46. private static final long serialVersionUID = -7103813289682393079L;
  47. public PairValuesNotEqualException() {
  48. super("Pair values not equal: " + Pair.this);
  49. }
  50. }
  51. public void checkState() {
  52. if (x != y) {
  53. System.err.println("x != y");
  54. throw new PairValuesNotEqualException();
  55. }
  56. }
  57. }
  58. /**
  59. * 对 Pair 进行管理的模板方法,如何对非线程安全的 Pair 进行自增?
  60. * 是同步整个方法?还是同步临界区?——子类实现
  61. */
  62. abstract class PairManager {
  63. //check x != y 的次数
  64. AtomicInteger checkCounter = new AtomicInteger(0);
  65. protected Pair p = new Pair();
  66. //synchronizedList 为线程安全,无论在同步块内还是同步块外都是线程安全的
  67. private List<Pair> storage = Collections
  68. .synchronizedList(new ArrayList<Pair>());
  69. public synchronized Pair getPair() {
  70. return new Pair(p.getX(), p.getY());
  71. }
  72. protected void store(Pair p) {
  73. storage.add(p);
  74. try {
  75. TimeUnit.MILLISECONDS.sleep(50);
  76. } catch (InterruptedException e) {
  77. e.printStackTrace();
  78. }
  79. }
  80. // 如何增长?synchronized 修饰方法 还是 synchronized 修饰临界区?
  81. public abstract void increment();
  82. }
  83. /**
  84. * synchronized 修饰整个方法
  85. * @author niushuai
  86. *
  87. */
  88. class PairManager1 extends PairManager {
  89. public synchronized void increment() {
  90. p.incrementX();
  91. p.incrementY();
  92. store(getPair());
  93. }
  94. }
  95. /**
  96. * synchronized 修饰临界区
  97. * @author niushuai
  98. *
  99. */
  100. class PairManager2 extends PairManager {
  101. public void increment() {
  102. Pair temp;
  103. synchronized (this) {
  104. p.incrementX();
  105. p.incrementY();
  106. temp = getPair();
  107. }
  108. store(temp);
  109. }
  110. }
  111. /**
  112. * 任务类1,可以使用不同的 PairManager 对 Pair 进行自增操作
  113. * @author niushuai
  114. *
  115. */
  116. class PairManipulator implements Runnable {
  117. private PairManager pm;
  118. public PairManipulator(PairManager pm) {
  119. this.pm = pm;
  120. }
  121. @Override
  122. public void run() {
  123. while (true) {
  124. pm.increment();
  125. }
  126. }
  127. public String toString() {
  128. return "Pair: " + pm.getPair() + " checkCounter = "
  129. + pm.checkCounter.get();
  130. }
  131. }
  132. /**
  133. * 任务类2,不断的去检测 Pair 中的 x == y 状态
  134. * @author niushuai
  135. *
  136. */
  137. class PairChecker implements Runnable {
  138. private PairManager pm;
  139. public PairChecker(PairManager pm) {
  140. this.pm = pm;
  141. }
  142. @Override
  143. public void run() {
  144. while (true) {
  145. pm.checkCounter.incrementAndGet();
  146. pm.getPair().checkState();
  147. }
  148. }
  149. }
  150. public class CriticalSection {
  151. static void testApproaches(PairManager pman1, PairManager pman2) {
  152. ExecutorService exec = Executors.newCachedThreadPool();
  153. PairManipulator pm1 = new PairManipulator(pman1);
  154. PairManipulator pm2 = new PairManipulator(pman2);
  155. PairChecker pcheck1 = new PairChecker(pman1);
  156. PairChecker pcheck2 = new PairChecker(pman2);
  157. exec.execute(pm1);
  158. exec.execute(pm2);
  159. exec.execute(pcheck1);
  160. exec.execute(pcheck2);
  161. try {
  162. TimeUnit.MILLISECONDS.sleep(500);
  163. } catch (InterruptedException e) {
  164. System.out.println("Sleep interrupted");
  165. }
  166. System.out.println("pm1: " + pm1 + "\npm2: " + pm2);
  167. System.exit(0);
  168. }
  169. public static void main(String[] args) {
  170. PairManager pman1 = new PairManager1();
  171. PairManager pman2 = new PairManager2();
  172. testApproaches(pman1, pman2);
  173. }
  174. }/*output:
  175. pm1: Pair: x: 165, y: 165 checkCounter = 6
  176. pm2: Pair: x: 166, y: 166 checkCounter = 370681186
  177. */

这段代码略长一点点,但是很简单。总结来说就是两个线程跑自增操作,区别是一个用的同步整个方法,一个是同步临界区,然后又又两个线程去检查x 和 y 是否相等。那么,区别在哪里呢?

区别在于速度。如果同步整个方法,那么一个线程在该方法内部就独占这个方法的资源,无论这个方法中是否有线程安全的部分,而且实际上,非线程安全的代码往往远小于线程安全的代码。这样的话,这个线程在运行线程安全的代码时,其他代码也无法进入这个方法;而锁临界区就比较好一点(不是数量级的差别!),因为我只在非线程安全的地方加锁,那么在这个方法的其他地方就会有多个线程并发执行,这里的优点很容易想象,我就不多啰嗦了。

上面把同步基本说完了,但是还有一个比较好玩的是:可以在其他对象上同步。什么意思呢?

synchronized 块必须给定一个在其上进行同步的对象,并且最合理的方式是,使用其方法正在被调用的对象synchronized(this),这也是上面 PairManager2的做法,在这种方式中,如果获得了 synchronized 块上的锁,那么该对象其他的 synchronized 方法和临界区就不能被调用了。因此,如果在 this 上同步,临界区的效果就会直接缩小在同步的范围内部。而有时,必须在另一个对象上同步,但是这样做的话,就必须确保所有相关的任务都是在同一个对象上同步。下面有一个小例子:

  1. class DualSynch {
  2. private Object syncObject = new Object();
  3. public synchronized void f() {
  4. for(int i = 0; i < 5; i++) {
  5. System.out.println("f()");
  6. Thread.yield();
  7. }
  8. }
  9. public void g() {
  10. synchronized(syncObject) {
  11. for(int i = 0; i < 5; i++) {
  12. System.out.println("g()");
  13. Thread.yield();
  14. }
  15. }
  16. }
  17. }
  18. public class SyncObject {
  19. public static void main(String[] args) {
  20. final DualSynch ds = new DualSynch();
  21. new Thread() {
  22. public void run() {
  23. ds.f();
  24. }
  25. }.start();
  26. ds.g();
  27. }
  28. }/*output:
  29. f()
  30. g()
  31. f()
  32. f()
  33. g()
  34. f()
  35. g()
  36. f()
  37. g()
  38. g()
  39. */

这个例子中,通过 Thread 创建了一个线程,这个线程会持续输出5次 f()才会停止,因为它是方法级别的,就是 this 级别的。那么其他 synchronized 方法或者 synchronized(this)临界区都无法同时运行。但是上面输出是同时的。因为我们用了另一个对象锁进行同步。这样就达到了同时运行的目的。但是也有一点需要注意:

所有和某个对象锁有关的任务,都必须使用同一个对象锁。不要两个和 A 锁有关的任务,一个使用 A 加锁,一个使用 B 加锁,那么肯定会出问题。

虽然在《Java 编程思想》中仅仅占用了1页的篇幅,但是感觉很有用处。果然挖出了不少东西,于是单独写一篇文章分析 ThreadLocal 吧,详情请见理解 ThreadLocal

posted @ 2021-12-12 02:52  CharyGao  阅读(14)  评论(0)    收藏  举报