Java 多线程面试题之一

谈谈对 Volatile 的理解

Volatile 的特性

Volatile 是 Java 虚拟机提供的轻量级的同步机制,具有以下三大特性:

  • 保证可见性
  • 不保证原子性
  • 禁止指令重排

Java 内存模型

JMM(Java 内存模型,简称 JMM)本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段、静态字段和构成数组对象的元素)的访问方式,即屏蔽掉 Java 程序在各种不同的硬件和操作系统对内存的访问的差异,这样就可以实现 Java 程序在各种不同的平台上都能达到内存访问的一致性。

JMM 关于同步的规定如下:

  • 1)线程解锁前,必须把共享变量的值刷新回主内存
  • 2)线程加锁前,必须读取主内存的最新值到自己的工作内存
  • 3)加锁解锁是同一把锁

由于 JVM 运行程序的实体是线程,而每个线程创建时 JVM 都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而在 Java 内存模型中规定所有变量都存储到主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取、复制等)必须在工作内存中进行。首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:

jmm-1

Java 内存模型的特性

Java 内存模型围绕着并发过程中如何处理原子性、可见性和有序性这三个特征来设计的。

原子性(Atomicity):

由 Java 内存模型来直接保证原子性的变量操作包括 read、load、use、assign、store、write 这六种操作,虽然存在 long 和 double 的特例,但基本可以忽略不计,目前虚拟机基本都对其实现了原子性。如果需要更大范围的控制,lock 和 unlock 也可以满足需求。lock 和 unlock 虽然没有被虚拟机直接提供给用户使用,但是提供了字节码层次的指令 monitorenter 和 monitorexit 对应这两个操作,对应到 Java 代码就是 synchronized 关键字,因此在 synchronized 块之间的代码都具有原子性。

可见性(Visibility):

可见性是指一个线程修改了一个变量的值后,其他线程立即可以感知到这个值的修改。正如前面所说,volatile 类型的变量在修改后会立即同步给主内存,在使用的时候会从主内存重新读取,是依赖主内存为中介来保证多线程下变量对其他线程的可见性的。除了 volatile 之外,synchronized 和 final 也可以实现可见性。synchronized 关键字是通过 unlock 之前必须把变量同步回主内存来实现的,final 则是在初始化后就不会更改,所以只要在初始化过程中没有把 this 指针传递出去也能保证对其他线程的可见性。

有序性:

有序性从不同的角度来看是不同的。单纯单线程来看都是有序的,但到了多线程就会跟我们预想的不一样。可以这么说:如果在本线程内部观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句说的就是线程内表现为串行的语义,后半句指的是指令重排现象和主内存与工作内存之间同步存在延迟的现象。保证有序性的关键字有 volatile 和 synchronized,其中 volatile 禁止了指令重排序,而 synchronized 则由一个变量在同一时刻只能被一个线程对其进行 lock 操作来保证。

总结:synchronized 对三种特性都有支持,虽然简单,但是如果无控制地滥用对性能就会产生较大影响。

Java 内存模型的可见性问题

各个线程对主内存中共享变量的操作,其本质都是各个线程各自拷贝共享变量到自己的工作内存中进行操作后再写回主内存中的。这就可能存在一个线程 A 修改了共享变量 X 的值但还未写回主内存时,另一个线程 B 又对主内存中同一个共享变量 X 进行操作,但此时 A 线程工作内存中的共享变量 X 对线程 B 来说并不是可见的,这种工作内存与主内存同步存在延迟现象就会造成可见性问题。此时可以使用 synchronized 或 volatile 关键字解决该问题,两者都可以使一个线程修改后的变量立即对其他线程可见。

Volatile 的验证代码

Volatile 保证可见性的验证代码

