多线程系列提高(2)--线程安全性

如果当多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误。有三种方式可以修复这个问题:
(1)不在线程之间共享该状态变量
(2)将状态变量修改为不可变的变量
(3)在访问状态变量时使用同步

注意:完全由线程安全类构成的程序并不一定就是线程安全的,而在线程安全类中也可以包含非线程安全的类。

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

竞态条件:这种由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况。当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件。最常见的竞态条件类型就是“先检查后执行”操作,即通过有个可能失效的观测结果来决定下一步的动作。

竞态条件的本质:基于一种可能失效的观察结果来做出判断或者执行某个计算,这种类型的竞态条件称为“先检查后执行”:首先观察到某个条件为真(例如文件X不存在),然后根据这个观察结果采用相应的动作(创建文件X),但事实上,在你观察到这个结果以及开始创建文件之间,观察结果可能变得无效(另一个线程在这期间创建了文件X),从而导致各种问题(未预期的异常、数据被覆盖、文件被破坏等),这是一系列的动作,并不是一个原子操作。

(1)延迟初始化中的竞态条件:
单例模式中的懒汉式加载方式:

public class SingleTon{
    private static SingleTon instance;
    private static SingleTon(){}
    public static SingleTon getInstance(){
        if(instance==null){
            instance=new SingleTon();
        }
        return instance;
    }
}

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

(2)复合条件
假定有两个操作A和B,如果从执行A的线程来看,当另一个线程执行B时,要么将B全部执行完,要么完全不执行B,那么A和B对彼此来说是原子的。原子操作是指,对于访问同一个状态的所有操作(包括该操作本身)来说,这个操作是一个以原子方式(不可分割)执行的操作。

(3)内置锁
Java提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block)。同步代码块包括两部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。以Synchronized来修饰的方法就是一种横跨整个方法体的同步代码块,其中该同步代码块的锁就是方法调用所在的对象。静态的synchronized方法以Class对象所谓锁。
synchronized(lock){
//访问或修改由锁保护的共享状态
}

每个Java对象都可以用来做一个实现同步的锁,这些锁称为内置锁或监视器锁。线程在进入同步代码块之前hi自动获取锁,并且在退出同步代码块时自动释放锁。获得内置锁的唯一途径就是进入这个锁保护的同步代码块或方法。
Java的内置锁相当于一种互斥锁,这意味着最多只有一个线程能持有这种锁,当线程A尝试获取一个由线程B持有的锁时,线程A必须等待或者阻塞,直到线程B释放这个锁。如果B永远不释放锁,那么A也将永远等待。
由这个锁保护的同步代码块会以原子方式执行,多个线程在执行该代码块时也不会相互干扰。并发环境中的原子性和事务应用程序中的原子性有着相同的含义–一组语句作为一个不可分割的单元被执行。任何一个执行同步代码块的线程,都不可能看到有其它线程正在执行由同一个锁保护的同步代码块。
同步方法块:
public synchronized void service(){

}

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

你可能感兴趣的:(Java多线程提高系列)