并发(2)线程安全性

一个对象是否需要考虑线程安全,取决于它是否被多个线程访问。这里的“访问”指的是在程序中对象被访问的方式,而不是对象所实现的功能。要使得对象是线程安全的,需要采用同步机制来协同对象可变状态的访问。如果无法实现协同,那么可能会导致数据破坏以及其它不可预知的结果。

如果没有合适的同步机制,那么程序就会出现错误,有以下三种方式可以修复这个问题:

1)不在线程之间共享变量

2)将变量改为不可变常量

3)使用同步访问

一、概念

当多个线程访问某个类的时候,主动调用的的代码不需要任何额外的同步或者协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

二、无状态

我们看一段代码

public class Test{
    public void service(){
        int i = 0;
        System.out.println(i);
    }
}

Test类是无状态的(没有任何共享变量),service方法无论怎么执行都不会出现安全性问题,局部变量i只在当前线程共享,所以无状态一定是线程安全的。

三、竞态条件

在多线程交替执行,执行顺序不定的情况下就会发生竞态条件,我们先看一段代码:

public class Test{
    private A a = null;
    public A getA(){
        if (a == null){
            a = new A();
        }
        return a;
    }
}

这段代码中虽然判断了a是否为null,但是在多线程的情况下有可能很多线程都判断为null,从而导致A被实例化了很多次。这就是典型的竞态条件“先检查后执行”,我们可以预想到存在竞态条件的代码执行结果有时候正确有时候不正确,所以竞态条件的执行结果只能看运气(注意:竞态条件与一般语义上的数据竞争不完全一致)。

四、复合操作

我们先来看一个原子操作代码片段

public class Test {

    private AtomicLong i = new AtomicLong(0L);

    public void incrI(){
        System.out.println(i.incrementAndGet());
    }
}

由于AtomicLong是原子对象,其操作都是原子操作,所以以上单个原子操作是不会出现竞态条件的,也就是单个原子操作是线程安全的

下面我们看一个复合操作:

public class Test {

    private AtomicLong i = new AtomicLong(0L);
    private AtomicLong count = new AtomicLong(0L);

    public void incrI(){
        if (i.incrementAndGet() >= 1) {
            count.incrementAndGet();
        }
    }
}

代码中有两个原子操作组成了复合操作,但是复合操作存在竞态条件,即使都是原子操作。我们可以预想到可能出现这样的情况,例如:

1) 线程1中 i = 1;线程2中 i = 2;

2)线程2中 count + 1 = 1;

1)线程1中 count + 1  = 2;

和我们预想的出现了差别:我们预期结果是i = 1的时候count = 1;i = 2的时候count = 2;也就是当多个原子操作组合复合操作的时候,这个单纯的复合操作是没有原子性的,也就非线程安全。

五、加锁

为了达到线程安全,我们给上面的复合操作进行加锁以达到线程安全的目的,例如

public synchronized static void incrI(){
        if (i.incrementAndGet() >= 1) {
            count.incrementAndGet();
        }
}

同步锁可以使线程安全,但请注意如果对于需要进行IO等耗时很久的操作的时候,如果采用同步锁会使性能非常低下。所以在程序中需要考虑线程安全性之外,还需要考虑性能问题而不能盲目为了达到安全性而随意采用同步锁等方式。

猜你喜欢

转载自www.cnblogs.com/lay2017/p/9278123.html