Java并发系列(一)线程安全基础

Author:Martin

E-mail:mwdnjupt@sina.com.cn

CSDN Blog:http://blog.csdn.net/ictcamera

Sina MicroBlog ID:ITCamera

Main Reference:

《Java并发编程实战》 Brian Goetz etc 童云兰等译

《Java并发设计教程》 温绍锦

 

1.        线程的基本概念

线程的优点:发挥对处理器的强大能力、建模简单、异步事件的简化处理(之间用同步IO,异步IO对操作系统要求高,处理复杂)、响应更灵敏的用户界面。

线程带来的风险:安全问题、活跃性问题、性能问题。

安全性问题­:(safety)多线程的并发安全性问题(也称竞态条件、原子性问题)。

活跃性问题:(Liveness)饥饿、死锁、活锁。

性能问题:(performance)服务时间长、响应不灵敏、吞吐量过低、资源消耗高、可伸缩性差。

2.        线程的安全性

由于多线程共享相同的内存地址空间,并且是并发运行,因此它们可能会访问或者修改其他线程正在使用的变量。

这种方式的优点是比其他通信机制更容易实现暑假共享,并且在没有实现同步的情况下有些随意安排操作执行的顺序和时间以及缓存等技术能带来了更优的性能;

带来的风险就是线程会由于无法预料数据的变化而发生错误,当多个线程修改相同的变量的时候,两会在串行编程模型中引入非串行因素,这种串行是很难分析的。

要解决这个问题,必须对共享变量的访问机制进行协同,这样才不会在线程之间发生彼此干扰。开发人员必须找出这些数据在哪些位置被多个线程共享,只有这样才能使得这些性能优化措施不破坏线程的安全性。下面是非线程安全的例子

例子:UnsafeSequence

Public class UnsafeSequence{

Private int value=0;

Public int getNext(){

//这个操作包含三个独立的操作:①读取vlaue,②将读取的值加1,③将结果写入value

return value++;

}

}

假设有线程A和线程B同时调用getNext()方法,在下面情况下取到的是相同的值,这个与实际的设计要求是不相符的。下面的流程是为了说明问题设计的糟糕情况,事实上,由于存在指令的重排序的肯能,实际情况可能会更糟糕。

A:①读取value=8—————>②8+1=9—————>③9赋给value

B:           ①读取value=8—————>②8+1=9—————>③9赋给value

2.1.        对象的线程安全(线程安全类)

定义:当多个线程访问某个类时,这个类始终表现出正确的行文,那么这个类是安全的。或者说在单线程环境和多线程环境都不被破坏,那么这个类是安全的。再或者说当多个线程访问某个类时,不管运行环境次啊用何种调度方式,或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步协同,这类能编写出正确的行为,那么这个类是线程安全的,这里正确的行为往往有规范约束,具有后验性。

编写线程安全的代码核心是对对象的状态访问进行管理,特别是共享(Shared)可变(Mutable)状态访。对象的状态是指存储的状态变量的值,共享意味着多个线程可以同时访问,可变意味着变量值在生命周期内可以发生变化。对可变对象的管理就要解决竞态条件数据竞争(主要包括对象的可见性)两方面问题(这两方面内容在后面说明)。

一个对象是否需要考虑线程安全取决于是否共享(是否会存在多个线程访问的可能,或者是否存在并发访问的需求),如果是共享的即被多个线程并发访问的,那么需要考虑对象的线程安全性(否则不需要考虑):

l  如果对象本身(对象状态)是不可变的,那么这个对象对应的类本身是线程安全的,不需要进行额外的线程安全措施。无状态对象是不可变对象的一种特殊情况,他不包含任何域值,也不包含对其他类中的域的引用,可能用到的变量全是局部变量。因此,无状态对象一定是线程安全的(这里线程的安全性可以模糊的说成对象或者对象所在的类的)。

l  如果对象是可变的那么需要线程安全的,需要采用同步机制协同对象可变状态的访问,即必须采用同步机制来协同这些线程对状态变量的访问(包括读取和设置),使得对象所在的类为线程安全的。Java中的同步包括了synchronized、volatile、atomic(原子变量)和Explicit Lock(显式锁)机制。

