Java内存模型与线程

一:什么是Java内存模型

Java内存模型:是为了消除或者屏蔽各类硬件和内存访问差异,从而实现java程序在各种平台下能达到一致的内存访问效果。

主要目标:定义程序中实例字段,静态字段,以及构成数组对象的元素,不包括局部变量和方法参数。因为后者是线程私有的,不会被共享,自然不存在资源竞争问题。

主要的一些性质:

1 Java 内存模型规定所有的共享变量都存储在主内存Main Memory中,每一个线程还有自己的工作内存Working Memory。

2 线程的工作内存这里面主要存储的是被该线程使用到的变量的主内存副本。什么意思呢?如果使用到一个变量,先在工作内存找有没有,如果没有,就从主内存copy一份放在工作内存。

3 线程对变量的读写操作都只能在工作内存中,而不能直接在主内存中操作这些变量

4 不同的线程也无法访问其他线程的变量,他们之间是隔离开的


线程 主内存 工作内存三者之间的关系

 

二:内存之间是如何操作的

关于一个共享变量从主内存拷贝到工作内存,然后工作内存同步回主内存之间的实现细节。Java内存模型定义了8种操作来完成,虚拟机在实现时,必须保证下面提及到的每一种操作都是原子的,不可再分的。

作用于主内存变量:

2.1 lock:锁定,作用于记为一条线程独占的状态。比如要从工作内存同步这个变量值的时候,可能其他工作线程需要从这里拷贝值,为了保证数据的安全性,需要先把变量锁定。

2.2 unlock:解锁 从线程锁定状态释放,释放完了之后,才哭被其他线程锁定。

2.3 read:读取 把变量的值从主内存传输到线程的工作内存,便于随后load动作使用。

2.4 write:写入 把工作内存store的变量的值同步到主内存中

作用于工作内存的变量:

2.5 load 载入:把read操作得到的变量值放入工作内存的变量副本中

2.6 use 使用:把工作内存的副本变量的值传递给执行引擎,每当虚拟机遇到一个需要使用的变量的值的字节码指令时将会执行这个操作

2.7 assign 赋值:把执行引擎得到的值赋给工作内存的变量,每当虚拟机遇到一个赋值的字节码指令执行此操作

2.8 store 存储:把工作内存的一个变量的值传送到主内存,一便于随后的write使用

三:Java内存模型执行上述8种类型的操作需要遵循一定的规则

3.1 不允许read 和 load,store 和 write单独操作,即不允许主内存读取了某一个变量,但是工作线程没有load,或者工作线程存储了某一个变量,但是没有进行回写到主内存,或者主内存不接受。

3.2 不允许工作线程改变(assign)一个变量之后,把这个操作给丢弃了,不同步回主内存。意思就是工作线程某个变量赋值以后,必须进行store和 write操作,同步到主内存。

3.3 不允许一个工作线程的任何变量,没有发生任何改变,就同步到主内存,即没有操作,就不允许执行store 和 write操作

3.4 工作线程不允许直接使用一个未被初始化的变量,这个新的变量必须在主内存诞生

3.5 一个变量某一个时刻,只能由某一个工作者线程进行lock操作,然后进行unlock其余线程才能使用,如果统一线程进行多次lock,那么也必须进行对应的unlock操作;相反如果没有被lock,那么就不能进行unlock操作

3.6 如果一个变量执行了,lock操作,那么工作内存的该变量值会被清空,如果要使用还得重新load或者assign

3.7 在执行unlock操作之前,必须先把变量同步到主内存,只有同步完了,才能unlock

 

四:volatile型变量

Volatile 是java虚拟机提供的最轻量级的同步机制。这个东西我们很多时候不会用,一碰到多线程竞争数据就是用锁机制。

它具备两种特性:

4.1 保证此变量对所有的线程的可见性:即一个工作线程某一个变量被修改了,其余线程马上就知道了(前提需要刷新),不像普通变量,还需要从主内存读取之后,才知道这个值已经改变了。

