前言
本来是想写两个线程,线程1输出1-98的奇数,线程2输出1-98的偶数,交替执行,在测试的时候发现线程安全问题,之后又引入到java内存模型,下面是几个demo。
1.版本1
//to print 1 ,3, 5...by thread1, print 2,4,6,8,10... by thread2 by turns public class Circle { public static boolean flag = true; public static void main(String[] args) { Thread t1 = new Thread(new Runnable() { int i = 1; @Override public void run() { while (i < 99) { if (flag == true) { System.out.println(Thread.currentThread().getName() + ": " + i); i = i + 2; flag = false; } } } }); Thread t2 = new Thread(new Runnable() { int i = 2; @Override public void run() { while (i < 99) { if (flag == false) { System.out.println(Thread.currentThread().getName() + ": " + i); i = i + 2; flag = true; } } } }); t1.start(); t2.start(); } }
版本1很多次结果输出正常,偶尔会出现线程停留在中间某步不继续执行。
2.版本2, 在版本1的基础上给其中一个线程加上sleep时间
//to print 1 ,3, 5...by thread1, print 2,4,6,8,10... by thread2 by turns public class Circle { public static boolean flag = true; public static void main(String[] args) { Thread t1 = new Thread(new Runnable() { int i = 1; @Override public void run() { while (i < 99) { try { Thread.sleep(3); } catch (Exception e) { e.printStackTrace(); } if (flag == true) { System.out.println(Thread.currentThread().getName() + ": " + i); i = i + 2; flag = false; } } } }); Thread t2 = new Thread(new Runnable() { int i = 2; @Override public void run() { while (i < 99) { if (flag == false) { System.out.println(Thread.currentThread().getName() + ": " + i); i = i + 2; flag = true; } } } }); t1.start(); t2.start(); } }
结果随机,可能停留在Thread-0: 1 ,也可能停留在中间某步。
3.版本3 给两个线程都sleep一段时间
//to print 1 ,3, 5...by thread1, print 2,4,6,8,10... by thread2 by turns public class Circle { public static boolean flag = true; public static void main(String[] args) { Thread t1 = new Thread(new Runnable() { int i = 1; @Override public void run() { while (i < 99) { try { Thread.sleep(3); } catch (Exception e) { e.printStackTrace(); } if (flag == true) { System.out.println(Thread.currentThread().getName() + ": " + i); i = i + 2; flag = false; } } } }); Thread t2 = new Thread(new Runnable() { int i = 2; @Override public void run() { while (i < 99) { try { Thread.sleep(3); } catch (Exception e) { e.printStackTrace(); } if (flag == false) { System.out.println(Thread.currentThread().getName() + ": " + i); i = i + 2; flag = true; } } } }); t1.start(); t2.start(); } }
结果正常(在我的机器上没演示出不正常的现象,理论上是会出现的)
4.再看版本4 在版本2的基础上加上volatile
//to print 1 ,3, 5...by thread1, print 2,4,6...by thread2 by turns public class Circle { public static volatile boolean flag = true; public static void main(String[] args) { Thread t1 = new Thread(new Runnable() { int i = 1; @Override public void run() { while (i < 99) { try { Thread.sleep(3); } catch (Exception e) { e.printStackTrace(); } if (flag == true) { System.out.println(Thread.currentThread().getName() + ": " + i); i = i + 2; flag = false; } } } }); Thread t2 = new Thread(new Runnable() { int i = 2; @Override public void run() { while (i < 99) { if (flag == false) { System.out.println(Thread.currentThread().getName() + ": " + i); i = i + 2; flag = true; } } } }); t1.start(); t2.start(); } }
结果执行正常,这是就涉及java volatile关键字了,volatile是保证线程之间的可见性,是保证全局变量flag对线程1和线程2的可见性。
分析;
1.背景知识
多线程三大特性:
原子性:保障线程安全问题
可见性:java内存模型 — 不可见
有序性: join方法 wait和notify
java内存模型:
主内存(存放共享的全局变量)
私有本地内存(本地线程私有变量 )
本地内存存放共享数据副本
2.本案例中,两个线程线程1和线程2共享一个全局变量flag,这两个线程的内存称为私有本地内存,主内存main方法称为主内存;
刚开始,主内存flag为true,主内存通知线程1和线程2的本地内存flag的值,两个线程获取到flag的值为true;
之后,两个线程进行判断,线程1满足条件,线程2不满足条件,线程1执行,执行完后设定flag=false;
再之后,线程1刷新flag的值为false到主内存;
之后,主内存通知线程2拿到flag的值,flag拿到为flase,开始执行,执行完毕设定flag为true;
在之后,线程2刷新flag的值为false到主内存;
再之后,主内存通知线程1拿到flag的值,依次执行.
重点是线程刷新flag最新的值到主内存的时间点,并不确定,借用别人一篇博客的摘录,这本书我没看过(打脸)
在多线程的环境下,如果某个线程首次读取共享变量,则首先到主内存中获取该变量,然后存入工作内存中,以后只需要在工作内存中读取该变量即可。同样如果对该变量执行了修改的操作,则先将新值写入工作内存中,然后再刷新至主内存中。但是什么时候最新的值会被刷新至主内存中是不太确定的,这也就解释了为什么VolatileFoo中的Reader线程始终无法获取到init_value最新的变化。
· 使用关键字volatile,当一个变量被volatile关键字修饰时,对于共享资源的读操作会直接在主内存中进行(当然也会缓存到工作内存中,当其他线程对该共享资源进行了修改,则会导致当前线程在工作内存中的共享资源失效,所以必须从主内存中再次获取),对于共享资源的写操作当然是先要修改工作内存,但是修改结束后会立刻将其刷新到主内存中。
· 通过synchronized关键字能够保证可见性,synchronized关键字能够保证同一时刻只有一个线程获得锁,然后执行同步方法,并且还会确保在锁释放之前,会将对变量的修改刷新到主内存当中。
· 通过JUC提供的显式锁Lock也能够保证可见性,Lock的lock方法能够保证在同一时 刻只有一个线程获得锁然后执行同步方法,并且会确保在锁释放(Lock的unlock方法)之前会将对变量的修改刷新到主内存当中。
摘自:《Java高并发编程详解:多线程与架构设计》 — 汪文君
所以说,使用volatile能够在修改结束后会立刻将其刷新到主内存中,所有的程序就是这样