多线程相关之线程可见

多线程相关复习

开胃菜

public class Test1 {

   private int i = 0;

   public void go(){
       new Thread(new Runnable() {
           @Override
           public void run() {

               while(true){
                   
                   if(i != 0){
                       break;
                  }
              }
               System.out.println("线程执行结束");
          }
      }).start();
  }

   public static void main(String[] args) {
       Test1 t = new Test1();
       t.go();
       try {
           Thread.sleep(1000);
      } catch (InterruptedException e) {
           e.printStackTrace();
      }
       t.i = 1;
  }
}

以上代码在运行过程中发现他会一直卡住,不会输出线程执行结束。

将代码改造一下

public class Test1 {

   private int i = 0;

   public void go(){
       new Thread(new Runnable() {
           @Override
           public void run() {

               while(true){
                   System.out.println();
                   if(i != 0){
                       break;
                  }
              }
               System.out.println("线程执行结束");
          }
      }).start();
  }

   public static void main(String[] args) {
       Test1 t = new Test1();
       t.go();
       try {
           Thread.sleep(1000);
      } catch (InterruptedException e) {
           e.printStackTrace();
      }
       t.i = 1;
  }
}

我们在while(true)这一行下面加一个空的输出语句,就可以打印出来了?为什么呢?

我们点开System.out.println()的源码,可以发现

 

在这里面加了synchorized关键字,就相当于是加了lock,对于底层指令而言,加入lock就相当于将工作内存中的值同步到住内存中

如果想要了解这个问题,首先我们要搞清楚java内存模型。

Java内存模型

 

主存与工作内存

java内存模型的主要目标是定义程序中各个变量的访问规则,也就是在jvm中将变量存储到内存和从内存中取出变量这样的底层细节。

此处的变量与java编程所说的变量略有区别,主要是不包括局部变量和方法参数。因为这两个是线程私有的,不会被共享,自然就不存在竞争。

jmm规定了所有的变量都存储在主存中。每条线程还有自己的工作内存,线程的工作内存中保存了被线程使用到的变量和主存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主存中的变量。不同线程之间也无法直接方位对方工作内存中的变量,线程变量值的传递需要通过主内存来完成。

内存交互

主存和工作内存之间的交互协议

JAVA内存存储模型是通过动作(actions)的形式描述。这些动作也就是变量如何在主内存进入到工作内存、工作内存中的数据如何进入主内存。具体包括了:

lock unlock read load use assign store write。

lock 作用于主存变量,把一个变量标识成为线程独占状态

unlock 作用于主存变量,把一个锁定的变量释放出来,释放后的变量才能被其他线程锁定。

read 作用于主内存变量 ,把一个变量的值从主内存传输到线程的工作内存

load 作用于工作内存变量,把read操作从主内存中得到的变量值放入到工作内存变量副本

use 作用于工作内存变量,把工作内存中的一个变量的值传递给执行引擎,每当jvm遇到一个需要使用到变量的值的字节码指令时会执行这个操作

assign 作用于工作内存变量,把一个从执行引擎收到的值付给工作内存变量,每当jvm遇到一个给变量赋值的指字节码指令时执行这个操作

store作用于工作内存变量,把工作内存中一个变量的值传给主存

write作用于主存变量,把store操作从工作内存中得到的变量放入到主存变量中

JMM还定义了执行上述八种操作必须满足的规则
  1. 不允许read和load 、store和write操作之一单独出现,也就是不允许一个变量从主存读取了但工作内存不接收,或者从工作内存发起了回写主存但主存不接收的情况出现

  2. 不允许一个线程丢弃他的最近的assign操作,也就是在工作内存中改变了之后必须把该变化同步回主内存

  3. 不允许一个线程没有发生过任何assign操作,就把数据从线程的工作内存同步回主存

  4. 一个新的变量只能在主存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。也就是对一个变量实施的use和store操作之前,必须先执行过assign和load操作

  5. 一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被一条线程重复执行多次,多次lock后,只有执行相同次数的unlock操作变量才会被解锁

  6. 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值

  7. 如果一个变量实现没有被lock操作锁定,则不允许对他进行unlock操作;也不允许unlock一个被其他线程锁定主的变量

  8. 对一个变量执行unlock操作之前,必须先把此变量同步回主存,也就是执行store和write

先行发生原则 happens-before

Java语言中的先行发生原则在我们平时编码中平时没有怎么注意到,但这个原则非常重要,是判断线程是否安全的一个主要依据。

先行发生原则就是说:动作内部的偏序关系。线程A和线程B,如果A先行发生于B,那么A所带来的影响能够同步到B,也就是说能被B发觉。这里所谓的影响主要是指:共享变量的值。

这里的先行发生原则主要有下面几条:

程序次序法则:在同一个线程中,程序按照代码的书写顺序执行。写在前面的先执行,写在后面的后执行(这里要考虑流程控制语句)。

监视器锁法则:一个unlock操作先行发生于后面对同一个锁的lock操作。同一个锁,后面指的是时间上先后顺序

volatile变量法则:对被volatile修饰的变量,写操作先行发生与后续对同一个变量的读操作。

线程启动法则:线程对象的启动方法先行发生于此线程内部的每一个操作。

线程终止法则:线程对象中的所有操作都先行发生与线程的终止。

线程中断法则:对线程interrupt方法的调用先行发生于被中断线程的代码检测到中断事件的发生。

对象终结法则:一个对象的构造方法结束先行于它的finalize方法的开始。

传递性:如果A先行发生与B,B先行发生于C,那么A先行发生于C。

java无需任何同步手段保障就可以成立的规则。

回到之前的问题,我们再将代码更改一下。

public class Test1 {

   private volatile int i = 0;

   public void go(){
       new Thread(new Runnable() {
           @Override
           public void run() {

               while(true){
//                   System.out.println();
                   if(i != 0){
                       break;
                  }
              }
               System.out.println("线程执行结束");
          }
      }).start();
  }

   public static void main(String[] args) {
       Test1 t = new Test1();
       t.go();
       try {
           Thread.sleep(1000);
      } catch (InterruptedException e) {
           e.printStackTrace();
      }
       t.i = 1;
  }
}

我们可以推断出,是将主线程中的变量更改对另外一个线程可见,另外一个线程感知到了变量的更改,从而更改了数据,跳出了while循环。

volatile关键字?

1.保证一个线程对一个变量的修改,对另外的线程是可见的。前提是:对变量的修改不依赖变量原本得值。

2.保证不会发生指令重排序。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章