Java并发编程实战:第3章 对象的共享

1. 可见性

其实关键字 synchronized 关键字不仅仅能用于实现原子性或者确定 “临界区(Critical Section)”

同步还有另一个重要的方面:内存可见性(Memory Visibility)。

我们不仅希望防止某个线程正常使用对象状态而另一个线程再同时修改该状态,而且希望确保当一个线程修改了对象状态之后,其他线程能够看到发生状态变化。我们可以通过同步或者类库中内置的同步保证对象安全地发布。

1.1 可见性

  • 对于单线程而言,如果向某个变量先写入值,然后在没有其他写入操作的情况下,读取这个变量,其值是正确的。

  • 多线程中。读操作、写操作,在不同的线程中执行,可能会导致读写不一致问题。

    1
    2
    3
    4
    5
    6
    7
    8
    data=1

    1. 线程A,读取data=1
    2. 线程A,修改data=2(还未写入)
    3. 线程B,读取data=1
    4. 线程A,写入data=2(此时data值为2)

    此时线程B读取的内容是data=1,而线程A已经将data修改为2了

1.2 在没有同步的情况下共享变量

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
/**
* Created by osys on 2022/08/28 21:48.
*/
public class NoVisibility {
/** 默认值为 false */
private static boolean ready;
/** 默认值为 0 */
private static int number;

private static class ReaderTread extends Thread {
@Override
public void run() {
this.setName("Reader");
while (!ready) {
/*
当一个线程使用了 Thread.yield() 方法之后,它就会把自己CPU执行的时间让掉,让自己或者其它的线程运行。

打个比方:
现在有很多人在排队上厕所,好不容易轮到这个人上厕所了,突然这个人说:“我要和大家来个竞赛,看谁先抢到厕所!”。
然后所有的人在同一起跑线冲向厕所,有可能是别人抢到了,也有可能他自己有抢到了。
我们还知道线程有个优先级的问题,那么手里有优先权的这些人就一定能抢到厕所的位置吗?
不一定的,他们只是概率上大些,也有可能没特权的抢到了。
*/
Thread.yield();
}
System.out.println(number);
}
}

// main 线程
public static void main(String[] args) {
// Reader 线程
new ReaderTread().start();
number = 66;
ready = true;
}
}
  1. 这里一共有两个线程,Main 线程和 Reader 线程
  2. 当多个线程在没有同步的情况下共享数据时,Main 线程和 Reader 线程都将访问共享变量 ready 和 number
  3. Main 线程启动 Reader 线程,然后将 number 设为66,并将 ready 设为 true。
  4. Reader 线程一直循环直到发现 ready 的值变为true,然后输出 number 的值。
  5. 虽然 NoVisibility 看起来会输出66,但事实上很可能输出 0,或者根本无法终止。
  6. 这是因为在代码中没有使用足够的同步机制,因此无法保证主线程写入的 ready 值和 number 值对于读线程来说是可见的。

1.3 失效数据

1
2
3
4
5
6
7
8
data=1

1. 线程A,读取data=1
2. 线程A,修改data=2(还未写入)
3. 线程B,读取data=1
4. 线程A,写入data=2(此时data值为2)

此时线程B读取的内容是data=1,而线程A已经将data修改为2了

当线程A,和线程B获取的 data 数据均为 1 时。线程A对数据进行更改,此时线程B中获取的 data=1 数据为失效数据。

程序清单 3-2、3-3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import net.jcip.annotations.NotThreadSafe;

/**
* Created by osys on 2022/08/28 21:48.
*/
@NotThreadSafe
public class MutableInteger {
private Integer value;

public Integer getValue() {
return value;
}

public void setValue(Integer value) {
this.value = value;
}
}

线程不安全:MutableInteger.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.ThreadSafe;

