关于volatile关键字可见性特性的学习,请查看上篇文章说明。
本篇主要对 System.out.println、Thread.sleep、Thread.yield 等代码调用对内存可见性的影响说明。
一、无可见性代码演示
现在有两个线程,分别是商品售卖线程和商品库存线程,售卖线程负责对商品进行出库销售,库存线程则进行监控商品的剩余数量,当数量为0时,进行告警提示“商品已无库存”。
@Test
public void saleProduct() {
ProductService productService = new ProductService(10);
Thread stockThread = new StockProductThread(productService);
stockThread.setName("库存线程");
stockThread.start();
Thread saleThread = new SaleProductThread(productService);
saleThread.setName("售卖线程");
saleThread.start();
try {
stockThread.join();
saleThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("[" + Thread.currentThread().getName() + "] 结束...");
}
/**
* 产品售卖线程
*/
class SaleProductThread extends Thread {
private ProductService productService;
public SaleProductThread(ProductService productService) {
this.productService = productService;
}
@Override
public void run() {
productService.sale();
}
}
/**
* 商品库存线程
*/
class StockProductThread extends Thread {
private ProductService productService;
public StockProductThread(ProductService productService) {
this.productService = productService;
}
@Override
public void run() {
productService.count();
}
}
/**
* 商品服务
*/
class ProductService {
/**
* 商品数量(剩余)
*/
private int count;
public ProductService(int count) {
this.count = count;
}
public void sale() {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count == 0) {
break;
}
count = count - 1;
System.out.println("[" + Thread.currentThread().getName() + "] 商品正在出库,剩余库存:[" + count + "]");
}
}
public void count() {
while (count != 0) {}
System.out.println("[" + Thread.currentThread().getName() + "] 商品已无库存");
}
}
程序执行结果:
通过图上的执行结果可以看出:当商品的库存为0时,库存线程并未进行提示 “商品已无库存”的消息,而是一直处于遍历循环监控商量数量的程序无法退出,这就存在内存可见性问题。当库存线程StockProductThread启动时,变量count=10存在于公共堆栈及库存线程的私有堆栈中。虽然售卖线程SaleProductThread一直对count数量进行减少操作,但是库存线程StockProductThread一直读取的值是线程私有堆栈中的count数据,所以库存线程一直存在于死循环中。
public void count() {
while (count != 0) {}
System.out.println("[" + Thread.currentThread().getName() + "] 商品已无库存");
}
通过前序文章(volatile 关键字理解一(保证可见性))的学习,我们知道这个问题其实就是私有堆栈中的值和公共堆栈中的值不同步造成的。解决这样的问题就是使用volatile关键字了,它主要的作用就是当库存线程访问count这个变量时,强制从公共堆栈中进行取值。
将 ProductService.java 代码修改如下:
class ProductService {
/**
* 商品数量(剩余),通过 volatile 关键字来修饰 count 属性
*/
volatile private int count;
public ProductService(int count) {
this.count = count;
}
public void sale() {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count == 0) {
break;
}
count = count - 1;
System.out.println("[" + Thread.currentThread().getName() + "] 商品正在出库,剩余库存:[" + count + "]");
}
}
public void count() {
while (count != 0) {}
System.out.println("[" + Thread.currentThread().getName() + "] 商品已无库存");
}
}
再看执行结果:
通过图上的执行结果可以看出:当随着售卖线程SaleProductThread的执行,商品数量为0时, 库存线程StockProductThread进行了“商品已无库存”的消息提示,并且主线程正常退出。
二、System.out.println 对内存可见性的影响
在学习volatile对可见性的影响的过程中,发现System.out.println对可见性也存在一定的影响,在此我们简单学习下。
修改 ProductService.java代码,去掉 volatile关键字修饰count属性,同时增加一行System.out.println,我们看下代码执行的效果。
class ProductService {
/**
* 商品数量(剩余)
*/
private int count;
public ProductService(int count) {
this.count = count;
}
public void sale() {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count == 0) {
break;
}
count = count - 1;
System.out.println("[" + Thread.currentThread().getName() + "] 商品正在出库,剩余库存:[" + count + "]");
}
}
public void count() {
while (count != 0) {
System.out.println("[" + Thread.currentThread().getName() + "]库存线程正在监控中");
}
System.out.println("[" + Thread.currentThread().getName() + "] 商品已无库存");
}
}
程序执行结果:
通过上图的执行结果可以看出:当随着售卖线程SaleProductThread的执行,商品数量为0时, 库存线程StockProductThread仍然进行了“商品已无库存”的消息提示,并且主线程正常退出。为什么呢?我们并没有通过volatile来修饰count属性,但是从效果上看仍然达到了内存可见性的效果。
我们来看一下 System.out.println 的源码:
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
我们发现代码中采用了synchronized进行了代码同步调用,使用了 synchronized 上锁会做以下操作:
(1)获得同步锁
(2)清空工作内存,即当前线程的私有堆栈
(3)从主内存拷贝对象副本到工作内存
(4)执行代码(计算或者输出等)
(5)刷新主内存数据
(6)释放同步锁
从2、3的说明我们知道,每次执行println,线程都会清空私有堆栈,然后从主内存拷贝对象副本到私有堆栈,即私有堆栈的数据始终都是保证为最近修改的数据,即达到了内存可见性的要求。
三、Thread.sleep / Thread.yield 对内存可见性的影响
修改 ProductService.java代码,去掉 volatile关键字修饰count属性,同时增加一行Thread.sleep(0) 或 Thread.yield(),我们看下代码执行的效果。
public void count() throws InterruptedException{
while (count != 0) {
Thread.sleep(0);
Thread.yield();
}
System.out.println("[" + Thread.currentThread().getName() + "] 商品已无库存");
}
程序执行结果:
通过上图的执行结果可以看出:当随着售卖线程SaleProductThread的执行,商品数量为0时, 库存线程StockProductThread仍然进行了“商品已无库存”的消息提示,并且主线程正常退出。为什么呢?我们并没有通过volatile来修饰count属性,但是从效果上看仍然达到了内存可见性的效果。
为了提升性能,线程里面有工作内存(线程的私有堆栈),这样访问数据不用去主内存读取,可以快一些。共享变量被线程修改后,该线程的工作内存中的值就会和其他线程不一致,也和主内存的值不一致,所以需要将工作内存的值刷入主存,但是这个刷入可能其他线程并没有看到。使用 volatile 后可以通过CPU指定屏障强制要求读操作发生在写操作之后,并且其他线程在读取该共享变量时,需要先清理自己的工作内存的该值,转而重新从主内存读取,volatile保证一定会刷新,但是不写也不一定其他线程看不见。像这种一直while循环,cpu一直占用,就没有机会刷新工作内存;如果加sleep或yield或其他让cpu得到切换就可以有机会刷新工作内存。所以,是否会刷新工作内存就成了不确定事件,而加了volatile后就可以保证一定会刷新工作内存。