也就是说如果需要线程安全的对象出现线程安全问题时(某个时刻还是会出现了不正确的结果),解决问题的办法要么使得对象的访问改为单线程的,即不在线程之间共享该状态变量,要么使得对象的状态是不可变的,还有就是提供同步机制

事后修正问题的代价往往比事先考虑到问题并且进行合理的设计的代价要大得多,所以我们要在一开始就考虑对象的线程安全性问题。

注意:线程安全类和线程安全程序含义基本上是一致的,但是两者不是完全对应的。线程安全的程序可以不是完全由线程安全的类组成,所有线程安全的类构成的程序也不一定是线程安全的,线程安全的类也可以包含非线程安全的类。

当设计线程安全的类时,良好的面向对象技术、不可修改性,以及明晰的不可变性规范都起到一定的帮助作用。

2.2.        竞态条件(Race Condition)

对象的共享性和可变性使得我们编码中提供额外的技术—使用同步技术,使得对象类是线程安全的。共享性和可变性使得并发执行中出现的不恰当的执行时序,从而出现不正确的这种情况就叫竞态条件(Race Condition)

大多数竞态条件是这样的情况:基于一种可能失效的观察结果作出判断或者执行某个计算(失效是因为观察结果到执行判断这段时间系统状态发生了变化,观察结果就不正确了),这种竞态条件称为“先检查后执行”。例如首先观察“文件X不存在”,然后根据这个结果作相应的动作去“创建X文件”,事实上,在你观察到这结果和开始创建文件之间,观察结果可能变得无效(系统状态发生了变化:另一个线程在这期间创建了X文件),从而导致各种问题(未预期的一场、数据被覆盖、文件被破坏)。

还有一种情况就是:“读取—修改—写入”操作,这种操作(比如递增一个计数器)中,基于对象之前的状态来定义对象状态的转换。要递增一个计数器,你必须知道它之前的值,并确保在执行更新的过程中没有其他线程会修改和使用这个值。

UnsafeSequence是一个“读取—修改—写入”竞态条件例子。此外还有如下两个例子:

竞争条件例子:UnsafeCountingFactorizer

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

}

这个例子中存在“读取—修改—写入”的操作。

竞争条件例子:LazyInitRace延迟初始化的竞争条件

Public class LazyInitRace

Private ExpensiveObject instance=null;

Public ExpensiveAObject getInstance(){

    If(instance==null){

    Instance=new ExpensiveObject();

}

return instance;

}

这例子中如果多线程并发访问,instance是否为空,要取决于不可预测的时序及线程调度方式,以及花多长时间来初始化ExpensiveObject。这是“先检查后执行”的操作。

2.3.        数据竞争(Data Race)

共享性和可变性使得在访问非final类型的域时如果没有采用同步来进行协同,那么会出现数据竞争(Data Race),例如当一个线程写入一个变量而另一个线程接下来读取这个变量,或者读取之前由另一个写入变量时,并且这两个线程没有使用同步,那么就可能出现数据竞争。

数据竞争主要指对象的可见性,多核CPU,同一个变量在内存、寄存器中可能存在多个实例。可见性就是保证每次访问这些实例时,都能看到最新的值。可见性是一种复杂的属性,在单线程环境中,如果我们向某个变量先写入值,然后在没有其他操作的情况下读取这个变量,那么总能得到相同的值,但是在多线程并发情况下未必了,我们也无法确保制定读操作的线程能够适时的看到其他线程写入的值,有事甚至是不可能的事情,这时候就需要通过同步机制来保证。

下面是没有同步情况下的访问共享变量出现的可见性问题:

Public class NoVisibility{

Private static Boolean readdy;

Private static int number;

Private static class ReaderThread extends Thread{

    Public void run(){

        While(!ready){

              Thread.yield();

              System.out.println(number);

}

}

}

Public static void main(String[] args){

    New ReaderThread.start();

    Number=42;

    Ready=true;

}

}

