java并发机制底层原理
Posted 空方块
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java并发机制底层原理相关的知识,希望对你有一定的参考价值。
0.术语
内存屏障:是一组处理器指令,用于实现对内存操作的顺序限制。
缓存行:缓存中可以分配的最小存储单元。
原子操作:不可中断的一个或一系列操作。
缓存行填充:当处理器识别到从内存中读取操作数是可缓存的,处理器读取整个缓存航到适当的缓存(L1,L2,L3的或所有)。
缓存命中:如果进行高速缓存航填充操作的内存位置仍然是下次处理器访问的地址是,处理器从缓存中读取操作数,而不是从内存。
写命中:当处理器将操作数写回到一个内存缓存的区域是,首先检查这个缓存的内存地址是否在缓存行中,如果存在一个有效的缓存行,则处理器将这个操作数写回到缓存,而不是写回到内存。
写缺失:一个有效的缓存行被写入到不存在的内存区域。
1.Volatile实现原理
#1:如果一个数据被声明为volatile,java线程内存模型确保所有线程看到这个变量的值是一致的。
#2:volatile变量修饰如果使用恰当,性能会比synchronized的使用和执行成本会更低,不会引起线程上下文的切换和调度。
1.1.Volatile实现原理
Java Code:
instance = new Singleton();// instance 是 Volatile变量
Compile Code:
0x01a3deld: movb $0x0,0x1104800(%esp);
0x01a3de24: <strong>lock</strong> addl $0x0,(%esp);
有Volatile变量修饰的共享变量,进行写操作时,会多第二行汇编代码。
lock前缀指令再多核处理器下,做了:
#1:将当前处理器缓存行的数据写回到系统内存。
锁定这块内存区域的缓存,并写回内存,并使用缓存一致性机制来确保修改的原子性。
#2:写回内存的操作会引起其他CPU里缓存该内存地址的数据无效。
Note:volatile运用在数组上。数组的引用会变成volatile,但是数组的元素不是volatile。
2.Synchronized实现原理
2.1.监视器
在Java中,每个对象都有一个关联的lock,当某个method被声明成synchronized的时候, 要执行的thread必须要在执行前能够捉取到该兑现该关联的lock。method完成的时候lock会自动被释放掉。
#1:普通同步方法,锁是当前实例对象。
#2:静态同步方法,锁是当前类的Class对象。
#3:同步块,锁是Synchronized括号里的对象。
#4:Java并不会盲目地进入synchronized程序代码时就,取lock。如果当前线程已经占有lock,不会等待lock被释放或者取lock,相反,会直接让synchronized程序段运行可以了。否则,不同类之间的synchronized方法互相调用,会造成死锁。
2.2.同步原理
代码块同步是使用monitorenter和monitorexit指令实现,monitorenter指令是在编译后插入同步代码块的开始位置,而monitorexit插入到方法结束处和异常处。JVM要保证每个monitorenter必须有monitorexit对应。
#1:Java对象头
长度 | 内容 | 说明 |
32/64bit | Mark Word | 存储对象的hashCode或锁信息 |
32/64bit | Class Metadata Address | 存储对象类型数据的指针 |
32/64bit | Array Length | 数组长度(如果当前对象是数组) |
#2:无锁状态的Mark Word
锁状态 | 25 bits | 4 bits | 1 bit 是否是偏向锁 | 2 bits 锁标志位 |
无锁状态 | 对象的hashCode | 对象分代年龄 | 0 | 01 |
#3:有锁状态的Mark Word
锁状态 | 25 bits | 4 bit | 1 bit | 2 bits |
23 | 2 bits | 是否是偏向锁 | 锁标志位 | ||
轻量级锁 | 指向栈中锁记录的指针 | 00 | ||
重量级锁 | 指向向互斥量的指针 | 10 | ||
GC标志 | 空 | 11 | ||
偏向锁 | 线程ID | Epoch | 对象分代年龄 | 1 | 01 |
2.3.锁的升级
java SE1.6里,锁有4种状态:无锁状态、偏向锁状态、轻量级锁状态和重量级状态。
锁可以升级,但不能降级,目的是为了提高获得锁和释放锁的效率。
2.31.偏向锁
#1:当一个线程访问同步块并获取锁时,会在对象头和帧栈的锁记录里存储锁偏向的线程ID,以后该线程进入和退出块是不需要CAS操作来加锁和解锁(前提是中途不存在其他线程获取该锁)。
#2:偏向锁是等到竞争出现才释放锁的机制。当其他线程尝试竞争该偏向锁时,持有偏向锁的线程才会释放锁。撤销的前提是,拥有该偏向锁的线程没有字节码正在执行。撤销首先会暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态,如果线程仍然活着,用有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或标记对象不适合作为偏向锁。
#3:偏向锁在Java 6 和 Java 7 都是默认启用的,但是在应用启动几秒钟之后才激活。关闭延迟-XX:BiasedLockingStartupDelay=0。
#4:如果确定所有锁都处于竞争状态,可关闭偏向锁-XX:UseBiaseLocking=false。
2.32.轻量级锁
#1:自旋尝试获取锁,会消耗大量CPU,为了避免无用的自旋,一旦锁升级为重量锁,就不会再恢复到轻量锁状态。锁处于重量级锁状态,其他线程尝试获取锁时,都会被阻塞住。当持有该锁的线程释放锁之后会唤醒这些线程。
2.33.锁的优缺点对比
锁 | 优点 | 缺点 | 适合场景 | |
偏向锁 | 加锁解锁不需要额外的消耗,和执行费同步方法比仅存纳米级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一条线程访问同步块场景 | |
轻量级锁 | 竞争的线程不会阻塞,提供了响应速度 | 如果始终得不到锁竞争的线程适用自旋会消耗CPU | 追求响应时间,同步块执行速度非常快 | |
重量级锁 | 线程竞争不使用自旋,不会消耗过多CPU | 线程阻塞,响应速度缓慢 | 追求吞吐量,同步块执行速度较长 |
3.原子操作的实现原理
JDK中提供了一些类来支持原子操作,如AtomicBoolean(原子更新boolean)、AtomicInteger(原子更新int)、AtomicLong(原子更新long)。都是使用了自旋CAS的方式实现原子操作。
CAS虽然高效,但是存在不足:
#1:ABA问题。CAS需要在操作值前检查下,值有没有发生变化,如果没有发生变化则更新。但是如果一个值原来是A,变成了B,又变成了A,使用CAS检查时会认为它的值没有发生变化,但是实际上是变化了。可以使用版本号来解决。
#2:循环时间长,CPU开销大。
#3:只能保证单一共享变量的原子操作。多个变量实现原子可以使用AtomicReference。
4.极简同步技巧
4.1.寄存器的效应
#1:计算机必须将数据从主存储器中读到寄存器中,对寄存器操作,然后将数据存放存储器;
#2:当操作系统将某thread分配给CPU时,他会把thread特有的信息加载到CPU的寄存器中;
#3:在分配不同的thread给CPU之前,它会将寄存器的信息存下来。所以thread间决不会共享保存在寄存器的数据;
#4:使用volatile关键字能够保证比那里不会保持在寄存器中,能够保证变量是真正地分享与thread之间。
4.2.重排语句的效应
synchronized块能够防止语句的重排,VM不能将语句从synchronized块移动到synchronized块之外。
4.3.双重检查的Locking
Foo foo;
public void useFoo()
if(foo == null)
synchronized(this)
if(foo == null)
foo = new Foo();
foo.invoke();
已经被完全唾弃了。foo会被初始化一次以上,原因寄存器的效应。
以上是关于java并发机制底层原理的主要内容,如果未能解决你的问题,请参考以下文章