从半个多月前接到阿里的面试电话,被多线程问题难住,到今天终于读完了《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. 【454】ML-DL相关链接

    GD(梯度下降)和SGD(随机梯度下降) 机器学习中的Bias和Variance 机器学习之判别式模型和生成式模型 笔记 | 什么是Cross Entropy

  2. istio1.0 实现蓝绿发布(未完成)

    istio1.0 实现蓝绿发布 环境: 192.168.0.91 master 192.168.0.92 node 第一步:安装k8s集群,参照:https://www.cnblogs.com/eff ...

  3. LODOP表格水平居中3(宽度为百分比)

    如果一个表格在css样式等中设置了固定的宽度,想要实现表格在纸张中水平居中,可根据固定的宽度设置合适的左边距,如果打印项内容在打印项宽度中居中,可以设置打印项在纸张中居中.方法1:宽度固定,纸张大小固 ...

  4. css3写下雨效果

    css3写下雨效果<pre><div class="xiayuxiaoguo"></div></pre> <pre>.x ...

  5. Servlet3.0对异步处理的支持

    Servlet工作流程 Servlet 3.0 之前,一个普通 Servlet 的主要工作流程大致如下: Servlet 接收到请求之后,可能需要对请求携带的数据进行一些预处理: 调用业务接口的某些方 ...

  6. 如何改变 select 元素的高度

    mozilla 对于美化 select 元素的样式有这样一段描述(用 CSS 美化 Select 元素): 众所周知,select 元素很难用 CSS 进行高效的设计.你可以影响任何元素的某些方面 - ...

  7. 最近公共祖先 LCA 递归非递归

    给定一棵二叉树,找到两个节点的最近公共父节点(LCA).最近公共祖先是两个节点的公共的祖先节点且具有最大深度.假设给出的两个节点都在树中存在. dfs递归写法 查找两个node的最近公共祖先,分三种情 ...

  8. STL源码剖析——算法#1 内存处理基本工具

    我们在学习序列式容器时,我们经常会遇到这三个函数:uninitialized_copy.uninitialized_fill.uninitialized_fill_n.在那时我们只是仅仅知道这些函数的 ...

  9. C++构造和解析JSON

    JSON是一种轻量级的数据交互格式,易于人阅读和编写,同时也易于机器解析和生成,并有效地提升网络传输效率,实际项目中经常用到,相比xml有很多优点,问问度娘,优点一箩筐. 第三方库 json解析选用j ...

  10. 【leetcode】590. N-ary Tree Postorder Traversal

    Recurisve: /* // Definition for a Node. class Node { public: int val; vector<Node*> children; ...