Java并发编程实战 02Java如何解决可见性和有序性问题

简介:

Java并发编程实战 02Java如何解决可见性和有序性问题

摘要#
在上一篇文章当中,讲到了CPU缓存导致可见性、线程切换导致了原子性、编译优化导致了有序性问题。那么这篇文章就先解决其中的可见性和有序性问题,引出了今天的主角:Java内存模型(面试并发的时候会经常考核到)

什么是Java内存模型?#
现在知道了CPU缓存导致可见性、编译优化导致了有序性问题,那么最简单的方式就是直接禁用CPU缓存和编译优化。但是这样做我们的性能可就要爆炸了~。我们应该按需禁用。
Java内存模型是有一个很复杂的规范,但是站在程序员的角度上可以理解为:Java内存模型规范了JVM如何提供按需禁用缓存和编译优化的方法。
具体包括 volatile、synchronized、final三个关键字,以及六项Happens-Before规则。

volatile关键字#
volatile有禁用CPU缓存的意思,禁用CPU缓存那么操作数据变量时直接是直接从内存中读取和写入。如:使用volatile声明变量 volatile boolean v = false,那么操作变量v时则必须从内存中读取或写入,但是在低于Java版本1.5以前,可能会有问题。
在下面这段代码当中,假设线程A执行了write方法,线程B执行了reader方法,假设线程B判断到了this.v == true进入到了判断条件中,那么此时的x会是多少呢?

Copy
public class VolatileExample {

private int x = 0;
private volatile boolean v = false;

public void write() {
    this.x = 666;
    this.v = true;
}

public void reader() {
    if (this.v == true) {
        // 这里的x会是多少呢?
    }
}

}
在1.5版本之前,该值可能为666,也可能为0;因为变量x并没有禁用缓存(volatile),但是在1.5版本以后,该值一定为666;因为Happens-Before规则。

什么是Happens-Before规则#
Happens-Before规则要表达的是:前面一个操作的结果对后续是可见的。如果第一次接触该规则,可能会有一些困惑,但是多去阅读几遍,就会加深理解。

1.程序的顺序性规则#
这条规则是指在一个线程中,按照程序顺序,前面的操作Happens-Before于后续的任意操作(意思就是前面的操作结果对于后续任意操作都是可以看到的)。就如上面的那段代码,按照程序的顺序:this.x = 666 Happens-Before于 this.v = true。

2.Volatile 变量规则#
这条规则指的是对一个Volatile变量的写操作,Happens-Before该变量的读操作。意思也就是:假设该变量被线程A写入后,那么该变量对于任何线程都是可见的。也就是禁用了CPU缓存的意思,如果是这样的话,那么和1.5版本以前没什么区别啊!那么如果再看一下规则3,就不同了。

3.传递性#
这条规则指的是:如果 A Happens-Before 于B,且 B Happens-Before 于 C。那么 A Happens-Before 于 C。这就是传递性的规则。我们再来看看刚才那段代码(我复制下来方便看)

Copy
public class VolatileExample {

private int x = 0;
private volatile boolean v = false;

public void write() {
    this.x = 666;
    this.v = true;
}

public void reader() {
    if (this.v == true) {
        // 读取变量x
    }
}

}
在上面代码,我们可以看到,this.x = 666 Happens-Before this.v = true,this.v = true Happens-Before 读取变量x,根据传递性规则this.x = 666 Happens-Befote 读取变量x,那么说明了读取到变量this.v = true时,那么此时的读取变量x的指必定为666
假设线程A执行了write方法,线程B执行reader方法且此时的this.v == true,那么根据刚才所说的传递性规则,读取到的变量x必定为666。这就是1.5版本对volatile语义的增强。而如果在版本1.5之前,因为变量x并没有禁用缓存(volatile),所以变量x可能为0哦。

4.管程中锁的规则#
这条规则是指对一个锁的解锁操作 Happens-Before 于后续对这个锁的加锁操作。管程是一种通用的同步原语,在Java中,synchronized是Java里对管程的实现。
管程中的锁在Java里是隐式实现的。如下面的代码,在进入同步代码块前,会自动加锁,而在代码块执行完后会自动解锁。这里的加锁和解锁都是编译器帮我们实现的。

Copy
synchronized(this) { // 此处自动加锁
// x是共享变量,初始值 = 0
if (this.x < 12) {

  this.x = 12;

}
} // 此处自动解锁
结合管程中的锁规则,假设x的初始值为0,线程A执行完代码块后值会变成12,那么当线程A解锁后,线程B获取到锁进入到代码块后,就能看到线程A的执行结果x = 12。这就是管程中锁的规则

5.线程的start()规则#
这条规则是关于线程启动的,该规则指的是主线程A启动子线程B后,子线程B能够看到主线程启动子线程B前的操作。
用HappensBefore解释:线程A调用线程B的start方法 Happens-Before 线程B中的任意操作。参考代码如下:

Copy

int x = 0;
public void start() {
    Thread thread = new Thread(() -> {
        System.out.println(this.x);
    });

    this.x = 666;
    // 主线程启动子线程
    thread.start();
}

此时在子线程中打印的变量x值为666,你也可以尝试一下。

6.线程join()规则#
这条规则是关于线程等待的,该规则指的是主线程A等待子线程B完成(主线A通过调用子线程B的join()方法实现),当子线程B完成后,主线程能够看到子线程的操作,这里的看到指的是共享变量 的操作,用Happens-Before解释:如果在线程A中调用了子线程B的join()方法并成功返回,那么子线程B的任意操作 Happens-Before 于主线程调用子线程Bjoin()方法的后续操作。看代码比较容易理解,示例代码如下:

Copy

int x = 0;
public void start() {
    Thread thread = new Thread(() -> {
        this.x = 666;
    });
    // 主线程启动子线程
    thread.start();
    // 主线程调用子线程的join方法进行等待
    thread.join();
    // 此时的共享变量 x == 666
}

被忽略的final#
在1.5版本之前,除了值不可改变以外,final字段其实和普通的字段一样。
在1.5以后的Java内存模型中,对final类型变量重排进行了约束。现在只要我们的提供正确的构造函数没有逸出,那么在构造函数初始化的final字段的最新值,必定可以被其他线程所看到。代码如下:

Copy
class FinalFieldExample {
final int x;
int y;
static FinalFieldExample f;
public FinalFieldExample() {

x = 3;
y = 4;

}

static void writer() {

f = new FinalFieldExample();

}

static void reader() {

if (f != null) {
  int i = f.x;
  int j = f.y;
}

}
当线程执行reader()方法,并且f != null时,那么此时的final字段修饰的f.x 必定为 3,但是y不能保证为4,因为它不是final的。如果这是在1.5版本之前,那么f.x也是不能保证为3。
那么何为逸出呢?我们修改一下构造函数:

Copy
public FinalFieldExample() {
x = 3;
y = 4;
// 此处为逸出
f = this;
}
这里就不能保证 f.x == 3了,就算x变量是用final修饰的,为什么呢?因为在构造函数中可能会发生指令重排,执行变成下面这样:

Copy

// 此处为逸出

f = this;
x = 3;
y = 4;
那么此时的f.x == 0。所以在构造函数中没有逸出,那么final修饰的字段没有问题。详情的案例可以参考这个文档

总结#
在这篇文章当中,我一开始对于文章最后部分的final约束重排一直看的不懂。网上不断地搜索资料和看文章当中提供的资料我才慢慢看懂,反复看了不下十遍。可能脑子不太灵活吧。
该文章主要的核心内容就是Happens-Before规则,把这几条规则搞懂了就ok。

参考文章:极客时间:Java并发编程实战 02

个人博客网址: https://colablog.cn/

如果我的文章帮助到您,可以关注我的微信公众号,第一时间分享文章给您

作者: Johnson木木

出处:https://www.cnblogs.com/Johnson-lin/p/12736004.html

相关文章
|
4月前
|
存储 Java 开发者
Java Map实战:用HashMap和TreeMap轻松解决复杂数据结构问题!
【10月更文挑战第17天】本文深入探讨了Java中HashMap和TreeMap两种Map类型的特性和应用场景。HashMap基于哈希表实现,支持高效的数据操作且允许键值为null;TreeMap基于红黑树实现,支持自然排序或自定义排序,确保元素有序。文章通过具体示例展示了两者的实战应用,帮助开发者根据实际需求选择合适的数据结构,提高开发效率。
117 2
|
25天前
|
存储 缓存 Java
Java中的分布式缓存与Memcached集成实战
通过在Java项目中集成Memcached,可以显著提升系统的性能和响应速度。合理的缓存策略、分布式架构设计和异常处理机制是实现高效缓存的关键。希望本文提供的实战示例和优化建议能够帮助开发者更好地应用Memcached,实现高性能的分布式缓存解决方案。
38 9
|
2月前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
2月前
|
Java
Java基础却常被忽略:全面讲解this的实战技巧!
本次分享来自于一道Java基础的面试试题,对this的各种妙用进行了深度讲解,并分析了一些关于this的常见面试陷阱,主要包括以下几方面内容: 1.什么是this 2.this的场景化使用案例 3.关于this的误区 4.总结与练习
|
2月前
|
Java 程序员
Java基础却常被忽略:全面讲解this的实战技巧!
小米,29岁程序员,分享Java中`this`关键字的用法。`this`代表当前对象引用,用于区分成员变量与局部变量、构造方法间调用、支持链式调用及作为参数传递。文章还探讨了`this`在静态方法和匿名内部类中的使用误区,并提供了练习题。
55 1
|
3月前
|
安全 Java 开发者
Java 多线程并发控制:深入理解与实战应用
《Java多线程并发控制:深入理解与实战应用》一书详细解析了Java多线程编程的核心概念、并发控制技术及其实战技巧,适合Java开发者深入学习和实践参考。
93 8
|
3月前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
4月前
|
存储 消息中间件 安全
JUC组件实战:实现RRPC(Java与硬件通过MQTT的同步通信)
【10月更文挑战第9天】本文介绍了如何利用JUC组件实现Java服务与硬件通过MQTT的同步通信(RRPC)。通过模拟MQTT通信流程,使用`LinkedBlockingQueue`作为消息队列,详细讲解了消息发送、接收及响应的同步处理机制,包括任务超时处理和内存泄漏的预防措施。文中还提供了具体的类设计和方法实现,帮助理解同步通信的内部工作原理。
JUC组件实战:实现RRPC(Java与硬件通过MQTT的同步通信)
|
4月前
|
开发框架 Java 程序员
揭开Java反射的神秘面纱:从原理到实战应用!
本文介绍了Java反射的基本概念、原理及应用场景。反射允许程序在运行时动态获取类的信息并操作其属性和方法,广泛应用于开发框架、动态代理和自定义注解等领域。通过反射,可以实现更灵活的代码设计,但也需注意其性能开销。
92 1
|
5月前
|
Java 数据中心 微服务
Java高级知识:线程池隔离与信号量隔离的实战应用
在Java并发编程中,线程池隔离与信号量隔离是两种常用的资源隔离技术,它们在提高系统稳定性、防止系统过载方面发挥着重要作用。
113 0

热门文章

最新文章