问题

前段时间在做服务注册发现的时候,有一处这样的逻辑,先发现下游服务,然后再是服务启动,服务启动完毕才能注册服务,因为注册一个在启动过程中的服务显然是不正确的设计。

然而很不巧,我们目前使用的TThreadpoolServer,通过serve()方法来启动服务,然后就阻塞在这个serve()方法里一直接while循环不会退出了。

要想判断此服务是否已经成功启动,只能通过其他线程去调用其isServing()方法来判断其是否已经启动完毕可以提供服务了,代码如下

    new Thread(new Runnable() {
@Override
public void run() {
try {
server.serve();
} catch (Exception e) {
LOGGER.error("server start error!,", e);
serveFail = true;
}
}
}).start();//启动服务
while (!server.isServing() && !serveFail) {//等待服务启动完毕
}
ServerRegistryUtil.getInstance().register()//注册服务

偶然出现的现象是,这个while循环永远不会退出。

但是服务明明已经启动起来了。

这显然是一个内存可见性的问题,为什么主线程去判断这个isServing的时候总是拿不到最新的结果呢?

机智的doug lea大神早就看穿了一切,在他的 supplement to the book Concurrent Programming in Java: Design Principles and PatternsVisibility中写到

In particular, it is always wrong to write loops waiting for values written by other threads unless the fields are volatile or accessed via synchronization (see §3.2.6).

既然有前人带路了,那就简单翻译一下大神的文章。

当然,熟悉jvm内存模型机制对于这类逻辑的理解还是很有必要的,简单的说就是jvm的内存模型分为主内存和工作内存,主内存类似于我们常说的内存,能被所有CPU访问,而工作内存是介于CPU和内存中的一层缓存,每个CPU的工作内存是互相独立的。

Visibility

只有在以下情况时,一个线程对某个字段的修改才能确保被其他线程‘看见’。

  • 写入的线程释放了一个同步锁(synchronization lock),并且读的线程随后获得了这个同步锁。

    本质上,释放一个锁意味着强制将所有的修改从线程的工作内存刷新到主内存。获得一个锁意味着强制让线程从主内存刷新其可以访问的值。同步锁不仅提供了对一个同步方法或者代码块的同步访问,还对这些线程执行时所需要使用到的字段的内存效果进行了此类定义。

    注意到synchronized有两种意义:首先,他提供了一种高级别的协议的锁。同时还处理了内存系统保持对于使用同一个锁的不同线程对于字段值的可见性保证(有时通过内屏障来实现)。这也从某种程度反映出相对于顺序编程,并发编程更类似于分布式编程。同步的另一个特性可以看做是一种机制,一个线程在运行同步的方法时,他将向其他线程发送或接收其他线程(在同步方法中)对字段的修改,从这点来看,使用锁和发送消息仅仅是语法不同而已。

  • 一个字段已经被申明成volatile。所有对它的值的修改,在此线程执行任何的后续内存操作前,都会被强制刷新,使得他的最新值对其他线程可见。每次读取volatile字段的值都必须强制从主内存刷新。

  • 一个线程第一次访问一个对象的某个字段的时候,他将会看到此字段的初始化值,或者是其他线程修改后的值。

    注意,将一个尚未完成构造的对象的引用暴露出来是一个错误的做法(见2.1.2),同样在一个构造方法里面启动新的线程也是危险的。

    特别是对于一个可以被继承的类。Thead.start有这样的内存效果:thread调用start的时候将释放一个锁,紧接着已经start的thread将获得这个锁,如果一个实现了Runnable的超类在子类的构造方法执行前调用了new Thread(this),这样对象就有可能在run方法执行时还尚未完成构造。

    同样的,如果你创建并且启动了一个新的线程T,在这之后你创建了一个对象X,并且你还在线程T里使用到了他,你不确定X的所有字段都能被线程T所看见,除非你在所有使用到了线程T的地方都加上同步锁,如果可以的话,你应该在T开始之前就创建X。(感觉这种方法也写不出来啊,编译器已经强制检查thread里用到了的x应该声明称final的)

  • 线程终止时,所有修改的变量都会被刷新到主存中。例如一个线程使用Thread.join来终止另一个线程,那么他肯定能看到另一个线程对变量值得修改。

注意,在单线程的方法间传递引用时,永远不会遇到内存可见性的问题。

线程模型保证了线程间的操作最终都会可见,一个线程对一个字段的修改最终都会被另一个线程看见。但是这个最终会花费多久就不好说了,没有使用同步的线程对于内存的可见性是很无助的。特别的,当一个字段不是volatile且也没有通过锁去同步时,一个线程在循环中单纯地去访问这个值,等待另一个线程对其进行修改,是永远也不可见的(参见3.2.6)。