/**
* Created by osys on 2022/08/28 21:48.
*/
@ThreadSafe
public class SynchronizedInteger {
@GuardedBy("this")
private Integer value;

public synchronized Integer getValue() {
return value;
}

public synchronized void setValue(Integer value) {
this.value = value;
}
}

这里使用了两个同步代码块,来进行get、set操作。不过针对不同线程分别调用这两个方法,还是换出现数据安全性问题(失效值)。

1.4 非原子的 64 位操作

计算机中存储数据和计算数据都是基于二进制来做的。在 Java 的基本数据类型中,long 和 double 是 64 位的。

类型 占用字节 占用位数 数值长度
byte 1 8 -128~127(-2的7次方到2的7次方-1)
short 2 16 -32768~32767(-2的15次方到2的15次方-1)
int 4 32 -2的31次方到2的31次方-1
long 8 64 -2的63次方到2的63次方-1
float 4 32 (e-45是乘以10的负45次方,e+38是乘以10的38次方) (2的-149次方~2的128次方-1)
double 8 64 (2的-1074次方 ~ 2的1024次方)
char 2 16
boolean 1

位数中,不同位代表的含义不一样,有符号位,指数位,尾数位。

非原子的 64 位操作

  • 最低安全性:当线程在没有同步的情况下读取变量时,可能会得到一个失效值,但至少这个值是由之前某个线程设置的,而不再一个随机值。
  • Java内存模型要求,变量的读取操作和写入操作都必须是原子操作。对于非 volatile 类型的 64 位数值变量(longdouble),JVM允许将64位的读操作或写操作分解为两个32为的操作。
  • 当读取一个非 volatile 类型的 64 位数值变量(longdouble) 时,如果对改变了的 操作和 操作在不同的线程中执行,那么很可能会读取到某个值的高32位和另一个值的低32位。
  • 因此,即使不考虑失效数据问题,在多线程中使用共享且可变的longdouble 等类型的变量也是不安全的,需要用 volatile 关键字来声明,或者用 保护起来。

1.5 加锁与可见性

内置锁可以用于确保某个线程以一种可预测的方式来查看另一个线程的执行结果。

1

当线程A执行某个同步代码块时,线程B随后进入由同一个锁保护的同步代码块。

在这种情况下可以保证,当线程B执行由锁保护的同步代码块时,可以看到前面线程A在同一个在代码块中的所有操作结果。

其实这就是为了确保某个线程写入该变量的值,对于其他线程来说都是可见的。

2. Volatile 关键字

2.1 什么是重排序

  • 在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序

    1
    源代码 --> 编译器优化的重排序 --> 指令级并行的重排序 --> 内存系统的重排序 --> 最终指向指令
  • 编译器优化的重排序

    编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

  • 指令级并行的重排序

    处理器将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

  • 内存系统的重排序

    处理器使用缓存和读/写缓冲区,使得加载和存储操作看上去可能是在乱序执行。

2.2 可见性

简单的理解,当一个线程修改了共享变量时,另一个线程可以读取到这个修改后的值。

2.3 volatile 关键字

Java 中一种稍弱的同步机制。用来确保将变量的更新操作通知到其他线程。

简单理解就是,读取 volatile 变量,总数返回最新写入的值。

对于以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.ThreadSafe;

/**
* Created by osys on 2022/08/28 21:48.
*/
@ThreadSafe
public class SynchronizedInteger {
@GuardedBy("this")
private Integer value;

public synchronized Integer getValue() {
return value;
}

public synchronized void setValue(Integer value) {
this.value = value;
}
}

可以将其修改位用 volatile 关键字修饰:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.ThreadSafe;

/**
* Created by osys on 2022/08/28 21:48.
*/
@ThreadSafe
public class SynchronizedInteger {
@GuardedBy("this")
private volatile Integer value;

public Integer getValue() {
return value;
}

public void setValue(Integer value) {
this.value = value;
}
}
  • volatile 修饰的变量,编译器与运行时,都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。
  • 在访问 volatile 变量时,不会执行加锁操作,因此也就不会指向线程阻塞,因此 volatile 变量是一种比 synchronized 关键字更轻量级的同步机制。

