多个线程同时执行,可能会运行同一行代码,如果程序每次运行结果与单线程执行结果一致,且变量的预期值也一样,就是线程案例的,反之则是线程不安全。
引发线程安全问题的根本原因:多个线程共享变量
如果多个线程对共享变量只有读操作,无写操作,那么此操作是线程安全的。
如果多个线程同时执行共享变量的写和读操作,则操作不是线程安全的。
下面以多窗口卖票为例
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给出了各种办法
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--);
}
}
}
}
}
}
很重要的三大特性:
JMM
)编译器和处理器会对执行指令进行重排序优化,目的是提高程序运行效率。
现象是,编写的java代码语句的先后顺序,不一定是按照写的顺序执行。
int count = 0;
boolean flag = false;
count =1;// 语句1
flag = true; //语句2
上述代码在执行过程中:语句1不一定在语句2之前先执行,由于指令重排,语句2可能先于指令1执行。
为什么要指令重排?
同步变异步,系统指令层面的优化。
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计算直接操作高速缓存数据时,运算结束将结果刷回主内存。
JMM线程操作内存基本规则:
JMM通过控制线程与本地内存之间的交互,来保证内存可见性。
使用JMM:synchronized、volatile,遵循了 happens-before
规则
在JMM中使用happens-before规则约束编译器优化行为,java允许编译器优化,但不能无条件优化。
如果一个操作的执行结果需要对另一个操作可见,那么这两个操作必须存在 happens-before
的关系!
一言以蔽之:就是当前操作,主内存的变量对需要的线程可见。
并发中-线程安全问题及三大特性,至此结束,如有疑问,欢迎评论区留言。