思考:
线程不安全是因为有多个线程访问一块共享的可变的内存资源。那么单线程肯定线程安全,不使用共享的内存资源线程安全,不使用可变的内存资源线程安全,这就是我们解决此问题的核心思想。
单线程程序
这个角度看似有点废话而往往被忽略,但是有时候解决实际问题的时候使用往往有奇效,简单而直接。
不使用共享的内存资源线程安全
public int add(int a, int b) {
return a + b + 10;
}
可重入函数,就是不使用共享内存资源的例子。我们可以看下“可重入函数”的定义,可以被中断的函数。就是说,你可以在这个函数执行的任何时候中断他的运行,在任务调度下去执行另外一段代码而不会出现什么错误,这是因为他没有使用任何外部变量。所以函数式编程在线程安全上有先天的优势。
static class Thread2 extends Thread {
static final ThreadLocal<String> id = new ThreadLocal<>();
@Override
public void run() {
String i = UUID.randomUUID().toString();
id.set(i);
System.out.println(Thread.currentThread().getName() + "-" + id.get());
}
}
使用ThreadLocal存储变量,ThreadLocal内部有个ThreadLocalMap存储变量,他又是和Thread实例绑定在一起,这样就做到了线程间的变量隔离。打个不完美的比喻,这个一块不会被刷新到“主内存”的“临时内存”,线程独享。
不使用可变的内存资源线程安全
不使用可变内存?什么情况下能不使用可变内存呢?其实大部分场景并不是不使用可变内存,而是在使用时保证该内存不可变。这就需要做到以下三点:
- 内存可见性
- 操作原子性
- 指令重排序
内存可见性
从java内存模型中可知,线程操作都是临时工作内存,然后根据jvm工作机制在合适的时机将临时内存同步到主内存中,这中间存在时间差,如果做不到内存可见性,程序就错乱了。使用volatile,final,锁可以保证可见性。
class WorkThread1 extends Thread {
volatile boolean isStop = false;
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
if (isStop)
break;
System.out.println("i = " + i);
}
}
}
上面就是使用volatile保证了内存的可见性,保证在外部改变了isStop的值,线程内部能马上知晓。
public class Test {
int a;
final int b;
public Test(int a,int b){
this.a = a;
this.b = b;
}
}
当我调用了Test(4,5),我再输出打印a,b的值,b一定能打印出5,而a并不一定。这其中涉及到了指令的重排序。因为指令被重排序了,虽然Test实例已经生成,但是a并没有马上赋值,而当他被赋值后,外部并不马上可知。
使用锁保证可见性的原理,就是加锁的时候,别人访问不了,释放的时候,强制刷新主内存。
指令重排序
volatile,final可以保证指令重排序。
操作原子性
操作原子性。比如a++就不是原子操作,他的指令分三步,第一步创建临时变量,第二步临时变量加1,第三步将临时变量赋值给a。当有两个以上的线程操作同一个变量,肯定就错乱了。那么怎么办呢?
- 加锁
- 使用CAS
- 使用原子数值类型
- 使用原子属性更新器