2.4 分析 volatile 变量

从内存可见性的角度来看,写入 volatile 变量相当于退出同步代码块,读取 volatile 变量相当于进入同步代码块。

1
2
volatile int value1 = 666;
int value2 = 123;
  1. 非 volatile 变量可以这样理解:

    1
    2
    3
    4
    5
    6
    7
    单线程:
    1. 平时修改数据(value2),首先读取数据(将value2拷贝一份出来) --- 每个线程读取都会拷贝一份value2=123
    2. 对(将value2=123) 拷贝出来的数据进行修改,value2=456
    3. 然后将 value2=456 写入到本体中,完成了修改操作

    多线程:
    4. 如果多线程情况下,每个线程同时读取,分别都拷贝了一份(value2=123),其中一个线程对其进行了修改并写入(value2=456),其他线程已读取的数据还是前面的(value2=123)
  2. 从内存角度来看 volatile 变量:

    1
    2
    3
    1. 有A、B两个线程,读取 volatile 变量,就相当于进入了 volatile 变量的同步代码块中
    2. 线程A修改了 volatile 变量,线程B也会同步知道线程 A修改了 volatile 变量
    3. 线程A修改了 volatile 变量后,进行写入操作,即退出了该同步代码块

2.5 局限性

volatile 关键字通常用做某个 操作完成发生中断状态 的标志。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* Created by osys on 2022/08/28 21:48.
*/
public class Demo8 {
/** 状态的标志 */
private volatile boolean aSleep;

public void mainMethod() {
startDoSomething();
while (!aSleep) {
stopDoSomething();
}
}

public void startDoSomething() {
System.out.println("开始 ------ 做某些事");
}

public void stopDoSomething() {
System.out.println("停止 ------ 做某些事");
}
}

volatile 关键字不足以确保递增操作 i++ 的原子性,因为我们不能确保只有一个线程对变量执行写操作。

3. 发布与逸出

3.1 发布(Publish)

  • 发布一个对象指的是,是对象能够再当前作用域之外的代码中使用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import sun.nio.ch.Secrets;

    import java.util.HashSet;
    import java.util.Set;

    public class PublishObject {

    private static Set<Secrets> publishSecrets = new HashSet<>();

    public static Set<Secrets> getInstance() {
    return publishSecrets;
    }
    }

3.2 逸出(Escape)

逸出:不应该发布的对象被发布

对于上面的代码:

1
2
3
4
5
6
7
8
public class PublishObject {

private static Set<Secrets> publishSecrets = new HashSet<>();

public static Set<Secrets> getInstance() {
return publishSecrets;
}
}
  • 当我们发布某个对象时,可能会间接地发布其他对象。
  • 如我们不是发布 Set 对象,而是 Secrets 对象
  • 如将别的 Secrets 对象添加到 publishSecrets 集合中,那么同样的,被添加的这个 Secrets 对象也会被发布。
  • 因为任何使用者都能遍历这个集合,并且获得这个新 Secrets 对象的引用。
1
2
3
4
5
6
7
public class UnsafeStates {
private String[] states = new String[] {"a", "b", "c"};

public String[] getStates() {
return states;
}
}
  • 任何调用者都可能会修改发布的对象,如上 states 对象,该数组已经逸出了它所在的作用域。
  • 当发布一个对象时,在该对象的非私有域应用的对象同样会被发布。
  • 一般来说,如果一个已经发布的对象能够通过非私有的变量引用方法调用到其他对象,那么这些对象也都会被发布。

3.3 安全地对象构造

