简介
本文作为多线程编程的第一篇文章,将从一个简单的例子开始,带你真正从代码层次理解什么是线程不安全,以及为什么会出现线程不安全的情况。文章中将提供一个完整的线程不安全示例,希望你可以跟随文章,自己真正动手运行一下此程序,体会一下多线程编程中必须要考虑的线程安全问题。
一.什么是线程安全
《Java Concurrency In Practice》作者Brian Goetz的定义:“当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。
为什么不把所有操作都做成线程安全的?
实现线程安全是有成本的,比如线程安全的程序运行速度会相对较慢、开发的复杂度也提高了,提高了人力成本。
二.从经典的线程不安全的示例开始
经典案例
: 两个线程,共同读写一个全局变量
count
,每个线程执行10000次
count++
,
count
的最终结果会是20000吗,在心中猜测一下运行结果?
经典案例的代码实现:
package com.study.synchronize.object;
/**
* 线程不安全案例:两个线程同时累加同一个变量,结果值会小于实际值
*/
public class ConcurrentProblem implements Runnable {
private static ConcurrentProblem concurrentProblem = new ConcurrentProblem();
private static int count;
public static void main(String[] args) {
Thread thread1 = new Thread(concurrentProblem);
Thread thread2 = new Thread(concurrentProblem);
thread1.start();
thread2.start();
try {
// 等待两个线程都运行结束后,再打印结果
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
//期待结果是20000,但是结果会小于这个值
System.out.println(count);
}
/**
* 多线程问题原因:count++这行代码要分三步执行;1:读取;2:修改;3:写入。
* 在这三步中,任何一步都可能被其他线程打断,导致值还没来得及写入,就被其他线程读取或写入,这就是多线程并行操作统一变量导致的问题。
*/
@Override
public void run() {
for (int k = 0; k < 10000; k++) {
count++;
}
}
}
多次运行结果:
count
最终值会小于等于20000。
三.剖析问题:多线程累加为什么会有小于预期值这种情况呢
1.理解JVM如何执行
count++
count++
程序执行
count++
这个操作时,JVM将会分为三个步骤完成(非原子性):
-
某线程从内存中读取
count
。 -
某线程修改
count
值。 -
某线程将
count
重新写入内存。
这个操作过程应该很好理解,你可简单的类比为
把大象装进冰箱里的三个步骤
。在单线程中执行上述代码是不会出现
小于2万
的这种情况,为什么多线程就发生了跟预期不一致的情况呢?为了彻底弄清楚这个问题,
你需要先理解什么是线程?
线程像
病毒
一样,不能够独立的存活于世间,需要寄生在
宿主细胞
中。线程也是不能够独立的生存在系统中,
线程
需要依附于
进程
存在。
什么是进程?
进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位
线程是进程的一个执行路径,一个进程至少有一个线程,多个线程则会共享进程中的资源。
2.理解问题的根源:
有了对线程的认识后,我们再去思考,
count++
的3个步骤,由于线程会共享进程中的资源,所以在这三步中,任何一步都可能被其他线程打断,导致
count
值还没来得及写入,就被其他线程读取或写入。
3.脑补还原出错的流程:
-
假如
count
值为1,线程1读取到
count
值后,将
count
修改为2,此时还没来得及将结果写入内存,内存中的count值还是1。 -
另一个线程2,读取到
count
值为1后,也将其修改为2,并成功写入内存中,此时内存中的
count
值变为了2。 -
随后线程1也将
count
的结果2写入到内存中,
count
在内存中的结果依然是2(理应为3)。
上述场景中,两个线程各自执行了一次
count++
,但count值却只增加了1,这就是问题所在。
总结
多线程可以并行执行一些任务,提高处理效率,但也同时带来了新的问题,也就是线程安全问题,多个线程之间,操作同一资源时,也出现了让人意向不到的的情况,其原因是这些操作可能不是原子性操作,简单的说,我们肉眼看起来程序执行了一步操作,但在JVM中可能需要分多个步骤执行,多个线程可能会打乱了JVM的执行顺序,随后也就发生了不可预知的问题。
那么在Java中,怎么应对这种问题呢?Java随着版本的升级,提供了很多解决方案,比如:Concurrent包中的类。但我们下一篇文章,将讲解一种最简单、最方便的一种解决方案,上述案例代码仅仅通过增加一个单词,就可以轻松避免线程安全的问题,它就是
synchronized
关键字。
喜欢本文,请收藏和点赞,也请继续阅读本专栏的其他文章,本专栏将结合各种场景代码,彻底讲透彻java中的并发问题和
synchronized
各种使用场景。