刷新什么意思呢?比如2个在工作线程 volatile a; 线程1 a=1

线程2a=1,但是现在线程1这个值变了,a=2,这个时候,如果线程2没有刷新,还是a=1,正是因为刷新了,a=2执行引擎看不到不一致的情况,才被认为不存在不一致的情况。

注意:如果这个变量要参与运算,并不能保证在并发下是线程安全的。

因为运算不是原子性,所谓的原子性

Code Example:

public class VolatileTest {

 

       public static volatile int race = 0;

 

       private static final int THREADS_COUNT = 20;

 

       public static void increase(){

              race++;

       }

 

       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 x = 0; x <10000; x++){

                                          increase();

                                   }

                            }

                     });

                     threads[i].start();

              }

              //等待所有累加线程都结束

              while (Thread.activeCount()> 1) {

                     Thread.yield();

              }

              System.out.println(race);

       }

}

这段代码运行结束,我们预期如果线程安全,值应该是20000,但是我们输出的值总是小于200000,原因就在于race++再参与运算,再取值的时候,还是正确的,但是但是在执行自增的时候,其他线程可能已经把这个值给改了,所以栈顶的race值就成了过期数据,所以最后同步回主内存的值是一个较小的race

所以在以下场景之外,我们仍然需要对volatile变量的你计算部分进行枷锁

4.1.1 运算结果并不依赖于当前变量值,或者能够确保只有单一的线程在修改它

4.1.2 如果涉及到状态的改变,可以不用枷锁,比如几个线程共享变量a, 如果a的状态发生改变,然后做其他事情,这时候不需要枷锁

 

4.2 此变量禁止指令重排序优化,普通变量仅能保证该方法的执行过程中所有依赖赋值的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序和程序代码中的执行顺序一致。

首先,我们需要知道一件事情:CPU允许将多条不同的指令不按照程序规定的顺序,分开发送给相对应的电路单元处理。并不是随意重排序指令,有依赖关系的指令还是按照先后顺序。

比如现在有以下几个指令:

指令1: 地址A中的值加10

指令2:地址A中的值*2

指令3:地址B中的值-3

指令1和指令2有依赖,所以指令1 和 2 是顺序执行的。

但是指令3可能会被重新排序到1,2之间或之前。

public class VolatileForbbidResort {

 

       private volatile boolean initiallized = false;

 

       public void test(){

              List<String> results = new ArrayList<String>();

              results.add("test1");

              results.add("test2");

              initiallized =true;

 

              while(!initiallized){

                     //do something

              }

       }

}

这段代码,如果在不同线程中,如果不加voaltile就有可能 initialized

被提前执行。

他的执行规则:

如果要使用volatile变量,必须先readload才能use,也就是我们所谓先从主内存刷新最新的值,用于保证其他线程对于该变量的可见。

如果要对变量赋值,必须进行store和write,让这个值马上同步到主内存。

五:原子性 可见性 有序性

Java内存模型就是围绕原子性 可见性 有序性三个特征来建立的。

5.1 原子性

JMM保证原子性的变量操作有哪些?

READ,LOAD,USE,STORE,WRITE

我们可以基本认为基本数据类型(long,double除外)都是具备原子性的。如果应用场景需要一个更大范围的原子性保证,JMM还提供了lock和unlock给用户操作,虽然提供了这两个操作给用户使用,但是JVM还是提供了跟高层次的字节码指令:monitorenter 和 monitorexit,它会反映到java代码的同步块:synchronized,所以synchronized块之间的操作也具备原子性。

5.2 可见性

当一个变量修改了一个共享变量的值,其他线程能够立刻感知这个修改。我们经常遇到的就是:

volatile:修改完毕能够立即同步到主内存。或者使用该变量都会从主内存刷新,拿到最新的值。

