0%

并发编程底层原理-下篇

在上篇文章中我们介绍了JMM和其一些特性,java中还有一大神器就是关键volatile,可以说是和synchronized各领风骚,其中奥妙,我们来共同探讨下。

volatile

volatile是一种同步机制,比synchronized或者lock相关类更轻量,因为使用volatile并不会发生上下文切换等开销很大的行为。

voaltile不适应与a++。代码如下:

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

import java.util.concurrent.atomic.AtomicInteger;

/**
* @author gelong
* @date 2020/6/25 21:40
*/
public class NoVolatile implements Runnable {

private static volatile int a = 0;
private static AtomicInteger atomicInteger = new AtomicInteger();

@Override
public void run() {
for (int i = 0; i < 100; i++) {
a++;
atomicInteger.incrementAndGet();
}
}

public static void main(String[] args) throws InterruptedException {
NoVolatile noVolatile = new NoVolatile();
Thread threadA = new Thread(noVolatile);
Thread threadB = new Thread(noVolatile);
threadA.start();
threadB.start();
threadA.join();
threadB.join();
System.out.println(a);
System.out.println(atomicInteger);
}
}

volatile1.jpg

volatile适用场合:boolean flag,如果一个共享变量自始至终只被各个线程赋值,而没有其他操作,那么就可以用volatile来替代sycnchronized或者替代原子变量,因为赋值本身是有原子性的,而volatile又保证了可见性且禁止重排序,所以就足以保证线程安全。

volatile还具有happends-before规则: 线程1写入了volatile变量v, 接着线程2读取了变量v, 那么线程1写入的v以及之前的操作对线程2都是可见的。

原子性

一系列的操作,要么全部执行成功,要么全部不执行,是不可分割的,这就是原子性。i++不是原子性的,它分为三步:从变量i中读取i的值->值+1->将+1后的值写回i中。当多个线程同时i++时,就会如下图一样,i的值有可能被吞掉。

i++.jpg

用synchronized可以保证原子性,因为synchronized修饰的代码块只能同时被一个线程执行。

java中的原子操作:

  • 出了long和double以外的基本类型的赋值操作。
  • 所以引用reference的赋值操作,不管是32位的机器还是64位的机器
  • j.u.c下面的atomic包中所有类的原子操作

在以前32位的JVM上,long和double的操作不是原子的,但是在现代64位JVM上是原子的。还需要注意的是:原子操作 + 原子操作 != 原子操作。

单例模式

饿汉式,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

/**
* 饿汉式(线程安全)
* @author gelong
* @date 2020/6/25 23:38
*/
public class Singleton1 {

private static final Singleton1 instance = new Singleton1();

private Singleton1() {}

public static Singleton1 getInstance() {
return instance;
}
}

懒汉式,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

/**
* 懒汉式(线程不安全)
* @author gelong
* @date 2020/6/25 23:38
*/
public class Singleton2 {

private static Singleton2 instance;

private Singleton2() {}

public static Singleton2 getInstance() {
if (instance == null) {
instance = new Singleton2();
}
return instance;
}
}

当多个线程同时调用getInstance()方法时,可能会导致instance = new Singleton2();会执行多次,导致instance有多个实例。所以我们可以对getInstance()进行加锁,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

/**
* 懒汉式(线程安全)
* @author gelong
* @date 2020/6/25 23:38
*/
public class Singleton3 {

private static Singleton3 instance;

private Singleton3() {}

public static synchronized Singleton3 getInstance() {
if (instance == null) {
instance = new Singleton3();
}
return instance;
}
}

这种做法虽然可以保证线程安全,但是效率太低。我们可以进行双重加锁校验来提高效率,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

/**
* 懒汉式(有瑕疵)
* @author gelong
* @date 2020/6/25 23:38
*/
public class Singleton4 {

private static Singleton4 instance;

private Singleton4() {}

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

这种做法虽然效率提高了,但是还是存在瑕疵,因为instance = new Singleton4();不是原子操作的。事实上在JVM中这句代码做了下面三件事情:

  1. 给instance分配内存
  2. 调用Singleton4的构造函数来初始化成员变量
  3. 将instance对象指向分配的内存空间

在JVM的即时编译存在指令的重排序的优化。也就是说上面的第二步和第三步顺序是不能完全保证的,最终的执行顺序可能是1-2-3也可能是1-3-2。如果是1-3-2,则在3执行完毕、2未执行之前,线程一刚好被调度器暂停,此时线程二刚刚进来第一重检查,看到的instance已经不是null了,线程二访问到的是一个还未初始化完成的对象。所以我们可以使用volatile关键字来禁止指令的重排序。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

/**
* 懒汉式(最优)
* @author gelong
* @date 2020/6/25 23:38
*/
public class Singleton5 {

private static volatile Singleton5 instance;

private Singleton5() {}

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

还可以用静态内部类实现单例模式,静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。即当Singleton6第一次被加载时,并不需要去加载Single,只有当getInstance()方法第一次被调用时,才会去初始化INSTANCE,第一次调用getInstance()方法会导致虚拟机加载Singler类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

/**
* 静态内部类
* @author gelong
* @date 2020/6/25 23:38
*/
public class Singleton6 {

private Singleton6() {}

private static class Single {
private static final Singleton6 INSTANCE = new Singleton6();
}

public static Singleton6 getInstance() {
return Single.INSTANCE;
}
}

在<>中说道,最佳的单例实现模式就是枚举模式。利用枚举的特性,让JVM来帮我们保证线程安全和单一实例的问题。除此之外,还不会因为反射机制和序列化而破环其单例。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13

/**
* 枚举
* @author gelong
* @date 2020/6/25 23:38
*/
public enum Singleton7 {
INSTANCE;

public void doSomething() {

}
}