Java中线程安全和线程不安全解析和示例

  • Post author:
  • Post category:java




简介

本文作为多线程编程的第一篇文章,将从一个简单的例子开始,带你真正从代码层次理解什么是线程不安全,以及为什么会出现线程不安全的情况。文章中将提供一个完整的线程不安全示例,希望你可以跟随文章,自己真正动手运行一下此程序,体会一下多线程编程中必须要考虑的线程安全问题。



一.什么是线程安全

《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++

这个操作时,JVM将会分为三个步骤完成(非原子性):

  1. 某线程从内存中读取

    count

  2. 某线程修改

    count

    值。
  3. 某线程将

    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

各种使用场景。



版权声明:本文为x541211190原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。