看起来会输出42,但事实上很可能输出0,或者根本无法终止。这是以因为在代码没有使用足够同步机制,因此无法保证主线程写入的ready、number值对读线程来说是可见的。NoVisibility可能会持续循环下去,因为读线程可能永远都看不到ready的值。一种更奇怪的现象是NoVisibility输出0,是因为读线程可能看到了写入的ready值,但是却没看到写入的number值,这种现象被称为“重排序(Reordering)”。只要某个线程中无法检测到重排序的情况(即使在其他线程中可以很明显的看到该线程中的重排序),那么就无法确保线程中的操作将按照程序中指定的顺序来执行。当主线程首先写入number,然后在没有同步的情况下写入ready,那么读线程看到的顺序可能与写入的顺序完全相反。在缺少同步的情况下,Java内存模型允许编译器对操作顺序进行重排序,并将数值缓存在寄存器中,此外还允许CPU对操作顺序进行重排序,并且将数值缓存在处理器特定的缓存中。该程序中在缺少同步的情况下可能产生错误的情况:失效数据。当读线程查看ready变量时,可能会得到一个已经失效的值。除非在每次访问变量时都使用同步,否则很可能获得该变量的一个失效值。更糟糕的是,失效值不会同时出现:一个线程可能获得某个变量的最新值,而获得另一个便利的失效值。失效值可能会导致一些严重的安全问题和活跃性问题。NoVisibility例子中就可能导致错误输出或者程序无法结束。

    下面的MutableInteger不是线程安全的,因为get和setr都在没同步的情况下访问value,与其他问题相比,失效值问题更容易出现:如果某个线程调用了set,那么另外一个正在调用get的线程可能会看到更新后的value,也可能看不到:

Public class MutableInteger{

Private int value;

Public int get(){return value;}

Public void set(int value){this.vlaue=value;}

}

    还有一个可见性问题就是非原子的64位操作。当线程在没有同步的情况下读取变量时,可能会得到一个失效的的值,但至少这个值是由之前的某个线程设置的值,而不是随机值。这种安全性保证也被称为最低安全性(out-of-thin-airsafety)。最低安全性适用与绝大对数变量,但是存在一个例外:非volatile类型的64位数值变量(double、long)。Java内存模型要求变量的读取和写入操作必须是原子性的,但对于非volatile类型的double和long变量,JVM允许将64为读或写操作分解成两个32位操作。那么多该变量的读写在不同线程中,那么很可能读到某个值的高32位和另外一个值的低32位,因此即使不考虑数据失效问题,在多线程程序中使用共享且可变的long和double等类型变量也是不安全的。除非用关键字volatile来声明它们,或者用锁来保护起来。

2.4.        竞态条件和数据竞争的关系

竞态条件和数据竞争一般都同时存在的,但是并非所有的竞态条件都是数据竞争,同样并非所有的数据竞争都是竞态条件。例子上面UnsafeCountingFactorizer就既存在竞态条件又存在数据竞争。

3.        线程的安全机制

3.1.        复合操作和原子性

UnsafeSequence、UnsafeCountingFactorizer和LazyInitRace例子中都需要包含一组需要以原子性方式执行的复合操作,这样才能避免竞态条件。原子操作是指,对于访问同一个状态的所有操作(包括该操作本身)来说,这个操作是以一个原子方式执行的操作。为了确保线程安全,必须使“先检查后执行”和“读取—修改—写入”的复合操作是原子性的。

由2.1可知,解决问题的办法要么使得对象的访问改为单线程的,即不在线程之间共享该状态变量(现实中这个几乎可能的),要么使得对象的状态是不可变的(实际中也很难做到),还有就是提供同步机制。Java中的同步包括了synchronized、volatile(严格上说volatile不是同步工具,只是针对可见性的机制,但是包括在同步的范畴中)、atomic(原子变量)和Explicit Lock(显式锁)机制。

3.2.        同步机制—Atomic

UnsafeCountingFactorizer修改后得到如下线程安全类

Public class AtomicCountingFactorizer implements Servlet

Private final AtomicLong count= new AtomicLong (0);

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

Public Synchronized void service (ServletRequest req,ServletResponse resp){

    BigInteger i=extractFromRequest(req);

    BigInteger[] factors=factor(i);

    count.incrementAndGet();

    encodeIntoResponse(resp,foctors);

}

