浅析并发编程(未完待续)

97

JAVA是怎么解决并发问题的: JMM(Java内存模型)

理解的第一个维度:核心知识点

JMM本质上可以理解为,Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法。具体来说,这些方法包括:

  • volatile、synchronized 和 final 三个关键字

  • Happens-Before 规则

理解的第二个维度:可见性,有序性,原子性

原子性:对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。

可见性:Java提供了volatile关键字来保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。(synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性)

有序性:在Java里面,可以通过volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。当然JMM是通过Happens-Before 规则来保证有序性的。

Happens-Before 规则(八个原则)

1.单一线程原则(Single Thread rule):

  • 在一个线程内,在程序前面的操作先行发生于后面的操作。

2.管程锁定规则(Monitor Lock Rule):

  • 一个 unlock 操作先行发生于后面对同一个锁的 lock 操作,同一对象解锁操作优先。

3.volatile 变量规则(Volatile Variable Rule):

  • 对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。

4.线程启动规则(Thread Start Rule):

  • Thread 对象的 start() 方法调用先行发生于此线程的每一个动作。

5.线程加入规则(Thread Join Rule):

  • Thread 对象的结束先行发生于 join() 方法返回。

6.线程中断规则(Thread Interruption Rule):

  • 对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 interrupted() 方法检测到是否有中断发生。

7.对象终结规则(Finalizer Rule):

  • 一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。

8.传递性(Transitivity):

  • 如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C。

线程安全:

一个类在可以被多个线程安全调用时就是线程安全的。

1. 不可变

不可变(Immutable)的对象一定是线程安全的,不需要再采取任何的线程安全保障措施。只要一个不可变的对象被正确地构建出来,永远也不会看到它在多个线程之中处于不一致的状态。

不可变的类型:

  • final 关键字修饰的基本数据类型

  • String

  • 枚举类型

  • Number 部分子类,如 Long 和 Double 等数值包装类型,BigInteger 和 BigDecimal 等大数据类型。但同为 Number 的原子类 AtomicInteger 和 AtomicLong 则是可变的。

对于集合类型,可以使用 Collections.unmodifiableXXX() 方法来获取一个不可变的集合。

public class ImmutableExample {
    public static void main(String[] args) {
        Map<String, Integer> unmodifiableMap = Collections.unmodifiableMap(new HashMap<>());
        unmodifiableMap.put("a", 1);
    }
}

Collections.unmodifiableXXX() 先对原始的集合进行拷贝,在对集合进行修改的方法都直接抛出异常,使得新集合无法被修改。

2. 绝对线程安全

不管运行时环境如何,调用者都不需要任何额外的同步措施。

3.相对线程安全

相对线程安全需要保证对这个对象单独的操作是线程安全的,在调用的时候不需要做额外的保障措施。但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。

在 Java 语言中,大部分的线程安全类都属于这种类型,例如 Vector、HashTable、Collections 的 synchronizedCollection() 方法包装的集合等。