目录
- 概述
- 线程
- 线程安全问题
- 线程安全的根本原因
- 验证
- 代码
- 执行结果
- 解决线程安全
- 代码
- 结果
- 线程并发三大特性
- 指令重排
- as-if-serial
- 可见性
- cpu和缓存一致性
- java内存模型(java memory model)
- 解决可见性问题及happens-before
- 结束
概述
线程
线程安全问题
多个线程同时执行,可能会运行同一行代码,如果程序每次运行结果与单线程执行结果一致,且变量的预期值也一样,就是线程案例的,反之则是线程不安全。
线程安全的根本原因
引发线程安全问题的根本原因:多个线程共享变量
如果多个线程对共享变量只有读操作,无写操作,那么此操作是线程安全的。
如果多个线程同时执行共享变量的写和读操作,则操作不是线程安全的。
验证
下面以多窗口卖票为例
代码
package com.fun.demo;
public class DemoTicket {
public static void main(String[] args) {
TicketTask task = new TicketTask();
new Thread(task, "窗口1").start();
new Thread(task, "窗口2").start();
new Thread(task, "窗口3").start();
}
static class TicketTask implements Runnable {
private int tickets = 100;
@Override
public void run() {
while (true) {
if (tickets > 0) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-正在卖票:" + tickets--);
}
}
}
}
}
执行结果
窗口显示
解决线程安全
为了解决线程安全问题,java给出了各种办法
- 同步机制 synchronized
- volatile 关键字:内存屏障
- 原子类:cas
- 锁:AQS
- 并发容器
代码
package com.fun.demo;
public class DemoTicket {
public static void main(String[] args) {
TicketTask task = new TicketTask();
new Thread(task, "窗口1").start();
new Thread(task, "窗口2").start();
new Thread(task, "窗口3").start();
}
static class TicketTask implements Runnable {
private final Object lock = new Object();
private int tickets = 100;
@Override
public void run() {
while (true) {
synchronized (lock) {
if (tickets > 0) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-正在卖票:" + tickets--);
}
}
}
}
}
}
结果
执行结果如下:
线程并发三大特性
很重要的三大特性:
- 原子性:一个系列指令代码,要么全执行,要么都不执行,执行过程不能被打断
- 有序性:程序代码按照先后顺序执行
- 产生无序问题是因为指令重排
- 可见性:当多个线程访问同一个变量时,一个线程修改了共享变量的值,其它线程能够立即看到
- 出现可见问题是因为java内存模型(
JMM
)
- 出现可见问题是因为java内存模型(
指令重排
编译器和处理器会对执行指令进行重排序优化,目的是提高程序运行效率。
现象是,编写的java代码语句的先后顺序,不一定是按照写的顺序执行。
int count = 0;
boolean flag = false;
count =1;// 语句1
flag = true; //语句2
上述代码在执行过程中:语句1不一定在语句2之前先执行,由于指令重排,语句2可能先于指令1执行。
为什么要指令重排?
同步变异步,系统指令层面的优化。
- 无论如何重排,不会影响最终执行结果,因为大部分指令并没有严格的前后执行顺序。
- 在单线程情况下,程序执行遵循as-if-serial语义。
as-if-serial
as-if-serial 指不管编译器和处理器怎么重排指令,单线程执行结果不受影响。
看下面例子:
int a = 10; // 语句1
int b = 10; // 语句2
a = a + 3; // 语句3
b = a * b; // 语句4
上面代码执行的顺序:语句2 —> 语句1—>语句3—> 语句4
不可能是:语句2 —> 语句1—>语句4—> 语句3
总结: 处理器在指令重排时,会考虑指令之间的数据依赖性。
重排不会影响单线程程序正确执行,但是会影响多线程。
看下面例子:
// 线程1
boolean init = false;// 语句1
String context = loadContext(init);// 语句2
init = true; // 语句3
// 线程2:
while (!init) {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
executeDemo(context);
// 下面是类中方法
private static void executeDemo(String context) {
}
private static String loadContext(boolean init) {
return "init:" + init;
}
当语句3与语句2执行顺序变化时,在多线程中会发生问题。
可见性
cpu和缓存一致性
在多核cpu中每个核都有自己的缓存,同一个数据的缓存与内存可能不一致。
cpu缓存的诞生是因为cpu执行速度和内存读取速度差距越来越大,导致cpu每次操作内存都要耗费很多等待时间。为了解决这个问题,在cpu和物理内存上新增调整缓存。
程序在运行过程中会将运算所需要数据 从主内存复制到cpu高速缓存,当cpu计算直接操作高速缓存数据时,运算结束将结果刷回主内存。
java内存模型(java memory model)
- java 为了保证满足原子性,可见性及有序性,诞生了jsr133规范,java内存模型,简称 JMM
- JMM规范解决了cpu多级缓存、处理器优化、指令重排等导致的内存访问问题
以下是JMM内存模型抽象结构示意图:
- JMM定义共享变量何时写入,何时对另一个线程可见
- 线程之间的共享变量存储在主内存
- 每个线程都有一个私有的本地内存,本地内存存储共享变量的副本
- 本地内存是抽象的、不真实存在,涵盖:缓存 、写缓冲区、寄存器等
JMM线程操作内存基本规则:
- 线程操作共享变量必须在本地内存中,不能直接操作主内存的。
- 线程间无法直接访问对方的共享变量,需要经过主内存传递。
JMM通过控制线程与本地内存之间的交互,来保证内存可见性。
解决可见性问题及happens-before
使用JMM:synchronized、volatile,遵循了 happens-before
规则
在JMM中使用happens-before规则约束编译器优化行为,java允许编译器优化,但不能无条件优化。
如果一个操作的执行结果需要对另一个操作可见,那么这两个操作必须存在 happens-before
的关系!
- 程序次序规则:在一个线程内,按照控制流顺序,如果操作A先行发生于操作B,那么操作A所产生的影响对于操作B是可见的。
- 管程锁定规则:对于同一个锁,如果一个unlock操作先行发生于一个lock操作,那么该unlock操作所产生的影响对于该lock操作是可见的。
- volatile变量规则:对于同一个volatile变量,如果对于这个变量的写操作先行发生于这个变量的读操作,那么对于这个变量的写操作所产的影响对于这个变量的读操作是可见的。
一言以蔽之:就是当前操作,主内存的变量对需要的线程可见。
结束
并发中-线程安全问题及三大特性,至此结束,如有疑问,欢迎评论区留言。