2.1 什么是线程安全性
1)在方法计算过程中的临时状态仅存在于线程栈上的局部变量中,并且只能由正在执行的线程访问。由于线程访问无状态对象的行为并不会影响其他线程中操作的正确性,因此无状态对象时线程安全的。无状态对象一定是线程安全的。
2.2 原子性
@NotThreadSafe
public class UnsafeCountingFactorizer implemnets 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**,并不是原子操作,由于它包括了三个独立的过程**“读取-修改-写入”**,并且其结果的状态完全依赖于之前的状态。
1)先检查后执行:是一种常见的延迟初始化,将对象的初始化操作推迟到实际被使用时才进行,同时要确保只被初始化一次。
2)复合操作:形成临界资源。
2.3 加锁机制
在线程安全性的定义中要求:多个线程之间的操作无论采用何种执行时序或交替方式,都要保证不变性条件不被破坏。
当在不变性条件中涉及多个变量时,各个变量之间并不是彼此独立的,而是某个变量的值会对其他变量的值产生约束。
要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。
1)内置锁:
同步代码块(Synchronized Block)----包括两个部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。
静态的synchronized方法以Class对象作为锁。
synchronized(lock){
//访问或修改有锁保护的共享状态
}
每个Java对象都可以用作一个实现同步的锁,这些锁被称为内置锁或监视器锁。线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁。
一组语句作为一个不可分割的单元被执行。任何一个执行同步代码块的线程,都不可能看到有其它线程正在执行由同一个锁保护的同步代码块。
2)重入:
由于内置锁是可重入的,因此如果某个线程识图获得一个已经由它自己持有的锁,那么这个请求就会成功。
当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取计数值设置为1,。如果同一个线程再次获取这个锁,计数值将递增,而当线程退出同步代码块时,计数器会相应的递减,当计数值为0时,这个锁将会被释放。
public class Widget{
public synchronized void doSometing(){
...
}
}
public class LoggingWidget extends Widget{
public synchronized void doSometing(){
System.out.println(toString() + ": calling doSometing") ;
super.doSometing() ;
}
}
重入进一步提升了加锁行为的封装性,因此简化了面向对象并发代码开发。子类改写了父类的synchronized方法,然后调用父类中的方法,此时如果没有可重入锁,那么上面这段代码将会造成死锁。
2.4 用锁来保护状态
由于锁能使其保护的代码路径以串行形式【多个线程依次以独占的方式访问对象,而不是并发的访问】,因此可以通过锁来构造一些协议以实现对共享状态的独占访问。
如果在复合操作的执行过程持有一个锁,那么会使复合操作成为原子操作。
对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的。
对于每个包含多个变量的不变性条件,其中涉及的所有变量都需要由同一个锁来保护。
2.5 活跃性与性能
有时候我们需要缩小同步代码块的作用范围,同时又维护线程安全性。尽量将不影响共享状态且执行时间较长的操作从同步代码块中分离出去,从而在这些操作的执行过程中,其它线程可以访问共享状态。
执行计算密集的操作或者无法快速完成的操作时(例如,网络I/O或者控制台I/O),一定不要持有锁。