8、线程同步机制(volatile与CAS)
2019-04-23 本文已影响0人
小manong
一、轻量级同步机制:valotile关键字
- volatile变量只没有被final修饰的实例或者静态变量称为volatile变量。
- valotile关键字是轻量级锁,仅保障可见性和有序性,但是不保障原子性(因为没有锁的排他性)。
1、作用
- valotile关键字保障共享变量的可见性和有序性,保障long、double型变量读写操作的原子性。
- valotile关键字在原子性方面仅仅保障被修饰的变量读、写操作本身的原子性。但是不能保障这些变量进行复合操作的原子性(比如i++)
- volatile关键可以保障共享变量的可见性和有序性,其实现原理和锁一样,底层都是通过内存屏障来实现的。但是和锁不同的是,valotile关键字不具备锁的排他性,并不能保障被修饰共享变量的原子性。
2、开销
- volatile关键字开销包括读变量和写变量,volatile修饰的变量进行读写操作时候都不会导致上下文切换,因此开销比较小。但是回比普通的变量开销大,因为jvm底层做了很多的保障可见性和有序性的操作。
3、应用场景
(1)使用volatile变量作为状态标志
应用程序的某一个状态由一个线程设置,其他线程会读取到该状态并以该状态作为计算的依据。使用volatile变量的好处是在多线程环境中,volatile作为同步机制能够及时通知另外一个线程某种事件的发生(网络异常等),而这些线程无需使用锁,从而保证了锁的开销以及相关问题。
(2)使用volatile代替锁
要使用volatile实现锁的原子性,可以把这一组状态变量封装为一个对象,那么对这些状态变量的操作就可以通过创建一个新的对象并将该对象引用赋值给相应的引用性变量来实现。利用了volatile的可见性和有序性。(锁适用于共享一个状态变量或者对象,volatile变量适用于共享一组状态变量,这组状态变量可以封装为一个对象)
(3)使用volatile关键字实现简易的读写锁
这个场合混合使用了锁和volatile关键字,锁用于保证对共享数据操作的原子性,volatile对于共享数据读取的可见性和有序性保证。
private volatile long count;
public long readCount() {
return count;
}
public void writeCount() {
synchronized (LockTest.class) {
count++;
}
}
5、使用案例分析
- 以一个负载均衡器的例子来说明,这个负载均衡器需要满足:
1、需要支持多种负载均衡算法,如随机算法、轮询算法、加权轮询算法等。
2、需要支持在系统运行中动态的调整负载均衡算法
3、需要在线剔除无用的机器
4、下游部件的节点信息可以动态调整(删除、添加等)
- 代码参考:(后面补齐)
6、volatile在单例模式下面的使用
- 使用双重检验的方式来保证线程安全和达到延迟加载的目的
public class SingletonByDoubleLock {
/**
* 使用volatile修饰保证有序性和可见性
*/
private static volatile SingletonByDoubleLock instance = null;
private SingletonByDoubleLock() {
}
public static SingletonByDoubleLock newInstance() {
if (instance == null) {//1、第一次检验
synchronized (SingletonByDoubleLock.class) {
if (instance == null) {//2、第二次检验
return new SingletonByDoubleLock();//3、操作3可能发生重排序
}
}
}
return instance;
}
}
- 使用静态内部类的方式是一种更加优越的方式。类的静态变量 在初次访问的时候会触发jvm对类进行初始化操作,因此在第一次访问的时候StaticInnerClass 会被初始化(保证了延迟性),而静态变量只会创建一次,因为保证了整个过程的(单例性)。
public class SingletonByStaticClass {
private SingletonByStaticClass() {
}
/**
* 静态内部试验的单例
*/
private static class StaticInnerClass {
private final static SingletonByStaticClass SINGLETON = new SingletonByStaticClass();
}
public static SingletonByStaticClass newInstance() {
return StaticInnerClass.SINGLETON;
}
}
- 还可以考虑更加简洁的单例模式,使用枚举类的实现
public enum SingletonEnum {
INSTANCE;
SingletonEnum() {
}
public void doSomeThing() {
}
}
二、CAS与原子变量
- CAS(compare and swap)是一种处理器指令的称呼。很多java多线程标准库借助CAS,因此了解CAS很重要。
- 锁是一种重量级保证线程安全的实现,但是有时候处于性能考虑,并不能使用锁,这个时候就可以借助于CAS来保证线程安全性,CAS能够将read-modify-write和check-then-act之类的操作转为原子性操作。
1、CAS介绍
- CAS是一个原子的check-then-act操作,当一个线程执行CAS指令操作时候,如果变量V的当前值和请求调用CAS时所提供的变量值A(及变量的旧值)是相等的,那么就说明其他线程并没有修改过变量V的值。执行CAS命令时候,如果没有其他线程修改过变量V的值,当前线程就会抢先将变量V的值更新为B(新值),其他线程的更新请求就会失败。这些失败的线程,可以选择在此尝试,直到修改成功。
boolean compareAndSwap(Variable V,Object A,Object B){
//check:检查变量是否被其他线程修改过
if (A==V.get()){
//act:更新变量,更新成功
V.set(B);
return true;
}
//变量值已经被其他的线程修改过,更新失败
return false;
}
- 案例(使用CAS实现i++的原子性操作),但是需要注意的是CAS操作只是保证了i++的原子性,并不保证可见性,因此需要用volatile关键字来修饰共享变量。
public class CASAtomicCounter {
public class CASAtomicCounter {
/**
* 使用volatile保证可见性,cas只保证原子性
*/
private volatile long count;
/**
* 这里使用AtomicLongFieldUpdater只是为了便于讲解和运行该实例,
* 实际上更多的情况是我们不使用AtomicLongFieldUpdater,而是使用
* java.util.concurrent.atomic包下的其他更为直接的类。
*/
private final AtomicLongFieldUpdater<CASAtomicCounter> fieldUpdater;
public CASAtomicCounter() {
fieldUpdater = AtomicLongFieldUpdater.newUpdater(CASAtomicCounter.class,
"count");
}
public long getCount() {
return count;
}
public void increment() {
long oldValue;
long newValue;
do {
//读取共享变量的当前值(旧值)
oldValue = count;
//计算共享变量的新值(新值)
newValue = oldValue + 1;
//CAS更新共享变量的值,循环用于更新失败的时候继续重试
} while (!compareAndSwap(oldValue, newValue));
}
/*
* 该方法是个实例方法,且共享变量count是当前类的实例变量,因此这里我们没有
必要在方法参数中声明一个表示共享变量的参数,最终底层是使用c语言实现(指令层面保证了)
*/
private boolean compareAndSwap(long oldValue, long newValue) {
boolean isOK = fieldUpdater.compareAndSet(this, oldValue, newValue);
return isOK;
}
2、原子操作工具:原子变量类
-
原子变量类是基于CAS实现的能够保证对共享变量进行read-modify-write更新操作的原子性和可见性的一组工具类。由于volatile无法保证自增的原子性,而原子变量类的内部实现通常借助于一个volatile变量保证该变量read-modify-write更新操作的原子性,因此可以看做是增强型的volatile变量。java原子变量类如下:
jdk原子变量类 - 原子变量类使用案例:统计分布式系统中性能测试指标(总请求数,处理成功数、处理失败数)
//指标统计器
public class Indicator {
/**
* 唯一实例
*/
private static final Indicator INSTANCE = new Indicator();
/**
* 总请求数量
*/
private final AtomicLong requestCount = new AtomicLong();
/**
* 成功数量
*/
private final AtomicLong successCount = new AtomicLong();
/**
* 请求失败数量
*/
private final AtomicLong failCount = new AtomicLong();
private Indicator() {
}
public void newRequestRecieved() {
//总请求加1
requestCount.incrementAndGet();
}
public void requestProcessSuccess() {
successCount.incrementAndGet();
}
public void requestProcessfail() {
failCount.incrementAndGet();
}
public long getRequestCount() {
return requestCount.get();
}
public long getSuccessCount() {
return successCount.get();
}
public long getFailCount() {
return failCount.get();
}
public void reset() {
requestCount.set(0);
successCount.set(0);
failCount.set(0);
}
}
3、ABA问题及解决
- 比如对于共享变量V,当前线程看到它的值为A的那一刻,其他线程已经将其值更新为值B了,接着在当前线程执行CAS时候该变量的值又被其他线程更新为A,这就是ABA问题,及共享变量的值经济过A->B->A的转化。
- 为了解决ABA问题可以引入一个版本号,每一次更新共享变量的时候版本号就增加1,也就是将共享变量V扩展为一个由实际值和版本号组成的元元组。因此可能发生的问题转为[A,0]->[B,1]->[A,1],AtomicStampedFeference类有相关的实现。
三、static和final关键字
1、static关键字
- java类在初始化的时候实际上采用了延迟加载技术,一个类被jvm加载之后,该类的所有静态变量的值都是其默认值(如引用变量为null),直到有一个线程初次访问了该类的任意一个静态变量才使这个类被初始化--静态代码块被执行、类的所有静态变量被赋予初始值。
- static关键字在多线程环境下面有特殊的含义,能够保证一个线程即时在未使用其他同步机制的情况下面总是可以读取到一个类的静态变量的初始值(而不是默认值)。但是这种可见性保证只是局限于线程初次读取变量值,如果这个静态变量在初始化之后被其他线程更新过,还是需要借助锁等来保证线程安全性
2、final关键字
- 当一个对象被其他线程访问的时候,final修饰的所有字段都是初始化完成的,及线程读取相应变量的时候总是有相应的初始值(而不是默认值)。而非final修饰的变量就可能读取到默认值(未完成初始化的值)
- final能保证访问的变量是初始化后的值的原因是能保证变量的有序性,但是final并不能保证变量的可见性。