隐式地是由 this 引用逸出:

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
public class ThisEscape {
public ThisEscape(EventSource source) {
source.registerListener(new EventListener() {
@Override
public void onEvent(Event event) {
doSomething(event);
}
});
}

void doSomething(Event event) {
}

/** 事件源 */
interface EventSource {
/**
* 注册事件监听
* @param eventListener 事件监听
*/
void registerListener(EventListener eventListener);
}

/** 事件监听 */
interface EventListener {
/**
* 一个事件
* @param event 一个事件
*/
void onEvent(Event event);
}

/** 事件 */
interface Event {
}
}
  • ThisEscape 中给出了逸出地一个特殊示例,即 this 引用在构造函数中逸出。
  • 当内部地 EventListener 实例发布时,在外部封装的 ThisEscape 实例逸出了。
  • 当从对象的构造函数中发布对象时,只是发布了一个尚未构造完成的对象。即使发布对象的语句位于构造函数的最后一行也是如此。
  • 如果 this 引用在构造过程中逸出,那么这种对象就被认为是不正确的构造。

使用工厂方法来防止 this 引用在构造过程中逸出:

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
/**
* Created by osys on 2022/08/28 21:48.
*/
public class SafeListener {
private final EventListener listener;

private SafeListener() {
listener = new EventListener() {
@Override
public void onEvent(Event event) {
doSomething(event);
}
};
}

public static SafeListener newInstance(EventSource source) {
SafeListener safeListener = new SafeListener();
source.registerListener(safeListener.listener);
return safeListener;
}

void doSomething(Event event) {
}

/** 事件源 */
interface EventSource {
/**
* 注册事件监听
* @param eventListener 事件监听
*/
void registerListener(EventListener eventListener);
}

/** 事件监听 */
interface EventListener {
/**
* 一个事件
* @param event 一个事件
*/
void onEvent(Event event);
}

/** 事件 */
interface Event {
}
}

如果想在构造函数中注册一个事件监听器或启动线程,那么可以使用一个使用的构造函数和一个公共的工程方法,从而避免不正确的构造过程。

4. 线程封闭

4.1 线程封闭

  • 当访问共享数据时,通常是要使用同步。
  • 如果要避免使用同步,就是不提供共享数据。
  • 如果仅在单线程中访问数据,就不需要同步。

4.2 Ad-hoc 线程封闭

Ad-hoc 线程封闭:线程封闭性的职责完全由程序实现来承担。

Ad-hoc 非常脆弱,因为它没有一种语言特性,例如可见性修饰符或局部变量,能将对象封闭到特定的线程上。

4.3 栈封闭(线程局部使用)

栈封闭是线程封闭的一种特例,在栈封闭中,只能通过局部变量才能访问对象。

局部变量的固有属性之一就是封闭在执行线程中。它们位于执行线程的栈中,其他线程无法访问这个栈。

基本类型的局部变量与引用变量的线程封闭性:

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

/**
* Created by osys on 2022/08/28 21:48.
*/
public class Animals {
Ark ark;
Species species;
Gender gender;

/**
* 加载动物对 载体
* @param candidates 候选动物
* @return 动物对数
*/
public int loadTheArk(Collection<Animal> candidates) {
SortedSet<Animal> animals;
int numPairs = 0;
// 候选
Animal candidate = null;

animals = new TreeSet<Animal>(new SpeciesGenderComparator());
animals.addAll(candidates);
// 依次循环所有候选动物,存在同物种,不同性别的动物,加入动物对中
for (Animal animal : animals) {
if (candidate == null || !candidate.isPotentialMate(animal)) {
candidate = animal;
} else {
// 同物种,不同性别。向【动物对】载体中添加这两个动物
ark.load(new AnimalPair(candidate, animal));
++numPairs;
candidate = null;
}
}
//动物对数
return numPairs;
}


/** 动物 */
static class Animal {
/** 物种 */
Species species;
/** 性别 */
Gender gender;

/** 是否是同物种,不同性别 */
public boolean isPotentialMate(Animal other) {
return this.species == other.species && this.gender != other.gender;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Animal)) {
return false;
}
Animal animal = (Animal) o;
return species == animal.species && gender == animal.gender;
}

