这篇文章让你轻松理解线程安全

前情提要

在之前的文章中,编哥带你理解了为啥有并发,有线程,为啥并发多线程会带来危险,又是哪些危险。

今天,我们看一看安全,什么是线程安全的定义?安全是如何做到的?

线程安全的本质是什么

其本质是:管理对 状态(state) 的访问(读和写) ,我们要在不可控制的并发访问中(至少有一个写线程时,全部是读取的话,就不怕),保护这些状态数据

我们使用 「同步」来协调对状态的访问

当我们面对一个类的时候,修复或设计它为线程安全的类的原则如下(能达到这些原则当然非常安全,但需要复杂的设计):

  • 不要跨线程共享其内部的变量
  • 使状态变量 成为 不可变的
  • 或者在任何访问操作发生时,使用同步
  • 充分利用OOP的封装特性

定义我们写的类确实是线程安全的,是一件难事,但是有个乐观的定义:只要不需要额外的同步,调用方的代码,不必为这个被调用的对象(类)作其他的协调,这个被调用的对象(类)就是线程安全的;

2023-02-23-23-51-18

也可以从被调用的类来说,因为我封装了所有必要的同步操作,所以任何调用我的客户,请放心,请你无需提供额外的同步代码

在这里插入图片描述

线程安全的例子

  • 无状态的servlet服务方法 就是一种线程安全;因为这种方法只是瞬时的纯计算,不依赖栈外的任何对象,也就是不共享任何状态
public class StatelessServlet implements Servlet {
	public void service(Request req, Response resp) {
		BigInteger i = extractFromRequest(req);
		BigInteger[] factors = factor(i); // 一个纯粹的数学计算函数,做因数分解,不依赖 StatelessServlet 的任何属性
		encodeToResponse(resp);
	}
}

线程不安全的例子

  • read-modify-write, check-then-act 很容易导致 race condition ;其原因就是这三个动作是一种「复合操作」,而不是一个原子性的动作,它们既然能分成3步,就有危险
    • check then act 的一个例子就是 惰性初始化 lazy-initialization
public class LazyInitRace {
	private ExpensiveObject instance = null;
	public ExpensiveObject newInstance() {
		if(instance == null) // 第一个线程来了看到 null,于是开始初始化,可怕的是,第二个线程也看到null,继续初始化另一个 ExpensiveObject
			instance = new ExpensiveObject();
		return instance;
	}
}

解决线程不安全问题

通过锁定,比如内部锁(intrinsic locks 又名 monitor locks),(其实就是 synchronized 关键字),我们就能人为构造出原子性;

因为锁能够让唯一的线程进入其保护的区域,于是多个线程,就能够串行地访问锁所保护的代码区域;

注意:对于每个涉及到了多个变量的 invariant constraint (不变约束), 我们需要同一个锁来保护其所有的变量;

这里的 invariant constraint (不变约束) 你可以理解为 银行存款总额:A账户减少的金额 和 B账号增加金额,对 这两者的操作必须用同一把锁锁定,这样才不会一群人存钱,一群人取钱,最后银行的钱总额变了

使用 synchronized 关键字的时候,要注意让其包裹的代码块足够小,这是性能上的一种优化考虑

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值