线程安全性

线程安全

要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享和可变状态的访问。

从非正式的意义来讲,对象的状态是指存储在状态变量(例如实例或静态域)中的数据,对象的状态可能包含其它依赖对象的域。

一个对象是否需要实现线程安全,取决于它是否会被多个线程访问。要使得对象是线程安全的,需要采取同步机制来协同对对象可变状态的访问。

Java同步机制:关键字synchronized、volatile类型的变量、显式锁(Lock)、原子变量。

无状态的对象一定是线程安全的。

原子性

竞态条件(Race Condition):计算的正确性取决于多个线程的交替执行时序时,就会发生竞态条件。例如“读取-修改-写入”操作和“先检查后执行”操作。

  • “读取-修改-写入”操作:最经典的就是自增操作。例如count++操作,该操作是非原子性的,实际上它包含三个操作:读取count的值,将值加一,将计算出的结果写入count。如果此时多个线程都访问count并++,那么不能保证最后结果正确。
  • “先检查后执行”操作:经典的例子就是单例模式

复合操作:要避免竞态条件问题就要保证在某个线程修改变量时,通过某种方式阻止其他线程使用该变量。“读取-修改-写入”操作和“先检查后执行”操作统称为复合操作:包含了一组必须以原子方式执行的操作以确保线程安全性。

加锁机制是Java中用于确保原子性的内置机制。

加锁机制

内置锁:

同步代码块(Synchronized Block)。内置锁可以支持原子性和可见性。同步代码块包含两部分:

  • 一个作为锁的对象引用;
  • 一个作为由这个锁保护的代码块;

其中该同步代码块的锁就是方法调用所在的对象。静态的synchronized方法以class对象作为锁。

Java的内置锁相当于一种互斥体(或互斥锁),这意味着最多只有一个线程能够持有这种锁。

重入:

当某个线程请求一个其他线程持有的锁时,就会阻塞。因为内置锁是可重入的,如果某个线程试图获得一个已经被自己占有的锁,就会成功。“重入”意味着获取锁的操作的粒度是“线程”,而不是“调用“。

重入的一种实现方法是为每一个锁设置一个计数器,同一个线程再次获取这个锁,计数值加一,而当线程退出同步代码块时,计数值减一。

如果内置锁是不可重入的,下面的代码将会死锁:

public class Widget{
    public synchronized void doSomeThing(){ ... }
}

public class LoggingWidget extends Widget{
    public synchronized void doSomeThing(){
        //如果是非重入的锁,获取Widget上的锁时就会发生死锁
        super.doSomeThing();
        ...
    }
}

锁的使用规则:

  • 对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁。这种情况也可以成为该变量是由这个锁保护的。
  • 每个共享的和可变的状态变量都应该只由一个锁来保护,从而使维护人员知道是哪一个锁。
  • 对于每个包含多个变量的不变性条件,其中涉及的所有变量都需要由同一个锁来保护。

注意:

对象的内置锁和对象的状态之间没有内在的关联。当获取对象关联的锁时,并不能阻止其他线程访问该对象,只能阻止其他线程获取同一个锁。

可以使用@GuardBy标签标注使用的是哪一个锁。

猜你喜欢

转载自my.oschina.net/HuoQibin/blog/1806015