多线程相关复习
开胃菜
public class Test1 {
private int i = 0;
public void go(){
new Thread(new Runnable() {
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() {
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还定义了执行上述八种操作必须满足的规则
-
不允许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
先行发生原则 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() {
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.保证不会发生指令重排序。