上一篇记录了多线程的简单使用,这里接着记录。
一、可见性
可见性:读操作和写操作在两个线程时,不能保证读操作可以实时的看到其他线程写入的值,如果能保证则说拥有可见性
保证可见性方法:
1,当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值(而不是高速缓存中)。
2,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。
对可见性,笔者测试代码如下:
package gcc.thread.test;
/**
* Created by gcc on 2018/3/29.
*/
public class NoVisibility {
private static boolean readFlag;//默认false
private static int count=0;
public static void main(String[] args) {
ReadThread read1 = new ReadThread();
Thread thread = new Thread(read1);
Thread thread2 = new Thread(read1);
Thread thread3 = new Thread(read1);
thread.start();
thread2.start();
thread3.start();
count=50;
readFlag=true;
System.out.println("main--count:"+count);
}
private static class ReadThread implements Runnable{
public void run(){
System.out.println("ReadThread1--count:"+Thread.currentThread().getName()+"--"+count);
while (!readFlag){
Thread.yield();//使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。此线程会把自己CPU执行的时间让掉,让自己或者其它的线程运行(重新竞争)
}
System.out.println("ReadThread2--count:"+Thread.currentThread().getName()+"--"+count);
}
}
}
结果如下:
ReadThread1--count:Thread-0--0
ReadThread1--count:Thread-2--50
ReadThread2--count:Thread-2--50
main--count:50
ReadThread1--count:Thread-1--0
ReadThread2--count:Thread-1--50
ReadThread2--count:Thread-0--50
这里有两类线程,主线程和读线程,主线程开启了三个读线程,读线程获取共享数据count,可以看到主线程应该先写入的count=50,有可能在读线程里没有被读到,结果就为0。这就引发了可见和不可见的问题。
二、有序性
有序性:即程序执行的顺序按照代码的先后顺序执行。举个简单的例子,看下面这段代码:
int i = 0;boolean flag = false;
i = 1; //语句1
flag = true; //语句2
从代码顺序上看,语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗?不一定,为什么呢?这里可能会发生指令重排序(Instruction Reorder)。
指令重排序:一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令2必须用到指令1的结果,那么处理器会保证指令1会在指令2之前执行。
多线程时重排序:
//线程1:context = loadContext(); //语句1
inited = true; //语句2
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此时线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被线程1初始化,就会导致程序出错。
注:要想并发程序正确地执行,必须保证原子性、可见性以及有序性。只要有一个没有被保证,就可能导致程序运行不正确。
三、线程封闭
这里记录下线程封闭的常用方法ThreadLocal。
线程封闭就是将共享的可变数据,封闭在一个线程内,即在单线程内访问数据,这样不用同步也是安全的。
ThreadLocal,线程本地变量,是一种特殊的线程绑定机制,将变量与线程绑定在一起,为每一个线程维护一个独立的变量副本,通过ThreadLocal可以将对象的可见范围限制在同一个线程内。
ThreadLocal可以理解为将对象的作用范围限制在一个线程上下文中,使得变量的作用域为“线程级”。
例如:
public final static ThreadLocal<String> RESOURCE = new ThreadLocal<String>();