Java基础篇之Java虚拟机(二)----Java内存模型与线程; 线程安全与锁优化; - Go语言中文社区

Java基础篇之Java虚拟机(二)----Java内存模型与线程; 线程安全与锁优化;


点个关注,一起进步!

内容要点:

Java内存模型与线程;

线程安全与锁优化;


 

​Java内存模型与线程

Java内存模型

 Java内存模型与JVM内存结构迷惑的的可以看下这个:

 Java基础篇之Java虚拟机(一)

主内存与工作内存

 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 VolatileTest{     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操作都 不做了,偏向锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。

长按关注,一起进步!

 

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/qq_36776216/article/details/103145361
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-03-07 10:57:38
  • 阅读 ( 986 )
  • 分类:Linux

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