Java 并发编程之美学习笔记之基础篇二

引言

upload successful

多线程或并发编程是Java体系中的重点和难点,新购入 《Java 并发编程之美》,本博以笔记的形式记录知识要点。


Java 中共享变量的内存可见性问题

Java 内存模型规定,将所有的变量都存放在主内存中,当线程使用变量时,会把主内存里面的变量复制到自己的工作空间或者叫工作内存,线程读写变量时操作的是自己工作内存中的变量。

Synchronized 关键字

Synchronized 块是 Java 提供的一种原子性内置锁,Java 中的每个对象都可以把它当作一个同步锁来使用。

Synchronized 的使用会导致上下文切换。

Synchronized 块的内存语义是把在 Synchronized 块内使用到的变量从线程的工作内存中清除,这样在 Synchronized 块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取。退出 Synchronized 块的内存语义是把在 Synchronized 块内对共享变量的修改刷新到主内存。

除可以解决共享变量内存可见性问题外,Synchronized 经常被用来实现原子性操作。

Volatile 关键字

Synchronized 锁太笨重,因为它会带来线程上下文的切换开销。对于解决内存可见性问题,Java 还提供了一种弱形式的同步,也就是使用 Volatile 关键字。该关键字可以确保对一个变量的更新对其他线程马上可见。当一个变量被声明为 Volatile 时,线程在写入变量时不会把值缓存在寄存器或其他地方,而是会把值刷新回主内存。当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。

Volatile 虽然提供了可见性保证,但并不保证操作的原子性。

那么一般什么时候才使用 Volatile 关键字呢?

  • 写入变量值不依赖变量的当前值时。因为如果依赖当前值,将是获取——计算——写入三步操作,这三步操作不是原子性的,而 Volatile 不保证原子性。

  • 读写变量值时没有加锁。因为加锁本身已经保证了内存可见性,这时候不需要把变量声明为 Volatile 的。

Java 中的 CAS 操作

Java 提供的非阻塞的 volatile 关键字可以解决共享变量的可见性问题,但不能解决读——改——写等的原子性问题。CAS 即 Compare and Swap,其是 JDK 提供的非阻塞原子性操作。

CAS 操作有个经典的 ABA 问题,ABA 问题的产生是因为变量的状态值产生了环形转换,就是变量的值可以从 A 到 B,然后再从 B 到 A。如果变量的值只能朝一个方向转换,不构成环形,就不会存在问题。JDK 中的 AtomicStampedReference 类给每个变量的状态值都配备了一个时间戳,从而避免了 ABA 问题的产生。

Unsafe 类

JDK 的 rt.jar 包中的 Unsafe 类提供了硬件级别的原子性操作,Unsafe 类中的方法都是 native 方法,它们使用 JNI 的方式访问本地 C++ 实现库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import sun.misc.Unsafe;

public class TestUnSafe {
//获取 Unsafe 的实例
static final Unsafe unsafe = Unsafe.getUnsafe();

//记录变量 state 在类 TestUnSafe 中的偏移值
static final long stateOffset;

private volatile long state =0;

static {
try{
//获取 state 变量在类 TestUnSafe 中的偏移值
stateOffset = unsafe.objectFieldOffset(TestUnSafe.class.getDeclaredField("state"));
}catch (Exception e) {
System.out.println (e.getLocalizedMessage());
throw new Error(e);
}
}

public static void main(String[] args) {
//创建实例,并且设置 state 值为1
TestUnSafe test = new TestUnSafe();

Boolean sucess = unsafe.compareAndSwapInt(test, stateOffset, 0, 1);
System.out.println(sucess);
}

}

这段代码执行结果如下

upload successful

查看 Unsafe 源码

upload successful
代码判断是不是 Bootstrap 类加载器加载的 localClass,很明显 TestUnSafe.class 是使用 AppClassLoader 加载的,所以这里直接抛了异常。

如果真的想要实例化 Unsafe 类,可以使用反射来获取 Unsafe 类实例方法。

Java指令重排序

Java 内存模型允许编译器和处理器对指令重排序以提高运行性能,并且只会对不存在数据依赖性的指令重排序。

重排序在多线程下会导致非预期的程序执行结果,而使用 volatile 修饰 ready 就可以避免重排序和内存可见性问题。

写 volatile 变量时,可以确保 volatile 写之前的操作不会被编译器重排序到 volatile 写之后。读 volatile 变量时,可以确保 volatile 读之后的操作不会被编译器重排序到 volatile 读之前。

锁的概述

悲观锁指对数据被外界修改持保守态度,认为数据很容易就会被其它线程修改,所以在数据被处理前先对数据进行加锁,并在整个数据处理过程中,使数据处于锁定状态。

乐观锁是相对悲观锁来说的,它认为数据在一般情况下不会造成冲突,所以在访问记录前不会加排它锁,而是在进行数据提交更新时,才会正式对数据冲突与否进行检测。

乐观锁并不会使用数据库提供的锁机制,一般在表中添加 version 字段或者使用业务状态来实现。乐观锁直到提交时才锁定,所以不会产生任何死锁。

根据线程获取锁的抢占机制,锁可以分为公平锁和非公平锁。公平锁表示线程获取锁的顺序是按照线程请求锁的时间早晚来决定的。而非公平锁则在运行时闯入。

在没有公平性需求的前提下尽量使用非公平锁,因为公平锁会带来性能开销。

根据锁只能被单个线程持有还是能被多个线程共同持有,锁可以分为独占锁和共享锁。独占锁是一种悲观锁,共享锁则是一种乐观锁。

Synchronized 内部锁是可重入锁。可重入锁的原理是在锁内部维护一个线程标示,用来标示该锁目前被哪个线程占用,然后关联一个计数器。一开始计数器值为0,说明该锁没有被任何线程占用。当一个线程获取了该锁时,计数器的值会变成1,这时其他线程再来获取该锁时会发现锁的所有者不是自己而被阻塞挂起。但是当获取了该锁的线程再次获取锁时发现锁拥有者是自己,就会把计数器值加1,当释放锁后计数器值-1.当计数器值为0时,锁里面的线程标示被重置为 null,这时候被阻塞的线程会被唤醒来竞争获取该锁。

自旋锁当前线程在获取锁时,如果发现锁已经被其它线程占用,它不马上阻塞自己,在不放弃CPU使用权的情况下,多次尝试获取。自旋锁是使用CPU时间换取线程阻塞与调度的开销。