前言
在Java并发编程中的一个核心问题就是线程的安全性,当对共享的和可变的状态进行访问时,就会存在线程安全。例如
++i这一操作,i就是共享数据,i的状态是可变的,当多个线程访问i的时候,由于++i这一操作不是原子性操作,线程A访问i时可能i的值为1,在执行i = i + 1这一操作前,线程B也访问了i,得到的值同样是1,可是此时线程B得到的值就是一个失效值。
线程安全性的定义
正确的含义是,某个类的行为与其规范完全一致。
就刚才举的++i的例子,这个类的行为可能是将i变量自增之后将值输出在控制台,可是由于在多线程环境下,会将同一个i值输出到控制台,这个行为就与规范不一致。
所见即所知(we know it when we see it)
如果一个类能够做到在多线程访问该类时,始终都能够表现出给规范一致的行为,就说这个类时线程安全的。
@ThreadSafe
public class StatelessFactorizer extends GenericServlet implements Servlet {
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
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 };
}
}
对于上面的这个StatelessFactorizer类,它是无状态的。所谓无状态,就是这个类既不包括任何域,也不包括任何对其他类中域的引用。
无状态对象一定是线程安全的
在多线程环境下,StatelessFactorizer是没有共享数据的,每个线程访问该类不会影响另一个线程访问StatelessFactorizer类的计算结果。如果这个类中声明了一个成员变量,并且这个成员变量是可变的,那么这个类就不是线程安全的。
2.原子性
public class UnsafeCountingFactorizer extends GenericServlet implements Servlet {
private long count = 0;
public long getCount() {
return count;
}
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
++count;
encodeIntoResponse(resp, factors);
}
void encodeIntoResponse(ServletResponse res, BigInteger[] factors) {
}
BigInteger extractFromRequest(ServletRequest req) {
return new BigInteger("7");
}
BigInteger[] factor(BigInteger i) {
// Doesn't really factor
return new BigInteger[] { i };
}
}
++count是一个“读取 - 修改 - 写入”的操作序列,其结果状态依赖于之前的状态,这段代码在单线程环境下能够正确运行,而在多线程环境下就存在竞态条件
在并发编程中,这种不恰当的执行时序而出现不正确的结果是一种非常严重的情况,它有一个正式的名字:竞态条件
最常见的竞态条件类型就是先检查后执行,我举一个这个书上一个例子:
你和你的朋友约在中午12点在某地的星巴克见面,而这个地方有两家星巴克,当你去了星巴克A的时候你发现你的朋友不在,此时存在以下情况:
- 你的朋友迟到了
- 他在星巴克B
十五分钟过后,你依然没有见到你的朋友,于是想去星巴克B看看他是否在,其实你的朋友在12点的时候已经到了星巴克B,与此同时你的朋友也想去星巴克A看看你是否在,这样你俩又再一次错过,就这样你俩互相认为对方爽约,并在两家星巴克直接走来走去。
public class CountingFactorizer extends GenericServlet implements Servlet {
private final AtomicLong count = new AtomicLong(0);
public long getCount() { return count.get(); }
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
count.incrementAndGet();
encodeIntoResponse(resp, factors);
}
void encodeIntoResponse(ServletResponse res, BigInteger[] factors) {}
BigInteger extractFromRequest(ServletRequest req) {return null; }
BigInteger[] factor(BigInteger i) { return null; }
}
AtomicLong 来代替long类型的计数器,这个能保证所有对计数器状态访问的操作都是原子的这样就不会存在线程安全问题。
3.加锁机制
常使用synchronize关键字对代码片段或者是方法进行加锁,当线程执行到加锁代码时,会获得相应的锁,如果这时另一个线程也要执行同步代码块时,由于锁没被释放,这个线程将进入阻塞状态,等待获得锁的线程执行完毕释放锁后才能继续运行。
加锁机制很好了保证了线程安全,但是加锁机制会导致低并发性,以为在同一时间只有一个线程执行同步代码块。
用锁来保护状态
对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的。
这样多个线程访问可变状态变量,这个变量的状态对于其他的线程来讲是有效值,因为一个线程获得锁后,其他线程想要访问这个变量的状态时已经是改变后的状态,这个改变就是代码表现出的正确行为,所以我们说它是线程安全的。
int count = 0;
++count
如果我们对count这个变量加锁,第一个线程在获得锁之后,在执行count增1的动作时,其他线程是访问不到count的状态的,直到第一个线程释放锁后,这时count的值对于所有线程来说都是1。