@Override
public int hashCode() {
return Objects.hash(species, gender);
}
}

/** 物种 */
enum Species {

// 土豚、孟加拉虎、驯鹿、野狗、大象、青蛙、GNU、土狼、
AARDVARK, BENGAL_TIGER, CARIBOU, DINGO, ELEPHANT, FROG, GNU, HYENA,
// 鬣蜥、美洲虎、猕猴桃、美洲豹、马斯塔顿、蝾螈、章鱼、
IGUANA, JAGUAR, KIWI, LEOPARD, MASTADON, NEWT, OCTOPUS,
// 食人鱼、格查尔、犀牛、蝾螈、三趾树懒、
PIRANHA, QUETZAL, RHINOCEROS, SALAMANDER, THREE_TOED_SLOTH,
// 独角兽、毒蛇、狼人、黄蜂、牦牛、斑马
UNICORN, VIPER, WEREWOLF, XANTHUS_HUMMINBIRD, YAK, ZEBRA
}

/** 性别 */
enum Gender {
// 雄性、雌性
MALE, FEMALE
}

/**一对动物 */
static class AnimalPair {
private final Animal one, two;

public AnimalPair(Animal one, Animal two) {
this.one = one;
this.two = two;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof AnimalPair)) {
return false;
}
AnimalPair that = (AnimalPair) o;
return Objects.equals(one, that.one) && Objects.equals(two, that.two);
}

@Override
public int hashCode() {
return Objects.hash(one, two);
}
}

/** 物种性别比较 */
static class SpeciesGenderComparator implements Comparator<Animal> {
/**
* 先进行物种比较,物种比较相同,再进行性别比较
* @param one 动物1
* @param two 动物2
* @return 0为物种相同,且
*/
@Override
public int compare(Animal one, Animal two) {
// Enum.compareTo()
// 将此枚举与订单的指定对象进行比较。当此对象小于、等于或大于指定对象时,返回负整数、零或正整数。
// 枚举常量只能与同一枚举类型的其他枚举常量进行比较。该方法实现的自然顺序是常量的声明顺序。
int speciesCompare = one.species.compareTo(two.species);
return (speciesCompare != 0)
? speciesCompare
: one.gender.compareTo(two.gender);
}
}

/** 动物对 载体 */
static class Ark {
private Set<AnimalPair> loadedAnimals = new HashSet<>();

/** 向 【动物对】 载体中添加 【动物对】 */
public void load(AnimalPair pair) {
loadedAnimals.add(pair);
}
}
}

loadTheArk 中的 animals 变量为局部变量,它被限制在保存本地变量的线程中。

5. ThreadLocal

  • ThreadLocal 允许 每个线程特有数值的对象 关联在一起。

  • ThreadLocal 提供了 get 和 set 访问器,为每个使用 ThreadLoacl 的线程维护一份单独的拷贝。

  • 因此 get 总是返回由当前只想线程通过 set 设置的最新值。

线程本地(ThreadLocal)变量通常用于防止基于可变的单例(Singleton)或全局变量的设计中,出现(不正确的)共享。

5.1 使用 ThreadLocal 确保线程封闭性

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
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

/**
* Created by osys on 2022/08/28 21:48.
*/
public class ConnectionDispenser {
static String DB_URL = "jdbc:mysql://localhost/test";

private ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
@Override
public Connection initialValue() {
try {
return DriverManager.getConnection(DB_URL);
} catch (SQLException e) {
throw new RuntimeException("无法获取连接, e");
}
}
};

public Connection getConnection() {
return connectionHolder.get();
}

public void setConnection(ThreadLocal<Connection> connection) {
this.connectionHolder = connection;
}
}

可以将 ThreadLocal<T> 看作 Map<Thread, T> 它存储了与线程相关的值(不过事实上它并非这样实现的)。

