//线程同步
一 监视器的理解
1 java中的监视器支持线程的两类工作方式: 互斥和协作, 互斥通过对象锁实现;协作通过Object类的wait方法和notify方法实现
2 通过一系列动作术语来理解监视器: 进入监视器 -> 获得监视器与持有监视器从而占据监视区域 -> 释放监视器
(1)监视器会保证在监视区域上同一时间只会执行一个线程
(2)一个线程想要进入监视器的惟一途径就是到达该监视器所关联的一个监视区域的开始处
(3)一个线程想要继续执行线程区域的惟一途径就是获得该监视器
二 等待并唤醒 监视器
1 原理: 在这种监视器中,一个★已经持有监视器的线程,可以通过执行一个等待命令,暂停自身的执行,当线程执行了等待命令后,它就会释放监视器,并进入一个等待区,这个线程会在等待区一直持续暂停
状态,直到一段时间后,这个监视器中的其他线程执行了唤醒命令,当一个线程执行了唤醒命令后,它会继续持有监视器,直到它主动释放监视器,如执行了一个等待命令或者执行完监视区域,当执行吃醋的线
程释放了监视器后,等待线程会苏醒,并重新获得监视器.
2 等待-通知 模型:
入口区等待的线程 -> 监视器持有者线程(执行监视区域) <--> 等待区线程
-> 出口(其他无关线程)
(1)■普通线程要变为和一个对象关联的监视器的入口区等待线程,就要尝试获取对象锁并且不能成功获取
(2)■监视器持有者线程执行监视区域是指执行特定对象的synchronized块或者方法
(3)如果上一个监视器的持有者在它释放监视器前没有执行唤醒命令,同时在此之前也没有任何等待线程被唤醒并等待苏醒,那么位于入口区的那些线程将会竞争获得监视器;但如果上一个持有者执行了唤
醒命令,入口区中的线程就不得不与一个或多个等待区中的线程来竞争.
(4)一个线程只有在它正持有监视器时才能执行等待命令,而且它只能通过再次成为监视器的持有者才能离开等待区
(5)关于java虚拟机如何从等待区以及入口区选择下一个线程来执行,在很大程度上取决于java虚拟机的设计者
3 Object类中的协同支持
■■■
三 对象锁
1 监视器者是和对象关联的,Object对象关联的监视器监视实例变量,Class对象关联的监视器监视类变量,如果无变量,则什么都不监视
2 在java程序中,只需要编写同步语句或者同步方法就可以标志一个监视区域,当java虚拟机运行程序时,每一次进入一个监视区域的时候都会自动锁上对象或者类(即是让线程获得了监视器,可以多次加锁
,当锁计数为0时释放锁).
3 关于synchronized块和方法
执行同样字节码的同步方法比同步语句更高效,因为synchronized方法被编译后,不包含进入和离开监视器的代码,不包含用于保存加锁对象的局部变量,也没有为方法创建异常表,方法被调用时由虚拟
机自动获取锁/释放锁
4 关于锁和监视器:
线程锁住一个对象,就是指线程获取对象相关联的监视器
JVM高级特性与最佳实践(第二版已经出来了):
第9章: tomcat类加载
//第12,13章: 并发
一 java内存模型
0 并发问题主要是指多个线程访问同一块内存的问题.java内存模型可以屏蔽掉硬件和操作系统的内存访问差异
1 java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节,注意此处的变量与java编程中所说的变量略有区别,它包括了实例字段
,静态字段,和构成数组对象的元素,但是不包括局部变量与方法参数,因为后者是线程私有的,还要注意,如果局部变量是一个reference类型,那它引用的对象不是线程私有的.
2 java内存模型规定了所有的变量都存储在主内存,每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作,都必须在工作内存中进行,而
不能直接读写主内存中的变量,(根据jvm规范,volatile变量依然有工作内存拷贝,但由于它特殊的操作顺序性规定,看起来如同直接在主内存中读写访问一般). 不同线程之间无法直接访问对方工作内存中
的变量.
建立工作内存的概念,是为了获取更好的运行速度,因为虚拟机及硬件系统可能会让工作内存优先存储于寄存器和高速缓存中,看以下的存储模型对比可认识到某种关系:
(1)处理器,高速缓存,主内存间的交互关系:
处理器1 <--> 高速缓存1 <--> 缓存一致性协议 <--> 主内存
处理器2 <--> 高速缓存2 <--> 缓存一致性协议 <--> 主内存
处理器3 <--> 高速缓存3 <--> 缓存一致性协议 <--> 主内存
(2)线程,主内存,工作内存三者的交互关系:
java线程1 <--> 工作内存1 <--> save和load操作 <--> 主内存
java线程2 <--> 工作内存2 <--> save和load操作 <--> 主内存
java线程3 <--> 工作内存3 <--> save和load操作 <--> 主内存
二 内存间交互操作
1 关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存,如何从工作内存同步回主内存之类的实现细节,java内存模型中定义了以下八种操作来完成,虚拟机实现时必须保
证每一种操作都是原子的,不可再分的(对于double和long类型变量来说,load,store,read,write操作在某些平台上可以有例外)
(1)lock(锁定):作用于主内存变量,它把一个变量标识为一条线程独占的状态。
(2)unlock(解锁):作用于主内存变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
(3)read(读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
(4)load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
(5)use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用 到的变量的值的字节码指令时将会执行这个操作。
(6)assign(赋值):作用工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个 给变量赋值的字节码指令时执行这个操作。
(7)store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
(8)write(写入):作用于主内存变量的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
需要注意以下几点
(1)★可以把这些指令当作线程对特定变量的动作,因为一个线程对应一个虚拟机执行引擎实例
(2)read+load,store+write是固定组合的
(3)一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行,只有执行相同次数的unlock操作后,变量才会被解锁
(4)如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值
(5)对一个变量执行unlock操作之前,必须先把此变量同步回主内存中
2 对于volatile型变量的特殊规则
(1)使用volatile变量的第一个语义是:当一个变量被定义成volatile之后,将会保证此变量对所有线程对其都是立即可见的,这种可见性是由于每次使用变量前都要从主内存中取,并且每次修改后都要立刻
同步回主内存,执行引擎看不到不一致的情况,而不能理解为volatile变量在并发情况下是安全的,是可以保证原子性的,因为线程仍没有对volatile变量进行lock操作,当运算结果依赖于变量当前值,或者
不只一个线程修改变量值,或者变量需要与其他状态变量共同参与不变约束,都要通过加锁来保证变量的原子性.
(2)使用volatile变量的第二个语义是禁止指令重排序优化,普通变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执
行顺序一到,例如,在线程A的方法中进行一项准备操作然后将标志位设为准备好(标志位是volatile的),这样其他线程就能感知已经准备好了,但是如果方法中进行了指令重排(标志位不是volatile的),先
把标志位设为准备好再进行准备,那么其他线程就会误以为真的准备好了,这是错误的.
注: 即使用字节码来分析并发问题,也是不严谨的,因为即使出来只有一条字节码指令,也并不意味着执行这条指令就是一个原子操作,一条字节码指令在解释执行时,解释器也要运行许多行代码才能实现它
的语义,如果是编译执行,一条字节码指令也可能转化成若干条本地机器码指令,而指令的原子性只能由反汇编出来的汇编指令,分析cpu对汇编指令的原子性支持,才能确定,★所以,在java层面分析操作的
原子性,要看jvm规范对操作的原子性支持,或者是编程中同步与锁的的设计
(3)性能:
在某些情况下,volatile同步机制的性能要优于锁,但这很难说,因为锁机制也可以实行许多消除和优化;
volatile变量读操作的性能消耗与普通变量几乎没有什么差别,但是写操作则可能会慢上一些,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序行
3 对于long和double型变量的特殊规则
java内存模型要求lock,unlock,read,load,assige,use,store,write这八个操作都具有原子性,但是对于64位的数据类型long和double,在模型中特别定义了一条宽松的规定: 允许虚拟机将没有被
volatile修饰的64位数据的★读写操作划分为两次32位的操作来进行,即允许虚拟机实现选择可以不保证64位数据类型的load,store,read,write这四个操作的原子性
如果有多个进程共享一个并未声明为volatile的long或double类型的变量,并且同时对它们进行读取和修改操作,那么某些线程可能会读取到一个既非原值,也不是其他线程修改值的代表了"半个变量"
的数值(★会不会是指令重排的原因),但实际上大多虚拟机实现也会把这两个变量的的读写操作设计成原子的.
★读操作要用read和load,写操作要用store和wite,这样的话读操作和写操作不可能是原子的?
答: 读操作中read是原子的,能保证从主内存中读一个变量的值是原子的,不会被其他线程打断,写操作中write是原子的,能保证将数据写入主内存是原子的,不会被其他线程打断.因为工作内存只能被一个
线程访问,不存在打断和错乱问题,而只要针对于主内存的操作是原子的,那么读写操作就是原子的
★反映在java代码中,怎样的读写操作才是原子的呢?
■■■
4 关于原子操作,可见性,有序性
(1)并发问题,其实是多个线程并行或并发地访问同一块内存的问题
(2)原子操作,是指对内存的操作不会被别的线程打断,造成错乱的操作
(3)可见性,是相对于工作内存与主内存数据的同步来说的,与原子性不是一个概念,但可见性的实现也是要基于原子性的;
除了volatile之外,java还有两个关键字能实现可见性,它们是synchronized和final,同步块的可见性是由"对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(store+write)"这条规则
获得的;而final关键字的可见性是指:被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把"this"的引用传递出去,那么其他线程中就能看见final字段的值,因为它是不可改变的.
(4)有序性: 对于java程序天然的有序性,是这样的:如果在线程内观察,所有的操作都是有序的;如果在线程之间观察,所有操作都是无序的(指令重排,或工作内存与主内存同步延迟)
java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,synchronized是由规则"一个变量在同一个时刻只允许一条线程对其进行lock操作"获得的,它决定了有同一个锁的
两个同步块只能品行地进入.
5 先行发生原则(happens-before)................至此p330
(1)先行发生原则指如果说操作A先行发生于操作B,即是说在发生操作B之前,操作A产生的影响能被操作B观察到
//Java并发容器
1 深入分析ConcurrentHashMap 参考http://www.infoq.com/cn/articles/ConcurrentHashMap
线程不安全的HashMap
效率低下的HashTable容器
HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法时,其他线程访问HashTable的同步方法时,可能会进入阻塞或轮询状态。如线程1使用put进行添加元素,线程2不但不能使用put方法添加元素,并且也不能使用get方法来获取元素,所以竞争越激烈效率越低
锁分段技术
HashTable容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问HashTable的线程都必须竞争同一把锁,那假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
2 BlockingQueue 与 SynchronousQueue
BlockingQueue是一个接口,SynchronousQueue是其实现类,
BlockingQueue定义的常用方法
BlockingQueue定义的常用方法如下:
| 抛出异常 | 特殊值 | 阻塞 | 超时 | |
| 插入 | add(e) | offer(e) | put(e) | offer(e, time, unit) |
| 移除 | remove() | poll() | take() | poll(time, unit) |
| 检查 | element() | peek() | 不可用 | 不可用 |
1)add(anObject):把anObject加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则招聘异常
2)offer(anObject):表示如果可能的话,将anObject加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回false.
3)put(anObject):把anObject加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻断直到BlockingQueue里面有空间再继续.
4)poll(time):取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null
5)take():取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到Blocking有新的对象被加入为止
其中:BlockingQueue 不接受null 元素。试图add、put 或offer 一个null 元素时,某些实现会抛出NullPointerException。null 被用作指示poll 操作失败的警戒值。
SynchronousQueue是 一种阻塞队列,其中每个 put 必须等待一个 take,反之亦然。SynchronousQueue内部并没有数据缓存空间,你不能调用peek()方法来看队列中是否有数据元素,因为数据元素只有当你试着取走的时候才可能存在,不取走而只想偷窥一下是不行的,当然遍历这个队列的操作也是不允许的。队列头元素是第一个排队要插入数据的线程,而不是要交换的数据。数据是在配对的生产者和消费者线程之间直接传递的,并不会将数据缓冲数据到队列中。可以这样来理解:生产者和消费者互相等待对方,握手,然后一起离开。
本文深入探讨了Java并发容器的工作原理,重点分析了ConcurrentHashMap的锁分段技术,以及BlockingQueue和SynchronousQueue的区别和使用场景。同时,文章详细解释了Java内存模型、监视器、对象锁等核心概念,以及volatile、synchronized和final关键字在并发编程中的应用。通过丰富的实例和代码,旨在帮助开发者理解并发容器和线程同步技术,提高程序并发处理能力。
2万+

被折叠的 条评论
为什么被折叠?



