【Java 并发】线程安全性
一个对象是否需要线程安全的,取决于他是否被多个线程访问,要对象是线程安全的,需要采用同步机制来协同对象可变状态的访问。
一,如果多个线程访问一个可变的状态变量时没有使用合适的同步机制,可以使用以下方法解决
1,不共享该状态变量。
2,将可变修改为不可变的变量。
3,在访问变量时使用同步。
线程安全的定义:多个线程访问某个类,这个类始终都能表现出正确的行为。
二,原子性
原子性一种很通俗的理解是。要么一次做完,不会中断,要么就不做。
1,原子操作
原子操作是不能被线程调度所中断的操作,一旦开始,那么它一定在可能发生线程切换之前完成。原子操作有除long和double之外的所有基本类型操作。因为JVM可以将64位的读写操作当做两个分离的32位操作来执行,这就可能产生在读写的过程中出现线程的切换。对域中的值做赋值和返回操作通常是原子操作。
看代码理解
public class UnsafeCountingFactorizer 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);
}
++count;虽然看上去像是一个操作,但是这个操作非原子性,它不是一个不可分割的操作,实际上有三个独立的操作:读取-修改-写入,三种操作的结果都是依赖于前一个操作。举个栗子:A线程读取count=1,在要进行加1操作时,B线程正好读取count,此时的count=1,结果A,B线程运行的结果都是count=2。如果A,B线程是按顺序执行时,则A的结果是count=2,B的结果是count=3。整个计算正确性取决于多个线程的交替执行时序时,就会发生竞态条件。也就是说正确的结果取决于运气。
2,竞态条件
本质:基于一个可能失败的观察结果来做出判断或者执行操作。简单来说就是“先检查后执行”。++count也存在竞态,要根据前一个结果,再执行加1操作。
再看个例子
public class LazyInitRace {
private ExpensiveObject instance = null;
public ExpensiveObject getInstance() {
if (instance == null)
instance = new ExpensiveObject();
return instance;
}
}
class ExpensiveObject { }
程序中的初始化操作推迟到实际使用时才进行,延迟初始化。程序中也存在竞态。假设A和B线程都调用 getInstance() ,A判断instance为空,于是new ExpensiveObject()实例。B线程也同样要判断instance是否为空,instance是否为空就取决于A线程创建对象的时间,若B线程判断判断instance为空,那么又new ExpensiveObject()实例,就存在两个实例了。
3,复合操作
之前两个程序++count和instance = new ExpensiveObject()都是复合操作,当然还有许多复合操作,这些操作都包含一组必须以原子方式执行的操作以确保线程安全性。先介绍一种方式:使用线程安全类。
public class CountingFactorizer 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);
}
}
在java.util.concurrent.atomic包中包含一些原子变量类,用于实现在数值和对象引用上的原子状态转换。 AtomicLong 代替Long。能够确保所有对计数器状态的访问操作都是原子的。
在平时编程时很少使用原子变量类,但是在性能调优时就大有用武之地。
三,加锁机制
先看段程序
public class UnsafeCachingFactorizer 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);
}
}
}
如果程序正常运行,程序中的lastNumber缓存的值应该等于lastFactors缓存的因数之积。所以这两个变量不是彼此独立的,是互相依赖的,lastNumber缓存的值改变则lastFactors缓存的因数也要同时做出相应改变。那么问题就来了,如果只修改的一个值,那么其他线程运行时就出问题了。还有一种可能,A线程正在读取两个值,而B线程在修改,那么就无法保证同时获取两个值。
Java提供了一种锁机制:同步代码块来解决这个问题。它包含两部分:锁的对象引用,有这个锁保护的代码块。用synchronized修饰的方法就是一个同步代码块。其中同步代码块的锁就是方法调用所在的对象。
synchronized(lock){
//代码块
}
修改成同步代码块
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);
}
}
}
锁机制还有一个重入问题
重入的一种实现方法是,为每个锁关联一个获取计数器和一个所有者线程,计数器为0时,这个锁没被任何线程持有,当线程请求一个未被持有的锁时,JVM记录锁的持有者,计数器加1,如果通过线程再次获取锁,则计数器相应增加。当线程退出同步代码块时,计数器递减。当计数器为0时,锁被释放。
使用锁机制时,要注意同个同步代码块只能由同一个所对象来保护。
以上都是阅读《Java编程思想》-并发和《Java并发编程实践》的笔记,不喜勿喷!