本文共 4721 字,大约阅读时间需要 15 分钟。
例子如下:
public class Visibility1 { public static boolean ready; public static int number;}
public class ReaderThread extends Thread { @Override public void run() { while (!Visibility1.ready){ Thread.yield(); System.out.println(Visibility1.number); } }}
public class Test1 { public static void main(String[] args) { new ReaderThread().start(); Visibility1.number = 42; Visibility1.ready = true; }}
多次运行结果分别如下:
可以看到多次运行所得到三种结果,分别为0,42,没有输出结果。
程序一开始执行,默认将ready赋值为false,ready默认赋值为0,一开始执行时,在ReaderThread中符合循环条件,进入循环,遇到
Thread.yield(); 这语句是就是说当一个线程使用了这个方法之后,它就会把自己CPU执行的时间让掉,让自己或者其它的线程运行。如果自己先走出现结果为0的情况,因为,默认初始int的类型数据为0,如果让别的线程先走让这样就又回到了Main方法中的继续执行
Visibility1.number = 42; Visibility1.ready = true; 这两条语句,问题来了,这两条语句会按照程序的顺序执行吗? 答案是,并不一定的。这就涉及到指令重排序问题,也就是说
CPU一般采用流水线来执行指令。一个指令的执行被分成:取指、译码、访存、执行、写回、等若干个阶段。然后,多条指令可以同时存在于流水线中,同时被执行。
指令流水线并不是串行的,并不会因为一个耗时很长的指令在“执行”阶段呆很长时间,而导致后续的指令都卡在“执行”之前的阶段上。
重排序的目的是为了性能。
也就是说,有时候CPU认为
Visibility1.ready = true; 语句执行要快,所以让Visibility1.ready = true;先执行。这就导致了会出现了无输出结果。这就是涉及了指令重排序问题,虽然这里如果出现了指令重排序问题,
进入生产开发会造成严重的后果的。 但是要注意一点,如下:
可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果。另一个线程马上就能看到。比如:用volatile修饰的变量,就会具有可见性。volatile修饰的变量不允许线程内部缓存和重排序,即直接修改内存。
所以对其他线程是可见的。但是这里需要注意一个问题,volatile只能让被他修饰内容具有可见性,但不能保证它具有原子性。比如 volatile int a = 0;之后有一个操作 a++;这个变量a具有可见性,但是a++ 依然是一个非原子操作,也就这这个操作同样存在线程安全问题。
先看如下程序:
public class Visibility { private static boolean bChanged; public static void main(String[] args) throws InterruptedException { new Thread(){ @Override public void run() { for (; ; ){ if (bChanged == true){ System.out.println("!="); System.exit(0); } } } }.start(); Thread.sleep(10); new Thread(){ @Override public void run() { for (; ; ){ bChanged = true; } } }.start(); }}
运行结果为:
可以看到运行为死循环。按道理来说应该会输出!=结束的。但是为什么会出现死循环呢? 原因是因为程序中bChanged如果没有手动赋值前,默认是false的,所以一开始没输出,但是第二线程命名修改了bChanged的值了,为什么还不退出呢,因为线程是CPU启动的,而CPU一开始从主存中取数据并没有立即将数据送到CPU,而是先送到了缓存中, 而,另外一个线程修改bChanged的值,是就该主存中的值,而那个输出结果的线程并没有从主存中取bChanged的值,而是去缓存中取了bChanged的值,而缓存中bChanged的值是false,这就是为什么会死循环的原因。如下图:
解决办法: 1.在成员变量中加入关键字volatile,如下:
private volatile static boolean bChanged;
这个关键字告诉线程一定要走内存,不要再从缓存中获取数据了。只要已修改,其他线程立马知道值已经被修改了,因为它们都是从主存中取的数据。 修改后的结果如下图:
可以看到程序立马结束。并且有输出。因为这个关键字让这个成员变量都可见了。这就是线程的可见性。
其实volatile关键字还有一个功能就是:阻止指令排序。但是这只是相对的阻止。如下:
以前官方推荐是volatile,但是现在synchronized已经优化的很好了。不要刻意去用volatile。如果一个类加上volatile关键字,那么它的成员变量也会默认加上volatile关键字。
volatile只能解决可见性。一定程度上解决指令排序,但是是相对的。
但是volatile解决了可见性,就一定可以解决问题了吗?并不是的。例子如下:
如果你只希望单一变量可见性,volatile是可以的,但是如果关注的不是单一变量volatile就不管用了。
2.用synchronize加锁的办法。它能解决可见性,原子性。
实现好的并发是一件困难的事情,所以很多时候我们都想躲避并发。避免并发最简单的方法就是线程封闭。什么是线程封闭呢?
当访问共享变量时,往往需要加锁来保证数据同步。一种避免使用同步的方式就是不共享数据。如果仅在单线程中访问数据,就不需要同步了。这种技术称为线程封闭。在Java语言中,提供了一些类库和机制来维护线程的封闭性,例如局部变量和ThreadLocal类,
线程封闭其实就是不共享数据就行了。
线程封闭方法有:
1.不要共享变量,在变量中加final关键字
2.栈封闭:栈封闭是我们编程当中遇到的最多的线程封闭。什么是栈封闭呢?简单的说就是局部变量。多个线程访问一个方法,此方法中的
局部变量都会被拷贝一分儿到线程栈中。所以局部变量是不被多个线程所共享的,也就不会出现并发问题。所以能用局部变量就别用全局的变量,全局变量容易引起并发问题。如果了解JVM应该懂得。3.ThreadLocal线程绑定。ThreadLocal内部维护了一个Map,Map的key是每个线程的名称,而Map的值就是我们要封闭的对象。每个线程中的对象都对应着Map中一个值,也就是ThreadLocal利用Map实现了对象的线程封闭。
想要操作什么数据,可以先把数据放在ThreadLocal中绑定,用的时候才取出来。
例子如下:
public class LocalTest { private int num; public int getNum() { return num; } public void setNum(int num) { this.num = num; }}
public class ThreadLocalDemo { private static ThreadLocalthreadLocal = new ThreadLocal (); public static void main(String[] args) throws InterruptedException{ final LocalTest local = new LocalTest(); new Thread(){ @Override public void run() { for (; ; ){ threadLocal.set(local); LocalTest l = threadLocal.get(); l.setNum(20); System.out.println(Thread.currentThread().getName() + "---" +threadLocal.get().getNum()); Thread.yield(); } } }.start(); new Thread(){ @Override public void run() { for (; ; ){ threadLocal.set(local); LocalTest l = threadLocal.get(); l.setNum(30); System.out.println(Thread.currentThread().getName() + "---" +threadLocal.get().getNum()); Thread.yield(); } } }.start(); }}
结果如下:
可以看到各个线程对应的值并没有乱。
转载地址:http://mqnkl.baihongyu.com/