线程安全性

什么是线程安全性

  1. 某个类的行为与其规范完全一致。在良好的规范中通常会定义各种不变性条件(Invariant)来约束对象的状态,以及定义各种后验条件(Postcondition)来描述对象操作的结果。由于我们通常不会为类编写详细的规范,那么如何知道这些类是否正确呢?我们无法知道,但这并不妨碍我们在确信“类的代码能工作”后使用它们。这种“代码可信性”非常接近于我们对正确性的理解,因此我们可以将单线程的正确性近似定义为“所见即所知(we know it when we see it)”。在对“正确性”给出了一个较为清晰的定义后,就可以定义线程安全性:当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的.

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

原子性

  • 原子性是指操作是不可分的。其表现在于对于共享变量的某些操作,应该是不可分的,必须连续完成。例如a++,对于共享变量a的操作,实际上会执行三个步骤,1.读取变量a的值 2.a的值+1 3.将值赋予变量a 。 这三个操作中任何一个操作过程中,a的值被人篡改,那么都会出现我们不希望出现的结果

竟态条件

  • 当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件

  • 延迟初始化中的竞态条件

 1 @NotThreadSafe
 2  3 public class LazyInitRace{
 4  5 private ExpensiveObject instance=null 6  7 public ExpensiveObject getInstance(){
 8  9 if(instance==null10 11 instance=new ExpensiveObject();
12 13 return instance;
14 15 }
16 17 }

 

假定线程A和线程B同时执行getInstance。A看到instance为空,因而创建一个新的ExpensiveObject实例。B同样需要判断instance是否为空。此时的instance是否为空,要取决于不可预测的时序,包括线程的调度方式,以及A需要花多长时间来初始化ExpensiveObject并设置instance。如果当B检查时,instance为空,那么在两次调用getInstance时可能会得到不同的结果,即使getInstance通常被认为是返回相同的实例

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

复合操作

  • 为了确保线程安全性,“先检查后执行”(例如延迟初始化)和“读取-修改-写入”(例如递增运算)等操作必须是原子的。我们将“先检查后执行”以及“读取-修改-写入”等操作统称为复合操作:包含了一组必须以原子方式执行的操作以确保线程安全性

加锁机制

  • Java提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block)

  • 每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁(Intrinsic Lock)或监视器锁(Monitor Lock)

  • 1 synchronized(lock){
    2 3 //访问或修改由锁保护的共享状态
    4 5 }

     

     

重入

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

posted @ 2020-01-08 14:40  webzom  阅读(115)  评论(0)    收藏  举报