线程模型同样也允许了在没有使用同步的情况下,可见性不一致的情况。例如,那个这个对象某一个字段的最新值,但是另一个字段的值却是旧的。同样,也可能读取到这个引用的值是最新的,即指向了一个新的对象,但是这个对象里面字段的值却是旧的。

不管怎样,线程模型只是允许了这种可见性不一致的发生,并不是说一定会发生。并不是说多个线程没有使用同步的话,就一定会出现内存可见性的问题,只是说有可能发生。从现在大多数的JVM的实现和表现来看,即使在使用了多处理器的JVM中,也很少出现这些问题。对于共享同一个CPU的多个线程来说,在缺少编译器优化,以及存在强缓存一致性的硬件,都会使得线程在更新字段值后立马传递给其他线程。所以想通过测试来发现由于线程可见性导致的错误是不现实的,因为这种情况极少发现,或者说只在你没有使用过的平台上出现,或者只会在将来的某个平台上出现。这些观点用于描述多线程编程的可能出现的问题来说更合适,在没有使用同步时,引起并发编程错误的原因有很多,其中就包括了内存一致性的问题。

思考

读完大师的文章,所以对于这里为什么出现问题就已经了解了,使用synchronized是一个办法。

使用volatile却并不能解决这里的问题,因为我们这里是需要查看isServing字段的可见性,而isServing字段是TServer的一个字段,我们无法将其修改成volatile的。

内存模型对于一致性的保证中,对于普通的一致性,会确保最终可见,最终可见这个事情其实是CPU帮你从主内存中重载了数据到工作内存。但是如果这个线程一直在while循环里进行单纯的CPU操作,那么就意味着线程一直占用着CPU,CPU完全没有时间来从主内存同步工作内存,所以会导致最终可见性永远不会发生。

调用Thread.sleep(),或者是其他一些可以让CPU闲下来的操作都可以使得最终可见性发生,比如涉及到内存分配或者IO的操作。而且从实践的经验来看,JVM的优化确实不错,几乎是立即可见了,所以在本例中只是单纯地使用了TimeUnit.MILLISECONDS.sleep(1)

使用synchronized这种强一致的方法进行,也存在着风险,如果等待通知的线程先获得了锁,那么服务就不会启动了,使用何种方法,应该由具体的业务场景来决定。

另一方面,昨天正好看了effective java这本书,case 70说的也是这个问题,看来还是书读的太少啊。

其他

接下来讲一下,Thread.yield(),Thread.sleep(),Object.wait()的异同点。

首先,他们都会让出CPU,此时CPU就可以去做一些其他的事情,比如上文中我们提到的,保证内存最终可见性的发生,也就是从主内存重新load数据到工作内存。

yield(),是指当前线程将让出cpu,让调度器去重新调度,线程本身还是处于就绪状态的,有可能调度器又调度了当前线程,这些都是不确定的,基于调度器的逻辑。

sleep(),则是让当前线程sleep一定的时间,此时线程处于sleep状态,时间到了之后,重新进入就绪状态。sleep并不会释放锁资源。

Object.wait(),则是让线程处于一个waiting状态,也可以说是一个阻塞状态。当被同一个Object在执行notify()或者notifyAll()的时候,线程会重新进入就绪状态。wait()主要的作用是用于线程间通信的。只能在被Synchronized的代码块中调用,否则IllegalMonitorStateException。

线程本身执行的代码中,有需要等待IO输入,或者需要等待内存分配或者访问的,或者是锁的存在,都会使得线程进入阻塞状态。直到等待的操作完成时,或者等待的资源被释放时,才会重新进入运行状态。

状态流转请看下图(来源)

接下来讲一个小错误,new Thread(Runnable() ->).start(),这是启动一个新的线程去执行里面的run方法,当run方法结束时,新线程退出。而new Thread(Runnable() ->).run(),则是在当前线程执行一次run方法。

参考文档

Synchronization and the Java Memory Model by doug lea

What is difference between wait and sleep in Java?

