Java并发编程——线程安全性

2.1 定义

当多个线程访问某个类时,不管何种调度方式,这些线程如何调度执行,在调用代码中不需要额外的同步或协同时,这个类都能表现正确的行为,这个类就是线程安全的。

2.2原子性

例如上一章说的,一个++操作需要进行读取-修改-写入三步,所以如果两个线程调用同一个非线程安全的函数,并且函数中有类似于a++这样的操作,则可能会出现两个线程同时读为9最后都设置为10,结果就将偏差1,也有可能出现其他不可预料的情况。

2.2.1 竞态条件

当某个计算的正确性取决于多个线程交替执行的顺序,就会发生竞态条件,也就是说结果取决于"运气"。

其实上面原子性的例子就是一个例子,竞态意味着竞争,而上面的例子就是两个线程竞争同一资源而导致结果不可预料,这就是一个典型的竞态条件。

2.2.2 实例:延迟初始化中的竞态条件

延迟初始化的目的是将对象的初始化操作推迟到实际被使用时才进行,也是常见的竞态条件之一——先检查后执行

@NotThreadSafe //线程不安全
public class LazyInitRace{
    private ExpensiveObject instance = null;
    
    public ExpensiveObject getInstance(){
        if(instance == null){
            instance = new ExpensiveObject();
        }
        return instance;
    }
}

假设线程A和B同时执行getInstance,A看到instance为空,从而创建一个新的ExpensiveObject实例。当B判断instance是否为空时要取决于不可预测的顺序,例如当A在初始化的过程中,B对instance进行了判断,此时为空,则会也创建一个新的对象,那么这两个线程就会返回不同的结果。

2.2.3 复合操作

++操作以及例如延迟初始化一类的操作如果要避免竞态条件都需要以原子方式执行,也就是当其中一个线程执行时,其他线程都不能访问。

例如有两个操作AB,从执行A的线程来看,另一个线程执行B时,要么执行完B要么不执行B,那么A和B对彼此来说就是原子的。
类似于递增操作这种需要进行多步的操作统称为复合操作:必须以原子方式执行以确保线程安全性
解决这个问题的其中一种操作就是原子变量类。例如用AtomicLong来代替long类型的计数器能够保证访问这个变量的操作都是原子的。

2.3 加锁机制

尽管使用原子变量可以保证操作是原子性的,但是在方法调用时仍然会出现竞态条件,因为如果有两次原子性操作,在两个线程调用时仍然会出现交替的情况,不变性条件就破坏了。

2.3.1 内置锁

Java提供一种内置的锁机制来支持原子性:同步代码块(Synchronized Block)
同步代码块包括两部分:锁的对象引用锁保护的代码块
以关键字synchronized来修饰的方法就是一种横跨整个方法体的同步代码块,每个Java 对象都可以用作一个实现同步的锁,这些锁被称为内部锁,线程进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁,而最多只有一个线程可以持有这种锁。
当我们使用synchronized修饰一个代码块时,这个同步代码块会以原子方式执行,多个线程在执行时也不会互相干扰,然而这种方法会使得同一时刻只有一个线程可以执行该方法,过于极端,效率很低。

2.3.2 重入

当某个线程请求一个由其他线程持有得锁时,请求会阻塞。而内置锁是可以重入的,如果某个线程请求一个已经由它自己持有的锁时,请求就会成功。

机制实现方法是每个锁关联一个获取计数值和一个所有者线程,线程请求一个未被持有的锁时,jvm记下锁的持有者,并且将计数值加一,当数值为0时锁将被释放。
例子

public class Widget{
    public synchronized void doSomething(){
        //..........
    }
}
public class LoggingWidget extends Widget{
    public synchronized void doSomething(){
        super.doSomething;
    }
}

代码中子类改写父类的同一方法,然后调用父类方法,如果锁不可重入,在调用父类方法时将无法获得Widget上的锁,那么将会发生死锁。

2.4 用锁来保护状态

对于可能被多个线程同时访问的可变状态变量,在访问呢它时都需要持有同一个锁,在这种情况下,状态变量时由这个锁保护的

2.5 活跃性与性能

当我们在一个servlet中使用共享状态,就需要通过servlet对象的内置锁来保护每一个状态变量,也就是对整个service方法进行同步,但代价很高。

当给出多个请求同时到达servlet时,请求将排队处理
A——>servlet
       B——>serlvet
              C——>servlet
这将大大降低程序的性能,所以我们要缩小同步代码块的作用范围,来使得某一部分操作时同步的,而同时可以处理很多请求。

猜你喜欢

转载自blog.csdn.net/MoForest/article/details/84716245