与线程相关的值存储在线程对象自身中,线程终止后,这些值会被垃圾回收。

6. 不可变性

多个线程总试图访问相同的可变状态,可能会出现未及时更新、访问的数据为过期数据。

如果对象的状态不能被修改,那么这些风险与复杂度就自然而然的消失了。

6.1 对象的不可变

不可变性病不生简单地等于将对中的所有域都声明为 final 类型,所有域都是 finel 类型的对象仍然是可变的,因为 final 域可以获得一个可变对象的引用。

1
2
3
4
不可变对象需要满足:
1. 对象创建后,状态不能被修改
2. 所有域都是 final 类型
3. 对象创建期间没有发生 this 引用的逸出

6.2 构造于底层可变对象之上的不可变类

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
import net.jcip.annotations.Immutable;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Vector;

/**
* Created by osys on 2022/08/28 21:48.
*/
@Immutable
public final class ThreeStooges {
private final Set<String> stooges = new HashSet<>();

public ThreeStooges() {
stooges.add("Moe");
stooges.add("Larry");
stooges.add("Curly");
}

public boolean isStooge(String name) {
return stooges.contains(name);
}

public String getStoogeNames() {
List<String> stooges = new Vector<>();
stooges.add("Moe");
stooges.add("Larry");
stooges.add("Curly");
return stooges.toString();
}

public static void main(String[] args) {
ThreeStooges threeStooges = new ThreeStooges();
System.out.println("threeStooges.stooges = " + threeStooges.stooges);
// threeStooges.stooges = [Moe, Larry, Curly]
}
}

尽管存储 StringSet 是可变的,不过 ThreeStooges 的设计使得 ThreeStooges 对象被创建后,就不能再修改 set

stooges 引用是 final 类型的,所以所有的对象状态只能通过 final 域进行询问(满足不可变对象的要求)

6.3 使用 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
import net.jcip.annotations.Immutable;

import java.math.BigInteger;
import java.util.Arrays;

/**
* 不可变容器,混存值
*/
@Immutable
public class OneValueCache {
private final BigInteger lastNumber;
private final BigInteger[] lastFactors;

public OneValueCache(BigInteger i,
BigInteger[] factors) {
lastNumber = i;
lastFactors = Arrays.copyOf(factors, factors.length);
}

public BigInteger[] getFactors(BigInteger i) {
if (lastNumber == null || !lastNumber.equals(i)) {
return null;
} else {
return Arrays.copyOf(lastFactors, lastFactors.length);
}
}
}

通过使用不可变对象来持有所有的变量,可以消除在访问和更新这些变量时的竟争条件。

若使用可变的容器对象,须使用锁以确保原子性。

使用不可变对象,一旦有一个线程获得了它的引用,永远不必担心其他线程会修改它的状态。

7. 安全发布

7.1 不可变容器的 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
import net.jcip.annotations.Immutable;

import java.math.BigInteger;
import java.util.Arrays;

/**
* 不可变容器,混存值
*/
@Immutable
public class OneValueCache {
private final BigInteger lastNumber;
private final BigInteger[] lastFactors;

public OneValueCache(BigInteger i,
BigInteger[] factors) {
lastNumber = i;
// 防止空指针异常
if (factors == null) {
lastFactors = null;
} else {
lastFactors = Arrays.copyOf(factors, factors.length);
}
}

public BigInteger[] getFactors(BigInteger i) {
if (lastNumber == null || !lastNumber.equals(i)) {
// lastNumber = null 或 lastNumber != i
return null;
} else {
return Arrays.copyOf(lastFactors, lastFactors.length);
}
}
}

使用到不可变容器对象的 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
import net.jcip.annotations.ThreadSafe;

import javax.servlet.GenericServlet;
import javax.servlet.Servlet;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.math.BigInteger;

