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各种使用场景。

  • 12
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值