java 锁
Posted 愚蠢的猴子
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java 锁相关的知识,希望对你有一定的参考价值。
一、 Java并发编程的三个概念
原子性:一个或多个操作要么全部执行成功要么全部执行失败;
可见性:当多个线程访问同一个变量时,如果其中一个线程对其作了修改,其他线程能立即获取到最新的值;
有序性:程序执行的顺序按照代码的先后顺序执行(处理器可能会对指令进行重排序);
二、单核CPU到多核CPU的变化
CPU越来越快,主存逐渐跟不上cpu的频率,cpu需要等待主存浪费资源。所以cache的出现主要为了解决cpu和内存之间频率不匹配的问题。
但cache也带来了新的问题,并发处理的不同步(不过可以通过总线锁和缓存一致性来解决)。
三、重排序
在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。
重排序分类
1、编译器优化的重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
2、指令级并行的重排序:现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。
如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
3、内存系统的重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。如下图:
重排序的原则:as-if-serial语义
as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果不能被改变。
编译器、runtime和处理器都必须遵守as-if-serial语义。
为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序。数据依赖关系如下图所示:
单线程情况下,控制依赖关系的重排序,不影响最终结果。多线程情况下,则可能会破坏程序的意图。
JMM禁止重排序的措施:
1、对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。
2、对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers,Intel称之为MemoryFence)指令,
通过内存屏障指令来禁止特定类型的处理器重排序。
JMM的内存屏障插入策略:Load:加载(读)、Store:保存(写),屏障名称就可以看出读写的先后顺序)
1、在每个volatile写操作前插入StroreStore屏障
2、在每个volatile写操作前插入StroreLoad屏障
3、在每个volatile读操作前插入LoadLoad屏障
4、在每个volatile读操作前插入LoadStore屏障
四、Volatile
volatile
是轻量级的synchronized
,它在多处理器开发中保证了共享变量的“可见性”。volatile
不会引起上下文的切换和调度(禁止指令重排),执行开销更小。
Volatile 的特性:
可见性:对一个volatile
变量的读,总是能看到(任意线程)对这个volatile
变量最后的写入。
原子性:对任意单个volatile
变量的读/写具有原子性,但类似于volatile++
这种复合操作不具有原子性。
Volatile 的原理:
1. 使用volitate修饰的变量在汇编阶段,会多出一条lock前缀指令(ACC_VOLATILE)
2. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;
即在执行到内存屏障这句指令时,在它前面的操作已经全部完成
3. 它会强制将对缓存的修改操作立即写入主存
4. 如果是写操作,它会导致其他CPU里缓存了该内存地址的数据无效
volatile写-读建立的happens-before关系:
volatile的写-读与锁的释放-获取有相同的内存效果。
这里A线程写一个volatile变量后,B线程读同一个volatile变量。
A线程在写volatile变量之前所有可见的共享变量,在B线程读同一个volatile变量后,将立即变得对B线程可见。
Volatile 写-读的内存语义:
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
注:关于volatile变量重排序,严格限制编译器和处理器对volatile变量与普通变量的重排序,确保volatile的写-读和锁的释放-获取具有相同的内存语义。
JMM重排序分为编译器重排序和处理器重排序,JMM会限制这两种类型的重排序类型来保证volatile的内存语义
1、第二个操作是volatile写时,第一个操作不管是什么,都不能重排序
2、第一个操作是volatile读时,第二个操作不管是什么,都不能重排序
3、第一个操作是volatile是写,第二个操作是volatile是读,不能重排序
volatile和synchronized的区别:
volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;
synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
五、Synchronized 锁
synchronized 是JVM实现的一种锁,其中锁的获取和释放分别是monitorenter 和 monitorexit 指令,该锁在实现上分为了偏向锁、轻量级锁和重量级锁,
其中偏向锁在 java1.6 是默认开启的,轻量级锁在多线程竞争的情况下会膨胀成重量级锁,有关锁的数据都保存在对象头中。
对于普通同步方法,锁是当前实例对象;
对于静态同步方法,锁是当前类Class对象;
对于同步方法块,锁是Synchronized括号里配置的对象;
synchronized 的原理
加了 synchronized 关键字的代码段,生成的字节码文件会多出 monitorenter 和 monitorexit 两条指令(利用javap -v *.class字节码文件)
加了 synchronized 关键字的方法,生成的字节码文件中会多一个 ACC_SYNCHRONIZED 标志位,当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,
如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。
synchronized 的缺点
1、会让没有得到锁的资源进入Block状态,争夺到资源之后又转为Running状态,这个过程涉及到操作系统用户模式和内核模式的切换,代价比较高。
2、Java1.6为 synchronized 做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能仍然较低。
锁的获取和释放 建立的happens-before关系
锁的释放和获取的内存语义
JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。
锁释放与volatile写有相同的内存语义;锁获取与volatile读有相同的内存语义。
线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量所做修改的)消息。
线程B获取一个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息。
线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息。
以上是关于java 锁的主要内容,如果未能解决你的问题,请参考以下文章
java中ReentrantReadWriteLock读写锁的使用
JUC并发编程 共享模式之工具 JUC CountdownLatch(倒计时锁) -- CountdownLatch应用(等待多个线程准备完毕( 可以覆盖上次的打印内)等待多个远程调用结束)(代码片段