事先声明 看zejian博客:并发专题 受益良多
https://blog.csdn.net/javazejian/article/category/6940462
1.线程不安全实例
涉及到JVM的运行时内存区域,这里不做讨论
通过一个代码引发的问题去展开探讨(不要纠结业务逻辑是否可优化)
public class MyThread extends Thread {
private boolean isRestFlag = false;//休息的指令
private boolean isWorkFlag = true;//干活的指令
public MyThread(String name) {
super(name);
}
public MyThread(String name, boolean isRestFlag) {
super(name);
this.isRestFlag = isRestFlag;
}
private static int anInt = 0;//干的事情大家都知道 多线程共享
private static List<Integer> anIntList = new ArrayList<>();
@Override
public void run() {
//老公就是一直干活
while (isWorkFlag) {
// anInt++ 分解成本来的两个动作;
int temp = anInt;
anIntList.add(anInt);
anInt = temp + 1;
// System.out.println(anInt++);
if (isRestFlag) {
//老婆下令结束 老公就可以休息了
isWorkFlag = false;
System.out.println(Thread.currentThread().getName() + "老公休息吧");
}
}
System.out.println(Thread.currentThread().getName() + "繁忙的一天结束了");
}
public static void main(String[] args) throws InterruptedException {
new MyThread("老公线程~~").start();//老公在干活
TimeUnit.SECONDS.sleep(1);
new MyThread("老婆线程~~", true).start();//老婆说休息
TimeUnit.SECONDS.sleep(3);
System.out.println("主线程结束 anInt:" + anInt);
System.out.println("anInt++ 中多线程情况下是否会取到重复的值" + anIntList.stream().collect(Collectors.groupingBy(Function
.identity(), Collectors.counting())).entrySet().stream().filter(x -> x.getValue() > 1).findFirst());
}
}
/* 打印信息 (备注 老公线程一直没有结束)
老婆线程~~老公休息吧
老婆线程~~繁忙的一天结束了
主线程结束 anInt:9346170
Exception in thread "main" java.lang.NullPointerException: element cannot be mapped to a null key
*/
- 线程间数据共享 场景1
这段程序很简单,就是两个线程 都操作两个变量 然后发现老公线程一直没有结束,空指针异常也是因为老公线程一直在处理list,而造成list foreach过程中抛异常, 为什么,老公线程一直没有结束–>isWorkFlag是实例变量,存储在栈区,属于线程独有的,无法共享,这个问题是入门级问题,不懂的可以看下java运行时内存
所以对应的放入栈区,简单的就是static修饰成类变量
private static boolean isWorkFlag = true;//干活的指令
/* 打印信息 (备注老公线程一直没有结束,但anInt++多线程下少被重复取到了)
老婆线程~~老公休息吧
老婆线程~~繁忙的一天结束了
老公线程~~繁忙的一天结束了
主线程结束 anInt:5880983
anInt++ 中多线程情况下是否会取到重复的值Optional[5880982=2]
*/
- 线程安全问题 场景2
线程间数据的传递解决了,来了个新问题,就是多线程下anInt++被重复取到,换个说法就是虽然线程间数据共享,but执行两次+1操作得到还是1,这可以理解为线程不安全,是由于anInt++两步动作不是原子操作引起的
- 又一种线程安全问题 场景3
我们再调整一下代码,去掉anIntList相关的 在效果方面system.out和list.add一样
// anIntList.add(anInt);
/* 老公线程又没有结束... 也就是static修饰的isWorkFlag 没有共享!
老婆线程~~老公休息吧
老婆线程~~繁忙的一天结束了
主线程结束 anInt:127657172
anInt++ 中多线程情况下是否会取到重复的值Optional.empty
*/
现在是isWorkFlag没有被看见,这也是线程安全问题,是由于数据可见性引起的
要解决线程安全问题得先了解java内存模型
2.java内存模型
JMM:java memory model
JMM是一种抽象的规定,并没有具体实现,规定了工作内存与主内存之间的交互方式,在多个线程同时操作主内存的情况下,如下图,在A线程修改数据为2的情况下,线程B是读到的是1呢还是2呢,这是不确定的,而这种不确定性 就是线程不安全的根因(线程操作主内存数据时存在不确定性就是线程不安全),而JMM的规定可以解决这个问题,同时JMM也为jvm与硬件内存的跨平台提供的解决方式
JMM规定了是围绕原子性,可见性,有序性三个特性展开的
原子性:和数据库原子性类似
可见性:读取时屏蔽工作内存的值,直接读取主内存的值,而写时立即会写主内存,同时通过这个内存屏障禁止指令重排序引起的多线程可见性问题(指令重排序知道cpu有这个优化手段就行)
有序性:使用内存屏障确保不会出现指令重排序,保证程序的有序性
对应的解决方案:
原子性:除了JVM自身提供的对基本数据类型读写操作的原子性外,对于方法级别或者代码块级别的原子性操作,可以使用synchronized关键字或者重入锁(ReentrantLock)保证程序执行的原子性
可见性和有序性:volatile关键字
场景3之所以static变量仍未能读取到主内存最新数据,是因为一直运行anInt++,根本不需要去主内存读取数据,所以isWorkFlag一直未刷新,而日常开发中较少碰到多线程情况下如此简单的业务场景,而略复杂的业务场景都需要读主内存数据,比如system.out/list.add(动态拓展时native方法)
所以上面代码的解决方案就是保证可见性或加synchronized也行
private volatile static boolean isWorkFlag = true;
JMM与jvm运行期内存区域的关系
没有关系,这不是一个层次的区分,java内存模型是一种抽象,jvm运行期内存区域是具体的数据存放划分,两者之间没有直接关系,仅有一些相似之处就是jvm栈可以比拟工作内存,堆可以比拟主内存