/**
* 使用到不可变容器对象的 volatile 类型引用,缓存最新的结果
*/
@ThreadSafe
public class VolatileCachedFactories extends GenericServlet implements Servlet {
private volatile OneValueCache cache = new OneValueCache(null, null);

@Override
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = cache.getFactors(i);
if (factors == null) {
factors = factor(i);
cache = new OneValueCache(i, factors);
}
encodeIntoResponse(resp, factors);
}

void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) {
}

BigInteger extractFromRequest(ServletRequest req) {
return new BigInteger("7");
}

BigInteger[] factor(BigInteger i) {
return new BigInteger[]{i};
}
}

VolatileCachedFactories 利用 OneValuecache 存储缓存的数字及其因数。

当一个线程设置 volatile 类型的 cache 域引用到一个新的 OnevalueCache 后,新数据会立即对其他线程可见。

7.2 在没有适当的同步的情况下就发布对象

1
2
3
4
5
6
7
8
9
10
/**
* Created by osys on 2022/08/28 21:48.
*/
public class Holder {
private int n;

public Holder(int n) {
this.n = n;
}
}
1
2
3
4
5
6
7
8
9
10
/**
* Created by osys on 2022/08/28 21:48.
*/
public class StuffIntoPublic {
public Holder holder;

public void initialize() {
holder = new Holder(42);
}
}

holder 容器还是会在其他线程中被设置为一个不一致的状态。

即使它的不变约束已经在构造函数中得以正确创建。如多个线程都调用 initialize(),其 holder 成员变量的引用会发生多次改变。

7.3 安全发布的模式

为了安全地发布对象,对象的引用以及对象的状态必须同时对其他线程可见

一个正确创建的对象可以通过下列条件安全地发布:

  • 通过静态初始化器初始化对象的引用;
  • 将它的引用存储到 volatile域AtomicReference
  • 将它的引用存储到正确创建的对象的 final 域中;
  • 或者将它的引用存储到由锁正确保护的域中。

7.4 高效不可变对象

被安全发布后,状态不能被修改的对象,叫作高效不可变对象。不可变对象是线程安全的。它们的常量(变量)是在构造函数中创建的。

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
* Created by osys on 2022/08/28 21:48.
*/
public class Demo9 {
public final Map<String, Date> lastLogin;

public Demo9() {
Map<String, Date> stringDate = new HashMap<>();
stringDate.put("LeeHua", new Date());
stringDate.put("Rainbow", new Date());
lastLogin = Collections.synchronizedMap(stringDate);
}
}

如果 Date 值在置入 Map 中后就不会改变,那么 synchronizedMap 中同步的实现,对于安全地发布 Date 值,是至关重要的。而访问这些 Date 值时就不再需要额外的同步。

7.5 可变对象

发布对象的必要条件依赖于对象的可变性:

  • 不可变对象可以通过任意机制发布;

  • 高效不可变对象必须要安全发布;

  • 可变对象必须要安全发布,同时必须要线程安全或者是被锁保护。

7.6 安全地共享对象

在并发程序中,使用和共享对象的一些最有效的策略如下:

  • 线程限制:一个线程限制的对象,通过限制在线程中,而被线程独占,且只能被占有它的线程修改。
  • 共享只读(shared read-only):一个共享的只读对象,在没有额外同步的情况下,可以被多个线程并发地访问 ,但是任何线程都不能修改它。共享只读对象包括可变对象与高效不可变对象。
  • 共享线程安全(shared thread-safe):一个线程安全的对象在内部进行同步,所以共他线程无须额外同步,就可以通过公共接口随意地访问它。
  • 被守护的(Guarded):一个被守护的对象只能通过特定的锁来访问。被守护的对象包括那些被线程安全对象封装的对象,和已知被特定的锁保护起来的已发布对象。

Java并发编程实战:第3章 对象的共享

https://osys.github.io/posts/fff0.html

作者

Osys

发布于

2022年08月29日

更新于

2022年08月29日

许可协议

评论