diff --git "a/01.\345\271\266\345\217\221\347\274\226\347\250\213\347\232\204\344\274\230\347\274\272\347\202\271/\345\271\266\345\217\221\347\274\226\347\250\213\347\232\204\344\274\230\347\274\272\347\202\271.md" "b/01.\345\271\266\345\217\221\347\274\226\347\250\213\347\232\204\344\274\230\347\274\272\347\202\271/\345\271\266\345\217\221\347\274\226\347\250\213\347\232\204\344\274\230\347\274\272\347\202\271.md" deleted file mode 100644 index e45bdd2..0000000 --- "a/01.\345\271\266\345\217\221\347\274\226\347\250\213\347\232\204\344\274\230\347\274\272\347\202\271/\345\271\266\345\217\221\347\274\226\347\250\213\347\232\204\344\274\230\347\274\272\347\202\271.md" +++ /dev/null @@ -1,124 +0,0 @@ -一直以来并发编程对于刚入行的小白来说总是觉得高深莫测,于是乎,就诞生了想写点东西记录下,以提升理解和堆并发编程的认知。为什么需要用的并发?凡事总有好坏两面,之间的trade-off是什么,也就是说并发编程具有哪些缺点?以及在进行并发编程时应该了解和掌握的概念是什么?这篇文章主要以这三个问题来谈一谈。 -# 1. 为什么要用到并发 # -一直以来,硬件的发展极其迅速,也有一个很著名的"摩尔定律",可能会奇怪明明讨论的是并发编程为什么会扯到了硬件的发展,这其中的关系应该是多核CPU的发展为并发编程提供的硬件基础。摩尔定律并不是一种自然法则或者是物理定律,它只是基于认为观测数据后,对未来的一种预测。按照所预测的速度,我们的计算能力会按照指数级别的速度增长,不久以后会拥有超强的计算能力,正是在畅想未来的时候,2004年,Intel宣布4GHz芯片的计划推迟到2005年,然后在2004年秋季,Intel宣布彻底取消4GHz的计划,也就是说摩尔定律的有效性超过了半个世纪戛然而止。但是,聪明的硬件工程师并没有停止研发的脚步,他们为了进一步提升计算速度,而不是再追求单独的计算单元,而是将多个计算单元整合到了一起,也就是形成了多核CPU。短短十几年的时间,家用型CPU,比如Intel i7就可以达到4核心甚至8核心。而专业服务器则通常可以达到几个独立的CPU,每一个CPU甚至拥有多达8个以上的内核。因此,摩尔定律似乎在CPU核心扩展上继续得到体验。因此,多核的CPU的背景下,催生了并发编程的趋势,通过**并发编程的形式可以将多核CPU的计算能力发挥到极致,性能得到提升**。 - -顶级计算机科学家Donald Ervin Knuth如此评价这种情况:在我看来,这种现象(并发)或多或少是由于硬件设计者无计可施了导致的,他们将摩尔定律的责任推给了软件开发者。 - -另外,在特殊的业务场景下先天的就适合于并发编程。比如在图像处理领域,一张1024X768像素的图片,包含达到78万6千多个像素。即时将所有的像素遍历一边都需要很长的时间,面对如此复杂的计算量就需要充分利用多核的计算的能力。又比如当我们在网上购物时,为了提升响应速度,需要拆分,减库存,生成订单等等这些操作,就可以进行拆分利用多线程的技术完成。**面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分** 。正是因为这些优点,使得多线程技术能够得到重视,也是一名CS学习者应该掌握的: - -- 充分利用多核CPU的计算能力; -- 方便进行业务拆分,提升应用性能 -# 2. 并发编程有哪些缺点 # -多线程技术有这么多的好处,难道就没有一点缺点么,就在任何场景下就一定适用么?很显然不是。 - - -## 2.1 频繁的上下文切换 ## - -时间片是CPU分配给各个线程的时间,因为时间非常短,所以CPU不断通过切换线程,让我们觉得多个线程是同时执行的,时间片一般是几十毫秒。而每次切换时,需要保存当前的状态起来,以便能够进行恢复先前状态,而这个切换时非常损耗性能,过于频繁反而无法发挥出多线程编程的优势。通常减少上下文切换可以采用无锁并发编程,CAS算法,使用最少的线程和使用协程。 - -- 无锁并发编程:可以参照concurrentHashMap锁分段的思想,不同的线程处理不同段的数据,这样在多线程竞争的条件下,可以减少上下文切换的时间。 - -- CAS算法,利用Atomic下使用CAS算法来更新数据,使用了乐观锁,可以有效的减少一部分不必要的锁竞争带来的上下文切换 -- 使用最少线程:避免创建不需要的线程,比如任务很少,但是创建了很多的线程,这样会造成大量的线程都处于等待状态 - -- 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换 - -由于上下文切换也是个相对比较耗时的操作,所以在"java并发编程的艺术"一书中有过一个实验,并发累加未必会比串行累加速度要快。 可以使用**Lmbench3测量上下文切换的时长** **vmstat测量上下文切换次数** - - -## 2.2 线程安全 ## - -多线程编程中最难以把握的就是临界区线程安全问题,稍微不注意就会出现死锁的情况,一旦产生死锁就会造成系统功能不可用。 - - - public class DeadLockDemo { - private static String resource_a = "A"; - private static String resource_b = "B"; - - public static void main(String[] args) { - deadLock(); - } - - public static void deadLock() { - Thread threadA = new Thread(new Runnable() { - @Override - public void run() { - synchronized (resource_a) { - System.out.println("get resource a"); - try { - Thread.sleep(3000); - synchronized (resource_b) { - System.out.println("get resource b"); - } - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - } - }); - Thread threadB = new Thread(new Runnable() { - @Override - public void run() { - synchronized (resource_b) { - System.out.println("get resource b"); - synchronized (resource_a) { - System.out.println("get resource a"); - } - } - } - }); - threadA.start(); - threadB.start(); - - } - } - -在上面的这个demo中,开启了两个线程threadA, threadB,其中threadA占用了resource_a, 并等待被threadB释放的resource _b。threadB占用了resource _b正在等待被threadA释放的resource _a。因此threadA,threadB出现线程安全的问题,形成死锁。同样可以通过jps,jstack证明这种推论: - - "Thread-1": - waiting to lock monitor 0x000000000b695360 (object 0x00000007d5ff53a8, a java.lang.String), - which is held by "Thread-0" - "Thread-0": - waiting to lock monitor 0x000000000b697c10 (object 0x00000007d5ff53d8, a java.lang.String), - which is held by "Thread-1" - - Java stack information for the threads listed above: - =================================================== - "Thread-1": - at learn.DeadLockDemo$2.run(DeadLockDemo.java:34) - - waiting to lock <0x00000007d5ff53a8(a java.lang.String) - - locked <0x00000007d5ff53d8(a java.lang.String) - at java.lang.Thread.run(Thread.java:722) - "Thread-0": - at learn.DeadLockDemo$1.run(DeadLockDemo.java:20) - - waiting to lock <0x00000007d5ff53d8(a java.lang.String) - - locked <0x00000007d5ff53a8(a java.lang.String) - at java.lang.Thread.run(Thread.java:722) - - Found 1 deadlock. - - -如上所述,完全可以看出当前死锁的情况。 - -那么,通常可以用如下方式避免死锁的情况: - -1. 避免一个线程同时获得多个锁; -2. 避免一个线程在锁内部占有多个资源,尽量保证每个锁只占用一个资源; -3. 尝试使用定时锁,使用lock.tryLock(timeOut),当超时等待时当前线程不会阻塞; -4. 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况 - -所以,如何正确的使用多线程编程技术有很大的学问,比如如何保证线程安全,如何正确理解由于JMM内存模型在原子性,有序性,可见性带来的问题,比如数据脏读,DCL等这些问题(在后续篇幅会讲述)。而在学习多线程编程技术的过程中也会让你收获颇丰。 - - -# 3. 应该了解的概念 # -## 3.1 同步VS异步 ## -同步和异步通常用来形容一次方法调用。同步方法调用一开始,调用者必须等待被调用的方法结束后,调用者后面的代码才能执行。而异步调用,指的是,调用者不用管被调用方法是否完成,都会继续执行后面的代码,当被调用的方法完成后会通知调用者。比如,在超时购物,如果一件物品没了,你得等仓库人员跟你调货,直到仓库人员跟你把货物送过来,你才能继续去收银台付款,这就类似同步调用。而异步调用了,就像网购,你在网上付款下单后,什么事就不用管了,该干嘛就干嘛去了,当货物到达后你收到通知去取就好。 - -## 3.2 并发与并行 ## -并发和并行是十分容易混淆的概念。并发指的是多个任务交替进行,而并行则是指真正意义上的“同时进行”。实际上,如果系统内只有一个CPU,而使用多线程时,那么真实系统环境下不能并行,只能通过切换时间片的方式交替进行,而成为并发执行任务。真正的并行也只能出现在拥有多个CPU的系统中。 - -## 3.3 阻塞和非阻塞 ## -阻塞和非阻塞通常用来形容多线程间的相互影响,比如一个线程占有了临界区资源,那么其他线程需要这个资源就必须进行等待该资源的释放,会导致等待的线程挂起,这种情况就是阻塞,而非阻塞就恰好相反,它强调没有一个线程可以阻塞其他线程,所有的线程都会尝试地往前运行。 - -## 3.4 临界区 ## -临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每个线程使用时,一旦临界区资源被一个线程占有,那么其他线程必须等待。 \ No newline at end of file diff --git "a/02.\347\272\277\347\250\213\347\232\204\347\212\266\346\200\201\350\275\254\346\215\242\344\273\245\345\217\212\345\237\272\346\234\254\346\223\215\344\275\234/futureTask\346\216\245\345\217\243\345\256\236\347\216\260\345\205\263\347\263\273.png" "b/02.\347\272\277\347\250\213\347\232\204\347\212\266\346\200\201\350\275\254\346\215\242\344\273\245\345\217\212\345\237\272\346\234\254\346\223\215\344\275\234/futureTask\346\216\245\345\217\243\345\256\236\347\216\260\345\205\263\347\263\273.png" deleted file mode 100644 index 1399637..0000000 Binary files "a/02.\347\272\277\347\250\213\347\232\204\347\212\266\346\200\201\350\275\254\346\215\242\344\273\245\345\217\212\345\237\272\346\234\254\346\223\215\344\275\234/futureTask\346\216\245\345\217\243\345\256\236\347\216\260\345\205\263\347\263\273.png" and /dev/null differ diff --git "a/02.\347\272\277\347\250\213\347\232\204\347\212\266\346\200\201\350\275\254\346\215\242\344\273\245\345\217\212\345\237\272\346\234\254\346\223\215\344\275\234/\344\270\255\346\226\255\347\272\277\347\250\213\346\226\271\346\263\225.png" "b/02.\347\272\277\347\250\213\347\232\204\347\212\266\346\200\201\350\275\254\346\215\242\344\273\245\345\217\212\345\237\272\346\234\254\346\223\215\344\275\234/\344\270\255\346\226\255\347\272\277\347\250\213\346\226\271\346\263\225.png" deleted file mode 100644 index 74112ae..0000000 Binary files "a/02.\347\272\277\347\250\213\347\232\204\347\212\266\346\200\201\350\275\254\346\215\242\344\273\245\345\217\212\345\237\272\346\234\254\346\223\215\344\275\234/\344\270\255\346\226\255\347\272\277\347\250\213\346\226\271\346\263\225.png" and /dev/null differ diff --git "a/02.\347\272\277\347\250\213\347\232\204\347\212\266\346\200\201\350\275\254\346\215\242\344\273\245\345\217\212\345\237\272\346\234\254\346\223\215\344\275\234/\347\272\277\347\250\213\347\212\266\346\200\201.png" "b/02.\347\272\277\347\250\213\347\232\204\347\212\266\346\200\201\350\275\254\346\215\242\344\273\245\345\217\212\345\237\272\346\234\254\346\223\215\344\275\234/\347\272\277\347\250\213\347\212\266\346\200\201.png" deleted file mode 100644 index 2128ac4..0000000 Binary files "a/02.\347\272\277\347\250\213\347\232\204\347\212\266\346\200\201\350\275\254\346\215\242\344\273\245\345\217\212\345\237\272\346\234\254\346\223\215\344\275\234/\347\272\277\347\250\213\347\212\266\346\200\201.png" and /dev/null differ diff --git "a/02.\347\272\277\347\250\213\347\232\204\347\212\266\346\200\201\350\275\254\346\215\242\344\273\245\345\217\212\345\237\272\346\234\254\346\223\215\344\275\234/\347\272\277\347\250\213\347\212\266\346\200\201\350\275\254\346\215\242\344\273\245\345\217\212\345\237\272\346\234\254\346\223\215\344\275\234.md" "b/02.\347\272\277\347\250\213\347\232\204\347\212\266\346\200\201\350\275\254\346\215\242\344\273\245\345\217\212\345\237\272\346\234\254\346\223\215\344\275\234/\347\272\277\347\250\213\347\212\266\346\200\201\350\275\254\346\215\242\344\273\245\345\217\212\345\237\272\346\234\254\346\223\215\344\275\234.md" deleted file mode 100644 index 7458d85..0000000 --- "a/02.\347\272\277\347\250\213\347\232\204\347\212\266\346\200\201\350\275\254\346\215\242\344\273\245\345\217\212\345\237\272\346\234\254\346\223\215\344\275\234/\347\272\277\347\250\213\347\212\266\346\200\201\350\275\254\346\215\242\344\273\245\345\217\212\345\237\272\346\234\254\346\223\215\344\275\234.md" +++ /dev/null @@ -1,255 +0,0 @@ -在上一篇文章中[并发编程的优缺点](https://github.com/CL0610/Java-concurrency/blob/master/1.并发编程的优缺点/并发编程的优缺点.md)谈到了为什么花功夫去学习并发编程的技术,也就是说我们必须了解到并发编程的优缺点,我们在什么情况下可以去考虑开启多个线程去实现我们的业务,当然使用多线程我们应该着重注意一些什么,在上一篇文章中会有一些讨论。那么,说了这么多,无论是针对面试还是实际工作中作为一名软件开发人员都应该具备这样的技能。万事开头难,接下来就应该了解如何新建一个线程?线程状态是怎样转换的?关于线程状态的操作是怎样的?这篇文章就主要围绕这三个方面来聊一聊。 -# 1. 新建线程 # -一个java程序从main()方法开始执行,然后按照既定的代码逻辑执行,看似没有其他线程参与,但实际上java程序天生就是一个多线程程序,包含了:(1)分发处理发送给给JVM信号的线程;(2)调用对象的finalize方法的线程;(3)清除Reference的线程;(4)main线程,用户程序的入口。那么,如何在用户程序中新建一个线程了,只要有三种方式: - -1. 通过继承Thread类,重写run方法; -2. 通过实现runable接口; -3. 通过实现callable接口这三种方式,下面看具体demo。 - - public class CreateThreadDemo { - - public static void main(String[] args) { - //1.继承Thread - Thread thread = new Thread() { - @Override - public void run() { - System.out.println("继承Thread"); - super.run(); - } - }; - thread.start(); - //2.实现runable接口 - Thread thread1 = new Thread(new Runnable() { - @Override - public void run() { - System.out.println("实现runable接口"); - } - }); - thread1.start(); - //3.实现callable接口 - ExecutorService service = Executors.newSingleThreadExecutor(); - Future future = service.submit(new Callable() { - @Override - public String call() throws Exception { - return "通过实现Callable接口"; - } - }); - try { - String result = future.get(); - System.out.println(result); - } catch (InterruptedException e) { - e.printStackTrace(); - } catch (ExecutionException e) { - e.printStackTrace(); - } - } - - } - -三种新建线程的方式具体看以上注释,需要主要的是: - - -- 由于java不能多继承可以实现多个接口,因此,在创建线程的时候尽量多考虑采用实现接口的形式; -- 实现callable接口,提交给ExecutorService返回的是异步执行的结果,另外,通常也可以利用FutureTask(Callable callable)将callable进行包装然后FeatureTask提交给ExecutorsService。如图, - -![FutureTask接口实现关系](https://github.com/CL0610/Java-concurrency/blob/master/2.%E7%BA%BF%E7%A8%8B%E7%9A%84%E7%8A%B6%E6%80%81%E8%BD%AC%E6%8D%A2%E4%BB%A5%E5%8F%8A%E5%9F%BA%E6%9C%AC%E6%93%8D%E4%BD%9C/futureTask%E6%8E%A5%E5%8F%A3%E5%AE%9E%E7%8E%B0%E5%85%B3%E7%B3%BB.png) - - -另外由于FeatureTask也实现了Runable接口也可以利用上面第二种方式(实现Runable接口)来新建线程; -- 可以通过Executors将Runable转换成Callable,具体方法是:Callable callable(Runnable task, T result), Callable callable(Runnable task)。 - -# 2. 线程状态转换 # - - -![线程状态转换图](https://github.com/CL0610/Java-concurrency/blob/master/2.%E7%BA%BF%E7%A8%8B%E7%9A%84%E7%8A%B6%E6%80%81%E8%BD%AC%E6%8D%A2%E4%BB%A5%E5%8F%8A%E5%9F%BA%E6%9C%AC%E6%93%8D%E4%BD%9C/%E7%BA%BF%E7%A8%8B%E7%8A%B6%E6%80%81%E8%BD%AC%E6%8D%A2%E5%85%B3%E7%B3%BB.png) - - - -此图来源于《JAVA并发编程的艺术》一书中,线程是会在不同的状态间进行转换的,java线程线程转换图如上图所示。线程创建之后调用start()方法开始运行,当调用wait(),join(),LockSupport.lock()方法线程会进入到**WAITING**状态,而同样的wait(long timeout),sleep(long),join(long),LockSupport.parkNanos(),LockSupport.parkUtil()增加了超时等待的功能,也就是调用这些方法后线程会进入**TIMED_WAITING**状态,当超时等待时间到达后,线程会切换到Runable的状态,另外当WAITING和TIMED _WAITING状态时可以通过Object.notify(),Object.notifyAll()方法使线程转换到Runable状态。当线程出现资源竞争时,即等待获取锁的时候,线程会进入到**BLOCKED**阻塞状态,当线程获取锁时,线程进入到Runable状态。线程运行结束后,线程进入到**TERMINATED**状态,状态转换可以说是线程的生命周期。另外需要注意的是: - -- 当线程进入到synchronized方法或者synchronized代码块时,线程切换到的是BLOCKED状态,而使用java.util.concurrent.locks下lock进行加锁的时候线程切换的是WAITING或者TIMED_WAITING状态,因为lock会调用LockSupport的方法。 - -用一个表格将上面六种状态进行一个总结归纳。 - -![JAVA线程的状态](https://github.com/CL0610/Java-concurrency/blob/master/2.%E7%BA%BF%E7%A8%8B%E7%9A%84%E7%8A%B6%E6%80%81%E8%BD%AC%E6%8D%A2%E4%BB%A5%E5%8F%8A%E5%9F%BA%E6%9C%AC%E6%93%8D%E4%BD%9C/%E7%BA%BF%E7%A8%8B%E7%8A%B6%E6%80%81.png) - - -# 3. 线程状态的基本操作 # -除了新建一个线程外,线程在生命周期内还有需要基本操作,而这些操作会成为线程间一种通信方式,比如使用中断(interrupted)方式通知实现线程间的交互等等,下面就将具体说说这些操作。 - -## 3.1. interrupted ## - -中断可以理解为线程的一个标志位,它表示了一个运行中的线程是否被其他线程进行了中断操作。中断好比其他线程对该线程打了一个招呼。其他线程可以调用该线程的interrupt()方法对其进行中断操作,同时该线程可以调用 -isInterrupted()来感知其他线程对其自身的中断操作,从而做出响应。另外,同样可以调用Thread的静态方法 -interrupted()对当前线程进行中断操作,该方法会清除中断标志位。**需要注意的是,当抛出InterruptedException时候,会清除中断标志位,也就是说在调用isInterrupted会返回false。** - -![线程中断的方法](https://github.com/CL0610/Java-concurrency/blob/master/2.%E7%BA%BF%E7%A8%8B%E7%9A%84%E7%8A%B6%E6%80%81%E8%BD%AC%E6%8D%A2%E4%BB%A5%E5%8F%8A%E5%9F%BA%E6%9C%AC%E6%93%8D%E4%BD%9C/%E4%B8%AD%E6%96%AD%E7%BA%BF%E7%A8%8B%E6%96%B9%E6%B3%95.png) - - - - -下面结合具体的实例来看一看 - - public class InterruptDemo { - public static void main(String[] args) throws InterruptedException { - //sleepThread睡眠1000ms - final Thread sleepThread = new Thread() { - @Override - public void run() { - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - super.run(); - } - }; - //busyThread一直执行死循环 - Thread busyThread = new Thread() { - @Override - public void run() { - while (true) ; - } - }; - sleepThread.start(); - busyThread.start(); - sleepThread.interrupt(); - busyThread.interrupt(); - while (sleepThread.isInterrupted()) ; - System.out.println("sleepThread isInterrupted: " + sleepThread.isInterrupted()); - System.out.println("busyThread isInterrupted: " + busyThread.isInterrupted()); - } - } - -输出结果 -> sleepThread isInterrupted: false -> busyThread isInterrupted: true - -开启了两个线程分别为sleepThread和BusyThread, sleepThread睡眠1s,BusyThread执行死循环。然后分别对着两个线程进行中断操作,可以看出sleepThread抛出InterruptedException后清除标志位,而busyThread就不会清除标志位。 - -另外,同样可以通过中断的方式实现线程间的简单交互, while (sleepThread.isInterrupted()) 表示在Main中会持续监测sleepThread,一旦sleepThread的中断标志位清零,即sleepThread.isInterrupted()返回为false时才会继续Main线程才会继续往下执行。因此,中断操作可以看做线程间一种简便的交互方式。一般在**结束线程时通过中断标志位或者标志位的方式可以有机会去清理资源,相对于武断而直接的结束线程,这种方式要优雅和安全。** - -## 3.2. join ## - -join方法可以看做是线程间协作的一种方式,很多时候,一个线程的输入可能非常依赖于另一个线程的输出,这就像两个好基友,一个基友先走在前面突然看见另一个基友落在后面了,这个时候他就会在原处等一等这个基友,等基友赶上来后,就两人携手并进。其实线程间的这种协作方式也符合现实生活。在软件开发的过程中,从客户那里获取需求后,需要经过需求分析师进行需求分解后,这个时候产品,开发才会继续跟进。如果一个线程实例A执行了threadB.join(),其含义是:当前线程A会等待threadB线程终止后threadA才会继续执行。关于join方法一共提供如下这些方法: -> public final synchronized void join(long millis) -> public final synchronized void join(long millis, int nanos) -> public final void join() throws InterruptedException - -Thread类除了提供join()方法外,另外还提供了超时等待的方法,如果线程threadB在等待的时间内还没有结束的话,threadA会在超时之后继续执行。join方法源码关键是: - - while (isAlive()) { - wait(0); - } - -可以看出来当前等待对象threadA会一直阻塞,直到被等待对象threadB结束后即isAlive()返回false的时候才会结束while循环,当threadB退出时会调用notifyAll()方法通知所有的等待线程。下面用一个具体的例子来说说join方法的使用: - - public class JoinDemo { - public static void main(String[] args) { - Thread previousThread = Thread.currentThread(); - for (int i = 1; i <= 10; i++) { - Thread curThread = new JoinThread(previousThread); - curThread.start(); - previousThread = curThread; - } - } - - static class JoinThread extends Thread { - private Thread thread; - - public JoinThread(Thread thread) { - this.thread = thread; - } - - @Override - public void run() { - try { - thread.join(); - System.out.println(thread.getName() + " terminated."); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - } - } - -输出结果为: - -> main terminated. -> Thread-0 terminated. -> Thread-1 terminated. -> Thread-2 terminated. -> Thread-3 terminated. -> Thread-4 terminated. -> Thread-5 terminated. -> Thread-6 terminated. -> Thread-7 terminated. -> Thread-8 terminated. - -在上面的例子中一个创建了10个线程,每个线程都会等待前一个线程结束才会继续运行。可以通俗的理解成接力,前一个线程将接力棒传给下一个线程,然后又传给下一个线程...... - -## 3.3 sleep ## -public static native void sleep(long millis)方法显然是Thread的静态方法,很显然它是让当前线程按照指定的时间休眠,其休眠时间的精度取决于处理器的计时器和调度器。需要注意的是如果当前线程获得了锁,sleep方法并不会失去锁。sleep方法经常拿来与Object.wait()方法进行比价,这也是面试经常被问的地方。 -> **sleep() VS wait()** - -两者主要的区别: - -1. sleep()方法是Thread的静态方法,而wait是Object实例方法 -2. wait()方法必须要在同步方法或者同步块中调用,也就是必须已经获得对象锁。而sleep()方法没有这个限制可以在任何地方种使用。另外,wait()方法会释放占有的对象锁,使得该线程进入等待池中,等待下一次获取资源。而sleep()方法只是会让出CPU并不会释放掉对象锁; -3. sleep()方法在休眠时间达到后如果再次获得CPU时间片就会继续执行,而wait()方法必须等待Object.notift/Object.notifyAll通知后,才会离开等待池,并且再次获得CPU时间片才会继续执行。 - - -## 3.4 yield ## -public static native void yield();这是一个静态方法,一旦执行,它会是当前线程让出CPU,但是,需要注意的是,让出的CPU并不是代表当前线程不再运行了,如果在下一次竞争中,又获得了CPU时间片当前线程依然会继续运行。另外,让出的时间片只会分配**给当前线程相同优先级**的线程。什么是线程优先级了?下面就来具体聊一聊。 - -现代操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当前时间片用完后就会发生线程调度,并等待这下次分配。线程分配到的时间多少也就决定了线程使用处理器资源的多少,而线程优先级就是决定线程需要或多或少分配一些处理器资源的线程属性。 - -在Java程序中,通过一个**整型成员变量Priority**来控制优先级,优先级的范围从1~10.在构建线程的时候可以通过**setPriority(int)**方法进行设置,默认优先级为5,优先级高的线程相较于优先级低的线程优先获得处理器时间片。需要注意的是在不同JVM以及操作系统上,线程规划存在差异,有些操作系统甚至会忽略线程优先级的设定。 - -另外需要注意的是,sleep()和yield()方法,同样都是当前线程会交出处理器资源,而它们不同的是,sleep()交出来的时间片其他线程都可以去竞争,也就是说都有机会获得当前线程让出的时间片。而yield()方法只允许与当前线程具有相同优先级的线程能够获得释放出来的CPU时间片。 - -# 4.守护线程Daemon # -守护线程是一种特殊的线程,就和它的名字一样,它是系统的守护者,在后台默默地守护一些系统服务,比如垃圾回收线程,JIT线程就可以理解守护线程。与之对应的就是用户线程,用户线程就可以认为是系统的工作线程,它会完成整个系统的业务操作。用户线程完全结束后就意味着整个系统的业务任务全部结束了,因此系统就没有对象需要守护的了,守护线程自然而然就会退。当一个Java应用,只有守护线程的时候,虚拟机就会自然退出。下面以一个简单的例子来表述Daemon线程的使用。 - - public class DaemonDemo { - public static void main(String[] args) { - Thread daemonThread = new Thread(new Runnable() { - @Override - public void run() { - while (true) { - try { - System.out.println("i am alive"); - Thread.sleep(500); - } catch (InterruptedException e) { - e.printStackTrace(); - } finally { - System.out.println("finally block"); - } - } - } - }); - daemonThread.setDaemon(true); - daemonThread.start(); - //确保main线程结束前能给daemonThread能够分到时间片 - try { - Thread.sleep(800); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - } - -输出结果为: - -> i am alive -> finally block -> i am alive - -上面的例子中daemodThread run方法中是一个while死循环,会一直打印,但是当main线程结束后daemonThread就会退出所以不会出现死循环的情况。main线程先睡眠800ms保证daemonThread能够拥有一次时间片的机会,也就是说可以正常执行一次打印“i am alive”操作和一次finally块中"finally block"操作。紧接着main 线程结束后,daemonThread退出,这个时候只打印了"i am alive"并没有打印finnal块中的。因此,这里需要注意的是**守护线程在退出的时候并不会执行finnaly块中的代码,所以将释放资源等操作不要放在finnaly块中执行,这种操作是不安全的** - -线程可以通过setDaemon(true)的方法将线程设置为守护线程。并且需要注意的是设置守护线程要先于start()方法,否则会报 -> Exception in thread "main" java.lang.IllegalThreadStateException -> at java.lang.Thread.setDaemon(Thread.java:1365) -> at learn.DaemonDemo.main(DaemonDemo.java:19) - -这样的异常,但是该线程还是会执行,只不过会当做正常的用户线程执行。 diff --git "a/02.\347\272\277\347\250\213\347\232\204\347\212\266\346\200\201\350\275\254\346\215\242\344\273\245\345\217\212\345\237\272\346\234\254\346\223\215\344\275\234/\347\272\277\347\250\213\347\212\266\346\200\201\350\275\254\346\215\242\345\205\263\347\263\273.png" "b/02.\347\272\277\347\250\213\347\232\204\347\212\266\346\200\201\350\275\254\346\215\242\344\273\245\345\217\212\345\237\272\346\234\254\346\223\215\344\275\234/\347\272\277\347\250\213\347\212\266\346\200\201\350\275\254\346\215\242\345\205\263\347\263\273.png" deleted file mode 100644 index 58f2620..0000000 Binary files "a/02.\347\272\277\347\250\213\347\232\204\347\212\266\346\200\201\350\275\254\346\215\242\344\273\245\345\217\212\345\237\272\346\234\254\346\223\215\344\275\234/\347\272\277\347\250\213\347\212\266\346\200\201\350\275\254\346\215\242\345\205\263\347\263\273.png" and /dev/null differ diff --git "a/03.java\345\206\205\345\255\230\346\250\241\345\236\213\344\273\245\345\217\212happens-before\350\247\204\345\210\231/JMM\345\206\205\345\255\230\347\273\223\346\236\204\346\212\275\350\261\241\347\273\223\346\236\204\347\244\272\346\204\217\345\233\276.png" "b/03.java\345\206\205\345\255\230\346\250\241\345\236\213\344\273\245\345\217\212happens-before\350\247\204\345\210\231/JMM\345\206\205\345\255\230\347\273\223\346\236\204\346\212\275\350\261\241\347\273\223\346\236\204\347\244\272\346\204\217\345\233\276.png" deleted file mode 100644 index b6a0cf1..0000000 Binary files "a/03.java\345\206\205\345\255\230\346\250\241\345\236\213\344\273\245\345\217\212happens-before\350\247\204\345\210\231/JMM\345\206\205\345\255\230\347\273\223\346\236\204\346\212\275\350\261\241\347\273\223\346\236\204\347\244\272\346\204\217\345\233\276.png" and /dev/null differ diff --git "a/03.java\345\206\205\345\255\230\346\250\241\345\236\213\344\273\245\345\217\212happens-before\350\247\204\345\210\231/JMM\345\261\202\347\272\247\345\233\276.png" "b/03.java\345\206\205\345\255\230\346\250\241\345\236\213\344\273\245\345\217\212happens-before\350\247\204\345\210\231/JMM\345\261\202\347\272\247\345\233\276.png" deleted file mode 100644 index ed51393..0000000 Binary files "a/03.java\345\206\205\345\255\230\346\250\241\345\236\213\344\273\245\345\217\212happens-before\350\247\204\345\210\231/JMM\345\261\202\347\272\247\345\233\276.png" and /dev/null differ diff --git "a/03.java\345\206\205\345\255\230\346\250\241\345\236\213\344\273\245\345\217\212happens-before\350\247\204\345\210\231/JMM\350\256\276\350\256\241\347\244\272\346\204\217\345\233\276.png" "b/03.java\345\206\205\345\255\230\346\250\241\345\236\213\344\273\245\345\217\212happens-before\350\247\204\345\210\231/JMM\350\256\276\350\256\241\347\244\272\346\204\217\345\233\276.png" deleted file mode 100644 index 137cd3d..0000000 Binary files "a/03.java\345\206\205\345\255\230\346\250\241\345\236\213\344\273\245\345\217\212happens-before\350\247\204\345\210\231/JMM\350\256\276\350\256\241\347\244\272\346\204\217\345\233\276.png" and /dev/null differ diff --git "a/03.java\345\206\205\345\255\230\346\250\241\345\236\213\344\273\245\345\217\212happens-before\350\247\204\345\210\231/Java\345\206\205\345\255\230\346\250\241\345\236\213\344\273\245\345\217\212happens-before.md" "b/03.java\345\206\205\345\255\230\346\250\241\345\236\213\344\273\245\345\217\212happens-before\350\247\204\345\210\231/Java\345\206\205\345\255\230\346\250\241\345\236\213\344\273\245\345\217\212happens-before.md" deleted file mode 100644 index e9ba673..0000000 --- "a/03.java\345\206\205\345\255\230\346\250\241\345\236\213\344\273\245\345\217\212happens-before\350\247\204\345\210\231/Java\345\206\205\345\255\230\346\250\241\345\236\213\344\273\245\345\217\212happens-before.md" +++ /dev/null @@ -1,145 +0,0 @@ -# 1. JMM的介绍 # -在上一篇文章中总结了[线程的状态转换和一些基本操作](http://www.jianshu.com/p/f65ea68a4a7f),对多线程已经有一点基本的认识了,如果多线程编程只有这么简单,那我们就不必费劲周折的去学习它了。在多线程中稍微不注意就会出现线程安全问题,那么什么是线程安全问题?我的认识是,在多线程下代码执行的结果与预期正确的结果不一致,该代码就是线程不安全的,否则则是线程安全的。虽然这种回答似乎不能获取什么内容,可以google下。在<<深入理解Java虚拟机>>中看到的定义。原文如下: -当多个线程访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替运行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获取正确的结果,那这个对象是线程安全的。 - -关于定义的理解这是一个仁者见仁智者见智的事情。出现线程安全的问题一般是因为**主内存和工作内存数据不一致性**和**重排序**导致的,而解决线程安全的问题最重要的就是理解这两种问题是怎么来的,那么,理解它们的核心在于理解java内存模型(JMM)。 - -在多线程条件下,多个线程肯定会相互协作完成一件事情,一般来说就会涉及到**多个线程间相互通信告知彼此的状态以及当前的执行结果**等,另外,为了性能优化,还会**涉及到编译器指令重排序和处理器指令重排序**。下面会一一来聊聊这些知识。 - -#2. 内存模型抽象结构 -线程间协作通信可以类比人与人之间的协作的方式,在现实生活中,之前网上有个流行语“你妈喊你回家吃饭了”,就以这个生活场景为例,小明在外面玩耍,小明妈妈在家里做饭,做晚饭后准备叫小明回家吃饭,那么就存在两种方式: - -小明妈妈要去上班了十分紧急这个时候手机又没有电了,于是就在桌子上贴了一张纸条“饭做好了,放在...”小明回家后看到纸条如愿吃到妈妈做的饭菜,那么,如果将小明妈妈和小明作为两个线程,那么这张纸条就是这两个线程间通信的共享变量,通过读写共享变量实现两个线程间协作; - -还有一种方式就是,妈妈的手机还有电,妈妈在赶去坐公交的路上给小明打了个电话,这种方式就是通知机制来完成协作。同样,可以引申到线程间通信机制。 - -通过上面这个例子,应该有些认识。在并发编程中主要需要解决两个问题:**1. 线程之间如何通信;2.线程之间如何完成同步**(这里的线程指的是并发执行的活动实体)。通信是指线程之间以何种机制来交换信息,主要有两种:共享内存和消息传递。这里,可以分别类比上面的两个举例。java内存模型是**共享内存的并发模型**,线程之间主要通过读-写共享变量来完成隐式通信。如果程序员不能理解Java的共享内存模型在编写并发程序时一定会遇到各种各样关于内存可见性的问题。 - -> 1.哪些是共享变量 - -在java程序中所有**实例域,静态域和数组元素**都是放在堆内存中(所有线程均可访问到,是可以共享的),而局部变量,方法定义参数和异常处理器参数不会在线程间共享。共享数据会出现线程安全的问题,而非共享数据不会出现线程安全的问题。关于JVM运行时内存区域在后面的文章会讲到。 - -> 2.JMM抽象结构模型 - -我们知道CPU的处理速度和主存的读写速度不是一个量级的,为了平衡这种巨大的差距,每个CPU都会有缓存。因此,共享变量会先放在主存中,每个线程都有属于自己的工作内存,并且会把位于主存中的共享变量拷贝到自己的工作内存,之后的读写操作均使用位于工作内存的变量副本,并在某个时刻将工作内存的变量副本写回到主存中去。JMM就从抽象层次定义了这种方式,并且JMM决定了一个线程对共享变量的写入何时对其他线程是可见的。 - -![JMM内存模型的抽象结构示意图](http://upload-images.jianshu.io/upload_images/2615789-8c0b960a27af28db.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/800) - -如图为JMM抽象示意图,线程A和线程B之间要完成通信的话,要经历如下两步: -1. 线程A从主内存中将共享变量读入线程A的工作内存后并进行操作,之后将数据重新写回到主内存中; -2. 线程B从主存中读取最新的共享变量 - -从横向去看看,线程A和线程B就好像通过共享变量在进行隐式通信。这其中有很有意思的问题,如果线程A更新后数据并没有及时写回到主存,而此时线程B读到的是过期的数据,这就出现了“脏读”现象。可以通过同步机制(控制不同线程间操作发生的相对顺序)来解决或者通过volatile关键字使得每次volatile变量都能够强制刷新到主存,从而对每个线程都是可见的。 - -# 3. 重排序 # -一个好的内存模型实际上会放松对处理器和编译器规则的束缚,也就是说软件技术和硬件技术都为同一个目标而进行奋斗:在不改变程序执行结果的前提下,尽可能提高并行度。JMM对底层尽量减少约束,使其能够发挥自身优势。因此,在执行程序时,**为了提高性能,编译器和处理器常常会对指令进行重排序**。一般重排序可以分为如下三种: - -![从源码到最终执行的指令序列的示意图](http://upload-images.jianshu.io/upload_images/2615789-4a1ae3e7c7906823.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/800) -1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序; -2. 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果**不存在数据依赖性**,处理器可以改变语句对应机器指令的执行顺序; -3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。 - - -如图,1属于编译器重排序,而2和3统称为处理器重排序。这些重排序会导致线程安全的问题,一个很经典的例子就是DCL问题,这个在以后的文章中会具体去聊。**针对编译器重排序**,JMM的编译器重排序规则会禁止一些**特定类型的编译器重排序**;**针对处理器重排序**,编译器在生成指令序列的时候会通过**插入内存屏障指令来禁止某些特殊的处理器重排序**。 - -那么什么情况下,不能进行重排序了?下面就来说说数据依赖性。有如下代码: - -> double pi = 3.14 //A -> -> double r = 1.0 //B -> -> double area = pi * r * r //C - -这是一个计算圆面积的代码,由于A,B之间没有任何关系,对最终结果也不会存在关系,它们之间执行顺序可以重排序。因此可以执行顺序可以是A->B->C或者B->A->C执行最终结果都是3.14,即A和B之间没有数据依赖性。具体的定义为:**如果两个操作访问同一个变量,且这两个操作有一个为写操作,此时这两个操作就存在数据依赖性**这里就存在三种情况:1. 读后写;2.写后写;3. 写后读,者三种操作都是存在数据依赖性的,如果重排序会对最终执行结果会存在影响。**编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖性关系的两个操作的执行顺序** - -另外,还有一个比较有意思的就是as-if-serial语义。 - -> **as-if-serial** - -as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提供并行度),(单线程)程序的执行结果不能被改变。编译器,runtime和处理器都必须遵守as-if-serial语义。as-if-serial语义把单线程程序保护了起来,**遵守as-if-serial语义的编译器,runtime和处理器共同为编写单线程程序的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的**。比如上面计算圆面积的代码,在单线程中,会让人感觉代码是一行一行顺序执行上,实际上A,B两行不存在数据依赖性可能会进行重排序,即A,B不是顺序执行的。as-if-serial语义使程序员不必担心单线程中重排序的问题干扰他们,也无需担心内存可见性问题。 - -# 4. happens-before规则 # -上面的内容讲述了重排序原则,一会是编译器重排序一会是处理器重排序,如果让程序员再去了解这些底层的实现以及具体规则,那么程序员的负担就太重了,严重影响了并发编程的效率。因此,JMM为程序员在上层提供了六条规则,这样我们就可以根据规则去推论跨线程的内存可见性问题,而不用再去理解底层重排序的规则。下面以两个方面来说。 - -## 4.1 happens-before定义 ## -happens-before的概念最初由Leslie Lamport在其一篇影响深远的论文(《Time,Clocks and the Ordering of Events in a Distributed System》)中提出,有兴趣的可以google一下。JSR-133使用happens-before的概念来指定两个操作之间的执行顺序。由于这两个操作可以在一个线程之内,也可以是在不同线程之间。因此,**JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证**(如果A线程的写操作a与B线程的读操作b之间存在happens-before关系,尽管a操作和b操作在不同的线程中执行,但JMM向程序员保证a操作将对b操作可见)。具体的定义为: - -1)如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。 - -2)两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。 - -上面的**1)是JMM对程序员的承诺**。从程序员的角度来说,可以这样理解happens-before关系:如果A happens-before B,那么Java内存模型将向程序员保证——A操作的结果将对B可见,且A的执行顺序排在B之前。注意,这只是Java内存模型向程序员做出的保证! - -上面的**2)是JMM对编译器和处理器重排序的约束原则**。正如前面所言,JMM其实是在遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。JMM这么做的原因是:程序员对于这两个操作是否真的被重排序并不关心,程序员关心的是程序执行时的语义不能被改变(即执行结果不能被改变)。因此,happens-before关系本质上和as-if-serial语义是一回事。 - -下面来比较一下as-if-serial和happens-before: - -> **as-if-serial VS happens-before** - -1. as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。 -2. as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。 -3. as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。 - -## 4.2 具体规则 ## -具体的一共有六项规则: - -1. 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。 -2. 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。 -3. volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。 -4. 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。 -5. start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。 -6. join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。 -7. 程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。 -7. 对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。 - -下面以一个**具体的例子来讲下如何使用这些规则进行推论**: - -依旧以上面计算圆面积的进行描述。利用程序顺序规则(规则1)存在三个happens-before关系:1. A happens-before B;2. B happens-before C;3. A happens-before C。这里的第三个关系是利用传递性进行推论的。A happens-before B,定义1要求A执行结果对B可见,并且A操作的执行顺序在B操作之前,但与此同时利用定义中的第二条,A,B操作彼此不存在数据依赖性,两个操作的执行顺序对最终结果都不会产生影响,在不改变最终结果的前提下,允许A,B两个操作重排序,即happens-before关系并不代表了最终的执行顺序。 - -# 5. 总结 # -上面已经聊了关于JMM的两个方面:1. JMM的抽象结构(主内存和线程工作内存);2. 重排序以及happens-before规则。接下来,我们来做一个总结。从两个方面进行考虑。1. 如果让我们设计JMM应该从哪些方面考虑,也就是说JMM承担哪些功能;2. happens-before与JMM的关系;3. 由于JMM,多线程情况下可能会出现哪些问题? - -## 5.1 JMM的设计 ## - -![JMM层级图](http://upload-images.jianshu.io/upload_images/2615789-b96f4b4edada03a6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/800) - -JMM是语言级的内存模型,在我的理解中JMM处于中间层,包含了两个方面:(1)内存模型;(2)重排序以及happens-before规则。同时,为了禁止特定类型的重排序会对编译器和处理器指令序列加以控制。而上层会有基于JMM的关键字和J.U.C包下的一些具体类用来方便程序员能够迅速高效率的进行并发编程。站在JMM设计者的角度,在设计JMM时需要考虑两个关键因素: - -1. **程序员对内存模型的使用** -程序员希望内存模型易于理解、易于编程。程序员希望基于一个强内存模型来编写代码。 -2. **编译器和处理器对内存模型的实现** -编译器和处理器希望内存模型对它们的束缚越少越好,这样它们就可以做尽可能多的优化来提高性能。编译器和处理器希望实现一个弱内存模型。 - -另外还要一个特别有意思的事情就是关于重排序问题,更简单的说,重排序可以分为两类: - -1. 会改变程序执行结果的重排序。 -2. 不会改变程序执行结果的重排序。 - -JMM对这两种不同性质的重排序,采取了不同的策略,如下。 - -1. 对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序。 -2. 对于不会改变程序执行结果的重排序,JMM对编译器和处理器不做要求(JMM允许这种 -重排序) - -JMM的设计图为: - -![JMM设计示意图](http://upload-images.jianshu.io/upload_images/2615789-b288451befb6a441.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/800) -从图可以看出: - -1. JMM向程序员提供的happens-before规则能满足程序员的需求。JMM的happens-before规则不但简单易懂,而且也向程序员提供了足够强的内存可见性保证(有些内存可见性保证其实并不一定真实存在,比如上面的A happens-before B)。 -2. JMM对编译器和处理器的束缚已经尽可能少。从上面的分析可以看出,JMM其实是在遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。例如,如果编译器经过细致的分析后,认定一个锁只会被单个线程访问,那么这个锁可以被消除。再如,如果编译器经过细致的分析后,认定一个volatile变量只会被单个线程访问,那么编译器可以把这个volatile变量当作一个普通变量来对待。这些优化既不会改变程序的执行结果,又能提高程序的执行效率。 -## 5.2 happens-before与JMM的关系 ## - - -![happens-before与JMM的关系](http://upload-images.jianshu.io/upload_images/2615789-dd96af34a8df5c49.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/800) - -一个happens-before规则对应于一个或多个编译器和处理器重排序规则。对于Java程序员来说,happens-before规则简单易懂,它避免Java程序员为了理解JMM提供的内存可见性保证而去学习复杂的重排序规则以及这些规则的具体实现方法 - - -## 5.3 今后可能需要关注的问题 ## -从上面内存抽象结构来说,可能出在数据“脏读”的现象,这就是**数据可见性**的问题,另外,重排序在多线程中不注意的话也容易存在一些问题,比如一个很经典的问题就是DCL(双重检验锁),这就是需要**禁止重排序**,另外,在多线程下原子操作例如i++不加以注意的也容易出现线程安全的问题。但总的来说,在多线程开发时需要从**原子性,有序性,可见性**三个方面进行考虑。J.U.C包下的并发工具类和并发容器也是需要花时间去掌握的,这些东西在以后得文章中多会一一进行讨论。 - - -> 参考文献 - -《java并发编程的艺术》 \ No newline at end of file diff --git "a/03.java\345\206\205\345\255\230\346\250\241\345\236\213\344\273\245\345\217\212happens-before\350\247\204\345\210\231/happens-before\344\270\216JMM\347\232\204\345\205\263\347\263\273.png" "b/03.java\345\206\205\345\255\230\346\250\241\345\236\213\344\273\245\345\217\212happens-before\350\247\204\345\210\231/happens-before\344\270\216JMM\347\232\204\345\205\263\347\263\273.png" deleted file mode 100644 index d5b2a1a..0000000 Binary files "a/03.java\345\206\205\345\255\230\346\250\241\345\236\213\344\273\245\345\217\212happens-before\350\247\204\345\210\231/happens-before\344\270\216JMM\347\232\204\345\205\263\347\263\273.png" and /dev/null differ diff --git "a/03.java\345\206\205\345\255\230\346\250\241\345\236\213\344\273\245\345\217\212happens-before\350\247\204\345\210\231/\344\273\216\346\272\220\347\240\201\345\210\260\346\234\200\347\273\210\346\211\247\350\241\214\347\232\204\346\214\207\344\273\244\345\272\217\345\210\227\347\232\204\347\244\272\346\204\217\345\233\276.png" "b/03.java\345\206\205\345\255\230\346\250\241\345\236\213\344\273\245\345\217\212happens-before\350\247\204\345\210\231/\344\273\216\346\272\220\347\240\201\345\210\260\346\234\200\347\273\210\346\211\247\350\241\214\347\232\204\346\214\207\344\273\244\345\272\217\345\210\227\347\232\204\347\244\272\346\204\217\345\233\276.png" deleted file mode 100644 index 4ea0ce3..0000000 Binary files "a/03.java\345\206\205\345\255\230\346\250\241\345\236\213\344\273\245\345\217\212happens-before\350\247\204\345\210\231/\344\273\216\346\272\220\347\240\201\345\210\260\346\234\200\347\273\210\346\211\247\350\241\214\347\232\204\346\214\207\344\273\244\345\272\217\345\210\227\347\232\204\347\244\272\346\204\217\345\233\276.png" and /dev/null differ diff --git "a/04.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/Mark Word\345\255\230\345\202\250\347\273\223\346\236\204.png" "b/04.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/Mark Word\345\255\230\345\202\250\347\273\223\346\236\204.png" deleted file mode 100644 index c4f310e..0000000 Binary files "a/04.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/Mark Word\345\255\230\345\202\250\347\273\223\346\236\204.png" and /dev/null differ diff --git "a/04.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/Mark Word\347\212\266\346\200\201\345\217\230\345\214\226.png" "b/04.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/Mark Word\347\212\266\346\200\201\345\217\230\345\214\226.png" deleted file mode 100644 index 2479a7e..0000000 Binary files "a/04.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/Mark Word\347\212\266\346\200\201\345\217\230\345\214\226.png" and /dev/null differ diff --git "a/04.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/java\345\205\263\351\224\256\345\255\227---synchronized.md" "b/04.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/java\345\205\263\351\224\256\345\255\227---synchronized.md" deleted file mode 100644 index fe91e9f..0000000 --- "a/04.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/java\345\205\263\351\224\256\345\255\227---synchronized.md" +++ /dev/null @@ -1,243 +0,0 @@ -# 1. synchronized简介 # - -在学习知识前,我们先来看一个现象: - - public class SynchronizedDemo implements Runnable { - private static int count = 0; - - public static void main(String[] args) { - for (int i = 0; i < 10; i++) { - Thread thread = new Thread(new SynchronizedDemo()); - thread.start(); - } - try { - Thread.sleep(500); - } catch (InterruptedException e) { - e.printStackTrace(); - } - System.out.println("result: " + count); - } - - @Override - public void run() { - for (int i = 0; i < 1000000; i++) - count++; - } - } - -开启了10个线程,每个线程都累加了1000000次,如果结果正确的话自然而然总数就应该是10 * 1000000 = 10000000。可就运行多次结果都不是这个数,而且每次运行结果都不一样。这是为什么了?有什么解决方案了?这就是我们今天要聊的事情。 - -在上一篇博文中我们已经了解了[java内存模型](https://juejin.im/post/5ae6d309518825673123fd0e)的一些知识,并且已经知道出现线程安全的主要来源于JMM的设计,主要集中在主内存和线程的工作内存而导致的**内存可见性问题**,以及**重排序导致的问题**,进一步知道了**happens-before规则**。线程运行时拥有自己的栈空间,会在自己的栈空间运行,如果多线程间没有共享的数据也就是说多线程间并没有协作完成一件事情,那么,多线程就不能发挥优势,不能带来巨大的价值。那么共享数据的线程安全问题怎样处理?很自然而然的想法就是每一个线程依次去读写这个共享变量,这样就不会有任何数据安全的问题,因为每个线程所操作的都是当前最新的版本数据。那么,在java关键字synchronized就具有使每个线程依次排队操作共享变量的功能。很显然,这种同步机制效率很低,但synchronized是其他并发容器实现的基础,对它的理解也会大大提升对并发编程的感觉,从功利的角度来说,这也是面试高频的考点。好了,下面,就来具体说说这个关键字。 -# 2. synchronized实现原理 # -在java代码中使用synchronized可是使用在代码块和方法中,根据Synchronized用的位置可以有这些使用场景: - - -![Synchronized的使用场景](https://user-gold-cdn.xitu.io/2018/4/30/16315cc79aaac173?w=700&h=413&f=png&s=33838) - - -如图,synchronized可以用在**方法**上也可以使用在**代码块**中,其中方法是实例方法和静态方法分别锁的是该类的实例对象和该类的对象。而使用在代码块中也可以分为三种,具体的可以看上面的表格。这里的需要注意的是:**如果锁的是类对象的话,尽管new多个实例对象,但他们仍然是属于同一个类依然会被锁住,即线程之间保证同步关系**。 - -现在我们已经知道了怎样synchronized了,看起来很简单,拥有了这个关键字就真的可以在并发编程中得心应手了吗?爱学的你,就真的不想知道synchronized底层是怎样实现了吗? -## 2.1 对象锁(monitor)机制 ## -现在我们来看看synchronized的具体底层实现。先写一个简单的demo: - - public class SynchronizedDemo { - public static void main(String[] args) { - synchronized (SynchronizedDemo.class) { - } - method(); - } - - private static void method() { - } - } -上面的代码中有一个同步代码块,锁住的是类对象,并且还有一个同步静态方法,锁住的依然是该类的类对象。编译之后,切换到SynchronizedDemo.class的同级目录之后,然后用**javap -v SynchronizedDemo.class**查看字节码文件: - - - -![SynchronizedDemo.class](https://user-gold-cdn.xitu.io/2018/4/30/16315cce259af0d2?w=700&h=330&f=png&s=68919) - - -如图,上面用黄色高亮的部分就是需要注意的部分了,这也是添Synchronized关键字之后独有的。执行同步代码块后首先要先执行**monitorenter**指令,退出的时候**monitorexit**指令。通过分析之后可以看出,使用Synchronized进行同步,其关键就是必须要对对象的监视器monitor进行获取,当线程获取monitor后才能继续往下执行,否则就只能等待。而这个获取的过程是**互斥**的,即同一时刻只有一个线程能够获取到monitor。上面的demo中在执行完同步代码块之后紧接着再会去执行一个静态同步方法,而这个方法锁的对象依然就这个类对象,那么这个正在执行的线程还需要获取该锁吗?答案是不必的,从上图中就可以看出来,执行静态同步方法的时候就只有一条monitorexit指令,并没有monitorenter获取锁的指令。这就是**锁的重入性**,即在同一锁程中,线程不需要再次获取同一把锁。Synchronized先天具有重入性。**每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一**。 - -任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取该对象的监视器才能进入同步块和同步方法,如果没有获取到监视器的线程将会被阻塞在同步块和同步方法的入口处,进入到BLOCKED状态(关于线程的状态可以看[这篇文章](https://juejin.im/post/5ae6cf7a518825670960fcc2) - -下图表现了对象,对象监视器,同步队列以及执行线程状态之间的关系: - -![对象,对象监视器,同步队列和线程状态的关系](https://user-gold-cdn.xitu.io/2018/4/30/16315cd5fa7cf91c?w=700&h=261&f=png&s=54962) - -该图可以看出,任意线程对Object的访问,首先要获得Object的监视器,如果获取失败,该线程就进入同步状态,线程状态变为BLOCKED,当Object的监视器占有者释放后,在同步队列中得线程就会有机会重新获取该监视器。 - - -## 2.2 synchronized的happens-before关系 ## -在上一篇文章中讨论过[happens-before](https://juejin.im/post/5ae6d309518825673123fd0e)规则,抱着学以致用的原则我们现在来看一看Synchronized的happens-before规则,即监视器锁规则:对同一个监视器的解锁,happens-before于对该监视器的加锁。继续来看代码: - - public class MonitorDemo { - private int a = 0; - - public synchronized void writer() { // 1 - a++; // 2 - } // 3 - - public synchronized void reader() { // 4 - int i = a; // 5 - } // 6 - } - -该代码的happens-before关系如图所示: - - - -![synchronized的happens-before关系](https://user-gold-cdn.xitu.io/2018/4/30/16315ce6ea84f240?w=650&h=629&f=png&s=61572) - - -在图中每一个箭头连接的两个节点就代表之间的happens-before关系,黑色的是通过程序顺序规则推导出来,红色的为监视器锁规则推导而出:**线程A释放锁happens-before线程B加锁**,蓝色的则是通过程序顺序规则和监视器锁规则推测出来happens-befor关系,通过传递性规则进一步推导的happens-before关系。现在我们来重点关注2 happens-before 5,通过这个关系我们可以得出什么? - -根据happens-before的定义中的一条:如果A happens-before B,则A的执行结果对B可见,并且A的执行顺序先于B。线程A先对共享变量A进行加一,由2 happens-before 5关系可知线程A的执行结果对线程B可见即线程B所读取到的a的值为1。 - -## 2.3 锁获取和锁释放的内存语义 ## -在上一篇文章提到过JMM核心为两个部分:happens-before规则以及内存抽象模型。我们分析完Synchronized的happens-before关系后,还是不太完整的,我们接下来看看基于java内存抽象模型的Synchronized的内存语义。 - -废话不多说依旧先上图。 - -![线程A写共享变量](https://user-gold-cdn.xitu.io/2018/4/30/16315cef21fd3ad8?w=557&h=440&f=png&s=10816) - - -从上图可以看出,线程A会首先先从主内存中读取共享变量a=0的值然后将该变量拷贝到自己的本地内存,进行加一操作后,再将该值刷新到主内存,整个过程即为线程A 加锁-->执行临界区代码-->释放锁相对应的内存语义。 - - - -![线程B读共享变量](https://user-gold-cdn.xitu.io/2018/4/30/16315cf41661491e?w=564&h=458&f=png&s=14468) - -线程B获取锁的时候同样会从主内存中共享变量a的值,这个时候就是最新的值1,然后将该值拷贝到线程B的工作内存中去,释放锁的时候同样会重写到主内存中。 - -从整体上来看,线程A的执行结果(a=1)对线程B是可见的,实现原理为:释放锁的时候会将值刷新到主内存中,其他线程获取锁时会强制从主内存中获取最新的值。另外也验证了2 happens-before 5,2的执行结果对5是可见的。 - -从横向来看,这就像线程A通过主内存中的共享变量和线程B进行通信,A 告诉 B 我们俩的共享数据现在为1啦,这种线程间的通信机制正好吻合java的内存模型正好是共享内存的并发模型结构。 - -# 3. synchronized优化 # -通过上面的讨论现在我们对Synchronized应该有所印象了,它最大的特征就是在同一时刻只有一个线程能够获得对象的监视器(monitor),从而进入到同步代码块或者同步方法之中,即表现为**互斥性(排它性)**。这种方式肯定效率低下,每次只能通过一个线程,既然每次只能通过一个,这种形式不能改变的话,那么我们能不能让每次通过的速度变快一点了。打个比方,去收银台付款,之前的方式是,大家都去排队,然后去纸币付款收银员找零,有的时候付款的时候在包里拿出钱包再去拿出钱,这个过程是比较耗时的,然后,支付宝解放了大家去钱包找钱的过程,现在只需要扫描下就可以完成付款了,也省去了收银员跟你找零的时间的了。同样是需要排队,但整个付款的时间大大缩短,是不是整体的效率变高速率变快了?这种优化方式同样可以引申到锁优化上,缩短获取锁的时间,伟大的科学家们也是这样做的,令人钦佩,毕竟java是这么优秀的语言(微笑脸)。 - -在聊到锁的优化也就是锁的几种状态前,有两个知识点需要先关注:(1)CAS操作 (2)Java对象头,这是理解下面知识的前提条件。 - -## 3.1 CAS操作 ## -### 3.1.1 什么是CAS? ### -使用锁时,线程获取锁是一种**悲观锁策略**,即假设每一次执行临界区代码都会产生冲突,所以当前线程获取到锁的时候同时也会阻塞其他线程获取该锁。而CAS操作(又称为无锁操作)是一种**乐观锁策略**,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。因此,线程就不会出现阻塞停顿的状态。那么,如果出现冲突了怎么办?无锁操作是使用**CAS(compare and swap)**又叫做比较交换来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。 -### 3.1.2 CAS的操作过程 ### -CAS比较交换的过程可以通俗的理解为CAS(V,O,N),包含三个值分别为:**V 内存地址存放的实际值;O 预期的值(旧值);N 更新的新值**。当V和O相同时,也就是说旧值和内存中实际的值相同表明该值没有被其他线程更改过,即该旧值O就是目前来说最新的值了,自然而然可以将新值N赋值给V。反之,V和O不相同,表明该值已经被其他线程改过了则该旧值O不是最新版本的值了,所以不能将新值N赋给V,返回V即可。当多个线程使用CAS操作一个变量是,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试,当然也可以选择挂起线程 - -CAS的实现需要硬件指令集的支撑,在JDK1.5后虚拟机才可以使用处理器提供的**CMPXCHG**指令实现。 - -> Synchronized VS CAS - -元老级的Synchronized(未优化前)最主要的问题是:在存在线程竞争的情况下会出现线程阻塞和唤醒锁带来的性能问题,因为这是一种互斥同步(阻塞同步)。而CAS并不是武断的间线程挂起,当CAS操作失败后会进行一定的尝试,而非进行耗时的挂起唤醒的操作,因此也叫做非阻塞同步。这是两者主要的区别。 - -### 3.1.3 CAS的应用场景 ### - -在J.U.C包中利用CAS实现类有很多,可以说是支撑起整个concurrency包的实现,在Lock实现中会有CAS改变state变量,在atomic包中的实现类也几乎都是用CAS实现,关于这些具体的实现场景在之后会详细聊聊,现在有个印象就好了(微笑脸)。 - -### 3.1.4 CAS的问题 ### - -**1. ABA问题** -因为CAS会检查旧值有没有变化,这里存在这样一个有意思的问题。比如一个旧值A变为了成B,然后再变成A,刚好在做CAS时检查发现旧值并没有变化依然为A,但是实际上的确发生了变化。解决方案可以沿袭数据库中常用的乐观锁方式,添加一个版本号可以解决。原来的变化路径A->B->A就变成了1A->2B->3C。java这么优秀的语言,当然在java 1.5后的atomic包中提供了AtomicStampedReference来解决ABA问题,解决思路就是这样的。 - -**2. 自旋时间过长** - -使用CAS时非阻塞同步,也就是说不会将线程挂起,会自旋(无非就是一个死循环)进行下一次尝试,如果这里自旋时间过长对性能是很大的消耗。如果JVM能支持处理器提供的pause指令,那么在效率上会有一定的提升。 - -**3. 只能保证一个共享变量的原子操作** - -当对一个共享变量执行操作时CAS能保证其原子性,如果对多个共享变量进行操作,CAS就不能保证其原子性。有一个解决方案是利用对象整合多个共享变量,即一个类中的成员变量就是这几个共享变量。然后将这个对象做CAS操作就可以保证其原子性。atomic中提供了AtomicReference来保证引用对象之间的原子性。 - -## 3.2 Java对象头 ## - -在同步的时候是获取对象的monitor,即获取到对象的锁。那么对象的锁怎么理解?无非就是类似对对象的一个标志,那么这个标志就是存放在Java对象的对象头。Java对象头里的Mark Word里默认的存放的对象的Hashcode,分代年龄和锁标记位。32为JVM Mark Word默认存储结构为(注:java对象头以及下面的锁状态变化摘自《java并发编程的艺术》一书,该书我认为写的足够好,就没在自己组织语言班门弄斧了): - -![Mark Word存储结构](https://user-gold-cdn.xitu.io/2018/4/30/16315cff10307a29?w=700&h=71&f=png&s=23717) - -如图在Mark Word会默认存放hasdcode,年龄值以及锁标志位等信息。 - -Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:**无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态**,这几个状态会随着竞争情况逐渐升级。**锁可以升级但不能降级**,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。对象的MarkWord变化为下图: - -![Mark Word状态变化](https://user-gold-cdn.xitu.io/2018/4/30/16315d056598e4c2?w=700&h=151&f=png&s=47968) - - - -## 3.2 偏向锁 ## - -HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。 - -> **偏向锁的获取** - -当一个线程访问同步块并获取锁时,会在**对象头**和**栈帧中的锁记录**里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程 - - -> **偏向锁的撤销** - -偏向锁使用了一种**等到竞争出现才释放锁**的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。 - - -![偏向锁撤销流程](https://user-gold-cdn.xitu.io/2018/4/30/16315d0b13b37da4?w=567&h=736&f=png&s=72325) - - -如图,偏向锁的撤销,需要等待**全局安全点**(在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word**要么**重新偏向于其他线程,**要么**恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。 - -下图线程1展示了偏向锁获取的过程,线程2展示了偏向锁撤销的过程。 - -![偏向锁获取和撤销流程](https://user-gold-cdn.xitu.io/2018/4/30/16315cb9175365f5?w=630&h=703&f=png&s=160223) - -> **如何关闭偏向锁** - -偏向锁在Java 6和Java 7里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用JVM参数来关闭延迟:**-XX:BiasedLockingStartupDelay=0**。如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁:**-XX:-UseBiasedLocking=false**,那么程序默认会进入轻量级锁状态 - -## 3.3 轻量级锁 ## -> **加锁** - -线程在执行同步块之前,JVM会先在当前线程的栈桢中**创建用于存储锁记录的空间**,并将对象头中的Mark Word复制到锁记录中,官方称为**Displaced Mark Word**。然后线程尝试使用CAS**将对象头中的Mark Word替换为指向锁记录的指针**。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。 - - -> **解锁** - -轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。下图是两个线程同时争夺锁,导致锁膨胀的流程图。 - - -![轻量级锁加锁解锁以及锁膨胀](https://user-gold-cdn.xitu.io/2018/4/30/16315cb9193719c2?w=794&h=772&f=png&s=287958) - -因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。 -## 3.5 各种锁的比较 ## - -![各种锁的对比](https://user-gold-cdn.xitu.io/2018/4/30/16315cb91da523d9?w=800&h=193&f=png&s=116058) - -# 4. 一个例子 # -经过上面的理解,我们现在应该知道了该怎样解决了。更正后的代码为: - - public class SynchronizedDemo implements Runnable { - private static int count = 0; - - public static void main(String[] args) { - for (int i = 0; i < 10; i++) { - Thread thread = new Thread(new SynchronizedDemo()); - thread.start(); - } - try { - Thread.sleep(500); - } catch (InterruptedException e) { - e.printStackTrace(); - } - System.out.println("result: " + count); - } - - @Override - public void run() { - synchronized (SynchronizedDemo.class) { - for (int i = 0; i < 1000000; i++) - count++; - } - } - } - -开启十个线程,每个线程在原值上累加1000000次,最终正确的结果为10X1000000=10000000,这里能够计算出正确的结果是因为在做累加操作时使用了同步代码块,这样就能保证每个线程所获得共享变量的值都是当前最新的值,如果不使用同步的话,就可能会出现A线程累加后,而B线程做累加操作有可能是使用原来的就值,即“脏值”。这样,就导致最终的计算结果不是正确的。而使用Syncnized就可能保证内存可见性,保证每个线程都是操作的最新值。这里只是一个示例性的demo,聪明的你,还有其他办法吗? - -> 参考文献 - -《java并发编程的艺术》 diff --git "a/04.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/synchronizedDemo.class.png" "b/04.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/synchronizedDemo.class.png" deleted file mode 100644 index cf9e87d..0000000 Binary files "a/04.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/synchronizedDemo.class.png" and /dev/null differ diff --git "a/04.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/synchronized\347\232\204happens-before\345\205\263\347\263\273.png" "b/04.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/synchronized\347\232\204happens-before\345\205\263\347\263\273.png" deleted file mode 100644 index a47c707..0000000 Binary files "a/04.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/synchronized\347\232\204happens-before\345\205\263\347\263\273.png" and /dev/null differ diff --git "a/04.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/synchronized\347\232\204\344\275\277\347\224\250\345\234\272\346\231\257.png" "b/04.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/synchronized\347\232\204\344\275\277\347\224\250\345\234\272\346\231\257.png" deleted file mode 100644 index 7b0c4ef..0000000 Binary files "a/04.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/synchronized\347\232\204\344\275\277\347\224\250\345\234\272\346\231\257.png" and /dev/null differ diff --git "a/04.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/\345\201\217\345\220\221\351\224\201\346\222\244\351\224\200\346\265\201\347\250\213.png" "b/04.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/\345\201\217\345\220\221\351\224\201\346\222\244\351\224\200\346\265\201\347\250\213.png" deleted file mode 100644 index 41635b9..0000000 Binary files "a/04.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/\345\201\217\345\220\221\351\224\201\346\222\244\351\224\200\346\265\201\347\250\213.png" and /dev/null differ diff --git "a/04.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/\345\201\217\345\220\221\351\224\201\350\216\267\345\217\226\345\222\214\346\222\244\351\224\200\346\265\201\347\250\213.png" "b/04.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/\345\201\217\345\220\221\351\224\201\350\216\267\345\217\226\345\222\214\346\222\244\351\224\200\346\265\201\347\250\213.png" deleted file mode 100644 index 0d88eab..0000000 Binary files "a/04.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/\345\201\217\345\220\221\351\224\201\350\216\267\345\217\226\345\222\214\346\222\244\351\224\200\346\265\201\347\250\213.png" and /dev/null differ diff --git "a/04.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/\345\220\204\347\247\215\351\224\201\347\232\204\345\257\271\346\257\224.png" "b/04.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/\345\220\204\347\247\215\351\224\201\347\232\204\345\257\271\346\257\224.png" deleted file mode 100644 index 690a5af..0000000 Binary files "a/04.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/\345\220\204\347\247\215\351\224\201\347\232\204\345\257\271\346\257\224.png" and /dev/null differ diff --git "a/04.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/\345\257\271\350\261\241\357\274\214\345\257\271\350\261\241\347\233\221\350\247\206\345\231\250\357\274\214\345\220\214\346\255\245\351\230\237\345\210\227\345\222\214\347\272\277\347\250\213\347\212\266\346\200\201\347\232\204\345\205\263\347\263\273.png" "b/04.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/\345\257\271\350\261\241\357\274\214\345\257\271\350\261\241\347\233\221\350\247\206\345\231\250\357\274\214\345\220\214\346\255\245\351\230\237\345\210\227\345\222\214\347\272\277\347\250\213\347\212\266\346\200\201\347\232\204\345\205\263\347\263\273.png" deleted file mode 100644 index 55deeba..0000000 Binary files "a/04.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/\345\257\271\350\261\241\357\274\214\345\257\271\350\261\241\347\233\221\350\247\206\345\231\250\357\274\214\345\220\214\346\255\245\351\230\237\345\210\227\345\222\214\347\272\277\347\250\213\347\212\266\346\200\201\347\232\204\345\205\263\347\263\273.png" and /dev/null differ diff --git "a/04.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/\347\272\277\347\250\213A\345\206\231\345\205\261\344\272\253\345\217\230\351\207\217.png" "b/04.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/\347\272\277\347\250\213A\345\206\231\345\205\261\344\272\253\345\217\230\351\207\217.png" deleted file mode 100644 index b5909f3..0000000 Binary files "a/04.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/\347\272\277\347\250\213A\345\206\231\345\205\261\344\272\253\345\217\230\351\207\217.png" and /dev/null differ diff --git "a/04.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/\347\272\277\347\250\213B\350\257\273\345\205\261\344\272\253\345\217\230\351\207\217.png" "b/04.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/\347\272\277\347\250\213B\350\257\273\345\205\261\344\272\253\345\217\230\351\207\217.png" deleted file mode 100644 index ec04967..0000000 Binary files "a/04.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/\347\272\277\347\250\213B\350\257\273\345\205\261\344\272\253\345\217\230\351\207\217.png" and /dev/null differ diff --git "a/04.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/\350\275\273\351\207\217\347\272\247\351\224\201\345\212\240\351\224\201\350\247\243\351\224\201\344\273\245\345\217\212\351\224\201\350\206\250\350\203\200.png" "b/04.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/\350\275\273\351\207\217\347\272\247\351\224\201\345\212\240\351\224\201\350\247\243\351\224\201\344\273\245\345\217\212\351\224\201\350\206\250\350\203\200.png" deleted file mode 100644 index 2d3cc60..0000000 Binary files "a/04.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/\350\275\273\351\207\217\347\272\247\351\224\201\345\212\240\351\224\201\350\247\243\351\224\201\344\273\245\345\217\212\351\224\201\350\206\250\350\203\200.png" and /dev/null differ diff --git "a/05.\345\275\273\345\272\225\347\220\206\350\247\243volatile/VolatileExample\347\232\204happens-before\345\205\263\347\263\273\346\216\250\345\257\274.png" "b/05.\345\275\273\345\272\225\347\220\206\350\247\243volatile/VolatileExample\347\232\204happens-before\345\205\263\347\263\273\346\216\250\345\257\274.png" deleted file mode 100644 index 41878f1..0000000 Binary files "a/05.\345\275\273\345\272\225\347\220\206\350\247\243volatile/VolatileExample\347\232\204happens-before\345\205\263\347\263\273\346\216\250\345\257\274.png" and /dev/null differ diff --git "a/05.\345\275\273\345\272\225\347\220\206\350\247\243volatile/java\345\205\263\351\224\256\345\255\227---volatile.md" "b/05.\345\275\273\345\272\225\347\220\206\350\247\243volatile/java\345\205\263\351\224\256\345\255\227---volatile.md" deleted file mode 100644 index 4d62eaf..0000000 --- "a/05.\345\275\273\345\272\225\347\220\206\350\247\243volatile/java\345\205\263\351\224\256\345\255\227---volatile.md" +++ /dev/null @@ -1,134 +0,0 @@ -# 1. volatile简介 # - -在上一篇文章中我们深入理解了java关键字[synchronized](https://juejin.im/post/5ae6dc04f265da0ba351d3ff),我们知道在java中还有一大神器就是关键volatile,可以说是和synchronized各领风骚,其中奥妙,我们来共同探讨下。 - -通过上一篇的文章我们了解到synchronized是阻塞式同步,在线程竞争激烈的情况下会升级为重量级锁。而volatile就可以说是java虚拟机提供的最轻量级的同步机制。但它同时不容易被正确理解,也至于在并发编程中很多程序员遇到线程安全的问题就会使用synchronized。[Java内存模型](https://juejin.im/post/5ae6d309518825673123fd0e)告诉我们,各个线程会将共享变量从主内存中拷贝到工作内存,然后执行引擎会基于工作内存中的数据进行操作处理。线程在工作内存进行操作后何时会写到主内存中?这个时机对普通变量是没有规定的,而针对volatile修饰的变量给java虚拟机特殊的约定,线程对volatile变量的修改会立刻被其他线程所感知,即不会出现数据脏读的现象,从而保证数据的“可见性”。 - -现在我们有了一个大概的印象就是:**被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。** - -# 2. volatile实现原理 # -volatile是怎样实现了?比如一个很简单的Java代码: -> instance = new Instancce() //instance是volatile变量 - -在生成汇编代码时会在volatile修饰的共享变量进行写操作的时候会多出**Lock前缀的指令**(具体的大家可以使用一些工具去看一下,这里我就只把结果说出来)。我们想这个**Lock**指令肯定有神奇的地方,那么Lock前缀的指令在多核处理器下会发现什么事情了?主要有这两个方面的影响: - -1. 将当前处理器缓存行的数据写回系统内存; -2. 这个写回内存的操作会使得其他CPU里缓存了该内存地址的数据无效 - - -为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现**缓存一致性**协议,**每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期**了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。因此,经过分析我们可以得出如下结论: - -1. Lock前缀的指令会引起处理器缓存写回内存; -2. 一个处理器的缓存回写到内存会导致其他处理器的缓存失效; -3. 当处理器发现本地缓存失效后,就会从内存中重读该变量数据,即可以获取当前最新值。 - -这样针对volatile变量通过这样的机制就使得每个线程都能获得该变量的最新值。 - -# 3. volatile的happens-before关系 # - -经过上面的分析,我们已经知道了volatile变量可以通过**缓存一致性协议**保证每个线程都能获得最新值,即满足数据的“可见性”。我们继续延续上一篇分析问题的方式(我一直认为思考问题的方式是属于自己,也才是最重要的,也在不断培养这方面的能力),我一直将并发分析的切入点分为**两个核心,三大性质**。两大核心:JMM内存模型(主内存和工作内存)以及happens-before;三条性质:原子性,可见性,有序性(关于三大性质的总结在以后得文章会和大家共同探讨)。废话不多说,先来看两个核心之一:volatile的happens-before关系。 - -在六条[happens-before规则](https://juejin.im/post/5ae6d309518825673123fd0e)中有一条是:**volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。**下面我们结合具体的代码,我们利用这条规则推导下: - - public class VolatileExample { - private int a = 0; - private volatile boolean flag = false; - public void writer(){ - a = 1; //1 - flag = true; //2 - } - public void reader(){ - if(flag){ //3 - int i = a; //4 - } - } - } - -上面的实例代码对应的happens-before关系如下图所示: - -![VolatileExample的happens-before关系推导](http://upload-images.jianshu.io/upload_images/2615789-c9c291d6c0b3e0f1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - - -加锁线程A先执行writer方法,然后线程B执行reader方法图中每一个箭头两个节点就代码一个happens-before关系,黑色的代表根据**程序顺序规则**推导出来,红色的是根据**volatile变量的写happens-before 于任意后续对volatile变量的读**,而蓝色的就是根据传递性规则推导出来的。这里的2 happen-before 3,同样根据happens-before规则定义:如果A happens-before B,则A的执行结果对B可见,并且A的执行顺序先于B的执行顺序,我们可以知道操作2执行结果对操作3来说是可见的,也就是说当线程A将volatile变量 flag更改为true后线程B就能够迅速感知。 -# 4. volatile的内存语义 # -还是按照**两个核心**的分析方式,分析完happens-before关系后我们现在就来进一步分析volatile的内存语义(按照这种方式去学习,会不会让大家对知识能够把握的更深,而不至于不知所措,如果大家认同我的这种方式,不妨给个赞,小弟在此谢过,对我是个鼓励)。还是以上面的代码为例,假设线程A先执行writer方法,线程B随后执行reader方法,初始时线程的本地内存中flag和a都是初始状态,下图是线程A执行volatile写后的状态图。 - -![线程A执行volatile写后的内存状态图](http://upload-images.jianshu.io/upload_images/2615789-9e5098f09d5ad065.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - - -当volatile变量写后,线程中本地内存中共享变量就会置为失效的状态,因此线程B再需要读取从主内存中去读取该变量的最新值。下图就展示了线程B读取同一个volatile变量的内存变化示意图。 - -![线程B读volatile后的内存状态图](http://upload-images.jianshu.io/upload_images/2615789-606771789255958f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - - -从横向来看,线程A和线程B之间进行了一次通信,线程A在写volatile变量时,实际上就像是给B发送了一个消息告诉线程B你现在的值都是旧的了,然后线程B读这个volatile变量时就像是接收了线程A刚刚发送的消息。既然是旧的了,那线程B该怎么办了?自然而然就只能去主内存去取啦。 - -好的,我们现在**两个核心**:happens-before以及内存语义现在已经都了解清楚了。是不是还不过瘾,突然发现原来自己会这么爱学习(微笑脸),那我们下面就再来一点干货----volatile内存语义的实现。 - -## 4.1 volatile的内存语义实现 ## -我们都知道,为了性能优化,JMM在不改变正确语义的前提下,会允许编译器和处理器对指令序列进行重排序,那如果想阻止重排序要怎么办了?答案是可以添加内存屏障。 - -> **内存屏障** - -JMM内存屏障分为四类见下图, - -![内存屏障分类表](http://upload-images.jianshu.io/upload_images/2615789-27cf04634cbdf284.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/680) - - -java编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序。为了实现volatile的内存语义,JMM会限制特定类型的编译器和处理器重排序,JMM会针对编译器制定volatile重排序规则表: - - -![volatile重排序规则表](http://upload-images.jianshu.io/upload_images/2615789-fa62c72e7ec4ccb0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/680) - - -"NO"表示禁止重排序。为了实现volatile内存语义时,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的**处理器重排序**。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎是不可能的,为此,JMM采取了保守策略: - -1. 在每个volatile写操作的**前面**插入一个StoreStore屏障; -2. 在每个volatile写操作的**后面**插入一个StoreLoad屏障; -3. 在每个volatile读操作的**后面**插入一个LoadLoad屏障; -4. 在每个volatile读操作的**后面**插入一个LoadStore屏障。 - -需要注意的是:volatile写是在前面和后面**分别插入内存屏障**,而volatile读操作是在**后面插入两个内存屏障** - -**StoreStore屏障**:禁止上面的普通写和下面的volatile写重排序; - -**StoreLoad屏障**:防止上面的volatile写与下面可能有的volatile读/写重排序 - -**LoadLoad屏障**:禁止下面所有的普通读操作和上面的volatile读重排序 - -**LoadStore屏障**:禁止下面所有的普通写操作和上面的volatile读重排序 - -下面以两个示意图进行理解,图片摘自相当好的一本书《java并发编程的艺术》。 - -![volatile写插入内存屏障示意图](http://upload-images.jianshu.io/upload_images/2615789-a31dbae587e8a946.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/620) - - -![volatile读插入内存屏障示意图](http://upload-images.jianshu.io/upload_images/2615789-dc628461898a66a6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/620) -# 5. 一个示例 # -我们现在已经理解volatile的精华了,文章开头的那个问题我想现在我们都能给出答案了。更正后的代码为: - - public class VolatileDemo { - private static volatile boolean isOver = false; - - public static void main(String[] args) { - Thread thread = new Thread(new Runnable() { - @Override - public void run() { - while (!isOver) ; - } - }); - thread.start(); - try { - Thread.sleep(500); - } catch (InterruptedException e) { - e.printStackTrace(); - } - isOver = true; - } - } - -注意不同点,现在已经**将isOver设置成了volatile变量**,这样在main线程中将isOver改为了true后,thread的工作内存该变量值就会失效,从而需要再次从主内存中读取该值,现在能够读出isOver最新值为true从而能够结束在thread里的死循环,从而能够顺利停止掉thread线程。现在问题也解决了,知识也学到了:)。(如果觉得还不错,请点赞,是对我的一个鼓励。) - -> 参考文献 - -《java并发编程的艺术》 \ No newline at end of file diff --git "a/05.\345\275\273\345\272\225\347\220\206\350\247\243volatile/volatile\345\206\231\346\217\222\345\205\245\345\206\205\345\255\230\345\261\217\351\232\234\347\244\272\346\204\217\345\233\276.png" "b/05.\345\275\273\345\272\225\347\220\206\350\247\243volatile/volatile\345\206\231\346\217\222\345\205\245\345\206\205\345\255\230\345\261\217\351\232\234\347\244\272\346\204\217\345\233\276.png" deleted file mode 100644 index 677f695..0000000 Binary files "a/05.\345\275\273\345\272\225\347\220\206\350\247\243volatile/volatile\345\206\231\346\217\222\345\205\245\345\206\205\345\255\230\345\261\217\351\232\234\347\244\272\346\204\217\345\233\276.png" and /dev/null differ diff --git "a/05.\345\275\273\345\272\225\347\220\206\350\247\243volatile/volatile\350\257\273\346\217\222\345\205\245\345\206\205\345\255\230\345\261\217\351\232\234\347\244\272\346\204\217\345\233\276.png" "b/05.\345\275\273\345\272\225\347\220\206\350\247\243volatile/volatile\350\257\273\346\217\222\345\205\245\345\206\205\345\255\230\345\261\217\351\232\234\347\244\272\346\204\217\345\233\276.png" deleted file mode 100644 index ce52f32..0000000 Binary files "a/05.\345\275\273\345\272\225\347\220\206\350\247\243volatile/volatile\350\257\273\346\217\222\345\205\245\345\206\205\345\255\230\345\261\217\351\232\234\347\244\272\346\204\217\345\233\276.png" and /dev/null differ diff --git "a/05.\345\275\273\345\272\225\347\220\206\350\247\243volatile/volatile\351\207\215\346\216\222\345\272\217\350\247\204\345\210\231\350\241\250.png" "b/05.\345\275\273\345\272\225\347\220\206\350\247\243volatile/volatile\351\207\215\346\216\222\345\272\217\350\247\204\345\210\231\350\241\250.png" deleted file mode 100644 index 0a81142..0000000 Binary files "a/05.\345\275\273\345\272\225\347\220\206\350\247\243volatile/volatile\351\207\215\346\216\222\345\272\217\350\247\204\345\210\231\350\241\250.png" and /dev/null differ diff --git "a/05.\345\275\273\345\272\225\347\220\206\350\247\243volatile/\345\206\205\345\255\230\345\261\217\351\232\234\345\210\206\347\261\273\350\241\250.png" "b/05.\345\275\273\345\272\225\347\220\206\350\247\243volatile/\345\206\205\345\255\230\345\261\217\351\232\234\345\210\206\347\261\273\350\241\250.png" deleted file mode 100644 index 76107dd..0000000 Binary files "a/05.\345\275\273\345\272\225\347\220\206\350\247\243volatile/\345\206\205\345\255\230\345\261\217\351\232\234\345\210\206\347\261\273\350\241\250.png" and /dev/null differ diff --git "a/05.\345\275\273\345\272\225\347\220\206\350\247\243volatile/\347\272\277\347\250\213A\346\211\247\350\241\214volatile\345\206\231\345\220\216\347\232\204\345\206\205\345\255\230\347\212\266\346\200\201\345\233\276.png" "b/05.\345\275\273\345\272\225\347\220\206\350\247\243volatile/\347\272\277\347\250\213A\346\211\247\350\241\214volatile\345\206\231\345\220\216\347\232\204\345\206\205\345\255\230\347\212\266\346\200\201\345\233\276.png" deleted file mode 100644 index 48f82eb..0000000 Binary files "a/05.\345\275\273\345\272\225\347\220\206\350\247\243volatile/\347\272\277\347\250\213A\346\211\247\350\241\214volatile\345\206\231\345\220\216\347\232\204\345\206\205\345\255\230\347\212\266\346\200\201\345\233\276.png" and /dev/null differ diff --git "a/05.\345\275\273\345\272\225\347\220\206\350\247\243volatile/\347\272\277\347\250\213B\350\257\273volatile\345\220\216\347\232\204\345\206\205\345\255\230\347\212\266\346\200\201\345\233\276.png" "b/05.\345\275\273\345\272\225\347\220\206\350\247\243volatile/\347\272\277\347\250\213B\350\257\273volatile\345\220\216\347\232\204\345\206\205\345\255\230\347\212\266\346\200\201\345\233\276.png" deleted file mode 100644 index f1c5c8c..0000000 Binary files "a/05.\345\275\273\345\272\225\347\220\206\350\247\243volatile/\347\272\277\347\250\213B\350\257\273volatile\345\220\216\347\232\204\345\206\205\345\255\230\347\212\266\346\200\201\345\233\276.png" and /dev/null differ diff --git "a/06.\344\275\240\344\273\245\344\270\272\344\275\240\347\234\237\347\232\204\344\272\206\350\247\243final\345\220\227\357\274\237/final\344\277\256\351\245\260\345\261\200\351\203\250\345\217\230\351\207\217.png" "b/06.\344\275\240\344\273\245\344\270\272\344\275\240\347\234\237\347\232\204\344\272\206\350\247\243final\345\220\227\357\274\237/final\344\277\256\351\245\260\345\261\200\351\203\250\345\217\230\351\207\217.png" deleted file mode 100644 index db8c2fe..0000000 Binary files "a/06.\344\275\240\344\273\245\344\270\272\344\275\240\347\234\237\347\232\204\344\272\206\350\247\243final\345\220\227\357\274\237/final\344\277\256\351\245\260\345\261\200\351\203\250\345\217\230\351\207\217.png" and /dev/null differ diff --git "a/06.\344\275\240\344\273\245\344\270\272\344\275\240\347\234\237\347\232\204\344\272\206\350\247\243final\345\220\227\357\274\237/final\344\277\256\351\245\260\346\210\220\345\221\230\345\217\230\351\207\217.png" "b/06.\344\275\240\344\273\245\344\270\272\344\275\240\347\234\237\347\232\204\344\272\206\350\247\243final\345\220\227\357\274\237/final\344\277\256\351\245\260\346\210\220\345\221\230\345\217\230\351\207\217.png" deleted file mode 100644 index 9c60de8..0000000 Binary files "a/06.\344\275\240\344\273\245\344\270\272\344\275\240\347\234\237\347\232\204\344\272\206\350\247\243final\345\220\227\357\274\237/final\344\277\256\351\245\260\346\210\220\345\221\230\345\217\230\351\207\217.png" and /dev/null differ diff --git "a/06.\344\275\240\344\273\245\344\270\272\344\275\240\347\234\237\347\232\204\344\272\206\350\247\243final\345\220\227\357\274\237/final\345\237\237\345\206\231\345\217\257\350\203\275\347\232\204\345\255\230\345\234\250\347\232\204\346\211\247\350\241\214\346\227\266\345\272\217.png" "b/06.\344\275\240\344\273\245\344\270\272\344\275\240\347\234\237\347\232\204\344\272\206\350\247\243final\345\220\227\357\274\237/final\345\237\237\345\206\231\345\217\257\350\203\275\347\232\204\345\255\230\345\234\250\347\232\204\346\211\247\350\241\214\346\227\266\345\272\217.png" deleted file mode 100644 index 627db3a..0000000 Binary files "a/06.\344\275\240\344\273\245\344\270\272\344\275\240\347\234\237\347\232\204\344\272\206\350\247\243final\345\220\227\357\274\237/final\345\237\237\345\206\231\345\217\257\350\203\275\347\232\204\345\255\230\345\234\250\347\232\204\346\211\247\350\241\214\346\227\266\345\272\217.png" and /dev/null differ diff --git "a/06.\344\275\240\344\273\245\344\270\272\344\275\240\347\234\237\347\232\204\344\272\206\350\247\243final\345\220\227\357\274\237/final\345\237\237\345\274\225\347\224\250\345\217\257\350\203\275\347\232\204\346\211\247\350\241\214\346\227\266\345\272\217.png" "b/06.\344\275\240\344\273\245\344\270\272\344\275\240\347\234\237\347\232\204\344\272\206\350\247\243final\345\220\227\357\274\237/final\345\237\237\345\274\225\347\224\250\345\217\257\350\203\275\347\232\204\346\211\247\350\241\214\346\227\266\345\272\217.png" deleted file mode 100644 index 183f31a..0000000 Binary files "a/06.\344\275\240\344\273\245\344\270\272\344\275\240\347\234\237\347\232\204\344\272\206\350\247\243final\345\220\227\357\274\237/final\345\237\237\345\274\225\347\224\250\345\217\257\350\203\275\347\232\204\346\211\247\350\241\214\346\227\266\345\272\217.png" and /dev/null differ diff --git "a/06.\344\275\240\344\273\245\344\270\272\344\275\240\347\234\237\347\232\204\344\272\206\350\247\243final\345\220\227\357\274\237/final\345\237\237\350\257\273\345\217\257\350\203\275\345\255\230\345\234\250\347\232\204\346\211\247\350\241\214\346\227\266\345\272\217.png" "b/06.\344\275\240\344\273\245\344\270\272\344\275\240\347\234\237\347\232\204\344\272\206\350\247\243final\345\220\227\357\274\237/final\345\237\237\350\257\273\345\217\257\350\203\275\345\255\230\345\234\250\347\232\204\346\211\247\350\241\214\346\227\266\345\272\217.png" deleted file mode 100644 index 3113b87..0000000 Binary files "a/06.\344\275\240\344\273\245\344\270\272\344\275\240\347\234\237\347\232\204\344\272\206\350\247\243final\345\220\227\357\274\237/final\345\237\237\350\257\273\345\217\257\350\203\275\345\255\230\345\234\250\347\232\204\346\211\247\350\241\214\346\227\266\345\272\217.png" and /dev/null differ diff --git "a/06.\344\275\240\344\273\245\344\270\272\344\275\240\347\234\237\347\232\204\344\272\206\350\247\243final\345\220\227\357\274\237/final\346\226\271\346\263\225\344\270\215\350\203\275\351\207\215\345\206\231.png" "b/06.\344\275\240\344\273\245\344\270\272\344\275\240\347\234\237\347\232\204\344\272\206\350\247\243final\345\220\227\357\274\237/final\346\226\271\346\263\225\344\270\215\350\203\275\351\207\215\345\206\231.png" deleted file mode 100644 index e5c81bf..0000000 Binary files "a/06.\344\275\240\344\273\245\344\270\272\344\275\240\347\234\237\347\232\204\344\272\206\350\247\243final\345\220\227\357\274\237/final\346\226\271\346\263\225\344\270\215\350\203\275\351\207\215\345\206\231.png" and /dev/null differ diff --git "a/06.\344\275\240\344\273\245\344\270\272\344\275\240\347\234\237\347\232\204\344\272\206\350\247\243final\345\220\227\357\274\237/final\347\261\273\344\270\215\350\203\275\347\273\247\346\211\277.png" "b/06.\344\275\240\344\273\245\344\270\272\344\275\240\347\234\237\347\232\204\344\272\206\350\247\243final\345\220\227\357\274\237/final\347\261\273\344\270\215\350\203\275\347\273\247\346\211\277.png" deleted file mode 100644 index 5aed3dc..0000000 Binary files "a/06.\344\275\240\344\273\245\344\270\272\344\275\240\347\234\237\347\232\204\344\272\206\350\247\243final\345\220\227\357\274\237/final\347\261\273\344\270\215\350\203\275\347\273\247\346\211\277.png" and /dev/null differ diff --git "a/06.\344\275\240\344\273\245\344\270\272\344\275\240\347\234\237\347\232\204\344\272\206\350\247\243final\345\220\227\357\274\237/java\345\205\263\351\224\256\345\255\227--final.md" "b/06.\344\275\240\344\273\245\344\270\272\344\275\240\347\234\237\347\232\204\344\272\206\350\247\243final\345\220\227\357\274\237/java\345\205\263\351\224\256\345\255\227--final.md" deleted file mode 100644 index 5991b9b..0000000 --- "a/06.\344\275\240\344\273\245\344\270\272\344\275\240\347\234\237\347\232\204\344\272\206\350\247\243final\345\220\227\357\274\237/java\345\205\263\351\224\256\345\255\227--final.md" +++ /dev/null @@ -1,297 +0,0 @@ -# 1. final的简介 # -final可以修饰**变量,方法和类**,用于表示所修饰的内容一旦赋值之后就不会再被改变,比如String类就是一个final类型的类。即使能够知道final具体的使用方法,我想对**final在多线程中存在的重排序问题**也很容易忽略,希望能够一起做下探讨。 -# 2. final的具体使用场景 # -final能够修饰变量,方法和类,也就是final使用范围基本涵盖了java每个地方,下面就分别以锁修饰的位置:变量,方法和类分别来说一说。 -## 2.1 变量 ## -在java中变量,可以分为**成员变量**以及方法**局部变量**。因此也是按照这种方式依次来说,以避免漏掉任何一个死角。 -### 2.1.1 final成员变量 ### -通常每个类中的成员变量可以分为**类变量(static修饰的变量)以及实例变量**。针对这两种类型的变量赋初值的时机是不同的,类变量可以在声明变量的时候直接赋初值或者在静态代码块中给类变量赋初值。而实例变量可以在声明变量的时候给实例变量赋初值,在非静态初始化块中以及构造器中赋初值。类变量有**两个时机赋初值**,而实例变量则可以有**三个时机赋初值**。当final变量未初始化时系统不会进行隐式初始化,会出现报错。这样说起来还是比较抽象,下面用具体的代码来演示。(代码涵盖了final修饰变量所有的可能情况,耐心看下去会有收获的:) ) - - -![final修饰成员变量](http://upload-images.jianshu.io/upload_images/2615789-768017317b5fab78.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -看上面的图片已经将每种情况整理出来了,这里用截图的方式也是觉得在IDE出现红色出错的标记更能清晰的说明情况。现在我们来将这几种情况归纳整理一下: - -1. **类变量**:必须要在**静态初始化块**中指定初始值或者**声明该类变量时**指定初始值,而且只能在这**两个地方**之一进行指定; -2. **实例变量**:必要要在**非静态初始化块**,**声明该实例变量**或者在**构造器中**指定初始值,而且只能在这**三个地方**进行指定。 -### 2.2.2 final局部变量 ### -final局部变量由程序员进行显式初始化,如果final局部变量已经进行了初始化则后面就不能再次进行更改,如果final变量未进行初始化,可以进行赋值,**当且仅有一次**赋值,一旦赋值之后再次赋值就会出错。下面用具体的代码演示final局部变量的情况: - -![final修饰局部变量](http://upload-images.jianshu.io/upload_images/2615789-7077bdb169d4d1c3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -现在我们来换一个角度进行考虑,final修饰的是基本数据类型和引用类型有区别吗? - -> **final基本数据类型 VS final引用数据类型** - -通过上面的例子我们已经看出来,如果final修饰的是一个基本数据类型的数据,一旦赋值后就不能再次更改,那么,如果final是引用数据类型了?这个引用的对象能够改变吗?我们同样来看一段代码。 - - public class FinalExample { - //在声明final实例成员变量时进行赋值 - private final static Person person = new Person(24, 170); - public static void main(String[] args) { - //对final引用数据类型person进行更改 - person.age = 22; - System.out.println(person.toString()); - } - static class Person { - private int age; - private int height; - - public Person(int age, int height) { - this.age = age; - this.height = height; - } - @Override - public String toString() { - return "Person{" + - "age=" + age + - ", height=" + height + - '}'; - } - } - } - -当我们对final修饰的引用数据类型变量person的属性改成22,是可以成功操作的。通过这个实验我们就可以看出来**当final修饰基本数据类型变量时,不能对基本数据类型变量重新赋值,因此基本数据类型变量不能被改变。而对于引用类型变量而言,它仅仅保存的是一个引用,final只保证这个引用类型变量所引用的地址不会发生改变,即一直引用这个对象,但这个对象属性是可以改变的**。 - -> **宏变量** - -利用final变量的不可更改性,在满足一下三个条件时,该变量就会成为一个“宏变量”,即是一个常量。 - -1. 使用final修饰符修饰; -2. 在定义该final变量时就指定了初始值; -3. 该初始值在编译时就能够唯一指定。 - -注意:当程序中其他地方使用该宏变量的地方,编译器会直接替换成该变量的值 - -## 2.2 方法 ## -> **重写?** - -当父类的方法被final修饰的时候,子类不能重写父类的该方法,比如在Object中,getClass()方法就是final的,我们就不能重写该方法,但是hashCode()方法就不是被final所修饰的,我们就可以重写hashCode()方法。我们还是来写一个例子来加深一下理解: -先定义一个父类,里面有final修饰的方法test(); - - public class FinalExampleParent { - public final void test() { - } - } - -然后FinalExample继承该父类,当重写test()方法时出现报错,如下图: - -![final方法不能重写](http://upload-images.jianshu.io/upload_images/2615789-5d831da449f512e9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -通过这个现象我们就可以看出来**被final修饰的方法不能够被子类所重写**。 - -> **重载?** - - public class FinalExampleParent { - public final void test() { - } - - public final void test(String str) { - } - } - -可以看出被final修饰的方法是可以重载的。经过我们的分析可以得出如下结论: - -**1. 父类的final方法是不能够被子类重写的** - -**2. final方法是可以被重载的** -## 2.3 类 ## -**当一个类被final修饰时,表名该类是不能被子类继承的**。子类继承往往可以重写父类的方法和改变父类属性,会带来一定的安全隐患,因此,当一个类不希望被继承时就可以使用final修饰。还是来写一个小例子: - - public final class FinalExampleParent { - public final void test() { - } - } - -父类会被final修饰,当子类继承该父类的时候,就会报错,如下图: - -![final类不能继承](http://upload-images.jianshu.io/upload_images/2615789-835b66d960e21e2e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -# 3. final的例子 # -final经常会被用作不变类上,利用final的不可更改性。我们先来看看什么是不变类。 -> 不变类 - -不变类的意思是创建该类的实例后,该实例的实例变量是不可改变的。满足以下条件则可以成为不可变类: - -1. 使用private和final修饰符来修饰该类的成员变量 -2. 提供带参的构造器用于初始化类的成员变量; -3. 仅为该类的成员变量提供getter方法,不提供setter方法,因为普通方法无法修改fina修饰的成员变量; -4. 如果有必要就重写Object类 的hashCode()和equals()方法,应该保证用equals()判断相同的两个对象其Hashcode值也是相等的。 - -JDK中提供的八个包装类和String类都是不可变类,我们来看看String的实现。 - - /** The value is used for character storage. */ - private final char value[]; -可以看出String的value就是final修饰的,上述其他几条性质也是吻合的。 - -# 4. 多线程中你真的了解final吗? # -上面我们聊的final使用,应该属于**Java基础层面**的,当理解这些后我们就真的算是掌握了final吗?有考虑过final在多线程并发的情况吗?在[java内存模型](https://juejin.im/post/5ae6d309518825673123fd0e)中我们知道java内存模型为了能让处理器和编译器底层发挥他们的最大优势,对底层的约束就很少,也就是说针对底层来说java内存模型就是一弱内存数据模型。同时,处理器和编译为了性能优化会对指令序列有**编译器和处理器重排序**。那么,在多线程情况下,final会进行怎样的重排序?会导致线程安全的问题吗?下面,就来看看final的重排序。 - -## 4.1 final域重排序规则 ## - -### 4.1.1 final域为基本类型 ### -先看一段示例性的代码: - - public class FinalDemo { - private int a; //普通域 - private final int b; //final域 - private static FinalDemo finalDemo; - - public FinalDemo() { - a = 1; // 1. 写普通域 - b = 2; // 2. 写final域 - } - - public static void writer() { - finalDemo = new FinalDemo(); - } - - public static void reader() { - FinalDemo demo = finalDemo; // 3.读对象引用 - int a = demo.a; //4.读普通域 - int b = demo.b; //5.读final域 - } - } - -假设线程A在执行writer()方法,线程B执行reader()方法。 - -> **写final域重排序规则** - - -写final域的重排序规则**禁止对final域的写重排序到构造函数之外**,这个规则的实现主要包含了两个方面: - -1. JMM禁止编译器把final域的写重排序到构造函数之外; -2. 编译器会在final域写之后,构造函数return之前,插入一个storestore屏障(关于内存屏障可以看[这篇文章](https://juejin.im/post/5ae6d309518825673123fd0e))。这个屏障可以禁止处理器把final域的写重排序到构造函数之外。 - -我们再来分析writer方法,虽然只有一行代码,但实际上做了两件事情: - -1. 构造了一个FinalDemo对象; -2. 把这个对象赋值给成员变量finalDemo。 - -我们来画下存在的一种可能执行时序图,如下: - -![final域写可能的存在的执行时序](http://upload-images.jianshu.io/upload_images/2615789-9e3937df955a9862.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/800) - -由于a,b之间没有数据依赖性,普通域(普通变量)a可能会被重排序到构造函数之外,线程B就有可能读到的是普通变量a初始化之前的值(零值),这样就可能出现错误。而final域变量b,根据重排序规则,会禁止final修饰的变量b重排序到构造函数之外,从而b能够正确赋值,线程B就能够读到final变量初始化后的值。 - -因此,写final域的重排序规则可以确保:**在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通域就不具有这个保障**。比如在上例,线程B有可能就是一个未正确初始化的对象finalDemo。 - - ->**读final域重排序规则** - -读final域重排序规则为:**在一个线程中,初次读对象引用和初次读该对象包含的final域,JMM会禁止这两个操作的重排序。**(注意,这个规则仅仅是针对处理器),处理器会在读final域操作的前面插入一个LoadLoad屏障。实际上,读对象的引用和读该对象的final域存在间接依赖性,一般处理器不会重排序这两个操作。但是有一些处理器会重排序,因此,这条禁止重排序规则就是针对这些处理器而设定的。 - -read()方法主要包含了三个操作: - -1. 初次读引用变量finalDemo; -2. 初次读引用变量finalDemo的普通域a; -3. 初次读引用变量finalDemo的final与b; - -假设线程A写过程没有重排序,那么线程A和线程B有一种的可能执行时序为下图: - - -![final域读可能存在的执行时序](http://upload-images.jianshu.io/upload_images/2615789-2a93b67948d7fc64.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/800) - -读对象的普通域被重排序到了读对象引用的前面就会出现线程B还未读到对象引用就在读取该对象的普通域变量,这显然是错误的操作。而final域的读操作就“限定”了在读final域变量前已经读到了该对象的引用,从而就可以避免这种情况。 - -读final域的重排序规则可以确保:**在读一个对象的final域之前,一定会先读这个包含这个final域的对象的引用。** - - - -### 4.1.2 final域为引用类型 ### -我们已经知道了final域是基本数据类型的时候重排序规则是怎么的了?如果是**引用数据类型**了?我们接着继续来探讨。 - -> **对final修饰的对象的成员域写操作** - -针对引用数据类型,final域写针对编译器和处理器重排序**增加了这样的约束**:在构造函数内对**一个final修饰的对象的成员域的写入,与随后在构造函数之外把这个被构造的对象的引用赋给一个引用变量**,这两个操作是不能被重排序的。注意这里的是“增加”也就说前面对final基本数据类型的重排序规则在这里还是使用。这句话是比较拗口的,下面结合实例来看。 - - - public class FinalReferenceDemo { - final int[] arrays; - private FinalReferenceDemo finalReferenceDemo; - - public FinalReferenceDemo() { - arrays = new int[1]; //1 - arrays[0] = 1; //2 - } - - public void writerOne() { - finalReferenceDemo = new FinalReferenceDemo(); //3 - } - - public void writerTwo() { - arrays[0] = 2; //4 - } - - public void reader() { - if (finalReferenceDemo != null) { //5 - int temp = finalReferenceDemo.arrays[0]; //6 - } - } - } - - -针对上面的实例程序,线程线程A执行wirterOne方法,执行完后线程B执行writerTwo方法,然后线程C执行reader方法。下图就以这种执行时序出现的一种情况来讨论(耐心看完才有收获)。 - - -![写final修饰引用类型数据可能的执行时序](http://upload-images.jianshu.io/upload_images/2615789-1f5f0a39a3f6977e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/800) - -由于对final域的写禁止重排序到构造方法外,因此1和3不能被重排序。由于一个final域的引用对象的成员域写入不能与随后将这个被构造出来的对象赋给引用变量重排序,因此2和3不能重排序。 - -> **对final修饰的对象的成员域读操作** - -JMM可以确保线程C至少能看到写线程A对final引用的对象的成员域的写入,即能看下arrays[0] = 1,而写线程B对数组元素的写入可能看到可能看不到。JMM不保证线程B的写入对线程C可见,线程B和线程C之间存在数据竞争,此时的结果是不可预知的。如果可见的,可使用锁或者volatile。 - ->**关于final重排序的总结** - -按照final修饰的数据类型分类: - -基本数据类型: - -1. final域写:禁止**final域写**与**构造方法**重排序,即禁止final域写重排序到构造方法之外,从而保证该对象对所有线程可见时,该对象的final域全部已经初始化过。 -2. final域读:禁止初次**读对象的引用**与**读该对象包含的final域**的重排序。 - -引用数据类型: - -额外增加约束:禁止在构造函数对**一个final修饰的对象的成员域的写入**与随后将**这个被构造的对象的引用赋值给引用变量** 重排序 - -# 5.final的实现原理 # -上面我们提到过,写final域会要求编译器在final域写之后,构造函数返回前插入一个StoreStore屏障。读final域的重排序规则会要求编译器在读final域的操作前插入一个LoadLoad屏障。 - -很有意思的是,如果以X86处理为例,X86不会对写-写重排序,所以**StoreStore屏障可以省略**。由于**不会对有间接依赖性的操作重排序**,所以在X86处理器中,读final域需要的**LoadLoad屏障也会被省略掉**。也就是说,**以X86为例的话,对final域的读/写的内存屏障都会被省略**!具体是否插入还是得看是什么处理器 - -# 6. 为什么final引用不能从构造函数中“溢出” # -这里还有一个比较有意思的问题:上面对final域写重排序规则可以确保我们在使用一个对象引用的时候该对象的final域已经在构造函数被初始化过了。但是这里其实是有一个前提条件的,也就是:**在构造函数,不能让这个被构造的对象被其他线程可见,也就是说该对象引用不能在构造函数中“逸出”**。以下面的例子来说: - - public class FinalReferenceEscapeDemo { - private final int a; - private FinalReferenceEscapeDemo referenceDemo; - - public FinalReferenceEscapeDemo() { - a = 1; //1 - referenceDemo = this; //2 - } - - public void writer() { - new FinalReferenceEscapeDemo(); - } - - public void reader() { - if (referenceDemo != null) { //3 - int temp = referenceDemo.a; //4 - } - } - } - -可能的执行时序如图所示: - -![final域引用可能的执行时序](http://upload-images.jianshu.io/upload_images/2615789-e020492056ee1242.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -假设一个线程A执行writer方法另一个线程执行reader方法。因为构造函数中操作1和2之间没有数据依赖性,1和2可以重排序,先执行了2,这个时候引用对象referenceDemo是个没有完全初始化的对象,而当线程B去读取该对象时就会出错。尽管依然满足了final域写重排序规则:在引用对象对所有线程可见时,其final域已经完全初始化成功。但是,引用对象“this”逸出,该代码依然存在线程安全的问题。 - - -> 参看文献 - -《java并发编程的艺术》 - -《疯狂java讲义》 \ No newline at end of file diff --git "a/06.\344\275\240\344\273\245\344\270\272\344\275\240\347\234\237\347\232\204\344\272\206\350\247\243final\345\220\227\357\274\237/\345\206\231final\344\277\256\351\245\260\345\274\225\347\224\250\347\261\273\345\236\213\346\225\260\346\215\256\345\217\257\350\203\275\347\232\204\346\211\247\350\241\214\346\227\266\345\272\217.png" "b/06.\344\275\240\344\273\245\344\270\272\344\275\240\347\234\237\347\232\204\344\272\206\350\247\243final\345\220\227\357\274\237/\345\206\231final\344\277\256\351\245\260\345\274\225\347\224\250\347\261\273\345\236\213\346\225\260\346\215\256\345\217\257\350\203\275\347\232\204\346\211\247\350\241\214\346\227\266\345\272\217.png" deleted file mode 100644 index 1c00d59..0000000 Binary files "a/06.\344\275\240\344\273\245\344\270\272\344\275\240\347\234\237\347\232\204\344\272\206\350\247\243final\345\220\227\357\274\237/\345\206\231final\344\277\256\351\245\260\345\274\225\347\224\250\347\261\273\345\236\213\346\225\260\346\215\256\345\217\257\350\203\275\347\232\204\346\211\247\350\241\214\346\227\266\345\272\217.png" and /dev/null differ diff --git "a/07.\344\270\211\345\244\247\346\200\247\350\264\250\346\200\273\347\273\223\357\274\232\345\216\237\345\255\220\346\200\247\343\200\201\345\217\257\350\247\201\346\200\247\344\273\245\345\217\212\346\234\211\345\272\217\346\200\247/\344\270\211\345\244\247\346\200\247\350\264\250\346\200\273\347\273\223\357\274\232\345\216\237\345\255\220\346\200\247\343\200\201\345\217\257\350\247\201\346\200\247\344\273\245\345\217\212\346\234\211\345\272\217\346\200\247.md" "b/07.\344\270\211\345\244\247\346\200\247\350\264\250\346\200\273\347\273\223\357\274\232\345\216\237\345\255\220\346\200\247\343\200\201\345\217\257\350\247\201\346\200\247\344\273\245\345\217\212\346\234\211\345\272\217\346\200\247/\344\270\211\345\244\247\346\200\247\350\264\250\346\200\273\347\273\223\357\274\232\345\216\237\345\255\220\346\200\247\343\200\201\345\217\257\350\247\201\346\200\247\344\273\245\345\217\212\346\234\211\345\272\217\346\200\247.md" deleted file mode 100644 index 6b93924..0000000 --- "a/07.\344\270\211\345\244\247\346\200\247\350\264\250\346\200\273\347\273\223\357\274\232\345\216\237\345\255\220\346\200\247\343\200\201\345\217\257\350\247\201\346\200\247\344\273\245\345\217\212\346\234\211\345\272\217\346\200\247/\344\270\211\345\244\247\346\200\247\350\264\250\346\200\273\347\273\223\357\274\232\345\216\237\345\255\220\346\200\247\343\200\201\345\217\257\350\247\201\346\200\247\344\273\245\345\217\212\346\234\211\345\272\217\346\200\247.md" +++ /dev/null @@ -1,116 +0,0 @@ -# 1. 三大性质简介 # -在并发编程中分析线程安全的问题时往往需要切入点,那就是**两大核心**:JMM抽象内存模型以及happens-before规则(在[这篇文章](https://juejin.im/post/5ae6d309518825673123fd0e)中已经经过了),三条性质:**原子性,有序性和可见性**。关于[synchronized](https://juejin.im/post/5ae6dc04f265da0ba351d3ff)和[volatile](https://juejin.im/post/5ae9b41b518825670b33e6c4)已经讨论过了,就想着将并发编程中这两大神器在 **原子性,有序性和可见性**上做一个比较,当然这也是面试中的高频考点,值得注意。 - -# 2. 原子性 # -原子性是指**一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉**。及时在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。我们先来看看哪些是原子操作,哪些不是原子操作,有一个直观的印象: - -> int a = 10; //1 -> -> a++; //2 -> -> int b=a; //3 -> -> a = a+1; //4 - -上面这四个语句中只**有第1个语句是原子操作**,将10赋值给线程工作内存的变量a,而语句2(a++),实际上包含了三个操作:1. 读取变量a的值;2:对a进行加一的操作;3.将计算后的值再赋值给变量a,而这三个操作无法构成原子操作。对语句3,4的分析同理可得这两条语句不具备原子性。当然,[java内存模型](https://juejin.im/post/5ae6d309518825673123fd0e)中定义了8中操作都是原子的,不可再分的。 - -1. lock(锁定):作用于主内存中的变量,它把一个变量标识为一个线程独占的状态; -2. unlock(解锁):作用于主内存中的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定 -3. read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便后面的load动作使用; -4. load(载入):作用于工作内存中的变量,它把read操作从主内存中得到的变量值放入工作内存中的变量副本 -5. use(使用):作用于工作内存中的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作; -6. assign(赋值):作用于工作内存中的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作; -7. store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送给主内存中以便随后的write操作使用; -8. write(操作):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。 - -上面的这些指令操作是相当底层的,可以作为扩展知识面掌握下。那么如何理解这些指令了?比如,把一个变量从主内存中复制到工作内存中就需要执行read,load操作,将工作内存同步到主内存中就需要执行store,write操作。注意的是:**java内存模型只是要求上述两个操作是顺序执行的并不是连续执行的**。也就是说read和load之间可以插入其他指令,store和writer可以插入其他指令。比如对主内存中的a,b进行访问就可以出现这样的操作顺序:**read a,read b, load b,load a**。 - -由原子性变量操作read,load,use,assign,store,write,可以**大致认为基本数据类型的访问读写具备原子性**(例外就是long和double的非原子性协定) - -> synchronized - -上面一共有八条原子操作,其中六条可以满足基本数据类型的访问读写具备原子性,还剩下lock和unlock两条原子操作。如果我们需要更大范围的原子性操作就可以使用lock和unlock原子操作。尽管jvm没有把lock和unlock开放给我们使用,但jvm以更高层次的指令monitorenter和monitorexit指令开放给我们使用,反应到java代码中就是---synchronized关键字,也就是说**synchronized满足原子性**。 - -> volatile -我们先来看这样一个例子: - - public class VolatileExample { - private static volatile int counter = 0; - - public static void main(String[] args) { - for (int i = 0; i < 10; i++) { - Thread thread = new Thread(new Runnable() { - @Override - public void run() { - for (int i = 0; i < 10000; i++) - counter++; - } - }); - thread.start(); - } - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - System.out.println(counter); - } - } - -开启10个线程,每个线程都自加10000次,如果不出现线程安全的问题最终的结果应该就是:10*10000 = 100000;可是运行多次都是小于100000的结果,问题在于 **volatile并不能保证原子性**,在前面说过counter++这并不是一个原子操作,包含了三个步骤:1.读取变量counter的值;2.对counter加一;3.将新值赋值给变量counter。如果线程A读取counter到工作内存后,其他线程对这个值已经做了自增操作后,那么线程A的这个值自然而然就是一个过期的值,因此,总结果必然会是小于100000的。 - -如果让volatile保证原子性,必须符合以下两条规则: - -1. **运算结果并不依赖于变量的当前值,或者能够确保只有一个线程修改变量的值;** -2. **变量不需要与其他的状态变量共同参与不变约束** - -# 3. 有序性 # - -> synchronized - -synchronized语义表示锁在同一时刻只能由一个线程进行获取,当锁被占用后,其他线程只能等待。因此,synchronized语义就要求线程在访问读写共享变量时只能“串行”执行,因此**synchronized具有有序性**。 - -> volatile - -在java内存模型中说过,为了性能优化,编译器和处理器会进行指令重排序;也就是说java程序天然的有序性可以总结为:**如果在本线程内观察,所有的操作都是有序的;如果在一个线程观察另一个线程,所有的操作都是无序的**。在单例模式的实现上有一种双重检验锁定的方式(Double-checked Locking)。代码如下: - - - public class Singleton { - private Singleton() { } - private volatile static Singleton instance; - public Singleton getInstance(){ - if(instance==null){ - synchronized (Singleton.class){ - if(instance==null){ - instance = new Singleton(); - } - } - } - return instance; - } - } - -这里为什么要加volatile了?我们先来分析一下不加volatile的情况,有问题的语句是这条: -> instance = new Singleton(); - -这条语句实际上包含了三个操作:1.分配对象的内存空间;2.初始化对象;3.设置instance指向刚分配的内存地址。但由于存在重排序的问题,可能有以下的执行顺序: - -![不加volatile可能的执行时序](http://upload-images.jianshu.io/upload_images/2615789-e7931260b0449eb1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - - -如果2和3进行了重排序的话,线程B进行判断if(instance==null)时就会为true,而实际上这个instance并没有初始化成功,显而易见对线程B来说之后的操作就会是错得。而**用volatile修饰**的话就可以禁止2和3操作重排序,从而避免这种情况。**volatile包含禁止指令重排序的语义,其具有有序性**。 -# 4. 可见性 # - -可见性是指当一个线程修改了共享变量后,其他线程能够立即得知这个修改。通过之前对[synchronzed](https://juejin.im/post/5ae6dc04f265da0ba351d3ff)内存语义进行了分析,当线程获取锁时会从主内存中获取共享变量的最新值,释放锁的时候会将共享变量同步到主内存中。从而,**synchronized具有可见性**。同样的在[volatile分析中](https://juejin.im/post/5ae9b41b518825670b33e6c4),会通过在指令中添加**lock指令**,以实现内存可见性。因此, **volatile具有可见性** - -# 5. 总结 # -通过这篇文章,主要是比较了synchronized和volatile在三条性质:原子性,可见性,以及有序性的情况,归纳如下: - -> **synchronized: 具有原子性,有序性和可见性**; -> **volatile:具有有序性和可见性** - - -> 参考文献 - -《java并发编程的艺术》 -《深入理解java虚拟机》 \ No newline at end of file diff --git "a/07.\344\270\211\345\244\247\346\200\247\350\264\250\346\200\273\347\273\223\357\274\232\345\216\237\345\255\220\346\200\247\343\200\201\345\217\257\350\247\201\346\200\247\344\273\245\345\217\212\346\234\211\345\272\217\346\200\247/\344\270\215\345\212\240volatile\345\217\257\350\203\275\347\232\204\346\211\247\350\241\214\346\227\266\345\272\217.png" "b/07.\344\270\211\345\244\247\346\200\247\350\264\250\346\200\273\347\273\223\357\274\232\345\216\237\345\255\220\346\200\247\343\200\201\345\217\257\350\247\201\346\200\247\344\273\245\345\217\212\346\234\211\345\272\217\346\200\247/\344\270\215\345\212\240volatile\345\217\257\350\203\275\347\232\204\346\211\247\350\241\214\346\227\266\345\272\217.png" deleted file mode 100644 index 257dc83..0000000 Binary files "a/07.\344\270\211\345\244\247\346\200\247\350\264\250\346\200\273\347\273\223\357\274\232\345\216\237\345\255\220\346\200\247\343\200\201\345\217\257\350\247\201\346\200\247\344\273\245\345\217\212\346\234\211\345\272\217\346\200\247/\344\270\215\345\212\240volatile\345\217\257\350\203\275\347\232\204\346\211\247\350\241\214\346\227\266\345\272\217.png" and /dev/null differ diff --git "a/08.\345\210\235\350\257\206Lock\344\270\216AbstractQueuedSynchronizer(AQS)/AQS\345\217\257\351\207\215\345\206\231\347\232\204\346\226\271\346\263\225.png" "b/08.\345\210\235\350\257\206Lock\344\270\216AbstractQueuedSynchronizer(AQS)/AQS\345\217\257\351\207\215\345\206\231\347\232\204\346\226\271\346\263\225.png" deleted file mode 100644 index 67b2388..0000000 Binary files "a/08.\345\210\235\350\257\206Lock\344\270\216AbstractQueuedSynchronizer(AQS)/AQS\345\217\257\351\207\215\345\206\231\347\232\204\346\226\271\346\263\225.png" and /dev/null differ diff --git "a/08.\345\210\235\350\257\206Lock\344\270\216AbstractQueuedSynchronizer(AQS)/AQS\346\217\220\344\276\233\347\232\204\346\250\241\346\235\277\346\226\271\346\263\225.png" "b/08.\345\210\235\350\257\206Lock\344\270\216AbstractQueuedSynchronizer(AQS)/AQS\346\217\220\344\276\233\347\232\204\346\250\241\346\235\277\346\226\271\346\263\225.png" deleted file mode 100644 index c5777d6..0000000 Binary files "a/08.\345\210\235\350\257\206Lock\344\270\216AbstractQueuedSynchronizer(AQS)/AQS\346\217\220\344\276\233\347\232\204\346\250\241\346\235\277\346\226\271\346\263\225.png" and /dev/null differ diff --git "a/08.\345\210\235\350\257\206Lock\344\270\216AbstractQueuedSynchronizer(AQS)/concurrent\345\214\205\345\256\236\347\216\260\346\225\264\344\275\223\347\244\272\346\204\217\345\233\276.png" "b/08.\345\210\235\350\257\206Lock\344\270\216AbstractQueuedSynchronizer(AQS)/concurrent\345\214\205\345\256\236\347\216\260\346\225\264\344\275\223\347\244\272\346\204\217\345\233\276.png" deleted file mode 100644 index 64e55cb..0000000 Binary files "a/08.\345\210\235\350\257\206Lock\344\270\216AbstractQueuedSynchronizer(AQS)/concurrent\345\214\205\345\256\236\347\216\260\346\225\264\344\275\223\347\244\272\346\204\217\345\233\276.png" and /dev/null differ diff --git "a/08.\345\210\235\350\257\206Lock\344\270\216AbstractQueuedSynchronizer(AQS)/concurrent\347\233\256\345\275\225\347\273\223\346\236\204.png" "b/08.\345\210\235\350\257\206Lock\344\270\216AbstractQueuedSynchronizer(AQS)/concurrent\347\233\256\345\275\225\347\273\223\346\236\204.png" deleted file mode 100644 index 02cc9d5..0000000 Binary files "a/08.\345\210\235\350\257\206Lock\344\270\216AbstractQueuedSynchronizer(AQS)/concurrent\347\233\256\345\275\225\347\273\223\346\236\204.png" and /dev/null differ diff --git "a/08.\345\210\235\350\257\206Lock\344\270\216AbstractQueuedSynchronizer(AQS)/mutex\347\232\204\346\211\247\350\241\214\346\203\205\345\206\265.png" "b/08.\345\210\235\350\257\206Lock\344\270\216AbstractQueuedSynchronizer(AQS)/mutex\347\232\204\346\211\247\350\241\214\346\203\205\345\206\265.png" deleted file mode 100644 index 908d7a6..0000000 Binary files "a/08.\345\210\235\350\257\206Lock\344\270\216AbstractQueuedSynchronizer(AQS)/mutex\347\232\204\346\211\247\350\241\214\346\203\205\345\206\265.png" and /dev/null differ diff --git "a/08.\345\210\235\350\257\206Lock\344\270\216AbstractQueuedSynchronizer(AQS)/\345\210\235\350\257\206Lock\344\270\216AbstractQueuedSynchronizer(AQS).md" "b/08.\345\210\235\350\257\206Lock\344\270\216AbstractQueuedSynchronizer(AQS)/\345\210\235\350\257\206Lock\344\270\216AbstractQueuedSynchronizer(AQS).md" deleted file mode 100644 index 329ff6f..0000000 --- "a/08.\345\210\235\350\257\206Lock\344\270\216AbstractQueuedSynchronizer(AQS)/\345\210\235\350\257\206Lock\344\270\216AbstractQueuedSynchronizer(AQS).md" +++ /dev/null @@ -1,246 +0,0 @@ -# 1. concurrent包的结构层次 # -在针对并发编程中,Doug Lea大师为我们提供了大量实用,高性能的工具类,针对这些代码进行研究会让我们队并发编程的掌握更加透彻也会大大提升我们队并发编程技术的热爱。这些代码在java.util.concurrent包下。如下图,即为concurrent包的目录结构图。 - -![concurrent目录结构.png](http://upload-images.jianshu.io/upload_images/2615789-da951eb99c5dabfd.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - - -其中包含了两个子包:atomic以及lock,另外在concurrent下的阻塞队列以及executors,这些就是concurrent包中的精华,之后会一一进行学习。而这些类的实现主要是依赖于volatile以及CAS(关于volatile可以看[这篇文章](https://juejin.im/post/5ae9b41b518825670b33e6c4),关于CAS可以看[这篇文章的3.1节](https://juejin.im/post/5ae6dc04f265da0ba351d3ff)),从整体上来看concurrent包的整体实现图如下图所示: - -![concurrent包实现整体示意图.png](http://upload-images.jianshu.io/upload_images/2615789-24da822ddc226b03.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - - -# 2. lock简介 # -我们下来看concurent包下的lock子包。锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源。在Lock接口出现之前,java程序主要是靠synchronized关键字实现锁功能的,而java SE5之后,并发包中增加了lock接口,它提供了与synchronized一样的锁功能。**虽然它失去了像synchronize关键字隐式加锁解锁的便捷性,但是却拥有了锁获取和释放的可操作性,可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。**通常使用显示使用lock的形式如下: - - Lock lock = new ReentrantLock(); - lock.lock(); - try{ - ....... - }finally{ - lock.unlock(); - } - -需要注意的是**synchronized同步块执行完成或者遇到异常是锁会自动释放,而lock必须调用unlock()方法释放锁,因此在finally块中释放锁**。 - -## 2.1 Lock接口API ## - -我们现在就来看看lock接口定义了哪些方法: - -> void lock(); //获取锁 -> void lockInterruptibly() throws InterruptedException;//获取锁的过程能够响应中断 -> boolean tryLock();//非阻塞式响应中断能立即返回,获取锁放回true反之返回fasle -> boolean tryLock(long time, TimeUnit unit) throws InterruptedException;//超时获取锁,在超时内或者未中断的情况下能够获取锁 -> Condition newCondition();//获取与lock绑定的等待通知组件,当前线程必须获得了锁才能进行等待,进行等待时会先释放锁,当再次获取锁时才能从等待中返回 - -上面是lock接口下的五个方法,也只是从源码中英译中翻译了一遍,感兴趣的可以自己的去看看。那么在locks包下有哪些类实现了该接口了?先从最熟悉的ReentrantLock说起。 - -> public class ReentrantLock implements **Lock**, java.io.Serializable - -很显然ReentrantLock实现了lock接口,接下来我们来仔细研究一下它是怎样实现的。当你查看源码时你会惊讶的发现ReentrantLock并没有多少代码,另外有一个很明显的特点是:**基本上所有的方法的实现实际上都是调用了其静态内存类`Sync`中的方法,而Sync类继承了`AbstractQueuedSynchronizer(AQS)`**。可以看出要想理解ReentrantLock关键核心在于对队列同步器AbstractQueuedSynchronizer(简称同步器)的理解。 - -## 2.2 初识AQS ## - -关于AQS在源码中有十分具体的解释: - - Provides a framework for implementing blocking locks and related - synchronizers (semaphores, events, etc) that rely on - first-in-first-out (FIFO) wait queues. This class is designed to - be a useful basis for most kinds of synchronizers that rely on a - single atomic {@code int} value to represent state. Subclasses - must define the protected methods that change this state, and which - define what that state means in terms of this object being acquired - or released. Given these, the other methods in this class carry - out all queuing and blocking mechanics. Subclasses can maintain - other state fields, but only the atomically updated {@code int} - value manipulated using methods {@link #getState}, {@link - #setState} and {@link #compareAndSetState} is tracked with respect - to synchronization. - -

