虚假唤醒 Resource有两个方法一个给number自增另一个自减,四个线程,分别扮演生产者和消费者的角色。当生产者将number加一后便进入等待状态并唤醒消费者,消费者消费完后进入等待状态并唤醒生产者。
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 71 class Resource { private int number = 0 ; public synchronized void increment () throws InterruptedException { if (number != 0 ){ this .wait(); } number++; System.out.println(Thread.currentThread().getName()+"\t" +number); this .notifyAll(); } public synchronized void decrement () throws InterruptedException { if (number == 0 ){ this .wait(); } number--; System.out.println(Thread.currentThread().getName()+"\t" +number); this .notifyAll(); } } public class ThreadWaitNotify { public static void main (String[] args) throws Exception { Resource resource = new Resource(); new Thread(() -> { for (int i = 1 ; i <= 10 ; i++) { try { resource.increment(); } catch (InterruptedException e) { e.printStackTrace(); } } },"A" ).start(); new Thread(() -> { for (int i = 1 ; i <= 10 ; i++) { try { resource.decrement(); } catch (InterruptedException e) { e.printStackTrace(); } } },"B" ).start(); new Thread(() -> { for (int i = 1 ; i <= 10 ; i++) { try { resource.increment(); } catch (InterruptedException e) { e.printStackTrace(); } } },"C" ).start(); new Thread(() -> { for (int i = 1 ; i <= 10 ; i++) { try { resource.decrement(); } catch (InterruptedException e) { e.printStackTrace(); } } },"D" ).start(); } }
一个理想的输出应当是下面这种形式
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 A 1 B 0 A 1 B 0 A 1 B 0 A 1 B 0 A 1 B 0 A 1 B 0 A 1 B 0 A 1 B 0 C 1 B 0 A 1 D 0 C 1 B 0 A 1 D 0 C 1 D 0 C 1 D 0 C 1 D 0 C 1 D 0 C 1 D 0 C 1 D 0 C 1 D 0 C 1 D 0
但实际的输出是这样:
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 A 1 B 0 A 1 B 0 C 1 B 0 A 1 B 0 C 1 D 0 B -1 B -2 B -3 B -4 B -5 B -6 A -5 D -6 D -7 D -8 D -9 D -10 D -11 D -12 D -13 D -14 C -13 A -12 C -11 A -10 C -9 A -8 C -7 A -6 C -5 A -4 C -3 A -2 C -1
这是因为在多线程环境下,由于CPU的时间片可能会在程序执行的任意时刻用完,假设线程B在执行时number==1
那么现在应该跳过if块,将number自减、然后唤醒其他线程,但此时间片用完,操作系统转而去执行线程D,由于之前还未将number自减所以number现在仍等于1所以线程D将顺利的执行完此时number变为0。然后时间片轮到线程B它从上次停下的地方继续执行,number将变为-1,至此number的值将永远不会变成0,也就是在B的时间片里它永远都不会调用wait()
方法,线程B将一直执行下去,直到for循环结束。这种现象称为虚假唤醒,即由于if所判断的对象被其他线程改变,而进入了不该进入的if块,或跳过的不该跳过的if块,从而唤醒错误的线程的现象。所以在线程唤醒的判断上一定使用的是while而不是if因为if只会判断一次,而while是会做重复判断的 也就是线程切回来的时候while会再检查一遍,从而避免的进入错误代码块的问题。
synchronized的范围 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 class Resource { public synchronized void add () throws Exception { System.out.println("add" ); } public synchronized void remove () throws Exception { System.out.println("remove" ); } } public class Lock { public static void main (String[] args) throws InterruptedException { Resource resource = new Resource(); new Thread(() -> { try { Resource.add(); } catch (Exception e){ e.printStackTrace(); } },"A" ).start(); Thread.sleep(200 ); new Thread(() -> { try { Resource.remove(); } catch (Exception e){ e.printStackTrace(); } },"B" ).start(); } }
Resource
类有两个同步方法,但synchronized锁的不是某个方法,它锁的是整个对象中所有同步方法。所以A,B线程绝对不会是同时持有Recourse
类,一定是A先执行,然后B再执行。也就是说某一时刻内,只能有唯一一个线程去访问synchronized方法。
CopyOnWriteArrayList原理 CopyOnWriteArrayList源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public boolean add (E e) { final ReentrantLock lock = this .lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len + 1 ); newElements[len] = e; setArray(newElements); return true ; } finally { lock.unlock(); } }
CopyOnWrite容器即写时复制的容器。往容器添加元素的时候,不直接往当前容器Object[]添加,而是先将当前容器Object[]进行复制,复制到一个新的容器Object[] newElements,然后往新容器里添加元素,加完后再将原来的容器的引用指向新的容器。这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器利用的是读写分离的思想,读和写用的是不同的容器。