从半个多月前接到阿里的面试电话,被多线程问题难住,到今天终于读完了《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. java Random 抢红包算法

    红包有一个总金额和总数量,领的时候随机分配金额. 维护一个剩余总金额和总数量,分配时,如果数量等于1,直接返回总金额,如果大于1,则计算平均值,并设定随机最大值为平均值的两倍,然后取一个随机值,如果随 ...

  2. python:字符串中提取特定的数据

    在日志文件中有一大堆,格式相同的文本,需要提取出接口耗时的时间 >>> 运单号:71742507538566,快递100接口耗时:8,返回结果:[{"lengthPre&q ...

  3. (CSDN迁移) JAVA多线程实现-实现Runnable接口

    实现Runnable接口  implements Runnable 重写run()方法 @Override public void run(){//TODO} 创建线程对象: Thread threa ...

  4. Toping Kagglers:Bestfitting,目前世界排名第一

    Toping Kagglers:Bestfitting,目前世界排名第一 Kaggle团队 |2018年5月7日   我们在排行榜上排名第一 - 这是两年前令人惊讶地加入该平台的竞争对手.Shubin ...

  5. strace调试工具编译移植

     源码下载:https://github.com/strace/strace/releases/tag/v4.18(使用的较老版本,最新版 5.4 编译时依赖较多,最终博主放弃使用) [ 编译步骤 ] ...

  6. 【C++】static关键字有哪些用法?其主要作用是什么?

    static关键字的用法: 1)将全局变量修饰为静态全局变量 存储在静态存储区,整个程序运行期间一直存在 静态全局变量在声明它的文件之外是不可见的,只要声明它的文件可见,而普通的全局变量则是所有文件可 ...

  7. c和c++中的枚举和 区别

    1.c中的枚举 c语言枚举 void test(){ // enum 枚举类型名字{枚举值, 枚举值, 枚举值}; enum WEEK { Mon, Tue };// 枚举类型定义 enum WEEK ...

  8. 【转】Isim——基本技巧

    来源:电子产品世界: 注:本文由NingHeChuan本人多出整理所得,原文章图片不清晰,自己整理配图后重新发表 安装好ISE,系统已经自带了ISim仿真软件,相比于专业的仿真软件Modelsim,I ...

  9. [cf 1236 E] Alice and the Unfair Game

    题意: 给定一个长度为m的序列$A$,你有一个长度为n的棋盘,可以任选一个位置x作为起点. 在时刻$[1,m+1]$你可以向左或向右移动一格. 设时刻i你移动后所在的位置为$B_i$,你需要满足对于任 ...

  10. go 学习笔记 ----资源自动回收

    在释放局部资源时, 可以用defer管理 Go语言版本基于defer的Mutex用法 func safeRead(Mutex *mu) []byte { mu.Lock() defer mu.Unlo ...