synchronized: 一个变量执行unlock操作之前,必须先把此变量同步回主内存,即store,write这条规则获得的。

Final: 被final修饰的字段在构造器一旦初始化完成,并且构造器没有把this给传递出去,那其他线程也能看到这个字段值。

5.3 有序性

如果在本线程观察,所有操作都有序;如果一个线程观察另外一个线程,所有操作都是无序的。前半句是指线程内表现为串行语义,后半句指的是指令重排序。

Java提供了2个关键字:

Volatile本身就包含禁止指令重排序

Synchronized: 规定一个变量在同一时刻只允许一个线程对其进行lock操作

 

六:先行发生原则

Happens-before :先行发生指的是JMM定义的两项操作之间的偏序关系。如果说操作A先行发生于操作B,操作A产生的影响能够被操作B观察到。该影响包括修改共享变量的值,发送了消息,调用了方法等。注意是操作,不是时间,时间先行发生不代表操作先行发生。比如指令重排序就否定了这一点。

int i = 0; -- > 线程A

int j = i; -- > 线程B

int i= 2; -- > 线程C

j =?

很明显j=2,因为线程B 在A改变值之后观察到了。如果这样的话:

int i = 0; -- > 线程A

int i= 2; -- > 线程C

int j = i; -- > 线程B

j =?

J这时候就不一定等于多少呢?因为线程C的值可能会被B检测到,也可能不会,因为我们不确定哪一个线程先执行。所以j可能等于2,也可能等于1.

有哪些规则呢?

6.1 程序顺序规则:在同一个线程,会按照程序的代码顺序执行。

6.2 监视锁规则: 如果多个线程竞争同一个锁,那么unlock操作先于lock操作

6.3 volatile变量规则:volatile写操作先于读操作

6.4 线程启动规则:Thread对象的start方法先行发生于此线程每一个动作

6.5 线程终止规则,线程所有操作都先于Thread对象的终止检测行为。比如Thread.join(),Thread.siAlive();

6.6 线程中断规则:对线程interrupt的调用先于代码检测到中断事件的可能性

6.6 对象终结规则:一个对象初始化完成动作先行发生于他的finalize方法调用

6.7 传递性: 如果操作A先行发生于B,如果操作B先行发生于C,那么如果操作A也先行发生于C

Code Example:

public class HappensBefore {

       private int a = 10;

       public int getA() {

              return a;

       }

 

       public void setA(int a) {

              this.a =a;

       }

}

以上代码,如果在多线程环境下:如果线程setValue=12,线程BgetValue=?

我们分析:

首先,是多线程,排除程序顺序规则;

其次:没有同步块,也就五所谓lock和unlock,排除监控锁机制

再次:不是volatile,变量,排除volatile变量规则

然后:线程启动,中断,终止和对象终结也和这里没有关系,所以线程getValue=?不确定。因为线程不安全。

如何变得安全呢?

2种办法:

套用监控锁规则:/getter/setter方法都进行synchronized同步

声明为volatile变量。

 

七:线程

7.1 线程调度

线程调度:指的是系统为线程分配CPU执行权的过程。

主要调度的方式有两种:

7.1.1 协同式线程调度CooperativeThread-Scheduling

特点:

线程的执行时间由线程自己本身控制

线程完成自己的工作,然后竹筒通知系统,进行线程切换

7.1.2 抢占式线程调度PreemptiveThread-Scheduling

特点:

线程的执行时间由系统来分配控制

线程的切换也不由本身决定

不会因为一个线程而阻塞整个进程,java使用的就是这个

 

7.2 线程状态转化

我们知道线程有5种状态:

新建: 创建好尚未启动

可运行: 调用了start方法,可能正等待获取CPU使用权

运行状态:可运行状态的线程获取了CPU的使用权

无期限wating: CPU不会分配执行时间,要显示的被其他线程wakeup,才会进running状态

没有设置timeoutObject.wait()

