从半个多月前接到阿里的面试电话,被多线程问题难住,到今天终于读完了《Java Concurrency In Practice》。想总结一下,又发现自己没有能力将一本书的内容都概括下来。还是把书里最后一部分Java内存模型相关的内容搬过来谈一谈吧。

1、happens-before

什么是Java内存模型?它是一个协议,规定了在怎样的条件下,一个线程对内存的操作对另一个线程是可见的。

例如:线程A将变量 variable 赋值为3,那在怎样的情况下,线程B中条件(variable == 3) 成立呢?多线程环境下有很多因素可以令其不成立,有可能新值仅储存在寄存器里,还有可能新值仅写入了线程A所在处理器的本地Cache,而没有写入主存。

什么是happens-before?它是Java定义的一种偏序关系,为了保证线程A执行的操作对线程B是可见的,A与B之间必须存在这种偏序关系。它有如下的规则:

  1. 程序顺序规则:一个线程中的每个操作都happens-before那个线程随后的操作。
  2. 监视器锁规则:一个线程释放监视器锁之前所有的操作都happens-before另一个线程获取同一个监视器锁随后的操作。
  3. volatile变量规则:一个线程写volatile变量之前的所有操作都hanppens-before另一个线程读那一个volatile变量随后的操作。
  4. 线程启动规则:一个线程调用 Thread.start() 之前所有的操作都happens-before被启动的线程中的所有操作。
  5. 线程终止规则:一个线程中的所有操作都happens-before其它发现该线程已经终止的线程随后的操作。
  6. 中断规则:一个线程使用 Thread.interrupt() 中断另一个线程前的所有操作都happens-before被中断的线程检测到该中断随后的操作。
  7. Finalizer规则:一个对象构造函数中的所有操作都happens-before该对象的 finalize() 函数被调用后的操作。
  8. 传递规则:如果A happens-before B, B happens-before C,那么 A happens-before C。

上图展示了两个线程在使用同一把锁进行线程同步时存在的偏序关系,需要注意的是必须是同一把锁才会存在偏序关系。

2、Double-checked Locking

在单例模式中使用懒加载是比较常见的手段,可以尽可能地减少应用的启动时间。一般来说使用懒加载都需要进行线程同步,早期的Java虚拟机在加锁时有较大的开销,导致许多开发者在编程时会尽量避免加锁。DCL的目标就是既可以实现懒加载又能避免加锁。

public class DoubleCheckedLocking {
// 正确写法:private static volatile Resource resource;
private static Resource resource;
public static Resource getInstance() {
if (resource == null) {
synchronized (DoubleCheckedLocking.class) {
if (resource == null)
resource = new Resource();
}
}
return resource;
}
}

首先,程序先判断 resource 是否为空,当其值为空时进入同步块,然后再次判断其值是否为空,因为另一个线程可能在本线程进入同步块之前已经对其进行了初始化。

这段熟悉的代码实际上存在着一个名为“安全发布”的问题,就是说当一个线程发现 resource 的值不为空时 Resource 实例中的变量对该线程可能是不可见的。因为根据偏序规则,一个线程释放锁前的所有操作只保证对另一个获取同一个锁的线程随后的操作可见。解决这个问题需要使用 volatile 对 resource 进行修饰,因为根据前面的规则,一个线程写volatile变量前的所有操作,对另一个线程读同一个volatile变量随后的操作都是可见的。

对于现代的虚拟机来说,在竞争比较小的情况下加锁操作是很快的,所以这种双重检查发挥的作用已经大大被减弱了,使用静态内部类完成懒加载是一种比较好的选择。

public class ResourceFactory {
private static class ResourceHolder {
public final static Resource resource = new Resource();
}
public static Resource getResource() {
return ResourceHolder.resource;
}
}

3、衍生规则

JDK提供的类中也存在一些偏序关系:

  • 一个线程将元素放入线程安全的集合前的所有操作happens-before另一个线程将其从集合中取出后的操作。
  • 一个线程CountDownLatch.countDown() 前的操作happens-before另一个线程 CountDownLatch.await() 后的操作。
  • 一个线程 Semaphore.release() 前的操作happens-before另一个线程 Semaphore.acquire() 后的操作。
  • 被Future代表的任务在线程中执行的操作happens-before另一个线程Future.get() 后的操作。
  • 一个线程向Executor提交Runnable或Callable前的操作happens-before该任务运行时所在线程的操作。
  • 一个线程在CyclicBarrier.await() 前的操作happens-before所有被该 CyclicBarrier 释放进程的操作。如果启用了BarrierAction,那一个线程在 CyclicBarrier.await() 前的操作happens-before BarrierAction中的操作。BarrierAction中的操作happens-before被该 CyclicBarrier 释放进程的操作。

java中的偏序关系的更多相关文章

  1. java中的锁

    java中有哪些锁 这个问题在我看了一遍<java并发编程>后尽然无法回答,说明自己对于锁的概念了解的不够.于是再次翻看了一下书里的内容,突然有点打开脑门的感觉.看来确实是要学习的最好方式 ...

  2. java中的字符串相关知识整理

    字符串为什么这么重要 写了多年java的开发应该对String不陌生,但是我却越发觉得它陌生.每学一门编程语言就会与字符串这个关键词打不少交道.看来它真的很重要. 字符串就是一系列的字符组合的串,如果 ...

  3. Java中的Socket的用法

                                   Java中的Socket的用法 Java中的Socket分为普通的Socket和NioSocket. 普通Socket的用法 Java中的 ...

  4. java中Action层、Service层和Dao层的功能区分

    Action/Service/DAO简介: Action是管理业务(Service)调度和管理跳转的. Service是管理具体的功能的. Action只负责管理,而Service负责实施. DAO只 ...

  5. Java中常用集合操作

    一.Map 名值对存储的. 常用派生类HashMap类 添加: put(key,value)往集合里添加数据 删除: clear()删除所有 remove(key)清除单个,根据k来找 获取: siz ...

  6. java中的移位运算符:<<,>>,>>>总结

    java中有三种移位运算符 <<      :     左移运算符,num << 1,相当于num乘以2 >>      :     右移运算符,num >& ...

  7. 关于Java中进程和线程的详解

    一.进程:是程序的一次动态执行,它对应着从代码加载,执行至执行完毕的一个完整的过程,是一个动态的实体,它有自己的生命 周期.它因创建而产生,因调度而运行,因等待资源或事件而被处于等待状态,因完成任务而 ...

  8. Java中的进程和线程

     Java中的进程与线程 一:进程与线程 概述:几乎任何的操作系统都支持运行多个任务,通常一个任务就是一个程序,而一个程序就是一个进程.当一个进程运行时,内部可能包括多个顺序执行流,每个顺序执行流就是 ...

  9. Java中的进程与线程(总结篇)

    详细文档: Java中的进程与线程.rar 474KB 1/7/2017 6:21:15 PM 概述: 几乎任何的操作系统都支持运行多个任务,通常一个任务就是一个程序,而一个程序就是一个进程.当一个进 ...

随机推荐

  1. [转]Gnome桌面的录屏插件easyscreencast

    原文地址:https://www.linuxprobe.com/gnome-easyscreencast.html

  2. 使用vue搭建应用二加入element

    安装使用 element 1.安装 yarn add element-ui 2.使用 (1)在 main.js 中引入 element main.js 为修改 import Vue from 'vue ...

  3. Kubernetes 及安装注意事项

    Docker Desktop for Mac/Windows开启Kubernetes 及安装注意事项 Table of Contents 1 解决方案 2 注意事项 2.1 Choose Kubeco ...

  4. proc 下创建与应用交互的可读写节点

    内核版本:Linux-4.14 随便写了个 proc 下节点的测试程序,可以用来与应用层交互. 也可以单独的用来做调试打印使用,例如封装个 my_printk 将信息单独存在节点内,然后可以在应用层 ...

  5. PyGSP

    PyGSP # PyGSP (0.5.1) # matplotlib (3.1.2) # networkx (2.4) # numpy (1.17.4) from pygsp import graph ...

  6. java中通过Adb判断PC是否连接了移动设备

    最近用到PC端和移动端通过USB连接传输数据的方式,于是总在使用Adb命令,为了逻辑的严谨和代码容错,想在传输数据的之前,PC和移动端先建立一次会话,防止移动端还未连接就直接传输数据会报错,找了很久并 ...

  7. Python之虚拟环境virtualenv、pipreqs生成项目依赖第三方包

    virtualenv简介 含义: virtual:虚拟,env:environment环境的简写,所以virtualenv就是虚拟环境,顾名思义,就是虚拟出来的一个新环境,比如我们使用的虚拟机.doc ...

  8. 【实战经验】STM32烧录

    1.编译 2.配置烧录工具 2.配置烧录工具 3.配置烧录工具(一般街上Jlink就能检测到对应的STM32芯片) 5.选择FLASH 4.烧录 5.烧录完成

  9. PS 有哪些小技巧让你好用到哭?

    作者:bart链接:https://www.zhihu.com/question/328895616/answer/763462289来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载 ...

  10. arc079

    D. Decrease (Contestant ver.) 大意: 每次操作选一个最大数$-n$,其余数全$+1$. 要求构造一个序列$a$, 使得恰好$k$次操作后最大值不超过$n-1$. 只要让$ ...