★展开代码★
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
* 1 验证volatile的可见性
* 1.1 加入int number=0,number变量之前没有添加volatile关键字修饰,没有可见性
* 1.2 添加了volatile关键字,可以解决可见性问题
*/
public class VolatileDemo
{
public static void main(String[] args)
{
MyData data = new MyData();

new Thread(() - >
{
System.out.println(Thread.currentThread().getName() + " thread come in");

try
{
TimeUnit.SECONDS.sleep(3);
}
catch(InterruptedException e)
{
e.printStackTrace();
}

data.setNumber();
System.out.println(Thread.currentThread().getName() + " thread set number is " + data.number);
}, "AAA").start();

while(data.number == 0)
{
// main线程一直在这里循环等待,直到number的值不再等于零
}

System.out.println(Thread.currentThread().getName() + " thread is over, the number is " + data.number);
}
}

class MyData
{
// int number = 0;

// volatile可以保证可见性,即可以及时通知其他线程,主内存中的变量值已经被修改
volatile int number = 0;

public void setNumber()
{
this.number = 60;
}
}
1
2
3
AAA thread come in
AAA thread set number is 60
main thread is over, the number is 60

Volatile 不保证原子性的验证代码

Volatile 不保证原子性,这里的原子性是指不可分割,完整性,即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割;需要整体完整,要么同时成功,要么同时失败。基于 Volatile 变量的运算在并发下不是线程安全的。Volatile 的规则保证了 read、load、use 的顺序和连续性,同理 assign、store、write 也是顺序和连续的。也就是这几个动作是原子性的,但是对变量的修改,或者对变量的运算,却不能保证是原子性的。如果对变量的修改是分为多个步骤的,那么多个线程同时从主内存拿到的值是最新的,但是经过多步运算后回写到主内存的值是有可能存在覆盖情况发生的。

★展开代码★
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
31
32
33
34
35
36
37
38
/**
* 1. 验证volatile不保证原子性
*/
public class VolatileDemo2
{
public static void main(String[] args)
{
MyData2 data = new MyData2();

for(int i = 0; i < 20; i++)
{
new Thread(() - >
{
for(int j = 0; j < 1000; j++)
{
data.add();
}
}).start();
}

while(Thread.activeCount() > 1)
{
Thread.yield();
}

System.out.println("the number is " + data.number);
}
}

class MyData2
{
volatile int number = 0;

public void add()
{
this.number++;
}
}
1
the number is 15386

上述代码就是对 volatile 类型的变量启动了 20 个线程,每个线程对变量执行 1000 次加 1 操作,如果 volatile 变量并发操作没有问题的话,那么结果应该是输出 20000,但是运行结果每次大概率都是小于 20000,这就是因为 number++ 操作不是原子性的(图解),是分多个步骤完成的。假设两个线程 a、b 同时取到了主内存的值是 0,这是没有问题的,在进行 ++ 操作的时候假设线程 a 执行到一半,线程 b 执行完了,这时线程 b 立即同步给了主内存,主内存的值为 1,而线程 a 此时也执行完了,同步给了主内存,此时的值仍然是 1,线程 b 的结果被覆盖掉了。

解决 Volatile 不保证原子性的问题

由于 Volatile 不保证原子性,导致基于 Volatile 变量的运算在并发下不是线程安全的,此时可以使用 AtomicInteger 这样的原子包装类来解决。

★展开代码★
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
31
32
33
34
35
36
37
38
39
/**
* 1. 使用AtomicInteger解决Volatile不保证原子性的问题
*/
public class VolatileDemo3
{
public static void main(String[] args)
{
MyData3 data = new MyData3();

for(int i = 0; i < 20; i++)
{
new Thread(() - >
{
for(int j = 0; j < 1000; j++)
{
data.add();
}
}).start();
}

while(Thread.activeCount() > 1)
{
Thread.yield();
}

System.out.println("the number is " + data.number.get());
}
}

class MyData3
{
// AtomicInteger类里的变量包含了volatile关键字
AtomicInteger number = new AtomicInteger();

public void add()
{
this.number.getAndIncrement();
}
}

单例模式中 Volatile 的使用分析

★展开代码★
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
31
32
33
34
35
36
37
38
39
40
41
/**
* 单例模式
* 1) DCL(双端检锁)机制不一定是线程安全的,原因是有指令重排序的存在
* 2) 原因在多线程环境下,某一个线程执行到第一个检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化
* 3) 指令重排只会保证串行语义的执行一致性(单线程),但并不会关心多线程间的语义一致性。所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题
*/
public class VolatileDemo4
{
private static VolatileDemo4 demo;

private VolatileDemo4()
{
System.out.println("inited ...");
}

public static VolatileDemo4 getInstance()
{
if(demo == null)
{
synchronized(VolatileDemo4.class)
{
if(demo == null)
{
demo = new VolatileDemo4();
}
}
}
return demo;
}

public static void main(String[] args)
{
for(int i = 0; i < 10; i++)
{
new Thread(() - >
{
VolatileDemo4.getInstance();
}).start();
}
}
}

其中代码 demo = new VolatileDemo4(); 可以分为以下三步完成(伪代码):

1
2
3
memory = allocate(); //1.分配对象内存空间
init(memory); //2.初始化对象
instance = memory; //3.设置 instance 指向刚分配的内存地址,此时 instance != null

步骤二和步骤三不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。指令重排后的伪代码如下:

1
2
3
memory = allocate(); //1.分配对象内存空间
instance = memory; //3.设置 instance 指向刚分配的内存地址,此时 instance != null,但是对象还没有完成初始化
init(memory); //2.初始化对象

指令重排只会保证串行语义的执行一致性(单线程),但并不会关心多线程间的语义一致性。所以当一个线程访问 instance 不为 null 时,由于 instance 实例未必已初始化完成,也就造成了线程安全问题。为了保证线程安全,可以加入 volatile 关键字来禁止指令重排,完整的示例代码如下:

★展开代码★
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
31
32
33
34
35
36
37
38
39
/**
* 单例模式中使用Volatile
* 1) DCL(双端检锁)机制不一定是线程安全的,原因是有指令重排序的存在,加入volatile可以禁止指令重排
*/
public class VolatileDemo4
{
private static volatile VolatileDemo4 demo;

private VolatileDemo4()
{
System.out.println("inited ...");
}

public static VolatileDemo4 getInstance()
{
if(demo == null)
{
synchronized(VolatileDemo4.class)
{
if(demo == null)
{
demo = new VolatileDemo4();
}
}
}
return demo;
}

public static void main(String[] args)
{
for(int i = 0; i < 10; i++)
{
new Thread(() - >
{
VolatileDemo4.getInstance();
}).start();
}
}
}

CAS

面试内容

一般由浅入深,涉及的内容为: 原子类 –> CAS –> UnSafe –> CAS 底层思想 –> ABA 问题 –> 原子更新引用 –> 如何解决 ABA 问题

CAS 是什么

CAS(Conmpare And Swap,比较和交换)是一条 CPU 并发原语。它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,否则继续比较到两者相同为止,这个过程是原子的。CAS 并发原语体现在 Java 语言中就是 sun.misc.Unsafe 类中的各个方法。调用 UnSafe 类中的 CAS 方法,JVM 会帮我们实现 CAS 汇编指令。这是一种完全依赖于硬件的功能,通过它可以实现原子操作。由于 CAS 是一种系统原语,原语属于操作系统用语范畴,是由若干指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断;也就是说 CAS 是一条 CPU 的原子指令,不会造成所谓的数据不一致性问题。java.util.concurrent 包中借助 CAS 实现了区别于 synchronouse 同步锁的一种乐观锁。

CAS 底层原理

谈谈对 Unsafe 的理解

Unsafe 是 CAS 的核心实现类,由于 Java 方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe 相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe 类存在于 sun.misc 包中,其内部方法操作可以像 C/C++ 的指针一样直接操作内存,即 Java 中 CAS 操作的执行依赖于 Unsafe 类的方法。特别注意:在 JDK 8 中,Unsafe 类中的大多数方法都是 native 修饰的,也就是说 Unsafe 类中的方法都直接调用操作系统底层资源来执行相应任务。在 AtomicInteger 的源码里(如下第一张图),变量 valueOffset 表示该变量在内存中的偏移地址,因为 Unsafe 就是根据内存偏移地址获取数据的。变量 value 用 volatile 修饰,保证了多线程之间的内存可见性。

AtomicInteger 类的源码

atommic-integer-source

AtomicInteger 类与 Unsafe 类的源码调用分析

atommic-integer-source-2

假设线程 A 和线程 B 两个线程同时执行 getAndAddInt() 方法(分别在不同的 CPU 上):

  • 1)AtomicInteger 里面的 value 原始值为 3,即主内存中 AtomicInteger 的 value 为 3,根据 JMM 模型,线程 A 和线程 B 各自拷贝一份值为 3 的 value 的副本到各自的工作内存中
  • 2)线程 A 通过 getIntVolatile(var1,var2) 拿到 value 值为 3,这时候线程 A 突然被挂起
  • 3)线程 B 也通过 getIntVolatile(var1,var2) 拿到 value 值为 3,此时刚好线程 B 没有被挂起,并执行 compareAndSwapInt() 方法比较主内存中的值也是 3,成功修改主内存中的值为 4,线程 B 至此完成任务操作
  • 4)这时候线程 A 恢复,执行 compareAndSwapInt() 方法比较,发现自己手里的数值和主内存中的数字 4 不一致,说明该值已经被其他线程抢先一步修改了,那 A 线程修改失败,只能重新操作一遍
  • 5)线程 A 重新获取 value 的值,因为变量 value 是 volatile 修饰,所以其他线程对它的修改,线程 A 总是能够感知到,线程 A 继续执行 compareAndSwapInt() 方法进行比较和交换,直到成功为止

值得一提的是,UnSafe 类中的 compareAndSwapInt() 是一个本地方法,该方法的具体实现位于 unsafe.cpp 中。

CAS 的缺点

  • 循环时间长开销大:如果 CAS 失败,会一直继续尝试;如果 CAS 长时间一直不成功,可能会给 CPU 带来很大的开销
  • 只能保证一个共享变量的原子操作:对于多个共享变量操作时,循环 CAS 就无法保证操作的原子性了,此时可以使用锁来保证原子性
  • 引出了 ABA 问题

CAS 的 ABA 问题

在原子类(如 AtomicInteger)中 CAS 会导致 “ABA 问题”,这是因为 CAS 算法实现的一个重要前提是需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差里数据可能会发生变化。比如一个线程 One 从内存位置 V 中取出 A,这个时候另一个线程 Two 也从内存中取出 A,并且线程 Two 进行了一些操作将内存位置 V 中的值改为 B,然后线程 Two 又将内存位置 V 的数据改为 A,这时候线程 One 进行 CAS 操作会发现内存中仍然是 A,然后线程 One 就认为操作成功了。尽管线程 One 的 CAS 操作成功,但是不代表这个过程就是没问题的,这里有点狸猫换太子的意思。

原子更新引用

原子引用

JDK 提供了 AtomicInteger、AtomicBoolean、AtomicLong 等原子类,但如果需要对自定义的类(如 User 类)进行原子包装,那么则需要使用原子引用类 AtomicReference 来实现,示例代码如下:

★展开代码★
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
* 原子引用类 AtomicReference 的使用
*/
public class AtomicReferenceDemo {

public static void main(String[] args) {
User user1 = new User(20, "Jim");
User user2 = new User(24, "Tom");

AtomicReference<User> atomicReference = new AtomicReference<User>();
atomicReference.set(user1);

boolean result = atomicReference.compareAndSet(user1, user2);
System.out.println(result + " " + atomicReference.get());

boolean result2 = atomicReference.compareAndSet(user1, user2);
System.out.println(result2 + " " + atomicReference.get());
}
}

class User {

private int age;

private String name;

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public User(int age, String name) {
this.age = age;
this.name = name;
}

@Override
public String toString() {
return "User [age=" + age + ", name=" + name + "]";
}
}
1
2
true User [age=24, name=Tom]
false User [age=24, name=Tom]

版本号原子引用

普通原子类(AtomicInteger)或者原子引用类(AtomicReference)会产生 ABA 问题,示例代码如下:

★展开代码★
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
/**
* 会产生 ABA 问题的代码
*/
public class ABADemo {

public static AtomicReference<Integer> atomicReference = new AtomicReference<Integer>(100);

public static void main(String[] args) {
new Thread(() -> {
atomicReference.compareAndSet(100, 101);
atomicReference.compareAndSet(101, 100);
}, "t1").start();

new Thread(() -> {
try {
// 暂定两秒t2线程,保证上面的t1线程完成了一次ABA操作
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}

boolean result = atomicReference.compareAndSet(100, 102);
System.out.println(result + " " + atomicReference.get());
}, "t2").start();
}

}
1
true 102

使用 AtomicStampedReference 版本号原子引用类解决 ABA 问题,示例代码如下:

★展开代码★
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
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* 使用 AtomicStampedReference 版本号原子引用类解决 ABA 问题
*/
public class AtomicStampedReferenceDemo {

public static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(100, 1);

public static void main(String[] args) {
new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "初始版本号: " + stamp);

try {
// 暂定一秒t1线程,保证下面的t2线程拿到的初始版本号与t1的初始版本号一致
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}

atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "第一次修改后的版本号: " + atomicStampedReference.getStamp());

atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "第二次修改后的版本号: " + atomicStampedReference.getStamp());
}, "t1").start();

new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "初始版本号: " + stamp);

try {
// 暂定两秒t2线程,保证上面的t1线程完成了一次ABA操作
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}

boolean result = atomicStampedReference.compareAndSet(100, 102, stamp, stamp + 1);
System.out.println(Thread.currentThread().getName() + "是否修改成功:" + result + ",当前实际最新的版本号为: " + atomicStampedReference.getStamp());
System.out.println("当前实际最新值为:" + atomicStampedReference.getReference());
}, "t2").start();
}
}
1
2
3
4
5
6
t1初始版本号: 1
t2初始版本号: 1
t1第一次修改后的版本号: 2
t1第二次修改后的版本号: 3
t2是否修改成功:false,当前实际最新的版本号为: 3
当前实际最新值为:100

ABA 问题解决总结

原子引用 + 版本号(类似时间戳)机制,可以直接使用 JDK 提供的版本号原子引用类 AtomicStampedReference 来解决 ABA 问题。

Java 锁

公平锁和非公平锁

公平锁和非公平锁介绍

  • JUC 包中的公平锁和非公平锁用的都是 ReentrantLock
  • 公平锁:是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先到先得
  • 非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。在高并发的情况下,有可能会造成优先级反转或者饥饿现象

公平锁和非公平锁的区别

  • 公平锁:公平锁就是很公平,在并发情况下,每个线程在获取锁时会查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照 FIFO 的规则从队列中取到自己

  • 非公平锁:非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采取类似公平锁那种方式(等待队列)处理

  • JUC 包中 ReentrantLock 的创建可以指定构造函数的 boolean 类型来得到公平锁或非公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。对于 Synchronized 而言,也是一种非公平锁。

可重入锁(递归锁)

可重入锁(递归锁)介绍

可重入锁(递归锁)指的是同一个线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一线程在外层方法获取锁的时候,在进入内层方法会自动获取锁(代码如下)。也就是说,线程可以进入任何一个它已经拥有的锁所有同步着的代码块。ReentrantLockSynchronized 都是典型的可重入锁(递归锁)。可重入锁最大的作用是可以避免死锁。

1
2
3
4
5
6
7
8
9
10
public class LockTest {

public synchronized void method1() {
method2();
}

public synchronized void method2() {

}
}

验证 ReentrantLock 是可重入锁的代码

★展开代码★
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
31
32
33
34
35
36
37
38
39
import java.util.concurrent.locks.ReentrantLock;

/**
* 可重入锁(递归锁) ReentrantLock 验证代码
*/
public class LockTest {

private static ReentrantLock lock = new ReentrantLock();

public static void get() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t invoked get()");
set();
} finally {
lock.unlock();
}
}

public static void set() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t invoked set()");
} finally {
lock.unlock();
}
}

public static void main(String[] args) {
new Thread(() -> {
get();
}, "t1").start();

new Thread(() -> {
get();
}, "t2").start();
}

}
1
2
3
4
t1	 invoked get()
t1 invoked set()
t2 invoked get()
t2 invoked set()

自旋锁(SpinLock)

自旋锁介绍

自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下切换的消耗,缺点是循环获取锁的操作会消耗 CPU 资源。在 CAS 中 Unsafe 类使用自旋锁的代码如下图:

juc-spinlock-1

验证自旋锁的代码

★展开代码★
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import java.util.concurrent.atomic.AtomicReference;

/**
* 自旋锁验证代码
*/
public class LockTest {

private AtomicReference<Thread> atomicReference = new AtomicReference<Thread>();

public void lock() {
System.out.println(Thread.currentThread().getName() + "\t come in");
Thread thread = Thread.currentThread();
// 自旋锁实现
while (!atomicReference.compareAndSet(null, thread)) {
// do something
}
System.out.println(Thread.currentThread().getName() + "\t lock");
}

public void unlock() {
System.out.println(Thread.currentThread().getName() + "\t unlock");
Thread thread = Thread.currentThread();
// 释放自旋锁
atomicReference.compareAndSet(thread, null);
}

public static void sleep(long mills) {
try {
Thread.sleep(mills);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public static void main(String[] args) {
LockTest4 test = new LockTest4();

new Thread(() -> {
test.lock();
sleep(5000);
test.unlock();
}, "t1").start();

sleep(1000);

new Thread(() -> {
test.lock();
test.unlock();
}, "t2").start();
}

}
1
2
3
4
5
6
t1	 come in
t1 lock
t2 come in
t1 unlock
t2 lock
t2 unlock

独占锁(写锁)与共享锁(读锁)

独占锁(写锁)与共享锁(读锁)介绍

  • 独占锁(写锁):指该锁一次只能被一个线程所持有,对于 ReentrantLockSynchronized 而言都是独占锁(写锁)
  • 共享锁(读锁):指该锁可被多个线程所持有,对于 ReentrantReadWriteLock,其读锁是共享锁,其写锁是独占锁。读锁(共享锁)可保证并发读是非常高效的,其中读写、写读、写写的过程是互斥的,而读读是可以共存的

验证独占锁(写锁)与共享锁(读锁)的代码

★展开代码★
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/*
* 验证读写锁,简单模拟MyBatis的缓存实现
* 多个线程同时读同一个资源没有问题,所以为了满足并发量,读取共享资源应该可以同时进行,但是写共享资源只能有一个线程
* 写操作:原子+独占,整个过程必须是一个完整的统一体,中间不许被分割,被打断
*/
class MyCache {

private volatile Map<String, Object> map = new HashMap<>();

private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();

public void put(String key, Object value) {
reentrantReadWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "\t 正在写入:" + key);
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "\t 写入完成");
} catch (Exception e) {
e.printStackTrace();
} finally {
reentrantReadWriteLock.writeLock().unlock();
}
}

public void get(String key) {
reentrantReadWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "\t 正在读取:" + key);
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object result = map.get(key);
System.out.println(Thread.currentThread().getName() + "\t 读取完成" + result);
} catch (Exception e) {
e.printStackTrace();
} finally {
reentrantReadWriteLock.readLock().unlock();
}
}

}

public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
for (int i = 1; i <= 5; i++) {
final int tempInt = i;
new Thread(() -> {
myCache.put(tempInt + "", tempInt + "");
}, String.valueOf(i)).start();
}

for (int i = 1; i <= 5; i++) {
final int tempInt = i;
new Thread(() -> {
myCache.get(tempInt + "");
}, String.valueOf(i)).start();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2	 正在写入:2
2 写入完成
1 正在写入:1
1 写入完成
5 正在写入:5
5 写入完成
1 正在读取:1
4 正在读取:4
1 读取完成1
4 读取完成null
4 正在写入:4
4 写入完成
3 正在写入:3
3 写入完成
3 正在读取:3
5 正在读取:5
2 正在读取:2
3 读取完成3
5 读取完成5
2 读取完成2