基础篇
第二章 线程安全性
在Java中同步的机制
volatile变量、显示的同步代码块(显示锁)、原子变量。
- 编写线程安全的代码的关键:利用以上三个机制合理控制对象共享的且是可变的状态(即类的field)的读写操作。
什么是线程安全性
当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的。
当多个线程访问某个类时,1.不管运行环境采用何种调度方式或者线程将如何交替执行,2.并且在主代码中不需要额外的同步或协同,3.这个类都能表现出正确的行为,那么这个类就是线程安全的
无状态的对象一定是线程安全的
- 自身没有任何域也不包含任何其他对象的引用。
- 例: 无状态的servlet
package net.jcip.examples;
import java.math.BigInteger;
import javax.servlet.*;
import net.jcip.annotations.*;
/**
* StatelessFactorizer
*
* A stateless servlet
*
* @author Brian Goetz and Tim Peierls
*/
@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 };
}
}
原子性
原子性就是不可再分割,以count++
为例,它是一个包含读取-修改-写入三个操作的复合操作,并且count++
的结果的正确性依赖于这三个操作的执行顺序。倘若有AB两个线程同时执行该复合操作,有可能出现A读取-A修改-B读取-B修改-A写入-B写入的操作序列,这显然是错误的,除此之外还有很多错误的组合方式,我们唯一希望看到的序列如下:A读取-A修改-A写入, B读取-B修改-B写入 或者 B读取-B修改-B写入 ,A读取-A修改-A写入 。
package net.jcip.examples;
import java.math.BigInteger;
import javax.servlet.*;
import net.jcip.annotations.*;
/**
* UnsafeCountingFactorizer
*
* Servlet that counts requests without the necessary synchronization
*
* @author Brian Goetz and Tim Peierls
*/
@NotThreadSafe
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++`的例子,由于不切当的执行时序导致错误的执行结果。我们把这种情况称作**竞态条件**。
最常见的静态条件就是**先检查后执行**。
例如延迟加载:
package net.jcip.examples;
import net.jcip.annotations.*;
/**
* LazyInitRace
*
* Race condition in lazy initialization
*
* @author Brian Goetz and Tim Peierls
*/
@NotThreadSafe
public class LazyInitRace {
private ExpensiveObject instance = null;
public ExpensiveObject getInstance() {
if (instance == null)
instance = new ExpensiveObject();
return instance;
}
}
class ExpensiveObject { }
上面的代码中instance
可能会被创建多次,而创建每一个实例的花销是特别大的,或者实例占用的资源是稀少的,比如数据库连接池、redis连接池。当然在某些需求中instance
需要保持唯一,这时候如果在并发环境中创建了多个实例,并被传递给其他对象,则会引发严重错误。
复合操作
假定有两个操作A、B,如果从执行A的线程来看,当另一个线程执行B的时候,要么B全部执行完,要么B完全不执行,那么A和B相对彼此来说是原子的。原子操作是指,对于访问同一个状态的所有操作来说,包括该操作本身,这个操作是一个以原子方式执行的操作。
与数据库中事务的原子性有异曲同工之意。事物之间互不影响,要么全部执行成功,要么全部执行失败。
- 使用
AtomicLong
来解决count++
的线程安全问题。
package net.jcip.examples;
import java.math.BigInteger;
import java.util.concurrent.atomic.*;
import javax.servlet.*;
import net.jcip.annotations.*;
/**
* CountingFactorizer
*
* Servlet that counts requests using AtomicLong
*
* @author Brian Goetz and Tim Peierls
*/
@ThreadSafe
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 变量能够保证,在多个线程对该变量操作时,能够保证个线程之间的操作是原子性的。
在实际情况中,应尽量使用线程安全的对象(例如 AtomicLong)
来管理对象的状态。
加锁机制
在上一个例子中我们使用`AtomicLong`解决了线程安全问题,
这是因为我们的Servlet
中只持有了一个状态。如果持有多个状态会怎样呢?我们希望将最后一次AtomicLong
因式分解的计算结果缓存起来。这里用到的是AtomicReference
(代替对象引用的线程安全类,后面会详细介绍各种原子变量。)在这里虽然能够保证对lastNumber
和lastFactors
上的操作是原子性的,但是它们两个之间存在着一对一的映射关系,当改变一个的时候,另一个也需要相应的做出改变,它们之间不是相互独立的,只保证它们两个相互独立的操作的原子性是不够的,而是需要保证 同时改变它们两个的一组操作 的原子性。
要保持状态的一致性,就需要在单个原子操作中更新所有相关的状变量
package net.jcip.examples;
import java.math.BigInteger;
import java.util.concurrent.atomic.*;
import javax.servlet.*;
import net.jcip.annotations.*;
/**
* UnsafeCachingFactorizer
*
* Servlet that attempts to cache its last result without adequate atomicity
*
* @author Brian Goetz and Tim Peierls
*/
@NotThreadSafe
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};
}
}
内置锁
内置锁机制:同步代码块 Synchronized Block,包括两部分,一个作为锁的对象引用,一个作为由这个锁保护的代码块。
- 同步代码块的锁
- 加在类的成员方法上时锁就是方法调用所在的对象,即`this`指向的对象,如下例中的`lastNumber`、`lastFactors`被`this`守护,即访问这两个状态的线程必须首先获取 以当前实例对象作为的锁
- 加在类的静态方法上的锁就是该类的Class对象。
- `Synchronized (lock) {}` 显示的制定锁,lock可以使任何对象。
> 每个java对象都可以作为一个实现同步的锁,这些锁被称为内置锁,或监视锁。
package net.jcip.examples;
import java.math.BigInteger;
import javax.servlet.*;
import net.jcip.annotations.*;
/**
* SynchronizedFactorizer
*
* Servlet that caches last result, but with unnacceptably poor concurrency
*
* @author Brian Goetz and Tim Peierls
*/
@ThreadSafe
public class SynchronizedFactorizer extends GenericServlet implements Servlet {
@GuardedBy("this") private BigInteger lastNumber;
@GuardedBy("this") private BigInteger[] lastFactors;
public synchronized void service(ServletRequest req,
ServletResponse resp) {
BigInteger i = extractFromRequest(req);
if (i.equals(lastNumber))
encodeIntoResponse(resp, lastFactors);
else {
BigInteger[] factors = factor(i);
lastNumber = i;
lastFactors = 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 };
}
}
重入
> 当某个线程请求一个由其他线程持有的锁的时候,发出请求的线程就会阻塞,但是当线程再次请求他自己已经持有的锁的时候,会请求成功,这就是线程的重入。如果线程不是可重入的,下例代码将发生死锁。
public class Widget {
public synchronized void doSomething(){
...
}
}
public class LoggingWidget extends Widget {
public synchronized void doSomething(){
...
super.doSomething();
}
}
用锁来保护状态
对于可能被多个线程同时访问的可变状态变量,在访问他时都需要持有同一个锁,在这种情况下我们称状态变量是由这个锁保护的。
- 见一个反例,下例中的
count++
虽然被synchronized
修饰,但它并不是线程安全的,欢迎大家留言来解释这个问题,提示:原因见上面用锁来保护状态
的解析。
public class Problem1 {
public static int count;
public static class TestThread implements Runnable{
@Override
public void run() {
doCount();
}
private synchronized void doCount(){
count++;
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new TestThread(),"T1");
Thread t2 = new Thread(new TestThread(),"T2");
t1.start();
t2.start();
}
}
每个共享的可变状态变量都应该只由一个锁来保护。
再举一个反例,下面的代码虽然使用了vector,但并不是线程安全的,为什么呢,因为在vector内部的synchronized方法 保证了,vector自己的域由内置锁也就是vector的这个实例守护,但是element 这个可变的状态变量却不是由vector的内置锁守护,所以其他线程是可以改变element变量的,从而形成竞态条件。
if(!vector.contains(element))
vector.add(element)
vector 的contains、add方法如下
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
public boolean contains(Object o) {
return indexOf(o, 0) >= 0;
}
public synchronized int indexOf(Object o, int index) {
if (o == null) {
for (int i = index ; i < elementCount ; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = index ; i < elementCount ; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
活跃性与性能
过分使用或滥用Synchronized代码块,会导致严重的性能问题。上面的例子中的 SynchronizedFactorizer
他继承了Servlet,我们都知道Servlet是单例的,他需要为所有的请求提供服务,并且他的service
方法被synchronized修饰,这导致了所有的线程必须一个的按顺序执行,就像一大堆人坐缆车下山,只提供了一辆容纳一人的缆车,尤其是当下山的路线特别长的时候,下山的效率可想而知。为了解决不良并发问题,提出了缓存机制,代码如下。
package net.jcip.examples;
import java.math.BigInteger;
import javax.servlet.*;
import net.jcip.annotations.*;
/**
* CachedFactorizer
* <p/>
* Servlet that caches its last request and result
*
* @author Brian Goetz and Tim Peierls
*/
@ThreadSafe
public class CachedFactorizer extends GenericServlet implements Servlet {
@GuardedBy("this") private BigInteger lastNumber;
@GuardedBy("this") private BigInteger[] lastFactors;
@GuardedBy("this") private long hits;
@GuardedBy("this") private long cacheHits;
public synchronized long getHits() {
return hits;
}
public synchronized double getCacheHitRatio() {
return (double) cacheHits / (double) hits;
}
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = null;
synchronized (this) {
++hits;
if (i.equals(lastNumber)) {
++cacheHits;
factors = lastFactors.clone();
}
}
if (factors == null) {
factors = factor(i);
synchronized (this) {
lastNumber = i;
lastFactors = factors.clone();
}
}
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};
}
}
当执行较长时间的计算或者无法快速完成的工作时(例,网络IO、或控制台IO),一定不要持有锁。