7

Java并发编程学习笔记2

 2 years ago
source link: https://codeshellme.github.io/2022/02/java-concurrent2/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

公号:码农充电站pro

主页:https://codeshellme.github.io

8,Java 内存模型

JMM(Java Memory Model) 体现在以下几个方面:

  • 原子性:保证指令不会受到线程上下文切换的影响
    • 使用 synchronized
  • 可见性:保证指令不会受 cpu 缓存的影响
    • 使用 volatile(比较轻量) 或 synchronized(比较重量)
  • 有序性:保证指令不会受 cpu 指令并行优化的影响
    • volatile 可以禁止指令重排
    • synchronized 也可以禁止指令重排
    • volatile 关键字可以禁止该关键字修饰的变量,变量代码出现的地方之前的代码发生指令重排
volatile boolean ready = false;
/////////////////////////
num = 2;
ready = true; // 该代码之前的代码可以防止指令重排

volatile 的原理:其底层实现原理是内存屏障

  • 对 volatile 变量的写指令后会加入写屏障
  • 对 volatile 变量的读指令后会加入读屏障

1,可见性问题 volatile

可见性问题示例

// 退不出的循环
// main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(run){
// ....
t.start();
sleep(1);
run = false; // 线程t不会如预想的停下来

原因分析:

  • 初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存
  • 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中, 减少对主存中 run 的访问,提高效率
  • 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量 的值,结果永远是旧值

解决办法 volatile(易变关键字)

  • volatile 它可以用来修饰成员变量和静态成员变量,可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存
  • 它保证的是在多个线程之间,一个线程对 volatile 变量的修改对另一个线程可见, 不能保证原子性,仅用在一个写线程,多个读线程的情况
  • 注意 synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是synchronized 是属于重量级操作,性能相对更低

2,有序性问题-指令重排

JVM 会在不影响正确性的前提下,可以调整语句的执行顺序,比如下面代码:

static int i;
static int j;
// 在某个线程内执行如下赋值操作
i = ...;
j = ...;

可以看到,至于是先执行 i 还是 先执行 j ,对最终的结果不会产生影响。所以,上面代码真正执行时,既可以是:

i = ...;
j = ...;

也可以是:

j = ...;
i = ...;

这种特性称之为『指令重排』,而 多线程下『指令重排』会影响正确性。为什么要有重排指令这项优化呢?从 CPU 执行指令的原理来理解一下吧,指令重拍可以增加指令的并行度

指令重排是 JIT 编译器在运行时的一些优化,这个现象需要通过大量测试才能复现。

一个例子:

int num = 0;
boolean ready = false;
// 线程1 执行此方法
public void actor1(I_Result r) {
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
// 线程2 执行此方法
public void actor2(I_Result r) {
// 注意这两处代码可能发生指令重排
num = 2;
ready = true;

I_Result 是一个对象,有一个属性 r1 用来保存结果,问,可能的结果有几种?

  • 线程1 先执行,这时 ready = false,所以进入 else 分支结果为 1
  • 线程2 先执行 num = 2,但没来得及执行 ready = true,线程1 执行,还是进入 else 分支,结果为1
  • 线程2 执行到 ready = true,线程1 执行,这回进入 if 分支,结果为 4

注意,下面两行代码可能会发生指令重排:

num = 2;
ready = true;
ready = true;
num = 2;

这种情况下:线程2 执行 ready = true,切换到线程1,进入 if 分支,相加为 0,再切回线程2 执行 num = 2。

此时就是指令重排导致了错误的结果

此时可以通过 volatile 关键字来修饰 ready 变量:

volatile boolean ready = false;

这样可以防止这两行代码发生重排序:

num = 2;
ready = true;

3,volatile 原理

volatile 可以解决可见性问题和指令重排问题。

volatile 的其底层实现原理是内存屏障

  • 对 volatile 变量的写指令后会加入写屏障
  • 对 volatile 变量的读指令后会加入读屏障

volatile 对可见性问题的处理:

写屏障保证在该屏障之前的,对共享变量的改动,都同步到主存当中

public void actor2(I_Result r) {
num = 2;
ready = true; // ready 是 volatile 赋值带写屏障
// 写屏障位置

而读屏障保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据:

public void actor1(I_Result r) {
// 读屏障位置
// ready 是 volatile 读取值带读屏障
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;

volatile 对指令重排问题的处理:

写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后:

public void actor2(I_Result r) {
num = 2;
ready = true; // ready 是 volatile 赋值带写屏障
// 写屏障位置

读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前:

public void actor1(I_Result r) {
// 读屏障位置
// ready 是 volatile 读取值带读屏障
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;

4,happens-before 规则

happens-before 规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见。

变量都是指成员变量或静态成员变量

  • 线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见
static int x;
static Object m = new Object();
new Thread(()->{
synchronized(m) {
x = 10;
},"t1").start();
new Thread(()->{
synchronized(m) {
System.out.println(x);
},"t2").start();
  • 线程对 volatile 变量的写,对接下来其它线程对该变量的读可见
volatile static int x;
new Thread(()->{
x = 10;
},"t1").start();
new Thread(()->{
System.out.println(x);
},"t2").start();
  • 线程 start 前对变量的写,对该线程开始后对该变量的读可见
static int x;
x = 10;
new Thread(()->{
System.out.println(x);
},"t2").start();
  • 线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或 t1.join()等待它结束)
static int x;
Thread t1 = new Thread(()->{
x = 10;
},"t1");
t1.start();
t1.join();
System.out.println(x);
  • 线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过t2.interrupted 或 t2.isInterrupted)
static int x;
public static void main(String[] args) {
Thread t2 = new Thread(()->{
while(true) {
if(Thread.currentThread().isInterrupted()) {
System.out.println(x);
break;
},"t2");
t2.start();
new Thread(()->{
sleep(1);
x = 10;
t2.interrupt();
},"t1").start();
while(!t2.isInterrupted()) {
Thread.yield();
System.out.println(x);
  • 对变量默认值(0,false,null)的写,对其它线程对该变量的读可见
  • 具有传递性,如果 x hb-> y 并且 y hb-> z 那么有 x hb-> z ,配合 volatile 的防指令重排,有下面的例子
volatile static int x;
static int y;
new Thread(()->{
y = 10;
x = 20;
},"t1").start();
new Thread(()->{
// x=20 对 t2 可见, 同时 y=10 也对 t2 可见
System.out.println(x);
},"t2").start();

9,共享模型之无锁

1,问题:实现一个多线程取款

Account 接口:

public interface Account {
// 获取余额
Integer getBalance();
void withdraw(Integer amount);
* 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
* 如果初始余额为 10000 那么正确的结果应当是 0
static void demo(Account account) {
List<Thread> ts = new ArrayList<>();
long start = System.nanoTime();
for (int i = 0; i < 1000; i++) {
ts.add(new Thread(() -> {
account.withdraw(10);
ts.forEach(Thread::start);
ts.forEach(t -> {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
long end = System.nanoTime();
System.out.println(account.getBalance()
+ " cost: " + (end-start)/1000_000 + " ms");

2,使用锁实现 synchronized

public class AccountSynchronized implements Account {
private Integer balance;
public AccountSynchronized(Integer balance) {
this.balance = balance;
@Override
public synchronized Integer getBalance() {
return balance;
@Override
public synchronized void withdraw(Integer amount) {
balance -= amount;
public static void main(String[] args) {
Account a = new AccountSynchronized(10000);
Account.demo(a);

3,使用无锁实现 Atomic

public class AccountAtomic implements Account {
private AtomicInteger balance;
public AccountAtomic(Integer balance) {
this.balance = new AtomicInteger(balance);
@Override
public Integer getBalance() {
return balance.get();
@Override
public void withdraw(nteger amount) {
while (true) {
int prev = balance.get();
int next = prev - amount;
// compareAndSet 是原子操作
// 先比较再赋值
if (balance.compareAndSet(prev, next)) {
break;
// 可以简化为下面的方法
// addAndGet 是原子操作
// balance.addAndGet(-1 * amount);
public static void main(String[] args) {
Account a = new AccountAtomic(10000);
Account.demo(a);

其中的关键是 compareAndSet,它的简称就是 CAS (也有 Compare And Swap 的说法),它必须是原子操作

另外,AtomicInteger 中的 value 属性是 volatile 的。 CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果。

其实 CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交换】的原子性。

在多核状态下,某个核执行到带 lock 的指令时,CPU 会让总线锁住,当这个核把此指令执行完毕,再开启总线。这个过程中不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性,是原子的。

原子操作比锁的性能更高。

  • 无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞
  • 线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等被唤醒又得重新打火、启动、加速… 恢复到高速运行,代价比较大
  • 单CPU无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换

4,CAS 的特点

结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。

  • CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗
  • synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会
  • CAS 体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思
    • 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
    • 如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

10,原子包装类

1,原子整数类

java.util.concurrent.atomic 包中提供了 3 个原子整数类:

  • AtomicBoolean
  • AtomicInteger
  • AtomicLong

以 AtomicInteger 为例:

AtomicInteger i = new AtomicInteger(0);
// 获取并自增(i = 0, 结果 i = 1, 返回 0),类似于 i++
System.out.println(i.getAndIncrement());
// 自增并获取(i = 1, 结果 i = 2, 返回 2),类似于 ++i
System.out.println(i.incrementAndGet());
// 自减并获取(i = 2, 结果 i = 1, 返回 1),类似于 --i
System.out.println(i.decrementAndGet());
// 获取并自减(i = 1, 结果 i = 0, 返回 1),类似于 i--
System.out.println(i.getAndDecrement());
// 获取并加值(i = 0, 结果 i = 5, 返回 0)
System.out.println(i.getAndAdd(5));
// 加值并获取(i = 5, 结果 i = 0, 返回 0)
System.out.println(i.addAndGet(-5));
// 获取并更新(i = 0, p 为 i 的当前值, 结果 i = -2, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.getAndUpdate(p -> p - 2));
// 更新并获取(i = -2, p 为 i 的当前值, 结果 i = 0, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.updateAndGet(p -> p + 2));
// 获取并计算(i = 0, p 为 i 的当前值, x 为参数1, 结果 i = 10, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
// getAndUpdate 如果在 lambda 中引用了外部的局部变量,要保证该局部变量是 final 的
// getAndAccumulate 可以通过 参数1 来引用外部的局部变量,但因为其不在 lambda 中因此不必是 final
System.out.println(i.getAndAccumulate(10, (p, x) -> p + x));
// 计算并获取(i = 10, p 为 i 的当前值, x 为参数1, 结果 i = 0, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.accumulateAndGet(-10, (p, x) -> p + x));

2,原子引用类

除了以上三种原子引用类型,想要保证其它的数据类型的原子操作,需要用到原子引用类型

  • AtomicReference
  • AtomicMarkableReference
  • AtomicStampedReference
1,AtomicReference
class DecimalAccountSafeCas implements DecimalAccount {
AtomicReference<BigDecimal> ref; // 对 BigDecimal 类型 进行原子包装
public DecimalAccountSafeCas(BigDecimal balance) {
ref = new AtomicReference<>(balance);

原子引用-ABA问题:

ABA 问题示例:

static AtomicReference<String> ref = new AtomicReference<>("A");
public static void main(String[] args) throws InterruptedException {
log.debug("main start...");
// 获取值 A
// 这个共享变量被它线程修改过?
String prev = ref.get();
other();
sleep(1);
// 尝试改为 C
log.debug("change A->C {}", ref.compareAndSet(prev, "C"));
private static void other() {
new Thread(() -> {
log.debug("change A->B {}", ref.compareAndSet(ref.get(), "B"));
}, "t1").start();
sleep(0.5);
new Thread(() -> {
log.debug("change B->A {}", ref.compareAndSet(ref.get(), "A"));
}, "t2").start();

主线程仅能判断出共享变量的值与最初值 A 是否相同,不能感知到这种从 A 改为 B 又 改回 A 的情况.

如果主线程希望:只要有其它线程【动过了】共享变量,那么自己的 cas 就算失败,这时,仅比较值是不够的,需要再加一个版本号

此时需要用到 AtomicStampedReference

2,AtomicStampedReference
static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
public static void main(String[] args) throws InterruptedException {
log.debug("main start...");
// 获取值 A
String prev = ref.getReference();
// 获取版本号
int stamp = ref.getStamp();
log.debug("版本 {}", stamp);
// 如果中间有其它线程干扰,发生了 ABA 现象
other();
sleep(1);
// 尝试改为 C,更新失败
log.debug("change A->C {}", ref.compareAndSet(prev, "C", stamp, stamp + 1));
private static void other() {
new Thread(() -> {
log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B", ref.getStamp(), ref.getStamp() + 1));
log.debug("更新版本为 {}", ref.getStamp());
}, "t1").start();
sleep(0.5);
new Thread(() -> {
log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A", ref.getStamp(), ref.getStamp() + 1));
log.debug("更新版本为 {}", ref.getStamp());
}, "t2").start();

AtomicStampedReference 可以给原子引用加上版本号,追踪原子引用整个的变化过程,如: A -> B -> A ->C ,通过AtomicStampedReference,我们可以知道,引用变量中途被更改了几次。

但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了AtomicMarkableReference

3,AtomicMarkableReference

换垃圾袋示例:垃圾袋为空的时候,主人才去换垃圾袋。

class GarbageBag {
String desc;
public GarbageBag(String desc) {
this.desc = desc;
public void setDesc(String desc) {
this.desc = desc;
@Override
public String toString() {
return super.toString() + " " + desc;
@Slf4j
public class TestABAAtomicMarkableReference {
public static void main(String[] args) throws InterruptedException {
GarbageBag bag = new GarbageBag("装满了垃圾");
// 参数2 mark 可以看作一个标记,表示垃圾袋满了
AtomicMarkableReference<GarbageBag> ref = new AtomicMarkableReference<>(bag, true);
log.debug("主线程 start...");
GarbageBag prev = ref.getReference();
log.debug(prev.toString());
new Thread(() -> {
log.debug("打扫卫生的线程 start...");
bag.setDesc("空垃圾袋");
while (!ref.compareAndSet(bag, bag, true, false)) {}
log.debug(bag.toString());
}).start();
Thread.sleep(1000);
log.debug("主线程想换一只新垃圾袋?");
boolean success = ref.compareAndSet(prev, new GarbageBag("空垃圾袋"), true, false);
log.debug("换了么?" + success);
log.debug(ref.getReference().toString());

3,原子数组

  • AtomicIntegerArray
  • AtomicLongArray
  • AtomicReferenceArray

4,字段更新器

  • AtomicReferenceFieldUpdater // 域 字段
  • AtomicIntegerFieldUpdater
  • AtomicLongFieldUpdater

5,原子累加器

6,Unsafe

11,共享模型之不可变类

不可变对象,实际是另一种避免竞争的方式。

1,不可变类

由于 SimpleDateFormat (是可变的)不是线程安全的,因此下面的多线程操作会出现问题:

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
// sdf.parse 会出现问题,因为其不是线程安全的
log.debug("{}", sdf.parse("1951-04-21"));
} catch (Exception e) {
log.error("{}", e);
}).start();

同步锁可以解决问题,但带来的是性能上的损失,并不算很好:

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 50; i++) {
new Thread(() -> {
synchronized (sdf) { // 同步处理
log.debug("{}", sdf.parse("1951-04-21"));
} catch (Exception e) {
log.error("{}", e);
}).start();

因为 SimpleDateFormat 是可变的,我们还可以使用不可变类 DateTimeFormatter 来替代 SimpleDateFormat。

如果一个对象不能够修改其内部状态(属性),那么它就是线程安全的,因为不存在并发修改。

这样的对象在Java 中有很多,例如在 Java 8 后,提供了一个新的日期格式化类 DateTimeFormatter:

DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
LocalDate date = dtf.parse("2018-10-01", LocalDate::from);
log.debug("{}", date);
}).start();

2,不可变类的设计 final

final 的使用:

  • 类和类中所有属性都是 final 的
  • 类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性

保护性拷贝

  • 通过创建副本对象来避免共享的手段称之为【保护性拷贝】

3,不可变类与享元模式

不可变类通常会使用【保护性拷贝】来避免对象可变,但缺点是会频繁创建对象对象,并且对象个数较多。

因此不可变类一般会与享元模式一起使用。

享元模式:可以重用数量有限的同一类对象,目的是重用现有的对象,而不是创建新的对象。

JDK 中的享元模式:

  • 在JDK中 Boolean,Byte,Short,Integer,Long,Character 等包装类提供了 valueOf 方法,例如 Long 的 valueOf 会缓存 -128~127 之间的 Long 对象,在这个范围之间会重用对象,大于这个范围,才会新建 Long 对象。
public static Long valueOf(long l) {
final int offset = 128;
if (l >= -128 && l <= 127) { // will cache
return LongCache.cache[(int)l + offset];
return new Long(l);
  • Byte, Short, Long 缓存的范围都是 -128~127
  • Character 缓存的范围是 0~127
  • Integer的默认范围是 -128~127
    • 最小值不能变
    • 但最大值可以通过调整虚拟机参数 -Djava.lang.Integer.IntegerCache.high 来改变
  • Boolean 缓存了 TRUE 和 FALSE

除了上面的包装类,String串池、BigDecimal 和 BigInteger 也都使用了享元模式。

4,无状态

在 web 阶段学习时,设计 Servlet 时为了保证其线程安全,都会有这样的建议,不要为 Servlet 设置成员变量,这种没有任何成员变量的类是线程安全的。

因为成员变量保存的数据也可以称为状态信息,因此没有成员变量就称之为【无状态】。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK