什么是线程安全?
线程安全,有两个重要的特征说明:“共享”和“可变”。
- 共享是指可以被多个线程同时访问;
- 可变是指变量的值在生命周期内是可以变化的;
如何实现线程安全
- 一个对象是否需要线程安全的,取决于它是否被多个线程访问;
- 如何保证一个对象的线程安全,则需要采用同步机制来协同对对象可变状态的访问;
- 给线程安全下一个明确的定义:当多个线程访问这个对象或者资源时,如果这个对象或资源始终都能表现出数据的一致性的状态,那么就称这个对象或者资源是线程安全的;
数据资源的有无状态化
- 无状态的对象一定是线程安全的。
- 有状态的对象,多线程环境下,多个线程共享资源,且进行的不是原子性操作,这个时候就要考虑线程的安全控制问题
比如: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线程占用,所以相互都不释放,于是就陷入了死锁。
饥饿
多个线程访问同一个同步资源,有些线程总是没有机会得到互斥锁,这种就叫做饥饿。
出现饥饿的三种情况
- 高优先级的线程吞噬了低优先级的线程的CPU时间片
- 理论上来说,线程优先级高的线程会比线程优先级低的线程获得更多的执行机会,但是java的线程优先级不是绝对出现这样的效果。
- 一般而言:优先级高的出现频率会比优先级低的高很多
- 不同的操作系统对线程的优先级支持是不同的,规定是在1-10之间,java通过3个常量来屏蔽这种操作系统的底层差异化。
- 线程被永久阻塞在等待进入同步代码块的状态
- 等待的线程永远不被唤醒
建议大家采用公平锁来代替synchronized这种互斥锁
活锁
两个人在走廊上碰见,大家都互相很有礼貌,互相礼让,A从左到右,B也从从左转向右,发现又挡住了地方,继续转换方向,但又碰到了,反反复复,一直没有机会运行下去。