多个线程共享进程的资源,比如内存地址。
同一进程中的线程访问相同的变量,并从同一个堆中分配对象,实现了良好的数据共享。但是如果没有明确的同步来管理共享数据,会导致线程安全问题。
无状态的对象肯定是线程安全的,那么对于有可变状态的对象,只要有多于一个线程访问给定的状态变量,而且其中一个线程会写入该变量,此时必须使用同步来协调线程对改变量的访问。可以用synchronized提供独占锁,也可以使用volatile,Lock或者原子变量。
举个栗子:
public class Test {
private int count;
public synchronized void addCount() {
System.out.println(Thread.currentThread().getName() + " " + ++this.count);
}
public synchronized void minusCount() {
System.out.println(Thread.currentThread().getName() + " " + --this.count);
}
public static void main(String[] args) {
Test test = new Test();
Thread t1 = new Thread(() -> {
while (true) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.addCount();
}
}, "ThreadAdd");
Thread t2 = new Thread(() -> {
while (true) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.minusCount();
}
}, "ThreadMinus");
t1.start();
t2.start();
}
}
没有synchronized的输出结果:
ThreadAdd 1
ThreadMinus -1
ThreadAdd 0
ThreadMinus -1
ThreadAdd 0
ThreadMinus -2
加了synchronized的输出结果:
ThreadAdd 1
ThreadMinus 0
ThreadMinus -1
ThreadAdd 0
ThreadAdd 1
ThreadMinus 0
在这个栗子里,两个线程对于count这个可变状态的修改,是属于竞态条件的。
最常见的竞态条件是:check-then-act(比如new Object())以及 read-modify-save(比如count++)这样的复合操作。为了保证线程安全性,这种复合操作必须是原子的。
那么总结一下如何做到线程安全呢?
- 无状态:没有任何成员变量的类。
-
让类不可变:让状态不可变
|-- 对于一个类,所有的成员变量应该都是私有的,而且所有的成员变量都应该加上final关键字。
|-- 根本就不提供可修改成员变量的地方,同时成员变量也不作为方法的返回值。 - 加锁:保证操作的原子性,加锁同样保证内存可见性。
- volatile和CAS:保证类的可见性,最适合一个线程写,多个线程读的情形,且只适合一个变量的原子操作。
- 栈封闭:定义方法的局部变量。
- 安全的发布:不能让别的线程有机会拿到并修改本线程内部数据的机会,要么get()返回一个线程安全的容器,要么返回对象的副本,深拷贝的副本。
- ThreadLocal:让共享状态变成线程私有。
网友评论