当多个线程访问同一个类时,如果不考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步及在调用方代码不必作其他的协调,这个类的行为仍然是正确的,那么称这个类是线程安全的。

一个无状态的Servlet

public void service(ServlerRequest req,ServletResponse resp){

  BigInteger i=extractFromRequest(req);

  BigInteger[] factors=factor(i);

  encodeIntoResponse(resp,factors);

}

上面的这个servlet 是线程安全的。

无状态对象永远是线程安全的。

如果我们在上面的方法中加入一个用于记录访问计数的状态变量count,则他就不是线程安全的。

public ExpensiveObject getInstance(){

  if(instance==null){

    instance=new ExpensiveObject();

  }

  return instance;

}


上面的代码不是线程安全的。

比如线程A和线程B同时判断instance为空时,就会出现问题。

 如果自增操作是一个原子操作,那么就可以修复问题。

private final AtomicLong count=new AtomicLong(0);

  public long getCount(){
    return count.get();
  }

  public void service(ServlerRequest req,ServletResponse resp){

    BigInteger i=extractFromRequest(req);

    BigInteger[] factors=factor(i);

    count.incrementAndGet();

    encodeIntoResponse(resp,factors);

}
上面的方法是线程安全的,里面引入了java.util.concurrent.atmoic 中的原子变量类。这些类实现了数字和对象引用的原子状态转换。

当向无状态的类中加入唯一的状态元素,而这个状态完全被线程安全的对象所管理,那么新的类仍然是线程安全的。

比如我们考虑这样的情形,缓存最新的计算结果,以应对两个连续的客户请求相同的数字进行因数分解。

  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 两个变量。

为了保护状态的一致性,要在单一的原子操作中更新相互关联的状态变量。

可以使用synchronized修饰方法,但是会严重影响性能。

当一个线程请求其他线程已经占有的锁时,请求线程将被阻塞,然而内部锁是可重入的,因此线程在试图获得它自己占有的锁时,请求会成功。

重进入实现方式是通过一个请求计数和一个占有他的线程,当计数为0时则认为未被占用,每次请求,计数加一,每次退出同步块,计数减一。直到为0,锁被释放。

public class Father{

  public synchronized void dosome(){
  }
}

public class Child extends Father{

  public synchronized void dosome(){

    System.out.println(“call dosome”);

    super.dosome();
  }

}

如果没有重进入,上面的代码会锁死。

一种常见的锁规则是在对象内部封装所有的可变状态,通过对象的内部锁来同步任何访问可变状态的代码路径,很多的线程安全类都是这个模式。

对于每一个涉及多个变量的不变约束,需要同一个锁保护其所有的变量。

Vector仅仅同步他的每个方法,并不能确保在Vector上执行的复合操作是原子的。


if(!vector.contains(element)){

  vector.add(element);

}

虽然containsadd都是原子的,但是缺少即加入的过程中是存在竞争条件的。

public class CachedFactorizer implements Servlet{

  private BIgInteger lastNumber;

  private BigInteger[] lastFactors;

  private long hits;

  private long cacheHits;

  public synchronized long getHits(){
    return hits;
  }

  public synchronized double getCacheHistRatio(){

    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);

  }

}


上面的例子中没有使用 AtmoicLong 来计数,而是使用了 long ,但是在这里是安全的。因为我们已经使用 synchronized 的关键字构造了原子操作。

请求与释放锁的操作也是需要开销的,所以synchronized块分解的过于琐碎也是不合适的。

有些耗时的计算或操作,比如网络或控制台IO,难以快速完成,执行这些操作期间不要占有锁。

通常,不能保证读线程及时地读取其他线程写入的值,甚至可以说根本不可能。为了确保跨线程写入的内存可见性,必须使用同步机制。

public class NoVisibility{

  private static boolean ready;

  private static int number;

  private static class ReadThread extends Thread{

    public void run(){

      while(!ready){

        Thread.yield();

      }

      System.out.println(number);

    }

  }

  pulbic static void main(String[] args){

    new ReaderThread().start();

    number=42;

    ready=true;

  }

}


看上去,程序会输出42,但实际情况可能是程序不会退出或者输出的结果是0

出现这种情况的原因是,number在还没有赋值之前,ready就已经被赋值为true了,并且对读线程可见,这就是重排序现象。

在单个线程中,只要重排序不会对结果产生影响,那么就不能保证其中的操作一定按照程序书写的书序执行——即使重排序对其他线程会产生明显的影响。

在没有同步的情况下,编译器、处理器、运行时安排操作的执行顺序可能完全出人意料。在没有进行适当同步的多线程程序中,尝试推断那些必然发生在内存中的动作时,你总是会判断错误。

上面的程序中,读线程看到了一个过期的变量。

public class MutableInteger{

  private int value;

  public int get(){

    return value;

  }

  public void set(int value){

    this.value=value;

  }

}

上面的程序中,就会出现获得过期数据的可能性,但是仅仅使用synchronized修饰 set 是不够的,调用 get 的线程仍然能够看见过期值。所以两个方法都需要修饰。

当一个线程在没有同步的情况下读取变量,可能会得到一个过期值,但是至少他可以看到某个线程设定的一个真实数值,而不是一个凭空而来的值。称为最低限的安全性。其适用于所有的变量,除一例外:没有声明为volatile64位数值的变量doublelongJava存储模型要求获取和存储操作都是原子的,但是对于非volatilelongdouble变量,JVM允许将64位的读或写划分为两个32位的操作。如果读和写发生在不同的线程,就会出现得到一个值的高32位和另一个值的低32位。

锁不仅仅是关于同步与互斥的,也是关于内存可见的。为了保证所有线程能够看到共享的、可变变量的最新值,读取和写入线程必须使用公共的锁进行同步。

只有当volatile变量能够简化实现和同步策略的验证时,才使用它们。当验证正确性必须推断可见性问题时,应该避免使用volatile变量。正确使用volatile变量的方式包括:用于确保它们所引用的对象状态的可见性,或者用于标识重要的生命周期事件(比如初始化或关闭)的发生。

volatile boolean asleep;

...

while(!asleep){

  countSomeSheep();

}


注意,volatile的语义不足以使得自增操作原子化。

加锁可以保证可见性和原子性;volatile变量只能保证可见性。

只有满足下面所有的标准后,才能使用volatile变量。

1,写入变量时并不依赖变量的当前值;或者能够确保只有单一的线程修改变量的值。

2,变量不需要与其他的状态变量共同参与不变约束。

3,而且,访问变量时,没有其他的原因需要加锁。

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