问题

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

然而很不巧,我们目前使用的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. JDBC连接数据库(MySQL)

    package com.dao; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSe ...

  2. hdu 3074 Multiply game(模板级线段树)

    离机房关门还有十分钟,这点时间能干些什么?故作沉思地仰望星空,重新捋一下一天的学习进度,或者,砍掉一棵模板级线段树. 纯模板,就是把单点更新,区间求和改为单点更新,区间求积. 1A. #include ...

  3. [每日一题] OCP1z0-047 :2013-07-19 Rules of Precedence――括号的使用

    这道题目的意思是你的公司决定给所有呆到5年或5年以上的所有员工每个月加50美元,然后算出总的年薪.每个月薪水:salary,每个月加到:salary+50,总的年薪: (salary+50)*12. ...

  4. 行业介绍:车载DVD MID 导航用料一般包含国腾LVDS芯片GM8284C/GM8283

    车载DVD MID 导航用料一般包含国腾LVDS芯片 GM8284C/GM8283/替代SN75LVDS83,THC63LVDM83C , 音频ES7144/CS4344,  龙讯MHL HDMI芯片 ...

  5. php设计模式之抽象工厂模式

    之前总结了一下工厂模式,其实准确地说是简单 工厂模式,在它的基础上再进行抽象就可以得到一种新的模式:抽象工厂模式. 专业一点的定义为: 抽象工厂模式(Abstact Factory)是一种常见的软件设 ...

  6. Java8:使用Lambda表达式增强版Comparator排序

    学习路上的自我记录-------路好长,就问你慌不慌,大声港,不慌.----jstarseven. 实体类: package com.server.model; /** * Created by js ...

  7. webpack入门必知必会

    关于 微信公众号:前端呼啦圈(Love-FED) 我的博客:劳卜的博客 知乎专栏:前端呼啦圈 前言 这是我第一篇介绍webpack的文章,先从一个入门教程开始吧,后续会有更多相关webpack的文章推 ...

  8. Java之路(六) 局部变量作用域最小化

    将局部变量的作用域最小化,可以增强代码的可读性和可维护性,并降低出错的可能性. 将局部变量的作用域最小化的方法有: 方法1:在第一次使用某个局部变量的地方进行声明. a.Java可以在任何可以出现语句 ...

  9. CODEFORCES-PROBLEMSET

    1A 水题   然而看不仔细爆int了 c++ #include <bits/stdc++.h> using namespace std; typedef long long ll; in ...

  10. maven初步入门demo

    Maven是跨平台的项目管理工具.作为Apache组织中的一个颇为成功的开源项目,主要服务于基于java平台的项目构建.依赖管理和项目信息管理. maven本身使用java开发而成,所以使用前确保电脑 ...