一个对象是否需要线程安全,取决于它是否被多个线程访问,这指的是在程序中访问对象的方式,而不是对象要实现的功能,要使得对象是线程安全的,必须采用同步机制来协同对象可变状态的访问。

当多个线程访问某个状态变量并且其中一个线程执行写入操作的时候,必须采用同步机制来协同这些线程对变量的访问。Java中主要的同步机制是synchronized,它提供了一种独占的枷锁方式。同步这类还包括volatile类型变量,显示锁,以及原子变量

如果多线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误。三种解决方式

  1. 不在线程间共享该状态变量
  2. 将状态变量修改为不可变变量
  3. 在访问状态变量时使用同步

线程安全类的定义:

当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协调,这个类都能表现出正确的行为,那么就称为这个类是线程安全的类                       

无状态的对象一定是线程安全的

竞态条件

当某个计算的正确性取决于多个线程的交替执行时序时了那么就会发生竞态条件、就是一个线程需要判断一个变量的状态,然后根据这个变量的状态来执行某个操作,在执行这个操作之前,这个变量的状态可能会被其他线程修改。

1
2
3
4
5
6
7
8
9
10
11
@NotThreadSafe
public class LazyInitRace {
private ExpensiveObject instance=null;
public ExpensiveObject getInstance(){
if(instance==null){
instance=new ExpensiveObject();
}
return instance;
}
}

在上面懒汉式单例对象创建的代码中, 在LazyInitRace 中包含了一个竞态条件,它可能会破坏这个类的正确性。假定线程A和线程B 同时执行getInstance 方法。A 看到instance 为空,因此A创建一个新的ExpensiveObject实例。B 同样需要判断instance 是否为空。此时的instance是否为空,要取决于不可预测的时序,包括线程的调度方式,以及A 需要花多长时间来初始化ExpensiveObject并设置instance。如果当B检查时,instance为空,那么在两次调用getInstance 时可能会得到不同的对象

原子操作

要避免竞态条件文件,就必须在某个线程修改该变量时,通过某种方式防止其他线程使用这个变量,从而确保其他线程只能修改操作完成之前或之后读取修改状态,而不是在修改过程中

假定有两个操作A和B,如果从执行A的线程来看,当另外一个线程执行B时,要么将B全部执行完,要么全部不执行B,那么A和B的执行对彼此来说都不原子的。原子操作是指,对于访问同一个状态的所有操作包括该操作本身来说,这个操作是一个以原子方式执行的操作。

在实际情况中,应尽可能的使用现有的线程安全对象,来管理类的状态,与非线程安全的对象相比,判断线程安全对象的可能状态以及其状态的转换情况,要更为容易,从而也更容易维护和验证线程的安全性。

要保持状态的一致性,就需要在单个原子操作中更新所有相关状态变量

锁机制

java 提供了一种内置的锁机制来支持原子性,同步代码块(Synchronized Block)同步代码块包含两部分:一个是作为锁的对象引用,一个是作为由这个锁保护的代码块,在方法声明中使用synchronized修饰的方法是一种横跨整个方法体的同步代码块,其中该同步代码块的锁就是就是方法调用所在的对象。静态的synchronized方法以Class 对象作为锁

每个Java对象都可以用作一个实现同步的锁,这些锁称为内置锁或者监视器锁,线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块之前自动释放锁,而无论是通过正常的控制路径退出,还是通过从代码块中抛出异常退出,获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法

Java内置的锁相当于一种互斥的锁,这就意味着最多只有一个线程能够持有这种锁,当线程A尝试获取一个由线程B持有的锁的时候,线程A必须等到或者阻塞,知道线程B释放这个锁,如果B线程永远不释放,那么讲A将永远等下去。

锁的重入机制

当某个线程请求一个由其他线程持有的锁的时候,发出请求的线程就会阻塞,然而,由于内置锁是可以重入的,因此如果某个线程视图获得一个已经由它自己持有的锁,那么这个请求就会成功,”重入”意味着获取锁的操作粒度为线程,而不是调用,重入的一种实现方法是,为每个锁关联一个计数值和一个所有者线程,当计数值为0时,这个锁就认为没有被任何线程持有。当线程请求一个未被持有的锁时,JVM记下锁的持有者,并将计数值设置为1.如果同一个线程再次获取这个锁,计数值递增,而当线程退出同步代码块时,计数值会相应的递减。当计数值为0时,这个锁会被释放。

锁的保护状态

对于可能被多个线程同时访问的可变状态变量,在访问它时,否需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的。
每个共享的和可变的变量都应该只由一个锁来保护,从而使开发人员知道是哪一个锁。
对于每个包含多个变量的不变性条件,其中涉及的所有变量都需要由同一个锁来保护。

锁的性能

通常在简单性与性能之间存在着相互制约的因素,当实现某个同步策略时,一定不要盲目的为了性能而牺牲简单性

当执行较长的计算或者可能无法快速完成的操作时,例如网络IO,一定不要持有锁。