从半个多月前接到阿里的面试电话,被多线程问题难住,到今天终于读完了《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. Xshell连接SqlPlus无法使用退格、删除键

    问题:在使用xshell连接CentOS7,进入SQLPLUS进行命令操作时,如果输错了信息,无法进行退格键删除(显示“^H”),同样按删除键,显示“^[[3~”. 解决:网上查找了相关资料,可以通过 ...

  2. Idea开发环境中,开发springboot类型的项目,如果只引入parent节点,不添加依赖节点,maven是不会加载springboot的任何依赖的

    在SpringBoot类型的项目中,我本来是要使用pringBoot,创建一个Console项目,我原本在pom.xml中添加paren节点了,天真的认为不需要再添加其他任何依赖了,可是接下来的1个小 ...

  3. POJ-动态规划-背包问题模板

    背包问题模板 一.0-1背包 状态:背包容量为j时,求前i个物品所能达到最大价值,设为dp[i][j].初始时,dp[0][j](0<=j<=V)为0,没有物品也就没有价值. 状态转移方程 ...

  4. find命令实例

    按时间查找也有参数 -atime 访问时间 -ctime 改变状态的时间 -mtime修改的时间. 这里的时间是以24小时为单位的. 查找最近30分钟修改的当前目录下的.php文件 find . -n ...

  5. jmeter jtl 文件

    一.获取.jtl文件 使用非 GUI 模式,即命令行模式运行 JMeter .执行完成jmeter后,会生成jtl文件. 1.1. 命令介绍 1)先cmd进入到jmeter的bin文件目录下(这里是 ...

  6. 解决非controller使用@Autowired注解注入为null问题

    在SpringMVC框架中,我们经常要使用@Autowired注解注入Service或者Mapper接口,我们也知道,在controller层中注入service接口,在service层中注入其它的s ...

  7. redis底层实现的几种数据结构

    redis底层数据结构 一.简单动态字符串(SDS) 定义: struct sdshdr{ int len;    //SDS所保存的字符串长度 int free //记录buf数组中为使用的字节数量 ...

  8. nginx访问认证+目目录浏览

    概述 在实际工作中,企业中有些网站,要求使用账号和密码才能访问,如网站后台.phpMyAdmin .Wiki 平台 等模块ngx_http_auth_basic_module 允许使用“HTTP基本认 ...

  9. git 学习笔记 -- 创建标签

    在Git中打标签非常简单,首先,切换到需要打标签的分支上: $ git branch * dev master $ git checkout master Switched to branch 'ma ...

  10. 《JAVA高并发编程详解》-并发编程有三个至关重要的特性:原子性,有序性,可见性