内容要点:java
Java内存模型与线程;面试
线程安全与锁优化;安全
Java内存模型与JVM内存结构迷惑的的能够看下这个:多线程
主内存与工做内存ide
Java内存模型规定了全部的变量都存储在主内存(Main Memory)中。每条线程还有本身的工做内存,线程的工做内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的全部操做(读取、赋值等)都必须在工做内存中进行,而不能直接读写主内存中的变量。不一样的线程之间也没法直接访问对方工做内存中的变量,线程间变量值的传递均须要经过主 内存来完成,线程、主内存、工做内存三者的交互关系以下图所示:函数
内存间交互操做性能
lock(锁定):做用于主内存的变量,它把一个变量标识为一条线程独占的状态。 学习
unlock(解锁):做用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放 后的变量才能够被其余线程锁定。 优化
read(读取):做用于主内存的变量,它把一个变量的值从主内存传输到线程的工做内 存中,以便随后的load动做使用。
load(载入):做用于工做内存的变量,它把read操做从主内存中获得的变量值放入工做内存的变量副本中。
use(使用):做用于工做内存的变量,它把工做内存中一个变量的值传递给执行引 擎,每当虚拟机遇到一个须要使用到变量的值的字节码指令时将会执行这个操做。
assign(赋值):做用于工做内存的变量,它把一个从执行引擎接收到的值赋给工做内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操做。
store(存储):做用于工做内存的变量,它把工做内存中一个变量的值传送到主内存 中,以便随后的write操做使用。
write(写入):做用于主内存的变量,它把store操做从工做内存中获得的变量的值放入主内存的变量中。
内存间交互操做必须知足如下规则:
不容许read和load、store和write操做之一单独出现,即不容许一个变量从主内存读取了但工做内存不接受,或者从工做内存发起回写了但主内存不接受的状况出现。
不容许一个线程丢弃它的最近的assign操做,即变量在工做内存中改变了以后必须把该变化同步回主内存。
不容许一个线程无缘由地(没有发生过任何assign操做)把数据从线程的工做内存同步回主内存中。
不容许在工做内存中直接使用一个未被初始化 (load或assign)的变量,一个新的变量只能在主内存中“诞生”,就是对一个变量实施use、store操做以前,必须先执行过了assign和load操做。
一个变量在同一个时刻只容许一条线程对其进行lock操做,但lock操做能够被同一条线程重复执行屡次,屡次执行lock后,只有执行相同次数的unlock操做,变量才会被解锁。
若是对一个变量执行lock操做,那将会清空工做内存中此变量的值,在执行引擎使用这个变量前,须要从新执行load或assign操做初始化变量的值。
若是一个变量事先没有被lock操做锁定,那就不容许对它执行unlock操做,也不容许去 unlock一个被其余线程锁定住的变量。
对一个变量执行unlock操做以前,必须先把此变量同步回主内存中(执行store、write操 做)。
volatile关键字
volatile是Java虚拟机提供的最轻量的同步机制
变量被volatile修饰具有两个特性:
第一保证变量对全部线程可见,这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其余线程来讲是能够当即得知的。而普通变量不能作到这一点,普通变量的值同步回主内存时间是不肯定的。
可是“基于volatile变量的运算在并发下是安全的”这个结论是不彻底正确的!
public class Volatile{
public static volatile int race=0;
public static void increase(){
race++;
}
private static final int THREADS_COUNT=20;
public static void main(String[]args){
Thread[]threads=new Thread[THREADS_COUNT];
for(int i=0;i<THREADS_COUNT;i++){
threads[i]=new Thread(new Runnable(){
@Override public void run(){
for(int i=0;i<10000;i++){ increase();
}
}
});
threads[i].start();
}
while(Thread.activeCount()>1)
Thread.yield();
System.out.println(race);
}
}复制代码
这段代码发起了20个线程,最后输出的结果应该是200000,可是输出的结果都不同,都是一个小于200000的数字, 这是为何呢?
问题就出如今自增运算“race++”之中,volatile关键字保证了race的值在此时是正确的,可是在执行“race++”并非原子操做,race+1而后再把值赋给race,若是完成了race+1,在赋值前另外一个线程把race已经赋值+1啦,那么两个线程最终只+1。
第二禁止指令重排序优化。普通变量仅仅能保证在该方法执行过程当中,获得正确结果,可是不保证程序代码的执行顺序,volatile能保证以前代码以前执行,以后代码以后执行,可是不能保证以前以及以后一部分代码具体的的执行顺序。
原子性可见性和有序性
原子性(Atomicity):咱们大体能够认为基本数据类型的访问读写是具有原子性的。对于更大范围的原子操做,Java内存模型字节码指令monitorenter和monitorexit来隐式地使用这两个lock和unlock操做,这两个字节码指令反映到Java代码中就是synchronized关键字。
可见性(Visibility):可见性是指当一个线程修改了共享变量的值,其余线程可以当即得知这个修改。Java内存模型是经过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的,普通变量,volatile变量都是这样,volatile的特殊规则保证了新值能当即同步到主内存,以及每次使用前当即从主内存刷新。 除volatile以外,即synchronized和final也能实现可见性。同步块的可见性是由“对一个变量执行unlock操做以前,必须先把此变量同步回主内存中,而final关键字的可见性是指:被final修饰的字段在构造器中一旦初始化完成,而且构造器没有把“this”的引用传递出去,那在其余线程中就能看见final字段的值。
有序性(Ordering):Java程序中自然的有序性能够总结为一句话:若是在本线程内观察,全部的操做都是有序的;若是在一个线程中观察另外一个线程,全部的操做都是无序的。Java语言提供了volatile和synchronized两个关键字来保证线程之间操做的有序性,volatile 关键字自己就包含了禁止指令重排序的语义,而synchronized则是由“一个变量在同一个时刻只容许一条线程对其进行lock操做”。
线程安全与锁优化
线程安全
什么是线程安全
“线程安全”有一个比较恰当的定义:“当多个线程访问一个对象时,若是不用考虑这些线程在运行时环境下的调度和交替执行,也不须要进行额外的同步,或者在调用方进行任何其余的协调操做,调用这个对象的行为均可以得到正确的结果,那这个对象是线程安全的”。
线程安全的实现
1,互斥同步(阻塞同步)
互斥同步是一种悲观的并发策略,认为不加锁就必定会出问题。
同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个(或者是一些, 使用信号量的时候)线程使用。而互斥是实现同步的一种手段,临界区(Critical Section)、互斥量(Mutex)和信号量(Semaphore)都是主要的互斥实现方式。
经常使用的synchronized关键字通过编译以后,会在同步块的先后造成monitorenter和monitorexit两个字节码,在执行monitorenter指令时,首先要尝试获取对象的锁。
a,若是这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,在执行monitorexit指令时会将锁计数器减1,当计数器为0时,锁就被释放。
b,若是获取对象锁失败,那当前线程就要阻塞等待,直到对象锁被另一个线程释放为止。
可是sysnchronized是重量级锁,滥用会极大影响自己业务代码的执行效率,因此只在肯定必要使用的状况下才去使用才更合理。
除了synchronized关键字,java.util.concurrent(简称JUC)包中的重入锁 (ReentrantLock)也能够实现同步(CopyOnWriteArray在增删改过程当中就是利用的重入锁实现同步的)相比synchronized,ReentrantLock;利用lock和unlock配合try,catch使用,并由如下特性,等待可中断、可实现公平锁,以及锁能够绑定多个条件。
a,等待可中断是指,当前持有锁的线程长期不释放锁,等待锁的线程能够选择放弃等待。
b,公平锁是指,多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次得到锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会得到锁。synchronized中的锁是非公平的,ReentrantLock默认状况下也是非公平的,但能够经过带布尔值的构造函数要求使用公平锁。
c,锁绑定多个条件是指,一个ReentrantLock对象能够同时绑定多个Condition对象,而在 synchronized中,锁对象的wait()和notify()或notifyAll()方法能够实现一个隐含的条件,若是要和多于一个的条件关联的时候,就不得不额外地添加一个锁,而ReentrantLock只须要屡次调用newCondition()方法。
2,非阻塞同步
非阻塞同步是一种基于冲突检测的乐观并发策略,先进行操做,冲突在进行弥补。
操做和冲突检测,须要基于硬件指令集的发展,来保证其原子性。
3,无同步方案
可重入代码
线程本地存储
锁优化
1,自旋锁
为了避免是每次等待锁的线程都去挂起,1.4.2中引入自旋锁,只不过默认是关闭的,可使用-XX:+UseSpinning 参数来开启,在JDK 1.6中就已经改成默认开启了,主要目的是可能会有线程等待锁时间比较短,让线程完成一个忙循环(自旋),不过问题在于若是线程长时间不释放锁,一直自旋不只浪费处理器资源还对完成任务没有任何帮助。自旋次数的默认值是10次,参数-XX:PreBlockSpin来更,在JDK 1.6中引入了自适应的自旋锁,根据前一次自旋获取锁的成功率来决定自旋时间,好比上次经过自选获取到了锁,那么此次也大概率会得到,因此自旋时间可能会比较长,相反会比较短。
2,锁消除
锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,可是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要断定依据来源于逃逸分析的数据支持,若是判断在一段代码中,堆上的全部数据都不会逃逸出去被其余线程访问到,那就能够把它们当作栈上数据对待,认为它们是线程私有的,同步加锁天然就无须进行。
3,锁粗化
原则上,同步快的左右范围要尽可能小,可是若是一系列联系操做,都对同一对象反复加锁和解锁,甚至加锁操做在循环体内,频繁的互斥同步也会致使没必要要的性能损耗,虚拟机检测到后对加锁同步范围进行扩充,达到只加一次锁的目的。
4,轻量级锁
是在没有多线程竞争的前提下,减小传统的重量级锁使用操做系统互斥量产生的性能消耗。若是没有竞争,轻量级锁使用CAS操做避免了使用互斥 量的开销,但若是存在锁竞争,除了互斥量的开销外,还额外发生了CAS操做,所以在有竞争的状况下,轻量级锁会比传统的重量级锁更慢。
5,偏向锁
若是说轻量级锁是在无竞争的状况下使用CAS操做去消除同步使用的互斥量,那偏向锁就是在无竞争的状况下把整个同步都消除掉,连CAS操做都 不作了,偏向锁会偏向于第一个得到它的线程,若是在接下来的执行过程当中,该锁没有被其余的线程获取,则持有偏向锁的线程将永远不须要再进行同步。
这次记录也是本身学习记录的过程,笔者能力有限若是您发现有不足或者错误之处,敬请雅正,不舍赐教。
若是你也在学习或者复习,能够关注个人公众号【Java成长录】,有系统的学习规划路线,每次学习记录文章。
Java 虚拟机面试题全面解析 - 做业部落 Cmd Markdown 编辑阅读器
周志明. 深刻理解 Java 虚拟机 [M]. 机械工业出版社, 2011.