From 40cd68bd0e21cfbd662f459b96f23d9cbde04af2 Mon Sep 17 00:00:00 2001 From: calvin Date: Sun, 15 Dec 2024 10:11:31 +0800 Subject: [PATCH] REVIEW --- ...205\263\351\224\256\345\255\227---synchronized.md" | 8 +++++--- ...ava\345\205\263\351\224\256\345\255\227--final.md" | 2 +- ...45\217\212\346\234\211\345\272\217\346\200\247.md" | 9 ++++++--- ...ock\344\270\216AbstractQueuedSynchronizer(AQS).md" | 8 ++++---- ...206\350\247\243AbstractQueuedSynchronizer(AQS).md" | 11 +++++++++-- 5 files changed, 25 insertions(+), 13 deletions(-) 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" index fe91e9f..1975f27 100644 --- "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" @@ -1,3 +1,5 @@ +Also refer to https://www.cnblogs.com/paddix/p/5405678.html + # 1. synchronized简介 # 在学习知识前,我们先来看一个现象: @@ -27,7 +29,7 @@ 开启了10个线程,每个线程都累加了1000000次,如果结果正确的话自然而然总数就应该是10 * 1000000 = 10000000。可就运行多次结果都不是这个数,而且每次运行结果都不一样。这是为什么了?有什么解决方案了?这就是我们今天要聊的事情。 -在上一篇博文中我们已经了解了[java内存模型](https://juejin.im/post/5ae6d309518825673123fd0e)的一些知识,并且已经知道出现线程安全的主要来源于JMM的设计,主要集中在主内存和线程的工作内存而导致的**内存可见性问题**,以及**重排序导致的问题**,进一步知道了**happens-before规则**。线程运行时拥有自己的栈空间,会在自己的栈空间运行,如果多线程间没有共享的数据也就是说多线程间并没有协作完成一件事情,那么,多线程就不能发挥优势,不能带来巨大的价值。那么共享数据的线程安全问题怎样处理?很自然而然的想法就是每一个线程依次去读写这个共享变量,这样就不会有任何数据安全的问题,因为每个线程所操作的都是当前最新的版本数据。那么,在java关键字synchronized就具有使每个线程依次排队操作共享变量的功能。很显然,这种同步机制效率很低,但synchronized是其他并发容器实现的基础,对它的理解也会大大提升对并发编程的感觉,从功利的角度来说,这也是面试高频的考点。好了,下面,就来具体说说这个关键字。 +在上一篇博文中我们已经了解了[java内存模型](https://juejin.im/post/5ae6d309518825673123fd0e) 的一些知识,并且已经知道出现线程安全的主要来源于JMM的设计,主要集中在主内存和线程的工作内存而导致的**内存可见性问题**,以及**重排序导致的问题**,进一步知道了**happens-before规则**。线程运行时拥有自己的栈空间,会在自己的栈空间运行,如果多线程间没有共享的数据也就是说多线程间并没有协作完成一件事情,那么,多线程就不能发挥优势,不能带来巨大的价值。那么共享数据的线程安全问题怎样处理?很自然而然的想法就是每一个线程依次去读写这个共享变量,这样就不会有任何数据安全的问题,因为每个线程所操作的都是当前最新的版本数据。那么,在java关键字synchronized就具有使每个线程依次排队操作共享变量的功能。很显然,这种同步机制效率很低,但synchronized是其他并发容器实现的基础,对它的理解也会大大提升对并发编程的感觉,从功利的角度来说,这也是面试高频的考点。好了,下面,就来具体说说这个关键字。 # 2. synchronized实现原理 # 在java代码中使用synchronized可是使用在代码块和方法中,根据Synchronized用的位置可以有这些使用场景: @@ -70,7 +72,7 @@ ## 2.2 synchronized的happens-before关系 ## -在上一篇文章中讨论过[happens-before](https://juejin.im/post/5ae6d309518825673123fd0e)规则,抱着学以致用的原则我们现在来看一看Synchronized的happens-before规则,即监视器锁规则:对同一个监视器的解锁,happens-before于对该监视器的加锁。继续来看代码: +在上一篇文章中讨论过[happens-before](https://juejin.im/post/5ae6d309518825673123fd0e) 规则,抱着学以致用的原则我们现在来看一看Synchronized的happens-before规则,即监视器锁规则:对同一个监视器的解锁,happens-before于对该监视器的加锁。继续来看代码: public class MonitorDemo { private int a = 0; @@ -122,7 +124,7 @@ ## 3.1 CAS操作 ## ### 3.1.1 什么是CAS? ### -使用锁时,线程获取锁是一种**悲观锁策略**,即假设每一次执行临界区代码都会产生冲突,所以当前线程获取到锁的时候同时也会阻塞其他线程获取该锁。而CAS操作(又称为无锁操作)是一种**乐观锁策略**,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。因此,线程就不会出现阻塞停顿的状态。那么,如果出现冲突了怎么办?无锁操作是使用**CAS(compare and swap)**又叫做比较交换来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。 +使用锁时,线程获取锁是一种 **悲观锁策略** ,即假设每一次执行临界区代码都会产生冲突,所以当前线程获取到锁的时候同时也会阻塞其他线程获取该锁。而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操作一个变量是,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试,当然也可以选择挂起线程 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" index 5991b9b..ef9695d 100644 --- "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" @@ -125,7 +125,7 @@ JDK中提供的八个包装类和String类都是不可变类,我们来看看St 可以看出String的value就是final修饰的,上述其他几条性质也是吻合的。 # 4. 多线程中你真的了解final吗? # -上面我们聊的final使用,应该属于**Java基础层面**的,当理解这些后我们就真的算是掌握了final吗?有考虑过final在多线程并发的情况吗?在[java内存模型](https://juejin.im/post/5ae6d309518825673123fd0e)中我们知道java内存模型为了能让处理器和编译器底层发挥他们的最大优势,对底层的约束就很少,也就是说针对底层来说java内存模型就是一弱内存数据模型。同时,处理器和编译为了性能优化会对指令序列有**编译器和处理器重排序**。那么,在多线程情况下,final会进行怎样的重排序?会导致线程安全的问题吗?下面,就来看看final的重排序。 +上面我们聊的final使用,应该属于**Java基础层面**的,当理解这些后我们就真的算是掌握了final吗?有考虑过final在多线程并发的情况吗?在[java内存模型](https://juejin.im/post/5ae6d309518825673123fd0e) 中我们知道java内存模型为了能让处理器和编译器底层发挥他们的最大优势,对底层的约束就很少,也就是说针对底层来说java内存模型就是一弱内存数据模型。同时,处理器和编译为了性能优化会对指令序列有**编译器和处理器重排序**。那么,在多线程情况下,final会进行怎样的重排序?会导致线程安全的问题吗?下面,就来看看final的重排序。 ## 4.1 final域重排序规则 ## 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" index 6b93924..fb0d4a9 100644 --- "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" @@ -1,5 +1,6 @@ # 1. 三大性质简介 # -在并发编程中分析线程安全的问题时往往需要切入点,那就是**两大核心**:JMM抽象内存模型以及happens-before规则(在[这篇文章](https://juejin.im/post/5ae6d309518825673123fd0e)中已经经过了),三条性质:**原子性,有序性和可见性**。关于[synchronized](https://juejin.im/post/5ae6dc04f265da0ba351d3ff)和[volatile](https://juejin.im/post/5ae9b41b518825670b33e6c4)已经讨论过了,就想着将并发编程中这两大神器在 **原子性,有序性和可见性**上做一个比较,当然这也是面试中的高频考点,值得注意。 +在并发编程中分析线程安全的问题时往往需要切入点,那就是**两大核心**:JMM抽象内存模型以及happens-before规则(在[这篇文章](https://juejin.im/post/5ae6d309518825673123fd0e)中已经经过了) ,三条性质:**原子性,有序性和可见性**。 +关于[synchronized](https://juejin.im/post/5ae6dc04f265da0ba351d3ff) 和 [volatile](https://juejin.im/post/5ae9b41b518825670b33e6c4) 已经讨论过了,就想着将并发编程中这两大神器在 **原子性,有序性和可见性**上做一个比较,当然这也是面试中的高频考点,值得注意。 # 2. 原子性 # 原子性是指**一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉**。及时在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。我们先来看看哪些是原子操作,哪些不是原子操作,有一个直观的印象: @@ -12,7 +13,8 @@ > > 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(解锁):作用于主内存中的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定 @@ -23,7 +25,8 @@ 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操作,将工作内存同步到主内存中就需要执行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的非原子性协定) 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" index 329ff6f..2d0ddfb 100644 --- "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" @@ -26,10 +26,10 @@ 我们现在就来看看lock接口定义了哪些方法: -> void lock(); //获取锁 -> void lockInterruptibly() throws InterruptedException;//获取锁的过程能够响应中断 -> boolean tryLock();//非阻塞式响应中断能立即返回,获取锁放回true反之返回fasle -> boolean tryLock(long time, TimeUnit unit) throws InterruptedException;//超时获取锁,在超时内或者未中断的情况下能够获取锁 +> void lock(); //获取锁 +> void lockInterruptibly() throws InterruptedException;//获取锁的过程能够响应中断 +> boolean tryLock();//非阻塞式响应中断能立即返回,获取锁放回true反之返回fasle +> boolean tryLock(long time, TimeUnit unit) throws InterruptedException;//超时获取锁,在超时内或者未中断的情况下能够获取锁 > Condition newCondition();//获取与lock绑定的等待通知组件,当前线程必须获得了锁才能进行等待,进行等待时会先释放锁,当再次获取锁时才能从等待中返回 上面是lock接口下的五个方法,也只是从源码中英译中翻译了一遍,感兴趣的可以自己的去看看。那么在locks包下有哪些类实现了该接口了?先从最熟悉的ReentrantLock说起。 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" index f76488d..214b2b3 100644 --- "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" @@ -3,7 +3,7 @@ -在[上一篇文章](https://juejin.im/post/5aeb055b6fb9a07abf725c8c)中我们对lock和AbstractQueuedSynchronizer(AQS)有了初步的认识。在同步组件的实现中,AQS是核心部分,同步组件的实现者通过使用AQS提供的模板方法实现同步组件语义,AQS则实现了对**同步状态的管理,以及对阻塞线程进行排队,等待通知**等等一些底层的实现处理。AQS的核心也包括了这些方面:**同步队列,独占式锁的获取和释放,共享锁的获取和释放以及可中断锁,超时等待锁获取这些特性的实现**,而这些实际上则是AQS提供出来的模板方法,归纳整理如下: +在[上一篇文章](https://juejin.im/post/5aeb055b6fb9a07abf725c8c) 中我们对lock和AbstractQueuedSynchronizer(AQS)有了初步的认识。在同步组件的实现中,AQS是核心部分,同步组件的实现者通过使用AQS提供的模板方法实现同步组件语义,AQS则实现了对**同步状态的管理,以及对阻塞线程进行排队,等待通知**等等一些底层的实现处理。AQS的核心也包括了这些方面:**同步队列,独占式锁的获取和释放,共享锁的获取和释放以及可中断锁,超时等待锁获取这些特性的实现**,而这些实际上则是AQS提供出来的模板方法,归纳整理如下: **独占式锁:** @@ -22,7 +22,14 @@ 要想掌握AQS的底层实现,其实也就是对这些模板方法的逻辑进行学习。在学习这些模板方法之前,我们得首先了解下AQS中的同步队列是一种什么样的数据结构,因为同步队列是AQS对同步状态的管理的基石。 # 2. 同步队列 # -当共享资源被某个线程占有,其他请求该资源的线程将会阻塞,从而进入同步队列。就数据结构而言,队列的实现方式无外乎两者一是通过数组的形式,另外一种则是链表的形式。AQS中的同步队列则是**通过链式方式**进行实现。接下来,很显然我们至少会抱有这样的疑问:**1. 节点的数据结构是什么样的?2. 是单向还是双向?3. 是带头结点的还是不带头节点的?**我们依旧先是通过看源码的方式。 +当共享资源被某个线程占有,其他请求该资源的线程将会阻塞,从而进入同步队列。就数据结构而言,队列的实现方式无外乎两者一是通过数组的形式,另外一种则是链表的形式。 +AQS中的同步队列则是**通过链式方式**进行实现。接下来,很显然我们至少会抱有这样的疑问: + + **1. 节点的数据结构是什么样的? + 2. 是单向还是双向? + 3. 是带头结点的还是不带头节点的?** + +我们依旧先是通过看源码的方式。 在AQS有一个静态内部类Node,其中有这样一些属性: