JSR 是什么?
JSR
是Java Specification Requests
全称,用于制定java
规范的东西。我们的JMM
就是制定在JSR
中。本文仅讨论JSR133
.
JMM是什么
JMM
是 JAVA Memory Model
,它定义了虚拟机
和编译器
所需要遵守的规则。
JMM存在的意义
多线程通信方式大体上分为两种形式:
1.消息传递
2.共享内存
两者分别有各自的优劣:
(1)消息传递:往往依赖于事件循环,效率略低,但不需要考虑多线并发问题,如Dart
语言便是其中一个。
(2)共享内存:多线程共享一个内存区域,效率高,但是往往带来大量的并发问题,如 缓存带来的脏数据,原子更新等,如Java
,C/C++
语言
由于Java是多线程
通信采用的共享内存
方式,所以必须制定一些标准,解决并发相关问题。
JMM
主要解决 编译重排序问题,和处理器编译重排序问题。内存缓存问题也可以理解为内存重排序问题。
消息传递模式
java
中的消息传递模式可使用如下代码完成:
public class Main {
public static void main(String[] args) throws InterruptedException {
new Thread() {
@Override
public void run() {
while (true) {
try {
ServerSocket serverSocket = new ServerSocket(9998);
Socket socket = serverSocket.accept();
InputStream inputStream = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
String msg = null;
while ((msg = br.readLine()) != null) {
System.out.println("服务端线程收到" + msg);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}.start();
try {
System.out.println("客户端启动");
Socket socket = new Socket("127.0.0.1", 9998);
OutputStream outputStream = socket.getOutputStream();
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(outputStream));
bw.write("你哈\r\n");
bw.flush();
TimeUnit.MILLISECONDS.sleep(300);
} catch (IOException e) {
e.printStackTrace();
}
}
}
上述代码存在两个线程一个是Main
线程,和一个服务端子线程
,两者使用本地socket
进行通信。两个线程并无共享变量代码,所以你根本不需要考虑所谓的原子性,有序性,可见性相关问题。但是相率可见一斑
共享内存模式
由于Java
天生就是此模式,所以例子非常多。所以我们直接举例一些共享内存所带来的的问题。
可见性,处理器优化问题
public class Main {
static boolean isStop = false;
public static void main(String[] args) throws InterruptedException {
Thread childThread = new Thread(new Runnable() {
@Override
public void run() {
while (!isStop) {
}
System.out.println("Child Thread over");
}
});
childThread.start();
TimeUnit.SECONDS.sleep(1);
isStop = true;
}
}
上述代码可能永远无法输出Child Thread over
。为什么?如果回答不上,这便是学习JMM
的重要性。这里简单说下出现的原因,和解决方案,但是只是简单介绍。
网上解释:
(1)由于每个线程都有自己的共享变量的副本,而主内存的更新了,但是childThread
线程副本没有更新。
(2)主线程Main中修改 isStop
,并没有刷新回主内存,而是存在Main
线程中。
(3)解释执行时处理器优化,childThread
运行时大量判断标识变量然后执行空循环,且在过段时间其他线程才修改了标识变量isStop
.所以处理器,会直接跳过判断标识变量isStop
然后直接方法体。
解决方案:
(1)内部添加存在并发语法的代码如println
,synchronized
等,所以你会发现内部加了一个输出也会停止循环。原因是println
内部存在并发语法关键字synchronized
,而这个关键字会触发缓存更新,且使执行器不轻易的优化跳过表示变量的判断。
(2)共享变量isStop
添加修饰符volatile
。volatile
会是缓存失效,每次都能读取到最新数值,且写入立即刷新到主内存。由于volatile
存在也会使执行器谨慎的优化 不会出现第三点。
关于第三点可以参看如下链接:
-server多线程由于可见性导致死循环
Java volatile 汇编代码研究
final语义于安全构造对象问题
在JSR133
标准之前的JDK
,会有一个非常著名的多线程字符串问题。
如下:
public class Main {
static volatile String msg = "";
public static void main(String[] args) throws InterruptedException {
Thread childThread = new Thread(new Runnable() {
@Override
public void run() {
String temp = "I love";
//(1)FLAG TWO
msg = temp.substring(1);
}
});
Thread childThread2 = new Thread(new Runnable() {
@Override
public void run() {
//(2)FLAG THREE
//假设此处FLAG TWO已经执行完成
if (msg.equals(" love")) {
System.out.println("true");
} else {
System.out.println("false");
}
}
});
childThread.start();
childThread2.start();
}
}
上面的假设执行(2)FLAG THREE
时已经完成 (1)FLAG TWO
,那么此处依然可能会输出false
.
原因是msg
此时的字符串是I love
,为什么I love
?
由于java
字符串类实现是由自身的三个属性构造,分别为数组,数组的起始坐标,字符串长度,三者皆为final
。而字符串之间可以共享字符串数组,减少不必要的内存对象。但是字符串构造时,字符串的起始坐标赋值可能位于构造函数之外,此时另一个线程读取了一个还未赋值开始坐标的字符串,具体详解后续文章描绘。
结束:文章大致说了由于共享内存模型的编程语言所带来的弊端,所以孕育而生了JMM
.