定义:当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的.
无状态对象一定是线程安全的
竞态条件
当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区
- eg.1 一个计数器类Counter 来源
public class Counter {
protected long count = 0;
public void add(long value){
this.count = this.count + value;
}
}
this.count = this.count+value;
包含了3个独立的动作:读取-修改-写入
JVM执行顺序
从内存获取 this.count 的值放到寄存器
将寄存器中的值增加value
将寄存器中的值写回内存
线程A和B交错执行
this.count = 0;
A: 读取 this.count 到一个寄存器 (0)
B: 读取 this.count 到一个寄存器 (0)
B: 将寄存器的值加2
B: 回写寄存器值(2)到内存. this.count 现在等于 2
A: 将寄存器的值加3
A: 回写寄存器值(3)到内存. this.count 现在等于 3
两个线程分别加了2和3到count变量上,两个线程执行结束后count变量的值应该等于5。然而由于两个线程是交叉执行的,两个线程从内存中读出的初始值都是0。然后各自加了2和3,并分别写回内存。最终的值并不是期望的5,而是最后写回内存的那个线程的值,上面例子中最后写回内存的是线程A,但实际中也可能是线程B。如果没有采用合适的同步机制,线程间的交叉执行情况就无法预料
- eg.2 错误的单例模式代码(延迟初始化的竞态条件)
public class LazyInitRace {
private ExpensiveObject instance = null;
public ExpensiveObject getInstance() {
if (instance == null)
instance = new ExpensiveObject();
return instance;
}
}
class ExpensiveObject { }
LazyInitRace包含了竞态条件,可能会破坏类的正确性.
A、B线程同时执行getInstance,A判空,创建新实例;同样,B判空,创建新实例;那么两次调用就会得到不同的结果
做到线程安全的几种策略
确保原子操作,使用线程安全的类
上面的例子都包含一组需要以原子方式执行的操作,简而言之就是"先检查后执行" <=> 读取-修改-写入 这一系列统筹为复合操作;必须以原子方式执行的操作才能确保其安全性.
- eg.1改造 使用AtomicLong原子类
public class Counter {
private final AtomicLong count = new AtomicLong(0);
public void add(long value) {
this.count.set(this.count.get() + value);
}
}
使用AtomicLong能够确保所有对count变量的访问操作都是原子的
加锁机制
eg.3 按照上面的建议使用线程安全的类
public class UnsafeCachingFactorizer extends GenericServlet implements Servlet {
private final AtomicReference<BigInteger> lastNumber
= new AtomicReference<BigInteger>();
private final AtomicReference<BigInteger[]> lastFactors
= new AtomicReference<BigInteger[]>();
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
if (i.equals(lastNumber.get()))
encodeIntoResponse(resp, lastFactors.get());
else {
BigInteger[] factors = factor(i);
lastNumber.set(i);
lastFactors.set(factors);
encodeIntoResponse(resp, factors);
}
}
void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) {
}
BigInteger extractFromRequest(ServletRequest req) {
return new BigInteger("7");
}
BigInteger[] factor(BigInteger i) {
// Doesn't really factor
return new BigInteger[]{i};
}
}
使用原子引用的情况下,lastNumber 和 lastFactors的set操作是原子的,没有问题;A线程读取的过程中,B线程可能修改了他们,这样A线程不变性条件被破坏了.
所以: 要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量,这样就引入了加锁机制.
内置锁
每个java对象都可以用作一个实现同步的锁,这些锁称之为内置锁或监视器锁。线程在进入同步代码块之前自动获取锁,退出时自动释放锁。获取内置锁唯一途径:进入由这个锁保护的同步代码块或方法
synchronized (lock){
// TODO
}
- synchronized修饰方法表示:在同一时刻,只有一个线程可以执行该方法
- A、B两个线程,A获得锁的同时,B只能等、
重入
“重入”意味着获取锁的操作的粒度是”线程”,而不是调用。重入的一种实现方法是,为每一个锁关联一个获取计数值和一个所有者线程。当计数值为0时,这个锁就被认为是没有被任何线程所持有,当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取计数值置为1,如果同一个线程再次获取这个锁,计数值将递增,而当线程退出同步代码块时,计数器会相应地递减。当计数值为0时,这个锁将被释放。
eg.4 子类继承重写父类的doSth方法
public class Father {
public synchronized void doSth() {
System.out.println("father.doSth()");
}
public static class Son extends Father {
@Override
public synchronized void doSth() {
System.out.println("son.doSth()");
doAnotherThing();
}
private synchronized void doAnotherThing() {
super.doSth();
System.out.println("son doAnotherThing()");
}
}
public static void main(String[] args) {
(new Son()).doSth();
}
}
// 输出
// son.doSth()
// father.doSth()
// son doAnotherThing()
synchronized是可重入的,不然上面的代码没法执行;
这里的对象锁只有一个,就是Son的对象锁;
- 1.son.doSth 获取son对象锁
- 2.doAnotherThing再次请求son对象锁
- 3.执行super.doSth 获取son对象锁
- 4.如果不是重入锁,后面两个操作会死锁执行失败
ps:由于super只是一个标记符,告诉jvm调用invoke父类的doSth方法(父类该方法将被继承到子类对象的方法表中),此时并未创建父类对象,所以这个父类方法的锁应该还是son对象的内部锁(唯一),由于可重入,所以持有内部锁后还可以再持有一次而不会陷入死锁
用锁来保护状态
对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护
- 对于每个包含多个变量的不变性条件,其中涉及的所有变量都需要由同一个锁来保护
- synchronized能保证单个操作的原子性,对于多个操作合并为一个复合操作的情况,需要额外的加锁机制