Subclasses should be defined as non-public internal helper - classes that are used to implement the synchronization properties - of their enclosing class. Class - {@code AbstractQueuedSynchronizer} does not implement any - synchronization interface. Instead it defines methods such as - {@link #acquireInterruptibly} that can be invoked as - appropriate by concrete locks and related synchronizers to - implement their public methods. - -同步器是用来构建锁和其他同步组件的基础框架,它的实现主要依赖一个int成员变量来表示同步状态以及通过一个FIFO队列构成等待队列。它的**子类必须重写AQS的几个protected修饰的用来改变同步状态的方法**,其他方法主要是实现了排队和阻塞机制。**状态的更新使用getState,setState以及compareAndSetState这三个方法**。 - -子类被**推荐定义为自定义同步组件的静态内部类**,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态的获取和释放方法来供自定义同步组件的使用,同步器既支持独占式获取同步状态,也可以支持共享式获取同步状态,这样就可以方便的实现不同类型的同步组件。 - -同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。可以这样理解二者的关系:**锁是面向使用者,它定义了使用者与锁交互的接口,隐藏了实现细节;同步器是面向锁的实现者,它简化了锁的实现方式,屏蔽了同步状态的管理,线程的排队,等待和唤醒等底层操作**。锁和同步器很好的隔离了使用者和实现者所需关注的领域。 - -## 2.3 AQS的模板方法设计模式 ## - -AQS的设计是使用模板方法设计模式,它将**一些方法开放给子类进行重写,而同步器给同步组件所提供模板方法又会重新调用被子类所重写的方法**。举个例子,AQS中需要重写的方法tryAcquire: - - protected boolean tryAcquire(int arg) { - throw new UnsupportedOperationException(); - } - -ReentrantLock中NonfairSync(继承AQS)会重写该方法为: - - protected final boolean tryAcquire(int acquires) { - return nonfairTryAcquire(acquires); - } -而AQS中的模板方法acquire(): - - public final void acquire(int arg) { - if (!tryAcquire(arg) && - acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) - selfInterrupt(); - } -会调用tryAcquire方法,而此时当继承AQS的NonfairSync调用模板方法acquire时就会调用已经被NonfairSync重写的tryAcquire方法。这就是使用AQS的方式,在弄懂这点后会lock的实现理解有很大的提升。可以归纳总结为这么几点: - -1. 同步组件(这里不仅仅值锁,还包括CountDownLatch等)的实现依赖于同步器AQS,在同步组件实现中,使用AQS的方式被推荐定义继承AQS的静态内存类; -2. AQS采用模板方法进行设计,AQS的protected修饰的方法需要由继承AQS的子类进行重写实现,当调用AQS的子类的方法时就会调用被重写的方法; -3. AQS负责同步状态的管理,线程的排队,等待和唤醒这些底层操作,而Lock等同步组件主要专注于实现同步语义; -4. 在重写AQS的方式时,使用AQS提供的`getState(),setState(),compareAndSetState()`方法进行修改同步状态 - -AQS可重写的方法如下图(摘自《java并发编程的艺术》一书): - -![AQS可重写的方法.png](http://upload-images.jianshu.io/upload_images/2615789-214b5823e76f8eb0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -在实现同步组件时AQS提供的模板方法如下图: - -![AQS提供的模板方法.png](http://upload-images.jianshu.io/upload_images/2615789-33aa10c3be109206.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - - -AQS提供的模板方法可以分为3类: -1. 独占式获取与释放同步状态; -2. 共享式获取与释放同步状态; -3. 查询同步队列中等待线程情况; - -同步组件通过AQS提供的模板方法实现自己的同步语义。 - -# 3. 一个例子 # -下面使用一个例子来进一步理解下AQS的使用。这个例子也是来源于AQS源码中的example。 - - class Mutex implements Lock, java.io.Serializable { - // Our internal helper class - // 继承AQS的静态内存类 - // 重写方法 - private static class Sync extends AbstractQueuedSynchronizer { - // Reports whether in locked state - protected boolean isHeldExclusively() { - return getState() == 1; - } - - // Acquires the lock if state is zero - public boolean tryAcquire(int acquires) { - assert acquires == 1; // Otherwise unused - if (compareAndSetState(0, 1)) { - setExclusiveOwnerThread(Thread.currentThread()); - return true; - } - return false; - } - - // Releases the lock by setting state to zero - protected boolean tryRelease(int releases) { - assert releases == 1; // Otherwise unused - if (getState() == 0) throw new IllegalMonitorStateException(); - setExclusiveOwnerThread(null); - setState(0); - return true; - } - - // Provides a Condition - Condition newCondition() { - return new ConditionObject(); - } - - // Deserializes properly - private void readObject(ObjectInputStream s) - throws IOException, ClassNotFoundException { - s.defaultReadObject(); - setState(0); // reset to unlocked state - } - } - - // The sync object does all the hard work. We just forward to it. - private final Sync sync = new Sync(); - //使用同步器的模板方法实现自己的同步语义 - public void lock() { - sync.acquire(1); - } - - public boolean tryLock() { - return sync.tryAcquire(1); - } - - public void unlock() { - sync.release(1); - } - - public Condition newCondition() { - return sync.newCondition(); - } - - public boolean isLocked() { - return sync.isHeldExclusively(); - } - - public boolean hasQueuedThreads() { - return sync.hasQueuedThreads(); - } - - public void lockInterruptibly() throws InterruptedException { - sync.acquireInterruptibly(1); - } - - public boolean tryLock(long timeout, TimeUnit unit) - throws InterruptedException { - return sync.tryAcquireNanos(1, unit.toNanos(timeout)); - } - } - -MutexDemo: - - public class MutextDemo { - private static Mutex mutex = new Mutex(); - - public static void main(String[] args) { - for (int i = 0; i < 10; i++) { - Thread thread = new Thread(() -> { - mutex.lock(); - try { - Thread.sleep(3000); - } catch (InterruptedException e) { - e.printStackTrace(); - } finally { - mutex.unlock(); - } - }); - thread.start(); - } - } - } - -执行情况: - -![mutex的执行情况.png](http://upload-images.jianshu.io/upload_images/2615789-cabcd4a169178b5b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -上面的这个例子实现了独占锁的语义,在同一个时刻只允许一个线程占有锁。MutexDemo新建了10个线程,分别睡眠3s。从执行情况也可以看出来当前Thread-6正在执行占有锁而其他Thread-7,Thread-8等线程处于WAIT状态。按照推荐的方式,Mutex定义了一个**继承AQS的静态内部类Sync**,并且重写了AQS的tryAcquire等等方法,而对state的更新也是利用了setState(),getState(),compareAndSetState()这三个方法。在实现实现lock接口中的方法也只是调用了AQS提供的模板方法(因为Sync继承AQS)。从这个例子就可以很清楚的看出来,在同步组件的实现上主要是利用了AQS,而AQS“屏蔽”了同步状态的修改,线程排队等底层实现,通过AQS的模板方法可以很方便的给同步组件的实现者进行调用。而针对用户来说,只需要调用同步组件提供的方法来实现并发编程即可。同时在新建一个同步组件时需要把握的两个关键点是: -1. 实现同步组件时推荐定义继承AQS的静态内存类,并重写需要的protected修饰的方法; -2. 同步组件语义的实现依赖于AQS的模板方法,而AQS模板方法又依赖于被AQS的子类所重写的方法。 - -通俗点说,因为AQS整体设计思路采用模板方法设计模式,同步组件以及AQS的功能实际上别切分成各自的两部分: - -**同步组件实现者的角度:** - -通过可重写的方法:**独占式**: tryAcquire()(独占式获取同步状态),tryRelease()(独占式释放同步状态);**共享式** :tryAcquireShared()(共享式获取同步状态),tryReleaseShared()(共享式释放同步状态);**告诉AQS怎样判断当前同步状态是否成功获取或者是否成功释放**。同步组件专注于对当前同步状态的逻辑判断,从而实现自己的同步语义。这句话比较抽象,举例来说,上面的Mutex例子中通过tryAcquire方法实现自己的同步语义,在该方法中如果当前同步状态为0(即该同步组件没被任何线程获取),当前线程可以获取同时将状态更改为1返回true,否则,该组件已经被线程占用返回false。很显然,该同步组件只能在同一时刻被线程占用,Mutex专注于获取释放的逻辑来实现自己想要表达的同步语义。 - -**AQS的角度** - -而对AQS来说,只需要同步组件返回的true和false即可,因为AQS会对true和false会有不同的操作,true会认为当前线程获取同步组件成功直接返回,而false的话就AQS也会将当前线程插入同步队列等一系列的方法。 - -总的来说,同步组件通过重写AQS的方法实现自己想要表达的同步语义,而AQS只需要同步组件表达的true和false即可,AQS会针对true和false不同的情况做不同的处理,至于底层实现,可以[看这篇文章](http://www.jianshu.com/p/cc308d82cc71)。 - - - - -> 参考文献 - -《java并发编程的艺术》 diff --git "a/09.\346\267\261\345\205\245\347\220\206\350\247\243AbstractQueuedSynchronizer(AQS)/LockDemo debug\344\270\213.png" "b/09.\346\267\261\345\205\245\347\220\206\350\247\243AbstractQueuedSynchronizer(AQS)/LockDemo debug\344\270\213.png" deleted file mode 100644 index d4d0183..0000000 Binary files "a/09.\346\267\261\345\205\245\347\220\206\350\247\243AbstractQueuedSynchronizer(AQS)/LockDemo debug\344\270\213.png" and /dev/null differ diff --git "a/09.\346\267\261\345\205\245\347\220\206\350\247\243AbstractQueuedSynchronizer(AQS)/banner.jpg" "b/09.\346\267\261\345\205\245\347\220\206\350\247\243AbstractQueuedSynchronizer(AQS)/banner.jpg" deleted file mode 100644 index 400b29e..0000000 Binary files "a/09.\346\267\261\345\205\245\347\220\206\350\247\243AbstractQueuedSynchronizer(AQS)/banner.jpg" and /dev/null differ diff --git "a/09.\346\267\261\345\205\245\347\220\206\350\247\243AbstractQueuedSynchronizer(AQS)/\345\275\223\345\211\215\350\212\202\347\202\271\345\274\225\347\224\250\347\272\277\347\250\213\350\216\267\345\217\226\351\224\201\357\274\214\345\275\223\345\211\215\350\212\202\347\202\271\350\256\276\347\275\256\344\270\272\351\230\237\345\210\227\345\244\264\347\273\223\347\202\271.png" "b/09.\346\267\261\345\205\245\347\220\206\350\247\243AbstractQueuedSynchronizer(AQS)/\345\275\223\345\211\215\350\212\202\347\202\271\345\274\225\347\224\250\347\272\277\347\250\213\350\216\267\345\217\226\351\224\201\357\274\214\345\275\223\345\211\215\350\212\202\347\202\271\350\256\276\347\275\256\344\270\272\351\230\237\345\210\227\345\244\264\347\273\223\347\202\271.png" deleted file mode 100644 index a3b18f3..0000000 Binary files "a/09.\346\267\261\345\205\245\347\220\206\350\247\243AbstractQueuedSynchronizer(AQS)/\345\275\223\345\211\215\350\212\202\347\202\271\345\274\225\347\224\250\347\272\277\347\250\213\350\216\267\345\217\226\351\224\201\357\274\214\345\275\223\345\211\215\350\212\202\347\202\271\350\256\276\347\275\256\344\270\272\351\230\237\345\210\227\345\244\264\347\273\223\347\202\271.png" and /dev/null differ diff --git "a/09.\346\267\261\345\205\245\347\220\206\350\247\243AbstractQueuedSynchronizer(AQS)/\346\267\261\345\205\245\347\220\206\350\247\243AbstractQueuedSynchronizer(AQS).md" "b/09.\346\267\261\345\205\245\347\220\206\350\247\243AbstractQueuedSynchronizer(AQS)/\346\267\261\345\205\245\347\220\206\350\247\243AbstractQueuedSynchronizer(AQS).md" deleted file mode 100644 index f76488d..0000000 --- "a/09.\346\267\261\345\205\245\347\220\206\350\247\243AbstractQueuedSynchronizer(AQS)/\346\267\261\345\205\245\347\220\206\350\247\243AbstractQueuedSynchronizer(AQS).md" +++ /dev/null @@ -1,539 +0,0 @@ -# 1. AQS简介 - - - - -在[上一篇文章](https://juejin.im/post/5aeb055b6fb9a07abf725c8c)中我们对lock和AbstractQueuedSynchronizer(AQS)有了初步的认识。在同步组件的实现中,AQS是核心部分,同步组件的实现者通过使用AQS提供的模板方法实现同步组件语义,AQS则实现了对**同步状态的管理,以及对阻塞线程进行排队,等待通知**等等一些底层的实现处理。AQS的核心也包括了这些方面:**同步队列,独占式锁的获取和释放,共享锁的获取和释放以及可中断锁,超时等待锁获取这些特性的实现**,而这些实际上则是AQS提供出来的模板方法,归纳整理如下: - -**独占式锁:** - -> void acquire(int arg):独占式获取同步状态,如果获取失败则插入同步队列进行等待; -> void acquireInterruptibly(int arg):与acquire方法相同,但在同步队列中进行等待的时候可以检测中断; -> boolean tryAcquireNanos(int arg, long nanosTimeout):在acquireInterruptibly基础上增加了超时等待功能,在超时时间内没有获得同步状态返回false; -> boolean release(int arg):释放同步状态,该方法会唤醒在同步队列中的下一个节点 - -**共享式锁:** -> void acquireShared(int arg):共享式获取同步状态,与独占式的区别在于同一时刻有多个线程获取同步状态; -> void acquireSharedInterruptibly(int arg):在acquireShared方法基础上增加了能响应中断的功能; -> boolean tryAcquireSharedNanos(int arg, long nanosTimeout):在acquireSharedInterruptibly基础上增加了超时等待的功能; -> boolean releaseShared(int arg):共享式释放同步状态 - - -要想掌握AQS的底层实现,其实也就是对这些模板方法的逻辑进行学习。在学习这些模板方法之前,我们得首先了解下AQS中的同步队列是一种什么样的数据结构,因为同步队列是AQS对同步状态的管理的基石。 - -# 2. 同步队列 # -当共享资源被某个线程占有,其他请求该资源的线程将会阻塞,从而进入同步队列。就数据结构而言,队列的实现方式无外乎两者一是通过数组的形式,另外一种则是链表的形式。AQS中的同步队列则是**通过链式方式**进行实现。接下来,很显然我们至少会抱有这样的疑问:**1. 节点的数据结构是什么样的?2. 是单向还是双向?3. 是带头结点的还是不带头节点的?**我们依旧先是通过看源码的方式。 - -在AQS有一个静态内部类Node,其中有这样一些属性: - -> volatile int waitStatus //节点状态 -> volatile Node prev //当前节点/线程的前驱节点 -> volatile Node next; //当前节点/线程的后继节点 -> volatile Thread thread;//加入同步队列的线程引用 -> Node nextWaiter;//等待队列中的下一个节点 - -节点的状态有以下这些: - -> int CANCELLED = 1//节点从同步队列中取消 -> int SIGNAL = -1//后继节点的线程处于等待状态,如果当前节点释放同步状态会通知后继节点,使得后继节点的线程能够运行; -> int CONDITION = -2//当前节点进入等待队列中 -> int PROPAGATE = -3//表示下一次共享式同步状态获取将会无条件传播下去 -> int INITIAL = 0;//初始状态 - -现在我们知道了节点的数据结构类型,并且每个节点拥有其前驱和后继节点,很显然这是**一个双向队列**。同样的我们可以用一段demo看一下。 - - public class LockDemo { - private static ReentrantLock lock = new ReentrantLock(); - - public static void main(String[] args) { - for (int i = 0; i < 5; i++) { - Thread thread = new Thread(() -> { - lock.lock(); - try { - Thread.sleep(10000); - } catch (InterruptedException e) { - e.printStackTrace(); - } finally { - lock.unlock(); - } - }); - thread.start(); - } - } - } - -实例代码中开启了5个线程,先获取锁之后再睡眠10S中,实际上这里让线程睡眠是想模拟出当线程无法获取锁时进入同步队列的情况。通过debug,当Thread-4(在本例中最后一个线程)获取锁失败后进入同步时,AQS时现在的同步队列如图所示: - - -![LockDemo debug下 .png](http://upload-images.jianshu.io/upload_images/2615789-d05d3f44ce4c205a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - - -Thread-0先获得锁后进行睡眠,其他线程(Thread-1,Thread-2,Thread-3,Thread-4)获取锁失败进入同步队列,同时也可以很清楚的看出来每个节点有两个域:prev(前驱)和next(后继),并且每个节点用来保存获取同步状态失败的线程引用以及等待状态等信息。另外AQS中有两个重要的成员变量: - - private transient volatile Node head; - private transient volatile Node tail; - -也就是说AQS实际上通过头尾指针来管理同步队列,同时实现包括获取锁失败的线程进行入队,释放锁时对同步队列中的线程进行通知等核心方法。其示意图如下: - - - -![队列示意图.png](http://upload-images.jianshu.io/upload_images/2615789-dbfc975d3601bb52.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - - - - -通过对源码的理解以及做实验的方式,现在我们可以清楚的知道这样几点: - -1. **节点的数据结构,即AQS的静态内部类Node,节点的等待状态等信息**; -2. **同步队列是一个双向队列,AQS通过持有头尾指针管理同步队列**; - -那么,节点如何进行入队和出队是怎样做的了?实际上这对应着锁的获取和释放两个操作:获取锁失败进行入队操作,获取锁成功进行出队操作。 - -# 3. 独占锁 # - -## 3.1 独占锁的获取(acquire方法) -我们继续通过看源码和debug的方式来看,还是以上面的demo为例,调用lock()方法是获取独占式锁,获取失败就将当前线程加入同步队列,成功则线程执行。而lock()方法实际上会调用AQS的**acquire()**方法,源码如下 - - public final void acquire(int arg) { - //先看同步状态是否获取成功,如果成功则方法结束返回 - //若失败则先调用addWaiter()方法再调用acquireQueued()方法 - if (!tryAcquire(arg) && - acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) - selfInterrupt(); - } - -关键信息请看注释,acquire根据当前获得同步状态成功与否做了两件事情:1. 成功,则方法结束返回,2. 失败,则先调用addWaiter()然后在调用acquireQueued()方法。 - -> **获取同步状态失败,入队操作** - - -当线程获取独占式锁失败后就会将当前线程加入同步队列,那么加入队列的方式是怎样的了?我们接下来就应该去研究一下addWaiter()和acquireQueued()。addWaiter()源码如下: - - private Node addWaiter(Node mode) { - // 1. 将当前线程构建成Node类型 - Node node = new Node(Thread.currentThread(), mode); - // Try the fast path of enq; backup to full enq on failure - // 2. 当前尾节点是否为null? - Node pred = tail; - if (pred != null) { - // 2.2 将当前节点尾插入的方式插入同步队列中 - node.prev = pred; - if (compareAndSetTail(pred, node)) { - pred.next = node; - return node; - } - } - // 2.1. 当前同步队列尾节点为null,说明当前线程是第一个加入同步队列进行等待的线程 - enq(node); - return node; - } - -分析可以看上面的注释。程序的逻辑主要分为两个部分:**1. 当前同步队列的尾节点为null,调用方法enq()插入;2. 当前队列的尾节点不为null,则采用尾插入(compareAndSetTail()方法)的方式入队。**另外还会有另外一个问题:如果 `if (compareAndSetTail(pred, node))`为false怎么办?会继续执行到enq()方法,同时很明显compareAndSetTail是一个CAS操作,通常来说如果CAS操作失败会继续自旋(死循环)进行重试。因此,经过我们这样的分析,enq()方法可能承担两个任务:**1. 处理当前同步队列尾节点为null时进行入队操作;2. 如果CAS尾插入节点失败后负责自旋进行尝试。**那么是不是真的就像我们分析的一样了?只有源码会告诉我们答案:),enq()源码如下: - - private Node enq(final Node node) { - for (;;) { - Node t = tail; - if (t == null) { // Must initialize - //1. 构造头结点 - if (compareAndSetHead(new Node())) - tail = head; - } else { - // 2. 尾插入,CAS操作失败自旋尝试 - node.prev = t; - if (compareAndSetTail(t, node)) { - t.next = node; - return t; - } - } - } - } - -在上面的分析中我们可以看出在第1步中会先创建头结点,说明同步队列是**带头结点的链式存储结构**。带头结点与不带头结点相比,会在入队和出队的操作中获得更大的便捷性,因此同步队列选择了带头结点的链式存储结构。那么带头节点的队列初始化时机是什么?自然而然是在**tail为null时,即当前线程是第一次插入同步队列**。compareAndSetTail(t, node)方法会利用CAS操作设置尾节点,如果CAS操作失败会在`for (;;)`for死循环中不断尝试,直至成功return返回为止。因此,对enq()方法可以做这样的总结: - -1. **在当前线程是第一个加入同步队列时,调用compareAndSetHead(new Node())方法,完成链式队列的头结点的初始化**; -2. **自旋不断尝试CAS尾插入节点直至成功为止**。 - -现在我们已经很清楚获取独占式锁失败的线程包装成Node然后插入同步队列的过程了?那么紧接着会有下一个问题?在同步队列中的节点(线程)会做什么事情了来保证自己能够有机会获得独占式锁了?带着这样的问题我们就来看看acquireQueued()方法,从方法名就可以很清楚,这个方法的作用就是排队获取锁的过程,源码如下: - - final boolean acquireQueued(final Node node, int arg) { - boolean failed = true; - try { - boolean interrupted = false; - for (;;) { - // 1. 获得当前节点的先驱节点 - final Node p = node.predecessor(); - // 2. 当前节点能否获取独占式锁 - // 2.1 如果当前节点的先驱节点是头结点并且成功获取同步状态,即可以获得独占式锁 - if (p == head && tryAcquire(arg)) { - //队列头指针用指向当前节点 - setHead(node); - //释放前驱节点 - p.next = null; // help GC - failed = false; - return interrupted; - } - // 2.2 获取锁失败,线程进入等待状态等待获取独占式锁 - if (shouldParkAfterFailedAcquire(p, node) && - parkAndCheckInterrupt()) - interrupted = true; - } - } finally { - if (failed) - cancelAcquire(node); - } - } - -程序逻辑通过注释已经标出,整体来看这是一个这又是一个自旋的过程(for (;;)),代码首先获取当前节点的先驱节点,**如果先驱节点是头结点的并且成功获得同步状态的时候(if (p == head && tryAcquire(arg))),当前节点所指向的线程能够获取锁**。反之,获取锁失败进入等待状态。整体示意图为下图: - - -![自旋获取锁整体示意图.png](http://upload-images.jianshu.io/upload_images/2615789-3fe83cfaf03a02c8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - - -> **获取锁成功,出队操作** - -获取锁的节点出队的逻辑是: - - //队列头结点引用指向当前节点 - setHead(node); - //释放前驱节点 - p.next = null; // help GC - failed = false; - return interrupted; - -setHead()方法为: - - private void setHead(Node node) { - head = node; - node.thread = null; - node.prev = null; - } - -将当前节点通过setHead()方法设置为队列的头结点,然后将之前的头结点的next域设置为null并且pre域也为null,即与队列断开,无任何引用方便GC时能够将内存进行回收。示意图如下: - - - - - -![当前节点引用线程获取锁,当前节点设置为队列头结点.png](http://upload-images.jianshu.io/upload_images/2615789-13963e1b3bcfe656.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - - -那么当获取锁失败的时候会调用shouldParkAfterFailedAcquire()方法和parkAndCheckInterrupt()方法,看看他们做了什么事情。shouldParkAfterFailedAcquire()方法源码为: - - private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { - int ws = pred.waitStatus; - if (ws == Node.SIGNAL) - /* - * This node has already set status asking a release - * to signal it, so it can safely park. - */ - return true; - if (ws > 0) { - /* - * Predecessor was cancelled. Skip over predecessors and - * indicate retry. - */ - do { - node.prev = pred = pred.prev; - } while (pred.waitStatus > 0); - pred.next = node; - } else { - /* - * waitStatus must be 0 or PROPAGATE. Indicate that we - * need a signal, but don't park yet. Caller will need to - * retry to make sure it cannot acquire before parking. - */ - compareAndSetWaitStatus(pred, ws, Node.SIGNAL); - } - return false; - } - - - -shouldParkAfterFailedAcquire()方法主要逻辑是使用`compareAndSetWaitStatus(pred, ws, Node.SIGNAL)`使用CAS将节点状态由INITIAL设置成SIGNAL,表示当前线程阻塞。当compareAndSetWaitStatus设置失败则说明shouldParkAfterFailedAcquire方法返回false,然后会在acquireQueued()方法中for (;;)死循环中会继续重试,直至compareAndSetWaitStatus设置节点状态位为SIGNAL时shouldParkAfterFailedAcquire返回true时才会执行方法parkAndCheckInterrupt()方法,该方法的源码为: - - private final boolean parkAndCheckInterrupt() { - //使得该线程阻塞 - LockSupport.park(this); - return Thread.interrupted(); - } - -该方法的关键是会调用LookSupport.park()方法(关于LookSupport会在以后的文章进行讨论),该方法是用来阻塞当前线程的。因此到这里就应该清楚了,acquireQueued()在自旋过程中主要完成了两件事情: - -1. **如果当前节点的前驱节点是头节点,并且能够获得同步状态的话,当前线程能够获得锁该方法执行结束退出**; -2. **获取锁失败的话,先将节点状态设置成SIGNAL,然后调用LookSupport.park方法使得当前线程阻塞**。 - -经过上面的分析,独占式锁的获取过程也就是acquire()方法的执行流程如下图所示: - - - - - -![独占式锁获取(acquire()方法)流程图.png](http://upload-images.jianshu.io/upload_images/2615789-a0d913dc40da5629.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - - -## 3.2 独占锁的释放(release()方法) ## - -独占锁的释放就相对来说比较容易理解了,废话不多说先来看下源码: - - public final boolean release(int arg) { - if (tryRelease(arg)) { - Node h = head; - if (h != null && h.waitStatus != 0) - unparkSuccessor(h); - return true; - } - return false; - } - -这段代码逻辑就比较容易理解了,如果同步状态释放成功(tryRelease返回true)则会执行if块中的代码,当head指向的头结点不为null,并且该节点的状态值不为0的话才会执行unparkSuccessor()方法。unparkSuccessor方法源码: - - private void unparkSuccessor(Node node) { - /* - * If status is negative (i.e., possibly needing signal) try - * to clear in anticipation of signalling. It is OK if this - * fails or if status is changed by waiting thread. - */ - int ws = node.waitStatus; - if (ws < 0) - compareAndSetWaitStatus(node, ws, 0); - - /* - * Thread to unpark is held in successor, which is normally - * just the next node. But if cancelled or apparently null, - * traverse backwards from tail to find the actual - * non-cancelled successor. - */ - - //头节点的后继节点 - Node s = node.next; - if (s == null || s.waitStatus > 0) { - s = null; - for (Node t = tail; t != null && t != node; t = t.prev) - if (t.waitStatus <= 0) - s = t; - } - if (s != null) - //后继节点不为null时唤醒该线程 - LockSupport.unpark(s.thread); - } - -源码的关键信息请看注释,首先获取头节点的后继节点,当后继节点的时候会调用LookSupport.unpark()方法,该方法会唤醒该节点的后继节点所包装的线程。因此,**每一次锁释放后就会唤醒队列中该节点的后继节点所引用的线程,从而进一步可以佐证获得锁的过程是一个FIFO(先进先出)的过程。** - -到现在我们终于啃下了一块硬骨头了,通过学习源码的方式非常深刻的学习到了独占式锁的获取和释放的过程以及同步队列。可以做一下总结: - -1. **线程获取锁失败,线程被封装成Node进行入队操作,核心方法在于addWaiter()和enq(),同时enq()完成对同步队列的头结点初始化工作以及CAS操作失败的重试**; -2. **线程获取锁是一个自旋的过程,当且仅当 当前节点的前驱节点是头结点并且成功获得同步状态时,节点出队即该节点引用的线程获得锁,否则,当不满足条件时就会调用LookSupport.park()方法使得线程阻塞**; -3. **释放锁的时候会唤醒后继节点;** - -总体来说:**在获取同步状态时,AQS维护一个同步队列,获取同步状态失败的线程会加入到队列中进行自旋;移除队列(或停止自旋)的条件是前驱节点是头结点并且成功获得了同步状态。在释放同步状态时,同步器会调用unparkSuccessor()方法唤醒后继节点。** - - -> **独占锁特性学习** - -## 3.3 可中断式获取锁(acquireInterruptibly方法) ## -我们知道lock相较于synchronized有一些更方便的特性,比如能响应中断以及超时等待等特性,现在我们依旧采用通过学习源码的方式来看看能够响应中断是怎么实现的。可响应中断式锁可调用方法lock.lockInterruptibly();而该方法其底层会调用AQS的acquireInterruptibly方法,源码为: - - public final void acquireInterruptibly(int arg) - throws InterruptedException { - if (Thread.interrupted()) - throw new InterruptedException(); - if (!tryAcquire(arg)) - //线程获取锁失败 - doAcquireInterruptibly(arg); - } - - -在获取同步状态失败后就会调用doAcquireInterruptibly方法: - - private void doAcquireInterruptibly(int arg) - throws InterruptedException { - //将节点插入到同步队列中 - final Node node = addWaiter(Node.EXCLUSIVE); - boolean failed = true; - try { - for (;;) { - final Node p = node.predecessor(); - //获取锁出队 - if (p == head && tryAcquire(arg)) { - setHead(node); - p.next = null; // help GC - failed = false; - return; - } - if (shouldParkAfterFailedAcquire(p, node) && - parkAndCheckInterrupt()) - //线程中断抛异常 - throw new InterruptedException(); - } - } finally { - if (failed) - cancelAcquire(node); - } - } - -关键信息请看注释,现在看这段代码就很轻松了吧:),与acquire方法逻辑几乎一致,唯一的区别是当**parkAndCheckInterrupt**返回true时即线程阻塞时该线程被中断,代码抛出被中断异常。 - -## 3.4 超时等待式获取锁(tryAcquireNanos()方法) -通过调用lock.tryLock(timeout,TimeUnit)方式达到超时等待获取锁的效果,该方法会在三种情况下才会返回: - -1. 在超时时间内,当前线程成功获取了锁; -2. 当前线程在超时时间内被中断; -3. 超时时间结束,仍未获得锁返回false。 - -我们仍然通过采取阅读源码的方式来学习底层具体是怎么实现的,该方法会调用AQS的方法tryAcquireNanos(),源码为: - - public final boolean tryAcquireNanos(int arg, long nanosTimeout) - throws InterruptedException { - if (Thread.interrupted()) - throw new InterruptedException(); - return tryAcquire(arg) || - //实现超时等待的效果 - doAcquireNanos(arg, nanosTimeout); - } - -很显然这段源码最终是靠doAcquireNanos方法实现超时等待的效果,该方法源码如下: - - private boolean doAcquireNanos(int arg, long nanosTimeout) - throws InterruptedException { - if (nanosTimeout <= 0L) - return false; - //1. 根据超时时间和当前时间计算出截止时间 - final long deadline = System.nanoTime() + nanosTimeout; - final Node node = addWaiter(Node.EXCLUSIVE); - boolean failed = true; - try { - for (;;) { - final Node p = node.predecessor(); - //2. 当前线程获得锁出队列 - if (p == head && tryAcquire(arg)) { - setHead(node); - p.next = null; // help GC - failed = false; - return true; - } - // 3.1 重新计算超时时间 - nanosTimeout = deadline - System.nanoTime(); - // 3.2 已经超时返回false - if (nanosTimeout <= 0L) - return false; - // 3.3 线程阻塞等待 - if (shouldParkAfterFailedAcquire(p, node) && - nanosTimeout > spinForTimeoutThreshold) - LockSupport.parkNanos(this, nanosTimeout); - // 3.4 线程被中断抛出被中断异常 - if (Thread.interrupted()) - throw new InterruptedException(); - } - } finally { - if (failed) - cancelAcquire(node); - } - } - -程序逻辑如图所示: - -![超时等待式获取锁(doAcquireNanos()方法)](http://upload-images.jianshu.io/upload_images/2615789-a80779d4736afb87.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - - - -程序逻辑同独占锁可响应中断式获取基本一致,唯一的不同在于获取锁失败后,对超时时间的处理上,在第1步会先计算出按照现在时间和超时时间计算出理论上的截止时间,比如当前时间是8h10min,超时时间是10min,那么根据`deadline = System.nanoTime() + nanosTimeout`计算出刚好达到超时时间时的系统时间就是8h 10min+10min = 8h 20min。然后根据`deadline - System.nanoTime()`就可以判断是否已经超时了,比如,当前系统时间是8h 30min很明显已经超过了理论上的系统时间8h 20min,`deadline - System.nanoTime()`计算出来就是一个负数,自然而然会在3.2步中的If判断之间返回false。如果还没有超时即3.2步中的if判断为true时就会继续执行3.3步通过**LockSupport.parkNanos**使得当前线程阻塞,同时在3.4步增加了对中断的检测,若检测出被中断直接抛出被中断异常。 - - - - -# 4. 共享锁 # -## 4.1 共享锁的获取(acquireShared()方法) ## -在聊完AQS对独占锁的实现后,我们继续一鼓作气的来看看共享锁是怎样实现的?共享锁的获取方法为acquireShared,源码为: - - public final void acquireShared(int arg) { - if (tryAcquireShared(arg) < 0) - doAcquireShared(arg); - } - -这段源码的逻辑很容易理解,在该方法中会首先调用tryAcquireShared方法,tryAcquireShared返回值是一个int类型,当返回值为大于等于0的时候方法结束说明获得成功获取锁,否则,表明获取同步状态失败即所引用的线程获取锁失败,会执行doAcquireShared方法,该方法的源码为: - - private void doAcquireShared(int arg) { - final Node node = addWaiter(Node.SHARED); - boolean failed = true; - try { - boolean interrupted = false; - for (;;) { - final Node p = node.predecessor(); - if (p == head) { - int r = tryAcquireShared(arg); - if (r >= 0) { - // 当该节点的前驱节点是头结点且成功获取同步状态 - setHeadAndPropagate(node, r); - p.next = null; // help GC - if (interrupted) - selfInterrupt(); - failed = false; - return; - } - } - if (shouldParkAfterFailedAcquire(p, node) && - parkAndCheckInterrupt()) - interrupted = true; - } - } finally { - if (failed) - cancelAcquire(node); - } - } - -现在来看这段代码会不会很容易了?逻辑几乎和独占式锁的获取一模一样,这里的自旋过程中能够退出的条件**是当前节点的前驱节点是头结点并且tryAcquireShared(arg)返回值大于等于0即能成功获得同步状态**。 - -## 4.2 共享锁的释放(releaseShared()方法) ## -共享锁的释放在AQS中会调用方法releaseShared: - - public final boolean releaseShared(int arg) { - if (tryReleaseShared(arg)) { - doReleaseShared(); - return true; - } - return false; - } - -当成功释放同步状态之后即tryReleaseShared会继续执行doReleaseShared方法: - - private void doReleaseShared() { - /* - * Ensure that a release propagates, even if there are other - * in-progress acquires/releases. This proceeds in the usual - * way of trying to unparkSuccessor of head if it needs - * signal. But if it does not, status is set to PROPAGATE to - * ensure that upon release, propagation continues. - * Additionally, we must loop in case a new node is added - * while we are doing this. Also, unlike other uses of - * unparkSuccessor, we need to know if CAS to reset status - * fails, if so rechecking. - */ - for (;;) { - Node h = head; - if (h != null && h != tail) { - int ws = h.waitStatus; - if (ws == Node.SIGNAL) { - if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) - continue; // loop to recheck cases - unparkSuccessor(h); - } - else if (ws == 0 && - !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) - continue; // loop on failed CAS - } - if (h == head) // loop if head changed - break; - } - } - - -这段方法跟独占式锁释放过程有点点不同,在共享式锁的释放过程中,对于能够支持多个线程同时访问的并发组件,必须保证多个线程能够安全的释放同步状态,这里采用的CAS保证,当CAS操作失败continue,在下一次循环中进行重试。 - -## 4.3 可中断(acquireSharedInterruptibly()方法),超时等待(tryAcquireSharedNanos()方法) ## -关于可中断锁以及超时等待的特性其实现和独占式锁可中断获取锁以及超时等待的实现几乎一致,具体的就不再说了,如果理解了上面的内容对这部分的理解也是水到渠成的。 - -通过这篇,加深了对AQS的底层实现更加清楚了,也对了解并发组件的实现原理打下了基础,学无止境,继续加油:);如果觉得不错,请给赞,嘿嘿。 - -> 参考文献 - -《java并发编程的艺术》 diff --git "a/09.\346\267\261\345\205\245\347\220\206\350\247\243AbstractQueuedSynchronizer(AQS)/\347\213\254\345\215\240\345\274\217\351\224\201\350\216\267\345\217\226\357\274\210acquire()\346\226\271\346\263\225\357\274\211\346\265\201\347\250\213\345\233\276.png" "b/09.\346\267\261\345\205\245\347\220\206\350\247\243AbstractQueuedSynchronizer(AQS)/\347\213\254\345\215\240\345\274\217\351\224\201\350\216\267\345\217\226\357\274\210acquire()\346\226\271\346\263\225\357\274\211\346\265\201\347\250\213\345\233\276.png" deleted file mode 100644 index 8d85dd7..0000000 Binary files "a/09.\346\267\261\345\205\245\347\220\206\350\247\243AbstractQueuedSynchronizer(AQS)/\347\213\254\345\215\240\345\274\217\351\224\201\350\216\267\345\217\226\357\274\210acquire()\346\226\271\346\263\225\357\274\211\346\265\201\347\250\213\345\233\276.png" and /dev/null differ diff --git "a/09.\346\267\261\345\205\245\347\220\206\350\247\243AbstractQueuedSynchronizer(AQS)/\350\207\252\346\227\213\350\216\267\345\217\226\351\224\201\346\225\264\344\275\223\347\244\272\346\204\217\345\233\276.png" "b/09.\346\267\261\345\205\245\347\220\206\350\247\243AbstractQueuedSynchronizer(AQS)/\350\207\252\346\227\213\350\216\267\345\217\226\351\224\201\346\225\264\344\275\223\347\244\272\346\204\217\345\233\276.png" deleted file mode 100644 index 63a9d1d..0000000 Binary files "a/09.\346\267\261\345\205\245\347\220\206\350\247\243AbstractQueuedSynchronizer(AQS)/\350\207\252\346\227\213\350\216\267\345\217\226\351\224\201\346\225\264\344\275\223\347\244\272\346\204\217\345\233\276.png" and /dev/null differ diff --git "a/09.\346\267\261\345\205\245\347\220\206\350\247\243AbstractQueuedSynchronizer(AQS)/\350\266\205\346\227\266\347\255\211\345\276\205\345\274\217\350\216\267\345\217\226\351\224\201\357\274\210doAcquireNanos()\346\226\271\346\263\225\357\274\211.png" "b/09.\346\267\261\345\205\245\347\220\206\350\247\243AbstractQueuedSynchronizer(AQS)/\350\266\205\346\227\266\347\255\211\345\276\205\345\274\217\350\216\267\345\217\226\351\224\201\357\274\210doAcquireNanos()\346\226\271\346\263\225\357\274\211.png" deleted file mode 100644 index f59c304..0000000 Binary files "a/09.\346\267\261\345\205\245\347\220\206\350\247\243AbstractQueuedSynchronizer(AQS)/\350\266\205\346\227\266\347\255\211\345\276\205\345\274\217\350\216\267\345\217\226\351\224\201\357\274\210doAcquireNanos()\346\226\271\346\263\225\357\274\211.png" and /dev/null differ diff --git "a/09.\346\267\261\345\205\245\347\220\206\350\247\243AbstractQueuedSynchronizer(AQS)/\351\230\237\345\210\227\347\244\272\346\204\217\345\233\276.png" "b/09.\346\267\261\345\205\245\347\220\206\350\247\243AbstractQueuedSynchronizer(AQS)/\351\230\237\345\210\227\347\244\272\346\204\217\345\233\276.png" deleted file mode 100644 index 30e5556..0000000 Binary files "a/09.\346\267\261\345\205\245\347\220\206\350\247\243AbstractQueuedSynchronizer(AQS)/\351\230\237\345\210\227\347\244\272\346\204\217\345\233\276.png" and /dev/null differ