没有设置timeoutThread.join()

Locckupport.park()

有期限waiting: CPU不会分配执行时间,但是无需被其他线程唤醒,只要时间一到,自动唤醒,进入running状态,比如

Thread.sleep()

设置了timeout参数的Object,.wait();

设置了timeout参数的Thread.join()

LockSupport.parkNanos();

LockSupport.parkUtil()

只要是等待状态,JVM都会把他们放入等待池中

Blocked:阻塞状态,一般是因为同步锁,该锁被其他线程使用,JVM会把这个线程放入锁池中,直到锁被释放,然后恢复。与waiting区别,waiting是一定时间之后,或者其他线程显示唤醒之后就恢复到可运行状态,而block是锁释放

结束:线程终止

 

线程状态转换关系图:


八:线程状态与锁优化

8.1 线程的分类

按照线程安全的强弱程度,我们可以将各种操作共享数据分为5大类:

不可变:

在Java中,Immutable的对象一定是线程安全的,比如String.无论是对象的方法还是方法的调用者,都是线程安全的。只要一个不可变的对象被正确构建,没有this引用逃逸的情况发生,其可见状态就永远不会变。

对于基本数据类型,只要加上final,就是不可变的。如果是一个对象,就需要保证对象的行为不会对其状态产生任何影响。

保证的途径很多,最简单的就是把带有状态的变量都声明为final.这样构造函数结束之后,都是不可变的。

绝对线程安全:

相对线程安全:他需要保证这个对象单独的操作是线程安全的。一般情况下,我们不需要再调用端做额外的同步手段;但是不排除个别情况,我们也需要做一些同步的手段。

线程兼容:对象本身不是线程安全的,需要调用端使用同步手段来确保线程安全。

线程对立:尽量避免,有可能一个线程试图中断线程,一个试图恢复,容易死锁。

 

8.2 线程安全的实现方法

8.2.1 互斥同步(阻塞同步)

最基本的互斥同步方法就是synchronized关键字。经过编译之后,会在同步块前后分别形成monitorenter和monitorexit这两个字节码指令。Monitorenter获取对象锁,把锁得计数器加1,monitorexit锁的计数器减1,当计数器为0,锁就被释放。

Concurrent 包下的ReentranLock来实现同步。与synchronized相比较而言,有哪些特性:

等待可中断:当持有锁的线程长期不释放锁,正在等待的线程可以选择放弃等待,然后去干别的事情。

公平锁:多个线程在等待同一个锁的时候,必选按照申请锁的时间顺序来依次获得锁。而非公平则不是这样,任何一个被等待的线程都有机会获得锁。Synchronized非公平的,ReentranLock默认也非公平,可以在构造的时候加一个布尔值来要求使用公平锁。

索绑定多个条件指的是ReentranLock对象,可以同时绑定多个Condition对象,但在Synchronized,锁对象的wait和notify 或者 notifyAll。

 

8.2.2 非阻塞同步(了解)

互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也称之为阻塞同步,是一种比较悲观的并发策略。

总是认为,我不这样做【加锁】就会出现问题,哪怕共享数据是否真的存在竞争,都要加锁,维护锁计数器,是否有被阻塞的线程需要唤醒。随着硬件指令的发展,我们有了另外的选择余地:基于冲突检测的乐观并发策略。

CAS: Comparep-and-Swap:Actomic一些原子类就采用这种方式,但是不能覆盖所有场景,主要适用一些计数器。

8.2.3无同步方案

要保证线程的安全,并不一定完全就要同步,两者没有因果关系,同步只是保证数据竞争的时候的手段。比如线程本地存储:Thread Local Storage,如果一段代码所需要的数据必须与其他代码共享,那就看这些共享数据的代码是否能保证在一个线程中执行,如果可以,我们就可以把共享数据的可见范围限制在同一个线程内,这样无需同步也能保证线程安全。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

莫言静好、

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值