天天看点

《深入理解java虚拟机》笔记:java内存模型JMM,volatile,java与线程

半个读书笔记,没什么含量。

JMM:

JMM围绕在并发过程中如何处理原子性,可见性和有序性三个特征来建立。

java内存模型JMM与实际硬件模型有比较类似的地方:

内存 => 高速缓存 => cpu

主内存 => 工作内存 => 线程

由于I/O速度与cpu速度差距过大,所以在二者之间加入一层与cpu速度相对接近的高速缓存。cpu将数据从内存读入高速缓存中,计算过后将数据再写入缓存,最后再重新写进内存。由此解决了速度上的矛盾,也带来了缓存一致性的问题。

JMM的模型与之类似,每个java线程都有自己的工作内存,线程私有,其他线程是访问不到的。java线程将变量在主内存中的副本拷贝到工作内存中,线程对变量的操作都在工作内存中进行。

在JMM模型中,主内存与工作内存之间有8种原子性交互操作:

lock(锁定):一个线程独占主内存的一个变量

unlock(解锁):将线程锁定的主内存变量释放出来

read(读取):将变量从主内存 -> 到工作内存中

load(加载):将变量值从工作内存 -> 放入工作副本

use(使用):工作内存变量 -> 放入执行引擎中

assign(赋值):从之型引擎接收到新值 -> 放入工作内存变量副本

store(存储):变量 工作内存 -> 放回主内存中

write(写入):将新值放回主内存变量中

JMM规定这8种操作需要满足一定的规则,简而言之为“先行发生原则”,如果不满足这个规则,即为线程不安全的。

需要注意“先行发生”并不代表“时间上的先发生”,因为有指令重排序等原因的存在

VOLATILE:

volatile变量可以保证可见性:

JMM规定,store和write要同时出现,不能单独出现其中一种操作。而volatile关键字则规定assign和store操作必须要连起来出现。所以意味着一旦变量值被修改,则会被强制写回到主内存当中。

JMM还规定,read和load操作要成对出现,而volatile关键字规定load与use操作必须要连起来出现,这样就保证了线程每次都要从主内存中读取最新的变量值进来。

通过以上的一些规定,volatile保证了可见性,即变量被某线程修改后,其他线程就会看见。

volatile不保证原子性,所以依然会有线程安全问题

典型例子:count++问题

public class FakeVolatile implements Runnable {
	
	static FakeVolatile fakeVolatile = new FakeVolatile();
	
	public static volatile int i = 0;

	public static void main(String[] args) throws InterruptedException {
		for(int j = 0 ; j < 20 ; j++) {
			new Thread(fakeVolatile).start();
		}
		while(Thread.activeCount() > 1) {
			
		}
//		Thread t1 = new Thread(fakeVolatile);
//		Thread t2 = new Thread(fakeVolatile);
//		Thread t3 = new Thread(fakeVolatile);
//		Thread t4 = new Thread(fakeVolatile);
//		Thread t5 = new Thread(fakeVolatile);
//		t1.start();
//		t2.start();
//		t3.start();
//		t4.start();
//		t5.start();
//		t1.join();
//		t2.join();
//		t3.join();
//		t4.join();
//		t5.join();
		System.out.println("i=" + i);
	}

	@Override
	public void run() {
		for(int x = 0 ; x < 10000 ; x++)
			i++;
	}

}
           

理论上的结果为200000,然而实际上什么结果都有,反正都比20万小。原因就是i++不是原子类操作,所以虽然可以保证volatile读到的变量是主内存最新的,但是在i++的三步操作中,可能就有别的线程已经更新值了,这样就出现了线程不安全的问题。

volatile还能禁止指令的重排序优化

作用:双重校验的单例模式,https://blog.csdn.net/qq_34785454/article/details/83049079

这篇文章里有提及,通过禁止指令重排序保证单例模式。

volatile编译后通过设置内存屏障来禁止重排序,后面的指令不能重排序到屏障之前的位置。

除了volatile,synchronized和final也能保证可见性。synchronized关键字规定,unlock解锁变量之前,必须先执行store和write操作;而lock上锁之前,则必须把线程工作内存中的副本清空。通过这样强制修改变量写入主内存,读取时也从主内存读取最新值。

final关键字变量在初始化完成后即可保证可见性。

线程的实现:

内核线程:直接由操作系统内核支持,程序中使用轻量级进程,与内核线程1对1关联。需要在用户态和内核态之间切换,消耗资源大,系统能支持的轻量级进程也有限,因为系统资源有限。
用户线程:建立在用户空间的线程库上,系统内核无法感知。基本不需要切换到内核态,速度快消耗低。劣势:没有内核支持,所有线程操作都要自己处理,现在基本不用了。
用户线程与轻量级进程混合实现:二者比例不固定,多对多。用户线程能处理的操作还在用户态,需要切换到内核态则使用轻量级进程。
java线程调度方式:1、协同式线程调度,线程通知系统进行切换,不怎么用了。2、抢占式线程调度,java使用的调度方式,系统自己决定执行的线程,可能参考线程优先级,优先级越高,越容易被系统选择执行。
java线程状态:
1、new
2、runnable:包括ready和running
3、waiting:等待被其他线程显式唤醒。wait()方法及join()方法等会进入这个状态。
4、Timed Waiting:一定时间后由系统自动唤醒。sleep()方法,带时间参数的wait,join方法等会进入这个状态。
5、blocked:线程等待锁时进入的状态
6、terminated:线程运行结束