Java内存一致性的更多相关文章

  1. Java内存模型深度解析:顺序一致性--转

    原文地址:http://www.codeceo.com/article/java-memory-3.html 数据竞争与顺序一致性保证 当程序未正确同步时,就会存在数据竞争.java内存模型规范对数据 ...

  2. java内存模型-顺序一致性

    数据竞争与顺序一致性保证 当程序未正确同步时,就会存在数据竞争.java 内存模型规范对数据竞争的定义如下: 在一个线程中写一个变量, 在另一个线程读同一个变量, 而且写和读没有通过同步来排序. 当代 ...

  3. 深入理解Java内存模型(三)——顺序一致性

    数据竞争与顺序一致性保证 当程序未正确同步时,就会存在数据竞争.java内存模型规范对数据竞争的定义如下: 在一个线程中写一个变量, 在另一个线程读同一个变量, 而且写和读没有通过同步来排序. 当代码 ...

  4. 【转】深入理解Java内存模型(三)——顺序一致性

    数据竞争与顺序一致性保证 当程序未正确同步时,就会存在数据竞争.java内存模型规范对数据竞争的定义如下: 在一个线程中写一个变量, 在另一个线程读同一个变量, 而且写和读没有通过同步来排序. 当代码 ...

  5. Java内存模型_顺序一致性

    数据竞争: 当程序未正确同步时,就会存在数据竞争.java内存模型规范对数据竞争的定义如下: 在一个线程中写一个变量 在另一个线程读同一个变量 而且写和读没有通过同步来排序 如果程序是正确同步的,程序 ...

  6. 深入理解JMM(Java内存模型) --(三)顺序一致性

    数据竞争与顺序一致性保证 当程序未正确同步时,就会存在数据竞争.Java内存模型规范对数据竞争的定义如下: 在一个线程中写一个变量, 在另一个线程读同一个变量, 而且写和读没有通过同步来排序. 当代码 ...

  7. Java内存模型(三)原子性、内存可见性、重排序、顺序一致性、volatile、锁、final

          一.原子性 原子性操作指相应的操作是单一不可分割的操作.例如,对int变量count执行count++d操作就不是原子性操作.因为count++实际上可以分解为3个操作:(1)读取变量co ...

  8. 【Java虚拟机4】Java内存模型(硬件层面的并发优化基础知识--缓存一致性问题)

    前言 今天学习了Java内存模型第一课的视频,讲了硬件层面的知识,还是和大学时一样,醍醐灌顶.老师讲得太好了. Java内存模型,感觉以前学得比较抽象.很繁杂,抽象. 这次试着系统一点跟着2个老师学习 ...

  9. Java 多线程之内存一致性错误

    当不同的线程针对相同的数据却读到了不同的值时就发生了内存一致性错误.内存一致性错误的原因是非常复杂的.幸运的是我们程序员不需要详细的理解这些原因,我们需要做的事情就是使用策略来规避这些. 避免内存一致 ...

随机推荐

  1. 通用高性能 Windows Socket 组件 HP-Socket v2.2.2 更新发布

    HP-Socket 是一套通用的高性能 Windows Socket 组件包,包含服务端组件(IOCP 模型)和客户端组件(Event Select 模型),广泛适用于 Windows 平台的 TCP ...

  2. java BigInteger源码学习

    转载自http://www.hollischuang.com/archives/176 在java中,有很多基本数据类型我们可以直接使用,比如用于表示浮点型的float.double,用于表示字符型的 ...

  3. LightOJ 1248 Dice (III)

    期望,$dp$. 设$dp[i]$表示当前已经出现过$i$个数字的期望次数.在这种状态下,如果再投一次,会出现两种可能,即出现了$i+1$个数字以及还是$i$个数字. 因此 $dp[i]=dp[i]* ...

  4. Java如何根据IP获取当前定位

    当今购物.旅游等服务型的网站如此流行,我们有时候也会碰到这样网站的开发. 在开发此类网站时,为了增加用户的体验感受,我们不得不在用户刚进入网站时定位到用户所在地, 更好的为用户推荐当地产品.比如去哪儿 ...

  5. SharePoint Framework (SPFx)安装配置以及开发-基础篇

    前言 SharePoint Framework(SPFx),是页面 和Webpart的模型,完全支持本地开发(即完全可以脱离SharPoint环境在本地进行开发),SPFx包含了一系列的client- ...

  6. 驱动10.nor flash

    1 比较nor/nand flash NOR                                  NAND接口:    RAM-Like,引脚多                 引脚少, ...

  7. 【BJG吐槽汇】第2期 - 我每周1-2次迟到都是因为你-->ios10!

    本期槽点嘉宾:苹果系统 ios10 小吐我记得iphone是在2008年出的,当时我还在用诺基亚N70,对iphone是十分的陌生,想必大家也是,直到iphone4的时候,黄牛成群,饥饿营销,包括出i ...

  8. PRD学习笔记:一些需要注意的说明

    控件说明 1)输入框 若输入框有默认提示,点击输入框,弹出软键盘. 当输入框内不为空(空格除外)时,默认显示消失. 2)软键盘的弹出及退去机制 当输入框内必须输入的为数字时,弹出数字软键盘.其余时候, ...

  9. sed常见用法,删除匹配行的上2行,下3行

    删除匹配的下一行到最后一行 [root@test200 ~]# cat test a b c d e f [root@test200 ~]# sed '/c/{p;:a;N;$!ba;d}' test ...

  10. js getByClass函数封装

    function getByClass(oParent, sClass) { var aEle=oParent.getElementsByTagName('*'); var aResult=[]; v ...