写点什么

【并发编程技术】「技术辩证分析」在并发编程模式下进行线程安全以及活跃性问题简析

用户头像
浩宇天尚
关注
发布于: 刚刚
【并发编程技术】「技术辩证分析」在并发编程模式下进行线程安全以及活跃性问题简析

什么是线程安全?

线程安全,有两个重要的特征说明:“共享”和“可变”。


  • 共享是指可以被多个线程同时访问;

  • 可变是指变量的值在生命周期内是可以变化的;

如何实现线程安全

  • 一个对象是否需要线程安全的,取决于它是否被多个线程访问;

  • 如何保证一个对象的线程安全,则需要采用同步机制来协同对对象可变状态的访问;

  • 给线程安全下一个明确的定义:当多个线程访问这个对象或者资源时,如果这个对象或资源始终都能表现出数据的一致性的状态,那么就称这个对象或者资源是线程安全的;

数据资源的有无状态化

  • 无状态的对象一定是线程安全的。

  • 有状态的对象,多线程环境下,多个线程共享资源,且进行的不是原子性操作,这个时候就要考虑线程的安全控制问题


比如:count++,其实是不具备原子性的,因为这个步骤实际会被拆分为三个步骤,即 读取、修改和写入,而这三个步骤有可能在某个时刻因 CPU 时间片的切换问题,而只执行其中一两个步骤,这就不具备原子性。


原子化能力支持

在 Java 中,为了解决这个问题,java.util.concurrent.atomic 包提供了很多的类,来保证数据操作的原子性,比如我们之前的程序可以修改为


  • 基本数据类型 AtomicInteger

  • 数组类型 AtomicIntegerArray


AtomicInteger integer = new AtomicInteger(0);integer.incrementAndGet()
复制代码


内部的原理是采用了 CAS 机制

那么什么是 CAS 机制?

CAS 有人翻译为 Compare And Set 或 Compare And Swap 都是正确的。


多线程并发执行的状态下,锁的状态改变,基本都是使用 CAS 原理,它有一个比较别扭的叫法“CPU 硬件同步原语”,算法是基于 CPU 硬件的,原子性操作,不会被其他线程打断。


CAS 的算法,比较当前值和期望的值是否相等,如果相等,则将当前值赋予一个新值。


再比如修改一个 Boolean 的类型的变量的值,我们也可以采用


private AtomicBoolean atomicBoolean = new AtomicBoolean(false);public void lock(){    //期望是false,如果是false,则可以修改为true    atomicBoolean.compareAndSet(false, true);}
复制代码

同步锁机制支持

只要程序中存在“先判断,再更新”,那么就要保证这两个操作在一个原子操作里面,才能保证线程安全。


public synchronized int getCount(){    return count++;}
复制代码
Java 锁机制的一些特点

监视锁、互斥锁、可重入锁都是在这个锁的特点。


  • 监视锁:java 的每一个对象都可以用来做监视锁,也就是为什么我们的 wait、notify 方法定义在 Object 类的原因。

  • 互斥锁:表示最多只有一个线程可以持有这把锁。

  • 可重入锁:是指当线程 A 请求一个由线程 B 持有的锁时,线程 B 会进入阻塞状态;而如果线程 A 如果再访问另一段代码,而这个代码的锁是已经被线程 A 持有的,这个时候请求是可以成功的,这就叫可重入。

Java 锁机制的简单原理

JVM 为每个锁设置两个属性,获取计数值和所有者线程,当计数值为 0 时,这个锁就被认为是没有被任何线程持有,当线程请求一个未被持有的锁时,JVM 将记录锁的持有者,并且计数值+1。


如果同一个线程再次获取这个锁,则计数值将递增,而当线程退出同步代码块时,计数器会相应递减,当计数值为 0,这个锁将被释放。

活跃性问题

承接上面解决安全性的问题分析,锁机制会存在活跃性问题,比如:死锁,饥饿,活锁,这些都是属于活跃性问题。

死锁

多个线程,各自占对方的资源,都不愿意释放,从而造成死锁,A 线程需要等待的锁被 B 线程占用,而 B 线程需要的等待的锁被 A 线程占用,所以相互都不释放,于是就陷入了死锁。

饥饿

多个线程访问同一个同步资源,有些线程总是没有机会得到互斥锁,这种就叫做饥饿。

出现饥饿的三种情况
  1. 高优先级的线程吞噬了低优先级的线程的 CPU 时间片

  2. 理论上来说,线程优先级高的线程会比线程优先级低的线程获得更多的执行机会,但是 java 的线程优先级不是绝对出现这样的效果。

  3. 一般而言:优先级高的出现频率会比优先级低的高很多

  4. 不同的操作系统对线程的优先级支持是不同的,规定是在 1-10 之间,java 通过 3 个常量来屏蔽这种操作系统的底层差异化。

  5. 线程被永久阻塞在等待进入同步代码块的状态

  6. 等待的线程永远不被唤醒


建议大家采用公平锁来代替 synchronized 这种互斥锁

活锁

两个人在走廊上碰见,大家都互相很有礼貌,互相礼让,A 从左到右,B 也从从左转向右,发现又挡住了地方,继续转换方向,但又碰到了,反反复复,一直没有机会运行下去。

发布于: 刚刚阅读数: 2
用户头像

浩宇天尚

关注

🏆 InfoQ写作平台-签约作者 🏆 2020.03.25 加入

【个人简介】酷爱计算机技术、醉心开发编程、喜爱健身运动、热衷悬疑推理的”极客狂人“ 【技术格言】任何足够先进的技术都与魔法无异 【技术范畴】Java领域、Spring生态、MySQL专项、APM专题及微服务/分布式体系等

评论

发布
暂无评论
【并发编程技术】「技术辩证分析」在并发编程模式下进行线程安全以及活跃性问题简析