diff --git "a/2.\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/2.\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" index 7458d85..44accc1 100644 --- "a/2.\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/2.\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" @@ -5,54 +5,52 @@ 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 +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) +![FutureTask接口实现关系](https://github.com/dypcoder/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接口)来新建线程; @@ -61,7 +59,7 @@ # 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) +![线程状态转换图](https://github.com/dypcoder/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) @@ -71,7 +69,7 @@ 用一个表格将上面六种状态进行一个总结归纳。 -![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) +![JAVA线程的状态](https://github.com/dypcoder/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. 线程状态的基本操作 # @@ -83,15 +81,15 @@ 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) +![线程中断的方法](https://github.com/dypcoder/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 { +```Java +public class InterruptDemo { + public static void main(String[] args) throws InterruptedException { //sleepThread睡眠1000ms final Thread sleepThread = new Thread() { @Override @@ -120,7 +118,7 @@ interrupted()对当前线程进行中断操作,该方法会清除中断标 System.out.println("busyThread isInterrupted: " + busyThread.isInterrupted()); } } - +``` 输出结果 > sleepThread isInterrupted: false > busyThread isInterrupted: true @@ -131,10 +129,10 @@ interrupted()对当前线程进行中断操作,该方法会清除中断标 ## 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 +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方法源码关键是: @@ -143,50 +141,50 @@ Thread类除了提供join()方法外,另外还提供了超时等待的方法 } 可以看出来当前等待对象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(); - } - } - } - } - +```Java + public class JoinDemo { + public static void main(String[] args) throws InterruptedException { + for (int i = 0; i <= 10; i++) { + MyThread thread = new MyThread(); + thread.start(); + try { + thread.join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println("主线程执行完毕"); + System.out.println("================"); + } + } + + static class MyThread extends Thread { + @Override + public void run() { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println("子线程执行完毕"); + } + } +} +``` 输出结果为: - -> 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个线程,每个线程都会等待前一个线程结束才会继续运行。可以通俗的理解成接力,前一个线程将接力棒传给下一个线程,然后又传给下一个线程...... +``` +子线程执行完毕 +主线程执行完毕 +~~~~~~~~~~~~~~~ +子线程执行完毕 +主线程执行完毕 +~~~~~~~~~~~~~~~ +子线程执行完毕 +主线程执行完毕 +~~~~~~~~~~~~~~~ +........ +``` + +在上面的例子中一个创建了10个线程,每个线程都会等待前一个线程结束才会继续运行。可以通俗的理解成接力,前一个线程将接力棒传给下一个线程,然后又传给下一个线程...... 配合阅读 https://juejin.im/post/5b3054c66fb9a00e4d53ef75 ## 3.3 sleep ## public static native void sleep(long millis)方法显然是Thread的静态方法,很显然它是让当前线程按照指定的时间休眠,其休眠时间的精度取决于处理器的计时器和调度器。需要注意的是如果当前线程获得了锁,sleep方法并不会失去锁。sleep方法经常拿来与Object.wait()方法进行比价,这也是面试经常被问的地方。 diff --git "a/4.\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/4.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/java\345\205\263\351\224\256\345\255\227---synchronized.md" index fe91e9f..89d1651 100644 --- "a/4.\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/4.\345\275\273\345\272\225\347\220\206\350\247\243synchronized/java\345\205\263\351\224\256\345\255\227---synchronized.md" @@ -130,7 +130,7 @@ CAS的实现需要硬件指令集的支撑,在JDK1.5后虚拟机才可以使 > Synchronized VS CAS -元老级的Synchronized(未优化前)最主要的问题是:在存在线程竞争的情况下会出现线程阻塞和唤醒锁带来的性能问题,因为这是一种互斥同步(阻塞同步)。而CAS并不是武断的间线程挂起,当CAS操作失败后会进行一定的尝试,而非进行耗时的挂起唤醒的操作,因此也叫做非阻塞同步。这是两者主要的区别。 +元老级的Synchronized(未优化前)最主要的问题是:在存在线程竞争的情况下会出现线程阻塞和唤醒锁带来的性能问题,因为这是一种互斥同步(阻塞同步)。而CAS并不是武断的将线程挂起,当CAS操作失败后会进行一定的尝试,而非进行耗时的挂起唤醒的操作,因此也叫做非阻塞同步。这是两者主要的区别。 ### 3.1.3 CAS的应用场景 ### diff --git "a/5.\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/5.\345\275\273\345\272\225\347\220\206\350\247\243volatile/java\345\205\263\351\224\256\345\255\227---volatile.md" index 4d62eaf..d4ff2f9 100644 --- "a/5.\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/5.\345\275\273\345\272\225\347\220\206\350\247\243volatile/java\345\205\263\351\224\256\345\255\227---volatile.md" @@ -128,7 +128,7 @@ java编译器会在生成指令系列时在适当的位置会插入内存屏障 } 注意不同点,现在已经**将isOver设置成了volatile变量**,这样在main线程中将isOver改为了true后,thread的工作内存该变量值就会失效,从而需要再次从主内存中读取该值,现在能够读出isOver最新值为true从而能够结束在thread里的死循环,从而能够顺利停止掉thread线程。现在问题也解决了,知识也学到了:)。(如果觉得还不错,请点赞,是对我的一个鼓励。) - +**总结来说:volatile关键字是通过内存屏障和cpu指令使cpu高速缓存失效(缓存一致性协议的作用)只能去内存中获取对应的变量,从而实现可见性** > 参考文献 -《java并发编程的艺术》 \ No newline at end of file +《java并发编程的艺术》 diff --git "a/7.\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/7.\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" index 6b93924..fa79289 100644 --- "a/7.\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/7.\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" @@ -12,7 +12,7 @@ > > 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个语句是原子操作**,将10赋值给线程工作内存的变量a,而语句2(a++),实际上包含了三个操作:1. 读取变量a的值;2:对a进行加一的操作;3.将计算后的值再赋值给变量a,而这三个操作无法构成原子操作。对语句3,4的分析同理可得这两条语句不具备原子性。当然,[java内存模型](https://juejin.im/post/5ae6d309518825673123fd0e)中定义了8种操作都是原子的,不可再分的。 1. lock(锁定):作用于主内存中的变量,它把一个变量标识为一个线程独占的状态; 2. unlock(解锁):作用于主内存中的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定 @@ -113,4 +113,4 @@ synchronized语义表示锁在同一时刻只能由一个线程进行获取, > 参考文献 《java并发编程的艺术》 -《深入理解java虚拟机》 \ No newline at end of file +《深入理解java虚拟机》 diff --git "a/8.\345\210\235\350\257\206Lock\344\270\216AbstractQueuedSynchronizer(AQS)/\345\210\235\350\257\206Lock\344\270\216AbstractQueuedSynchronizer(AQS).md" "b/8.\345\210\235\350\257\206Lock\344\270\216AbstractQueuedSynchronizer(AQS)/\345\210\235\350\257\206Lock\344\270\216AbstractQueuedSynchronizer(AQS).md" index 329ff6f..36fee34 100644 --- "a/8.\345\210\235\350\257\206Lock\344\270\216AbstractQueuedSynchronizer(AQS)/\345\210\235\350\257\206Lock\344\270\216AbstractQueuedSynchronizer(AQS).md" +++ "b/8.\345\210\235\350\257\206Lock\344\270\216AbstractQueuedSynchronizer(AQS)/\345\210\235\350\257\206Lock\344\270\216AbstractQueuedSynchronizer(AQS).md" @@ -1,5 +1,5 @@ # 1. concurrent包的结构层次 # -在针对并发编程中,Doug Lea大师为我们提供了大量实用,高性能的工具类,针对这些代码进行研究会让我们队并发编程的掌握更加透彻也会大大提升我们队并发编程技术的热爱。这些代码在java.util.concurrent包下。如下图,即为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) @@ -10,7 +10,7 @@ # 2. lock简介 # -我们下来看concurent包下的lock子包。锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源。在Lock接口出现之前,java程序主要是靠synchronized关键字实现锁功能的,而java SE5之后,并发包中增加了lock接口,它提供了与synchronized一样的锁功能。**虽然它失去了像synchronize关键字隐式加锁解锁的便捷性,但是却拥有了锁获取和释放的可操作性,可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。**通常使用显示使用lock的形式如下: +我们下来看concurent包下的lock子包。锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源。在Lock接口出现之前,java程序主要是靠synchronized关键字实现锁功能的,而java SE5之后,并发包中增加了lock接口,它提供了与synchronized一样的锁功能。**虽然它失去了像synchronize关键字隐式加锁解锁的便捷性,但是却拥有了锁获取和释放的可操作性,可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。** 通常使用显示使用lock的形式如下: Lock lock = new ReentrantLock(); lock.lock(); @@ -26,11 +26,11 @@ 我们现在就来看看lock接口定义了哪些方法: -> void lock(); //获取锁 -> void lockInterruptibly() throws InterruptedException;//获取锁的过程能够响应中断 -> boolean tryLock();//非阻塞式响应中断能立即返回,获取锁放回true反之返回fasle -> boolean tryLock(long time, TimeUnit unit) throws InterruptedException;//超时获取锁,在超时内或者未中断的情况下能够获取锁 -> Condition newCondition();//获取与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说起。 @@ -93,7 +93,7 @@ ReentrantLock中NonfairSync(继承AQS)会重写该方法为: } 会调用tryAcquire方法,而此时当继承AQS的NonfairSync调用模板方法acquire时就会调用已经被NonfairSync重写的tryAcquire方法。这就是使用AQS的方式,在弄懂这点后会lock的实现理解有很大的提升。可以归纳总结为这么几点: -1. 同步组件(这里不仅仅值锁,还包括CountDownLatch等)的实现依赖于同步器AQS,在同步组件实现中,使用AQS的方式被推荐定义继承AQS的静态内存类; +1. 同步组件(这里不仅仅指锁,还包括CountDownLatch等)的实现依赖于同步器AQS,在同步组件实现中,使用AQS的方式被推荐定义继承AQS的静态内存类; 2. AQS采用模板方法进行设计,AQS的protected修饰的方法需要由继承AQS的子类进行重写实现,当调用AQS的子类的方法时就会调用被重写的方法; 3. AQS负责同步状态的管理,线程的排队,等待和唤醒这些底层操作,而Lock等同步组件主要专注于实现同步语义; 4. 在重写AQS的方式时,使用AQS提供的`getState(),setState(),compareAndSetState()`方法进行修改同步状态 @@ -222,8 +222,8 @@ MutexDemo: ![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修饰的方法; +上面的这个例子实现了独占锁的语义,在同一个时刻只允许一个线程占有锁。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的功能实际上别切分成各自的两部分: diff --git "a/9.\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/9.\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" index f76488d..3cc2840 100644 --- "a/9.\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/9.\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" @@ -7,38 +7,38 @@ **独占式锁:** -> void acquire(int arg):独占式获取同步状态,如果获取失败则插入同步队列进行等待; -> void acquireInterruptibly(int arg):与acquire方法相同,但在同步队列中进行等待的时候可以检测中断; -> boolean tryAcquireNanos(int arg, long nanosTimeout):在acquireInterruptibly基础上增加了超时等待功能,在超时时间内没有获得同步状态返回false; -> boolean release(int arg):释放同步状态,该方法会唤醒在同步队列中的下一个节点 +> 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):共享式释放同步状态 +> 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中的同步队列则是**通过链式方式**进行实现。接下来,很显然我们至少会抱有这样的疑问:**1. 节点的数据结构是什么样的?2. 是单向还是双向?3. 是带头结点的还是不带头节点的?** 我们依旧先是通过看源码的方式。 在AQS有一个静态内部类Node,其中有这样一些属性: -> volatile int waitStatus //节点状态 -> volatile Node prev //当前节点/线程的前驱节点 -> volatile Node next; //当前节点/线程的后继节点 -> volatile Thread thread;//加入同步队列的线程引用 -> Node nextWaiter;//等待队列中的下一个节点 +> 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;//初始状态 +> int CANCELLED = 1//节点从同步队列中取消 +> int SIGNAL = -1//后继节点的线程处于等待状态,如果当前节点释放同步状态会通知后继节点,使得后继节点的线程能够运行; +> int CONDITION = -2//当前节点进入等待队列中 +> int PROPAGATE = -3//表示下一次共享式同步状态获取将会无条件传播下去 +> int INITIAL = 0;//初始状态 现在我们知道了节点的数据结构类型,并且每个节点拥有其前驱和后继节点,很显然这是**一个双向队列**。同样的我们可以用一段demo看一下。 @@ -92,7 +92,7 @@ Thread-0先获得锁后进行睡眠,其他线程(Thread-1,Thread-2,Thread-3, # 3. 独占锁 # ## 3.1 独占锁的获取(acquire方法) -我们继续通过看源码和debug的方式来看,还是以上面的demo为例,调用lock()方法是获取独占式锁,获取失败就将当前线程加入同步队列,成功则线程执行。而lock()方法实际上会调用AQS的**acquire()**方法,源码如下 +我们继续通过看源码和debug的方式来看,还是以上面的demo为例,调用lock()方法是获取独占式锁,获取失败就将当前线程加入同步队列,成功则线程执行。而lock()方法实际上会调用AQS的 **acquire()** 方法,源码如下 public final void acquire(int arg) { //先看同步状态是否获取成功,如果成功则方法结束返回 @@ -128,7 +128,7 @@ Thread-0先获得锁后进行睡眠,其他线程(Thread-1,Thread-2,Thread-3, return node; } -分析可以看上面的注释。程序的逻辑主要分为两个部分:**1. 当前同步队列的尾节点为null,调用方法enq()插入;2. 当前队列的尾节点不为null,则采用尾插入(compareAndSetTail()方法)的方式入队。**另外还会有另外一个问题:如果 `if (compareAndSetTail(pred, node))`为false怎么办?会继续执行到enq()方法,同时很明显compareAndSetTail是一个CAS操作,通常来说如果CAS操作失败会继续自旋(死循环)进行重试。因此,经过我们这样的分析,enq()方法可能承担两个任务:**1. 处理当前同步队列尾节点为null时进行入队操作;2. 如果CAS尾插入节点失败后负责自旋进行尝试。**那么是不是真的就像我们分析的一样了?只有源码会告诉我们答案:),enq()源码如下: +分析可以看上面的注释。程序的逻辑主要分为两个部分:**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 (;;) { @@ -148,7 +148,7 @@ Thread-0先获得锁后进行睡眠,其他线程(Thread-1,Thread-2,Thread-3, } } -在上面的分析中我们可以看出在第1步中会先创建头结点,说明同步队列是**带头结点的链式存储结构**。带头结点与不带头结点相比,会在入队和出队的操作中获得更大的便捷性,因此同步队列选择了带头结点的链式存储结构。那么带头节点的队列初始化时机是什么?自然而然是在**tail为null时,即当前线程是第一次插入同步队列**。compareAndSetTail(t, node)方法会利用CAS操作设置尾节点,如果CAS操作失败会在`for (;;)`for死循环中不断尝试,直至成功return返回为止。因此,对enq()方法可以做这样的总结: +在上面的分析中我们可以看出在第1步中会先创建头结点,说明同步队列是 **带头结点的链式存储结构** 。带头结点与不带头结点相比,会在入队和出队的操作中获得更大的便捷性,因此同步队列选择了带头结点的链式存储结构。那么带头节点的队列初始化时机是什么?自然而然是在 **tail为null时,即当前线程是第一次插入同步队列** 。compareAndSetTail(t, node)方法会利用CAS操作设置尾节点,如果CAS操作失败会在`for (;;)`for死循环中不断尝试,直至成功return返回为止。因此,对enq()方法可以做这样的总结: 1. **在当前线程是第一个加入同步队列时,调用compareAndSetHead(new Node())方法,完成链式队列的头结点的初始化**; 2. **自旋不断尝试CAS尾插入节点直至成功为止**。