多线程基础线程安全解决方案
Posted 烟锁迷城
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程基础线程安全解决方案相关的知识,希望对你有一定的参考价值。
1、线程安全
当多个线程访问某个对象时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
1.1、线程安全问题产生的原因
在操作系统中CPU的速度最快,内存次之,磁盘最慢,CPU资源又是最宝贵的,所以为了平衡三者之间的速度,做出如下优化:
- CPU增加了高速缓存,均衡与内存的速度差异
- 操作系统增加进程,线程,分时复用CPU,均衡CPU与IO设备的速度差异
- 编译程序优化指令的执行顺序,是的能够更加合理的利用缓存。
1.2、可见性问题
当多个线程在不同CPU上运行的时候,CPU各自的私有高速缓存互相不可见。
可见性问题示例:
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while (flag){
}
System.out.println("线程终止");
}).start();
Thread.sleep(1000);
flag = false;
}
1.3、原子性问题
CPU时间片的切换导致程序出现原子性问题
原子性问题示例,其计算结果必然少于等于1000:
public static int count = 0;
public static void incr() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
new Thread(() -> Count.incr()).start();
}
Thread.sleep(2000);
System.out.println("结果:" + count);
}
上面的代码中,产生原子性问题的是count++
,它不是一次性操作完成的,在多线程并发条件下,可能会出现在计数之前切换线程导致的计数丢失。
1.4、有序性问题
在程序执行的时候,可能出现指令重排序的问题,这些重排序会导致可见性问题。
- 编译器优化重排序
- 指令级并行重排序
- 内存系统重排序
2、解决方案(java内存模型)
java内存模型(JMM)是一种抽象结构,它提供了合理的禁用缓存以及禁用重排序的方法来解决可见性,有序性问题。
2.1、锁(Synchronized)
Synchronized加锁的范围
- 对于普通同步方法,锁是当前实例对象
- 对于静态同步方法,锁是当前类的Class对象
- 对于同步方法块,锁是Synchronized括号里配置的对象
public class SynchronizedDemo {
//实例对象级别
synchronized void method1() {}
//实例对象级别
void method2() {
synchronized (this) {}
}
//类级别
synchronized static void method3() {}
//类级别
void method4() {
synchronized (SynchronizedDemo.class) {}
}
//代码块
Object object = new Object();
void method5() {
synchronized (object) {}
}
}
上面示例的解决方案,在incr()
方法上加锁即可保证结果必然是1000。
public static int count = 0;
public synchronized static void incr() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
new Thread(() -> Test.incr()).start();
}
Thread.sleep(2000);
System.out.println("结果:" + count);
}
在多个线程争抢资源的时候,会通过监视器抢占锁,成功抢占到的会继续执行,失败的进入同步队列,等待唤醒。
2.2、volatile(可见性)
volatile可以用来解决可见性和有序性问题。
本质上来说,volatile实际上是通过内存屏障来防止指令重排序以及禁止cpu高速缓存来解决可见性问题。
#Lock指令本意上是禁止高速缓存解决可见性,但实际上是表示一种内存屏障功能,也就是说,针对当前读硬件环境,JMM层面采用Lock指令作为内存屏障来解决可见性问题。
在CPU中,包含三种屏障:
- Store Barrier:强制所有在store屏障指令之前的store指令,都在该store屏障指令执行之前被执行,,并把store缓冲区的数据都刷到cpu缓存
- Load Barrier:强制所有在load屏障执行之后的load指令,都在该load屏障指令之后被执行人,并且一直等到load缓冲区被该cpu读完才能执行之后的load指令。
- Full Barrier:复合load和store屏障功能
在JVM中,包含四种屏障:
屏障类型 | 指令示例 | 说明 |
---|---|---|
LoadLoad Barriers | Loadl; LoadLoad; Load2 | 确保Loadl数据的装载先于Load2及所有后续装载指令的装载 |
StoreStore Barriers | Store1 ; StoreStore; Store2 | 确保Store1数据对其他处理器可见(刷新到内存)先于Store2及所有后续存储指令的存储 |
LoadStore Barriers | Loadl; LoadStore; Store2 | 确保Loadl数据装载先于Store2及所有后续的存储指令刷新到内存 |
StoreLoad Barriers | Storel; StoreLoad; Load2 | 确保Storel数据对其他处理器变得可见(指刷新到内存)先于Load2及所有后续装载指令的装载。StoreLoad Barriers会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令 |
上文示例可以对flag变量增加volatile关键字解决
private volatile static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while (flag){
}
System.out.println("线程终止");
}).start();
Thread.sleep(1000);
flag = false;
}
2.3、final域
final在java中是一个保留的关键字,可以声明成员变量,方法,类和本地变量,一旦被声明,就无法被改变。
对于final,编译器和处理器要遵循两个重排序规则:
- 在构造函数内对一个final的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序
- 初次读取一个包含final的对象的引用,与随后初次读这个final,这两个操作之间不能重排序。
- JMM禁止编译器把final域的写重排序到构造函数之外,
- 编译器会在final写之后,构造函数return之前,插入一个StoreStore屏障,这个屏障禁止处理器把final的写重排序到构造函数之外。
- 在一个线程中,初次读对象引用与初次读该对象包含的final,JMM禁止处理器重排序这两个操作,编译器会在读final操作的前面插入一个LoadLoad屏障。
在构造方法中,如果存在外部可见的赋值,就会造成构造函数的溢出,final也无法保证有序性。
2.4、Happens-Before模型(可见性模型)
在java中,有一些场景是不需要添加volatile关键字就能保证没有可见性问题的。
2.4.1、程序顺序规则(as-if-serial语义)
1、不能改变程序的执行结果(在单线程环境下,执行的结果不变)
假如在单线程情况下,设置a = 1,b = 1,那么无论a和b如何重排序,都不会互相影响。
2、依赖问题,如果两个指令存在依赖关系,不允许重排序。
假如在单线程情况下,设置a = 1,b = 1,c = a+b,那么c的结果需要依赖a和b的结果,所以禁止重排序。
2.4.2、传递性规则
假如a的结果对b可见,b的结果对c可见,那么a的结果对c也可见。
2.4.3、volatile变量规则
volatile修饰的变量的写操作,一定对happens-before后续对于volatile变量的读操作,即对于volatile修饰的变量,其写操作的结果一定对后续读操作的结果可见。
volatile修饰的写操作与普通写操作是不能进行指令重排序。
在以下代码中,执行结果为i=1。
执行结果过程是a=1先于flag=true,volatile修饰的写操作与普通写操作是不能进行指令重排序
flag的写操作先于if(flag)读操作,volatile修饰的变量的写操作,一定对happens-before后续对于volatile变量的读操作
if(flag)读操作先于i = a写操作,依赖问题,如果两个指令存在依赖关系,不允许重排序。
a=1操作先于i = a,a的结果对b可见,b的结果对c可见,那么a的结果对c也可见。
private int a = 0;
private volatile boolean flag = flag
public void writer() {
a=1;
flag=true;
}
public void reader() {
if(flag) {
int i = a;
}
}
2.4.4、监视器锁规则
一个锁释放后的资源对后续所有线程都可见
以下程序,执行结果一定为x=40
int x = 10;
synchronized(this)[
x = 40
}
2.4.5、start规则
在线程start之前赋予变量的值,在线程中也能获取到。
以下程序执行的结果为x = 30
int x=0;
Thread thread = new Thread(()->{
System.out.println(x);
})
x = 30;
thread.start();
2.4.5、JOIN规则
在线程内赋予变量的值,在线程join之后也能获取到
以下程序执行的结果为x = 30
int x=0;
Thread thread = new Thread(()->{
x = 30;
})
thread.start();
thread.join();
System.out.println(x);
2.5、Atomic原子类
属于JUC工具包,是保证线程安全的一系列原子性类,它包含有四大类型
- 原子更新基本类型
- 原子更新数组
- 原子更新引用类型
- 原子更新字段类
上面的加锁示例,可以直接将非原子性操作的++
替换为原子性的AtomicInteger
public static AtomicInteger count = new AtomicInteger(0);
public static void incr() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count.incrementAndGet();
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
new Thread(() -> SynchronizedDemo.incr()).start();
}
Thread.sleep(2000);
System.out.println("结果:" + count.get());
}
AtomicInteger
能保证原子性操作主要依靠两个机制
- Unsafe:unsafe是一个类提供了很多拓展性底层内存操作,
- CAS:
compareAndSwapInt(object,offset,expect,update)
,CAS类似于乐观锁的思想,参数Object是调用者本身,offset是内存偏移量,用来的到当前值,except是预期值,update是更新值,只有预期值和当前值一致,才能修改为更新值。这样的操作避免加锁,提高整个的执行性能。
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
2.6、ThreadLcoal
ThreadLocal是一种可以保证单一线程内数据安全的类,在示例代码证中,就算赋予local值增加5,在另一个线程内调用local依然保证取到的初始值是0,多个线程之间不会互相影响。
static ThreadLocal<Integer> local = new ThreadLocal<Integer>() {
protected Integer initialValue() {
return 0;
}
};
public static void main(String[] args) {
Thread thread1 = new Thread(()->{
int num = local.get();
local.set(num+=5);
System.out.println(Thread.currentThread().getName()+"->"+num);
},"th1");
Thread thread2 = new Thread(()->{
int num = local.get();
local.set(num+=5);
System.out.println(Thread.currentThread().getName()+"->"+num);
},"th2");
thread1.start();
thread2.start();
}
2.6.1、get方法
在ThreadLocal的get方法中,先获取当前线程,然后从当前线程中获取ThreadLocalMap ,也就是说,ThreadLocalMap是每一个Thread都拥有的一个Key-Value形式的数据结构。
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocalMap的初始化方法中,使用了初始长度为16的数据存储数据,用hash计算下标位置,每个线程可能拥有不止一个ThreadLocal数值,所以使用数组保存
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
2.6.2、setInitialValue方法
在ThreadLocal的setInitialValue()方法中,如果重写initialValue()方法,那么initialValue()方法的返回值就是ThreadLocal的初始值。
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
2.6.3、set方法
在ThreadLocal的set()方法中,会将弱引用类型Entry被内存回收的值清除防止内存泄露。对于Hash冲突,ThreadLocal使用线性探索,也叫开放寻址法,就是当某个Hash值产生冲突后,就查看下一个位置是否为空,若不为空就继续查找,直到查到空地址,直接插入。当hash值超过16个,Entry数组将会扩容。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
以上是关于多线程基础线程安全解决方案的主要内容,如果未能解决你的问题,请参考以下文章
阶段1 语言基础+高级_1-3-Java语言高级_05-异常与多线程_第3节 线程同步机制_4_解决线程安全问题_同步代码块