Java并发编程(一):线程安全性

147 阅读3分钟

什么是线程安全性?

线程安全性:当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的。

以下两种情况,一定是线程安全的:

  • 无状态对象一定是线程安全的。
  • 当在无状态的类中添加一个状态时,如果该状态完全由线程安全的对象来管理,那么这个类依然是线程安全的。

下面给出一个无状态对象的例子:

//这是一个实现因数分解功能的Servlet
public class StatelessFactorizer implements Servlet {
    public void service(servletRequest req, ServletResponse resp){
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        encodeIntoResponse(resp, factors);
    }
}

在这个例子中,StatelessFactorizer是没有状态的,所以这是一个线程安全类。

现在,我们向这个类添加一个状态,用来记录请求数量。

public class Factorizer implements Servlet {
    private final AtomicLong count = new AtomicLong(0);
    
    public void service(servletRequest req, ServletResponse resp){
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        count.incrementAndGet();        
        encodeIntoResponse(resp, factors);
    }
}

在这里,我们使用了AtomicLong对象来记录命中次数,这个类可以保证对它进行的操作是原子性的。当前类的状态就是这个计数器的状态,而这个计数器是线程安全的,所以这个类也是线程安全的。

原子性

对于上述的计数器,我们不采用AtomicLong,而是使用Long,如下:

public class Factorizer implements Servlet {
    private Long count = 0;
    
    public void service(servletRequest req, ServletResponse resp){
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        ++count;      
        encodeIntoResponse(resp, factors);
    }
}

这个时候Factorizer类就再是线程安全的。因为++count这个操作不是原子的,它包含了三个独立的操作:读取count的值,将值加1,将结果写入count。也就是说,其结果状态依赖于之前的状态,存在竞态条件

竞态条件:由于不恰当的执行时序而出现不正确的结果。

最常见的竞态条件类型就是“先检查后执行(Check-Then-Act)”操作,即通过一个可能失效的观测结果来决定下一步的动作。

例如

public class LazyInit {
    private Object instance = null;
    
    public Object getInstance() {
        if (instance == null)
            instance = new Object();
        return instance;
    }
}

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

锁机制

锁能够保证被其保护的代码块以串行方式访问,以保证其状态的一致性。

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

Java的内置锁是一种互斥锁,最多只能由一个线程获得该锁。同时,Java的内置锁是可以重入的。

重入:当某个线程试图获得一个由它自己持有的锁时,这个请求会成功。

如果在复合操作的执行过程中持有一把锁,那么会使复合操作编程原子操作。需要注意的是,无论是写入或是访问共享变量,都需要使用同步,并且每一个共享的和可变的变量都应该只由一个锁来保护,从而使维护人员知道是哪一个锁。

只有被多个线程同时访问的可变数据才需要用锁来保护。并且对于每个包含多个变量的不变性条件,其中设计的所有变量都需要由同一个锁来保护。