这里用Atomic对象实现了计数器的原子性操作,能够保证所有对计数器状态访问的操作是原子的。Atomic对象内部实现使用了乐观锁机制-CAS(Compare and Swap)算法。CAS的基本思想是CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做,但是会导致著名的“ABA问题”,从这方面看,严格意义上说上面Atomic机制不能保证线程安全,但是这种概率是极其小的,可以认为是线程安全的。在实际情况中,应该尽可能地使用现有的线程安全对象(例如AtomicLong)来管理类的状态。

3.3.        同步机制—synchronized

Synchronized机制是用锁机制实现代码同步,包括同步代码和同步方法。将UnsafeCountingFactorizer修改如下实现线程安全

Public class SynchronizedMethdCountingFactorizer implements Servlet

Private long count=0;

Public long getCount{ return count;}

Public Synchronized void service (ServletRequest req,ServletResponse resp){

    BigInteger i=extractFromRequest(req);

    BigInteger[] factors=factor(i);

    ++count;//统计已处理请求

    encodeIntoResponse(resp,foctors);

}

同步方法隐式使用了同步方法“this”是内置锁,也可以改成如下形式

Public class SynchronizedCodeBlockCountingFactorizer 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);

    Synchronized(this)

       ++count;//统计已处理请求

        encodeIntoResponse(resp,foctors);

}

显式使用了“this”做为锁。

无状态的类肯定能是线程安全的,那么如果在这个类中增加一个状态时,如果该状态完全由线程安全的对象来管理,那么这个新类也是线程安全的。然后如果增加的状态数量由一个变为多个时,并不会像状态变量由零个变为一个那么简单。为了说明这个问题假设我们希望提升Servlet的性能:将最近的计算结果缓存起来,当两个连续的请求对相同的数值进行因数分解时,可以直接使用上一次的计算结果,而无须从新计算。要实现缓存策略,需要保存两个状态:最近执行因素分解的值以及分解结果。很容想到如下实现

Public class UnsafeAtomicCachingFactorizer 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.eaquals(lastNumber.get())){

         encodeIntoResponse(resp,foctors.get());

}else{

         BigInteger[] factors=factor(i);

lastNumber.set(i);

lastFactors.set(factors);

         encodeIntoResponse(resp,foctors);

}

}

这种方法并不正确。尽管这些原子应用本身都是线程安全的,但是在这个实现中存在竞态条件,可能产生错误的结果。这里的不变条件之一是:在lastFactors中缓存的因素之积应该等于lastNumber中缓存的值。在不变条件中设计多个变量时,多个变量之间并不是彼此独立的,而是某个变量值会对其他变量的值产生约束。因此,更新某一个变量时,需要在同一个原子操作中对其它变量同时进行更新。为了解决这个问题很容易想到使用同步方法实现线程安全。

Public class SynchronizedMethdCachingFactorizer implements Servlet

Private BigInteger lastNumber;

Private BigInteger[] lastFactors;

Public Synchronized void service (ServletRequest req,ServletResponse resp){

    BigInteger i=extractFromRequest(req);

    If(i.eaquals(lastNumber)){

         encodeIntoResponse(resp,foctors);

}else{

         BigInteger[] factors=factor(i);

listNumber=I;

lastFactors= factors;

         encodeIntoResponse(resp,foctors);

}

}

这种做法虽然能实现线程安全但是并发性(性能)却是糟糕的。

无论是同步方法或者是同步代码块,都使用了锁机制。当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞(线程阻塞在锁池等待队列中),直到获得锁为止。那么如果线程已经获取了锁,再次请求锁会发生什么?先看下面的例子:

Public class Widget{

    Public Synchronized void doSomething(){……}

Public class LoggingWidget extends Widget{

Public Synchronized void doSomething(){

  Super.doSomething();

  ……

}

如果子类调用父类的同步方法时已经获取了锁对象,如果这时调用父类的同步方法请求同一个锁(注意:这里只有一个对象,不是说有两个对象,父类一个对象,子类一个对象),如果不做特殊处理的话,这是有就进入了阻塞状态,就形成了死锁。Java通过锁计数方法来解决这个问题,使得这样的再次请求锁不会出现问题,这种机制就是重入,即内置锁是可以重入的。

    锁机制能使得保护的代码路径以串行的方式来访问,实现了访问共享状态的复合操作的原子性问题,从而避免了竞态条件的产生。这里的访问包括了读和写,常见的错误是认为只有写入共享变量才是需要使用同步,然后事实应该是读写同时需要使用同步。对于可能被多个线程同时访问的可变状态变量,在访问他时需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的。当然内置锁只是一个最常用也是最简便的锁,他和对象状态变量没有必然的关系,状态变量也可以用其他锁来保护,以实现同步。但是如果要实现同步,每个共享的可变的变量都应该由一个锁来保护,从而使维护人员知道是哪一个锁。并非所有的数据都需要锁的保护,只有被多个线程同时访问的可变数据需要通过锁来保护。对于包含多个变量的不变性条件,其中涉及的所有变量都需要由同一个锁来保护。

如果类的每个方法不加区分都使用Synchronized,那么可能程序过多的同步,还会导致活跃性(liveness)问题和性能(performance)问题。如果只是将每个方法都简单的同步,那么不足以确保复合操作都是原子的,例如Vector的下面例子

If(!vector.contains(element)){

    Vector.add(element);

}

虽然contain和add方法都是原子性的,但是上面的如果不存则添加的操作中任然存在竞态条件。虽然Synchronized方法可以保证单个操作的原子性,但是如果把多个操作合并为一个复合操作,还是需要额外的枷锁机制。这个在后面再举例说明。

    SynchronizedMethdCachingFactorizer例子锁虽然解决了安全性问题,但是引入了并发性问题,这个并发性是极差的,现在通过缩小同步锁范围来保证安全的同时尽可能的提高并发性能。

Public class MinSynchronizedCachingFactorizer implements Servlet

Private BigInteger lastNumber;

Private BigInteger[] lastFactors;

Private long ints;

Private long cacheHits;

Public synchronized long getHints(){return hints;}

Public synchronized long getCacheHitTation(){

    return (double)cacheHints/(double)hits;

}

Public void service (ServletRequest req,ServletResponse resp){

    BigInteger i=extractFromRequest(req);

    BigInteger[] factors=null;

    Synchronized(this){

        ++hits;

        If(i.equals(lastNumber)){

             ++cahceHits;

            factors=lastFactors.clone();

}

}

If(factors==null){

    factors=factor(i);

    synchronized(this){

        lastNumber=I;

        lastFactors=factors.clone();

}

}

    encodeIntoResponse(resp,foctors);

}

上面的代码中尽可能的减少了同步的范围,时间了简单的同步方法(整个方法同步)和并发性的平衡。但是也保证了多个可变状态变量使用同一个锁对象来保护的同步措施。一般同步中不能锁计算密集或者操作耗时的或者阻塞的操作,否则会出现并发问题(性能问题)和活跃性问题。

3.4.        同步机制—显式锁Lock

在Java5.0之前,协调对共享对象的访问时可以使用的机制只有Synchronized和volatile。Java5.0增加了一种新的机制:ReentrantLock。ReentrantLock并不是一种替代内置加锁的方法,而是当内置加锁不适用时,作为一种可选的高级功能(例如中断等待获取锁的线程等机制)。显式锁保护最简单的使用方式如下:

Lock lock=new ReentrantLock();

……

lock.lock();

try{

//更新对象状态

}catch(Exception e){

    //捕获异常,在必要时恢复不变性条件

}finally{

    lock.unlock();

}

示例的finally中一定要释放锁,否则相当于启动了一个“定时炸弹”。

3.5.        synchronized与可见性

由2.1可知,要编写正确的并发程序,关键问题在于:在访问共享的可变状态时需要进行正确的管理,这里正确的管理主要解决竞态条件和数据竞争(主要包括对象可见性)问题,我们知道同步方法和同步代码可以确保以原子的方式执行操作,因此错误的会认为Synchronized只能用于实现原子性和确定临界区(Critical Section)即解决竞态条件问题,其实同步还有重要的一方面就是内存(对象)的可见性(Visibly)。我们不仅希望防止某个线程正在使用对象状态而另外一个线程在同时修改该状态,而且希望确保当一个线程修改了对象状态后其他线程能够看到发生的状态变化。如果没有同步,这种可见性就无法实现。

可见性问题听起来有点恐怖,但是有一种简单的方法能够避免这类复杂的问题:只要有数据在多个线程之间共享,那么就使用正确的同步。下面使用同步方法进行同步,可以使SafeMutableInteger称为一个线程安全类。注意这里仅仅使get或者set方法同步是不够的,任然会出现数据失效,必须两个操作都同步,并且同步锁对象是同一个对象(下面两个同步方法都用了隐含的this同步对象)。当然显式锁等其他同步机制也能解决可见性问题。加锁的含义不仅仅局限于互斥行为,还包括内存的可见性。为了确保所有的线程都能看到共享变量的最新值,所有执行读操作或者写操作线程都必须在同一个锁上同步。

Public class SafeMutableInteger{

Private int value;

Public synchronized int get(){return value;}

Public synchronized void set(int value){this.vlaue=value;}

}

3.6.        volatile与可见性

由2.3可知,在多线程程序中使用共享且可变的long和double等类型变量也是不安全的。除非用关键字volatile来声明它们,或者用锁来保护起来。Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。Volatile变量不会被缓存在寄存器(这点很重要)或者对其他处理器不可见的地方,因此咋读取volatile类型的变量时中会返回最新写入的值。下面的例子没有使用同步方法,即读写volatile变量是不执行加锁操作,不使执行的线程阻塞,因此volatile变量是一种比Synchronized更轻量级的通过不机制。当然严格意义上是不同步的,因为不能保证原子性。

Public class UnSafeMutableInteger{

Private volatile long value;

Public long get(){return value;}

Public void set(long value){this.vlaue=value;}

}

Volatile变量对可见性的影响比volatile变量本身更为重要。但线程A首先写入一个volatile变量并且线程B随后读取该变量时,在写入volatile变量之前对A可见的所有变量值,在B读取了volatile变量后,对B也是可见的。因此,从内存的可见性角度来看,写入volatile变量相当于退出同步代码块,而读取volatile变量相当于进入同步代码块。然而,我们并不建议过度依赖volatile变量提供可见性。如果在代码中依赖volatile变量控制状态的可见性,通常比使用锁的代码更脆弱,也难以理解。当且仅当volatile变量能够简化代码的实现以及对同步策略的验证时,才应该使用它们。如果在验证正确性时需要对可见性进行复杂的判断,那么就不要使用volatile变量。正确的使用方式包括:确保它们自身状态的可见性,确保它们所引用对象的状态的可见性,以及标识一些重要的程序生命周期事件的发生(比如初始化或关闭)。下面给出了一个典型的用法:检查某个状态标记以判断是否退出循环。

volatile boolean asleep;

……

While(!asleep){

    countSomeSleep();

}

这个例子中,线程试图通过类似于数绵羊的传统方法进去休眠状态沦为了时这个例子能正确执行,asleep必须为volatile变量。否则当asleep被另外一个线程修改时,执行判断的线程却发现不了。我们也可以用锁来确保asleep更新操作的可见性,但是这将会使代码变得更加复杂。volatile变量通常做某个操作完成、发生中断或者状态的标识(例如本例)。虽然volatile变量很方便,但是也存在一些局限性。尽管volatile便利也可以用于表示其他的状态信息,但是使用时要非常小心。例如,volatile变量语义不足以确保递增操作的原子性(count++),除非你能确保只有一个线程对变量执行写操作(要确保递增可见性和原子性建议用atomic)。也就是说如果一个变量的操作都是原子操作,那么可以考虑相对轻量级的volatile。但是如果不是原子操作呢,可以使用java.util.concurrent.atomic.*中的实现。加锁机制即可以确保可见性又可以确保原子性,而volatile变量只能保证可见性。当且仅当满足一下所有条件时,才应该使用volatile变量:

l  对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。

l  该变量不会与其他状态变量一起纳入不变性条件中。

l  在访问变量时不需要加锁。

3.7.        各种同步机制比较

Volatile、Atomic、Synchronized、显式锁,这些同步机制各有各的特点,下面作简单比较、总结:

l  Volatile具备可见性,但是不能保证原子操作。

l  大多数情况下,AtmoicXXX可以替换Volatile。并且还具备一定的原子操作能力。

l  Synchronized开销最大,功能最完整。

l  在特殊情况下才需要使用显式锁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值