问题

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

然而很不巧,我们目前使用的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. hdu 1542 Atlantis(求矩形面积并)

    分别记录x坐标和y坐标,将其分别按照从左到有的方向排序.然后对于一个输入的矩形的x,y坐标范围内的下标进行标记.以两个相邻的坐标为最小单位分割图形,最后求总面积. #include<stdio. ...

  2. C#中数组,ArrayList与List对象的区别

    在C#中,当我们想要存储一组对象的时候,就会想到用数组,ArrayList,List这三个对象了.那么这三者到底有什么样的区别呢? 我们先来了解一下数组,因为数组在C#中是最早出现的. 数组 数组有很 ...

  3. 算法线性编程珠玑读书笔记之----->使用线性算法求解连续子序列的最大和

    这段时间笔者几篇文章介绍了改算法线性的文章. 关联文章的地址 这个算法我在我的博客里应用动态规划做过,详细实现请参阅我的dp板块,下面给出书上最快的算法,时间复杂度为O(n),称之为线性算法. #in ...

  4. Linux文件系统十问,你知道吗?

    关于文件系统,相信大家都不陌生.身为攻城狮的我们几乎天天都会与之打交道,但是细深剖一下,其中又有多少是我们理解深度不够的呢.那么让我们一起来看一下下面这一组Linux文件系统相关的问题吧: 1.机械磁 ...

  5. JAVA的abstract修饰符 && 接口interface用法 && 抽象类和interface的差别

    abstract修饰符可以修饰类和方法. (1)abstract修饰类,会使这个类成为一个抽象类,这个类将不能生成对象实例,但可以做为对象变量声明的类型(见后面实例),也就是编译时类型.抽象类就相当于 ...

  6. js jquery 实现html页面之间参数传递(单一参数、对象参数传递)

    最近自己在忙着做毕业设计,后台程序员,前端菜鸡,因为需要,所以实现了html页面之间参数传递.------jstarseven .菜鸡的自我修养. 页面A代码如下: <!DOCTYPE html ...

  7. JavaScript系列文章:详解正则表达式之三

    在上两篇文章中博主介绍了JavaScript中的正则常用方法和正则修饰符,今天准备聊一聊元字符和高级匹配的相关内容. 首先说说元字符,想必大家也都比较熟悉了,JS中的元字符有以下几种: / \ | . ...

  8. iOS开发-大文件下载与断点下载思路

    大文件下载方案一:利用NSURLConnection和它的代理方法,及NSFileHandle(iOS9后不建议使用)相关变量: @property (nonatomic,strong) NSFile ...

  9. mac 显示隐藏文件方法

    终端执行命令: 显示:#defaults write com.apple.finder AppleShowAllFiles -bool true隐藏:#defaults write com.apple ...

  10. python绝技 — 嗅探FTP登录口令

    代码: #!/usr/bin/python #--*--coding=utf-8--*-- import optparse from scapy.all import * def ftpsniff(p ...