因为在之前的学习中,也学到了多个线程安全的方法组合到一块却不是线程安全的。如果采用synchronized这种方式,锁比较重。所以提供一系列的基于CAS的组合方法类的API,来用以保证线程安全。
AtomicInteger,AtomicLong,AtomicBoolean
在jdk1.8的文档里,这三种类型的方法基本上都是类似的。源码的底层都是do{}While()循环调用CAS方法完成操作。
package com.bo.threadstudy.six;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 基本类型的AtomicInteger,AtomicLong,AtomicBoolean使用
*/
@Slf4j
public class AtomicIntegerTypeTest {
public static void main(String[] args) throws InterruptedException {
AtomicInteger atomicInteger = new AtomicInteger(1000);
// extracted(atomicInteger);
// extracted1(atomicInteger);
// extracted2(atomicInteger);
extracted3(atomicInteger);
}
/**
* 这个方法就是使用accumulateAndGet方法来对值进行原子性的函数运算
* accumulate 积累的意思
* @param atomicInteger
* @throws InterruptedException
*/
private static void extracted3(AtomicInteger atomicInteger) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(2);
int num1 = 5;
int num2 = 10;
new Thread(() -> {
try{
for (int i = 0; i < 1000; i++) {
//这个和UpdateAndGet方法从源码层面都类似,只不过代入了两个入参来进行操作
//left是当前atomicInteger对象的值,而right代表的第一个参数
atomicInteger.accumulateAndGet(num1,(left, right) -> left+right);
}
countDownLatch.countDown();
}catch (Exception e){
log.error("222",e);
}
}).start();
new Thread(() -> {
try{
for (int i = 0; i < 1000; i++) {
atomicInteger.accumulateAndGet(num2,(left, right) -> left+right);
}
countDownLatch.countDown();
}catch (Exception e){
log.error("111",e);
}
}).start();
countDownLatch.await();
log.debug(atomicInteger.get()+"");
}
private static void extracted2(AtomicInteger atomicInteger) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(2);
int t = 2;
int m = 3;
//updateAndGet函数,传入的是一个函数表达式IntUnaryOperator接口,这里的话只需要计算结果,而不需要返回结果
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
atomicInteger.updateAndGet(x -> x+t);
//和上面的这个类似,只是返回结果不一样
// atomicInteger.getAndUpdate(x -> x+t);
}
countDownLatch.countDown();
}).start();
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
atomicInteger.updateAndGet(x -> x+m);
}
countDownLatch.countDown();
}).start();
countDownLatch.await();
log.debug(atomicInteger.get()+"");
}
//校验getAndAdd以及addAndGet的原子性
private static void extracted1(AtomicInteger atomicInteger) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(2);
new Thread(() -> {
//底层与getAndIncrement等类似,都是调用了Unsafe类名下的getAndAddInt方法
for (int i = 0; i < 1000; i++) {
atomicInteger.getAndAdd(5);
}
countDownLatch.countDown();
}).start();
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
//只是返回结果不同,底层调用的都是Unsafe类名下的
atomicInteger.addAndGet(-5);
}
countDownLatch.countDown();
}).start();
countDownLatch.await();
log.debug(atomicInteger.get()+"");
}
//验证多线程的++以及--,累减操作是否线程安全
private static void extracted(AtomicInteger atomicInteger) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(2);
//验证AtomicInteger的累加以及累减原子性
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
//递加1000次(先获取到主存的值再++)
atomicInteger.getAndIncrement();
//先进行累加,再获取主存的值
// atomicInteger.incrementAndGet();
}
countDownLatch.countDown();
}).start();
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
//递减1000次,先获取到值,再进行--
atomicInteger.getAndDecrement();
//先进行--再获取到--后的值
// atomicInteger.decrementAndGet();
}
countDownLatch.countDown();
}).start();
countDownLatch.await();
log.debug(atomicInteger.get()+"");
}
}
AtomicReference
AtomicReference底层是一个泛型结构,底层使用==来保证compareAndSet中的compare,所以可以搭配一些不可变类来进行使用。比如String,BigInteger,BigDecimal。因为底层采用了==,即比较地址的方式来比较,不可变类因为每次的结果都是new出来的,保证只要被修改过,底层地址肯定是不同的。而在普通类的使用,如果只是对某个属性发生改变,最终compareAndSet来比较这个对象的话,还是原先的地址,compareAndSet底层采用了==,就算线程A对某个属性发生改变,线程B这时compareAndSet比当前对象还是相等的,就没有办法保证线程安全。
package com.bo.threadstudy.six;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
/**
* 测试引用类型的原子操作
* AtomicReference 引用类型原子操作,底层都是基于泛型实现的
* AtomicStampReference 加了版本号的引用类型,防止aba问题
* AtomicMarkableReference 加了一个boolean的引用类型
* 我看了下api,这里的compare都是采用==判定的,而不是equals判定的
*/
@Slf4j
public class AtomicReferenceTest {
public static void main(String[] args) throws InterruptedException {
AtomicReference<BigDecimal> ref = new AtomicReference<>(new BigDecimal("10000"));
CountDownLatch countDownLatch = new CountDownLatch(2);
//在使用的时候一定要用对方法,不然会出问题,我最开始用的getAndSet,我想Set的这个值,我内部写了方法,不是原子的
//捋一下操作流程,getAndSet我传入了一个替换的值,然后在源码中,通过while循环,不断获取当前主存的值,并且使用compareAndSet方法来与主存进行比较,它是没有办法保证我的计算是原子的
//底层所有的代码都可以按照CAS方法来实现,有时间试着写一下复杂逻辑
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
//这里就没有累加方法了,不过也正常,引用什么类型都可能出现
ref.getAndUpdate(bigDecimal -> bigDecimal.add(new BigDecimal(10)));
}
countDownLatch.countDown();
}).start();
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
//bigDecimal是地址层面发生了改变,此时comareAndSet比较就不同
ref.getAndUpdate(bigDecimal -> bigDecimal.add(new BigDecimal(10)));
}
countDownLatch.countDown();
}).start();
countDownLatch.await();
log.debug(ref.get().intValue()+"");
}
}
AtomicStampReference
虽然可以通过AtomicReference可以保证线程安全的执行。但是,会存在ABA问题.。
即假如线程A想将状态A改成状态B,此时已经获取到了A的状态,但未进行CAS比较。但此时线程B先插了一手,将A改成B,然后再改成A,此时A线程采用CAS的方式来进行比较时,虽然现在的A不是当初的A,但可以成功的用A替换掉B的。
针对这种场景,我就想在原先的A状态替换掉,而不是更改后的A状态替换,此时就可以使用AtomicStampReference,这个里面我们可以手动维护一个版本号,达到只能在原先的A状态替换的效果。
package com.bo.threadstudy.six;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* 解决ABA问题,引入了AtomicStampReference,利用版本号,来对ABA的值来进行控制
*/
@Slf4j
public class AtomicStampReferenceTest {
//之所以用bigDecimal,是因为bigDecimal是不可变类,这里采用不可变类,才能在使用compareAndSet方式时,根据地址的不同来进行比较,普通的对象是不行的
public static void main(String[] args) {
AtomicStampedReference<String> stringAtomicStampedReference = new AtomicStampedReference<String>("A",0);
String reference = stringAtomicStampedReference.getReference();
int stamp = stringAtomicStampedReference.getStamp();
new Thread(() -> {
String reference1 = stringAtomicStampedReference.getReference();
int stamp1 = stringAtomicStampedReference.getStamp()+1;
//原子性可以保证的,如果用了循环的话,因为stamp值肯定会被更改
log.debug(stringAtomicStampedReference.compareAndSet(reference1,"B",stamp,stamp1)+"");
}).start();
new Thread(() -> {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
String reference1 = stringAtomicStampedReference.getReference();
int stamp1 = stringAtomicStampedReference.getStamp()+1;
log.debug(stringAtomicStampedReference.compareAndSet(reference1,"A",stamp1-1,stamp1)+"");
}).start();
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(stringAtomicStampedReference.compareAndSet(reference,"B",stamp,stamp+1)){
log.debug("修改成功");
}else{
//打印了修改失败,虽然reference没有发生变化,但版本号发生了变化,也就不行了
log.debug(stringAtomicStampedReference.getReference());
log.debug(stringAtomicStampedReference.getStamp()+"");
log.debug("修改失败");
}
}
AtomicMarkableReference
和AtomicStampReference有点像,但是使用场景要更简单点,有的时候我们在修改的时候,并不想知道它具体修改了多少次,只是想观察它是否修改过。这时,可以使用AtomicMarkableReference,这个里面维护了个boolean类型,可以保证当前是否被其它线程修改过。
package com.bo.threadstudy.six;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicMarkableReference;
/**
* AtomicMarkableReference,这个的话和AtomicStampReference类似,只不过内部是由一个true,fasle状态来控制
* 这种现象只是想查看元素是否被更改过的情况,可以写个换垃圾袋的情况
*/
@Slf4j
public class AtomicMarkableReferenceTest {
public static void main(String[] args) throws InterruptedException {
AtomicMarkableReference<String> stringAtomicMarkableReference = new AtomicMarkableReference<String>("A",true);
String reference = stringAtomicMarkableReference.getReference();
boolean marked = stringAtomicMarkableReference.isMarked();
new Thread(() -> {
stringAtomicMarkableReference.compareAndSet(reference,"B",marked,false);
}).start();
Thread.sleep(1000);
String reference1 = stringAtomicMarkableReference.getReference();
boolean marked1 = stringAtomicMarkableReference.isMarked();
log.debug(stringAtomicMarkableReference.compareAndSet(reference1,"A",marked1,true)+"");
}
}
AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
原子数组类型,这个其实和AtomicInteger等类似,只不过在修改时需要指明数组下标。刚才也说过,CAS是按照==来根据地址进行比较。数组比较地址,肯定是不行的。只能比较下标元素。而比较下标元素,就和元素的类型有关系了。
这里其实没啥难度,就是涉及到了函数表达式编程,这个当时有点迷,现在也清除了。
package com.bo.threadstudy.six;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicIntegerArray;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicReferenceArray;
import java.util.concurrent.locks.Condition;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* 这里的目的是为了测试Atomic***Array系列的,因为原先我也试过,AtomicReference这种方式比较的是底层的地址,所以如果数组这么比,完蛋
* 原子数组总共有三种,AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray。
* 按这个AtomicReferenceArray来吧,有一定通用性,先看看API
*/
@Slf4j
public class AtomicArrayTest {
public static void main(String[] args) throws InterruptedException {
//这里用的Reference模拟的Integer,我暂时明白了
demo(() ->{
return new AtomicReferenceArray<Integer>(10);
},
(array) -> array.length(),
(array, index) -> array.getAndUpdate(index,s -> {
if(s == null)
s = 0;
return ++s;
}),
array -> System.out.println(array.toString())
);
}
/**
参数1,提供数组、可以是线程不安全数组或线程安全数组(没有入参,有返回结果,使用supplier)
参数2,获取数组长度的方法 有一个入参,以及返回结果,使用function
参数3,自增方法,回传 array, index 需要入参,数组以及下标 但不需要返回结果
参数4,打印数组的方法 入参传入数组,不需要打印结果
*/
// supplier 提供者 无中生有 ()->结果
// function 函数 一个参数一个结果 (参数)->结果 , BiFunction (参数1,参数2)->结果
// consumer 消费者 一个参数没结果 (参数)->void, BiConsumer (参数1,参数2)->
private static <T>void demo(Supplier<T> supplier,
Function<T,Integer> function,
BiConsumer<T,Integer> biConsumer,
Consumer<T> consumer) throws InterruptedException {
//得到数组返回的元素
T t = supplier.get();
//后面的Integer是返回结果啊
Integer length = function.apply(t);
List<Thread> threads = new ArrayList<>();
CountDownLatch countDownLatch = new CountDownLatch(length);
//根据数组长度来进行遍历,对值进行累加
for (Integer i = 0; i < length; i++) {
//创建数组长度数量的线程
threads.add(new Thread(() -> {
//在每个线程中,都对参数中传入的数组进行1000次累加
for (int i1 = 0; i1 < 1000; i1++) {
//入参为数组以及下标
biConsumer.accept(t,i1%length);
}
countDownLatch.countDown();
}));
}
threads.forEach(thread -> thread.start());
countDownLatch.await();
//打印所有的元素值
consumer.accept(t);
}
}
AtomicReferenceFieldUpdater,AtomicIntegerFieldUpdater,AtomicLongFieldUpdater
原子修改器,之前所说的,可变类如果采用CAS的方式来进行比较compare,那么最终采用的是==方式,比较的是地址。所以为了针对某一类型中某个元素的改动,引入了原子修改器AtomicReferenceFieldUpdater,可以修改对象内元素。
要被修改的元素一定要标记volatile来保证可见性。
这种修改方式基于反射来进行修改,先生成一个修改器,再使用修改器修改对应的对象属性。
package com.bo.threadstudy.six;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
/**
* 原子更新器,来更改对象内部的属性
* AtomicInteger与AtomicIntegerFieldUpdater虽然功能类似,但是后者有一个好处,在大批量需要对原子整型进行操作时,后者只需创建一个对象就可以
* 前者需要创建大批量对象,对内存不友好
*/
@Slf4j
public class AtomicReferenceFieldUpdaterTest {
public static void main(String[] args) {
Person person = new Person();
person.setName("123");
//使用原子修改器对值进行修改,必须要配合volatile类来进行操作,而且不能是private,否则利用反射其不可见
AtomicReferenceFieldUpdater<Person, String> nameReference = AtomicReferenceFieldUpdater.newUpdater(Person.class, String.class, "name");
//这里假设采用debug的setValue改造成345,那么输出结果就会是345,而不是456
log.debug(nameReference.compareAndSet(person,"123","456")+"");
log.debug(person.toString());
}
}
@Data
class Person{
public volatile String name;
private Integer age;
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
我当时在修改的时候有个疑问,在AtomicInteger,AtomicLong已经存在的基础上,为什么要增加一个AtomicIntegerFieldUpdater,AtomicLongFieldUpdater呢?后面专门查了下,如果要对许多个Integer对象来进行原子性修改,那么需要创建很多个AtomicInteger对象。而使用修改器,只需要创建一个修改器就好,对内存相对比较友好。
原子累加器
原子累加器,在使用过程中,虽然AtomicLong这些方法存在累加方法,但是和原子累加器相比,性能要低了10倍以上。之所以造成这个的原因在于,使用AtomicLong在使用过程中会出现指令交错,一旦不符合自己的预期值,就会重新循环,造成性能的降低。而LongAddr以及DoubleAddr这些方法,在内部模拟了一个数组,每个线程在自己内部进行累加后,最终发给到这个数组上,统一完成最后的汇总,性能也就较快。
性能提升的原因很简单,就是在有竞争时,设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加Cell[1]... 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能。
源码的话老师讲的没听明白,先过了,不是面试重灾区。
package com.bo.threadstudy.six;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.Consumer;
import java.util.function.Supplier;
/**
* 原子累加器测试类,和AtomicLong相比,性能较快,
*/
@Slf4j
public class LongAddrTest {
public static void main(String[] args) throws InterruptedException {
//只有LongAddr以及DoubleAddr这两种自增
demo(
() -> {return new AtomicLong();
},atomicInteger -> {
atomicInteger.getAndIncrement();
}
);
//从纳秒层面来说,性能快了10倍
//性能提升的原因很简单,就是在有竞争时,设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加
//Cell[1]... 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性
//能s
demo(
() -> {
return new LongAdder();
},longAdder -> {
longAdder.increment();
}
);
}
//代入两个方法,1方法来提供一个数据,2方法来提供针对于该数据的测试方法以及性能监控
private static<T> void demo(Supplier<T> supplier, Consumer<T> consumer) throws InterruptedException {
//得到当前数据
T t = supplier.get();
List<Thread> threads = new ArrayList<>();
CountDownLatch countDownLatch = new CountDownLatch(40);
long l = System.nanoTime();
for (int i = 0; i < 40; i++) {
//线程中记录的方法
threads.add(new Thread(() -> {
for (int i1 = 0; i1 < 500000; i1++) {
consumer.accept(t);
}
countDownLatch.countDown();
}));
}
threads.forEach(thread -> thread.start());
countDownLatch.await();
//统计出执行完成的时间
log.debug( System.nanoTime() - l +"");
}
}
Unsafe
Unsafe其实是一个基础类,底层基本都是native方法,可以通过反射获取到Unsafe对象,并调用内部方法得到偏移量,可以通过内部的compareAndSwap方法完成CAS操作。
package com.bo.threadstudy.six;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
/**
* LongAddr源码先过,面试不是重点,工作中也暂时用不到
*/
@Slf4j
public class UnsafeTest {
//针对于Unsafe类来做一些操作。源码层面的东西,unsafe内部都是源码层面的东西
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe)theUnsafe.get(null);
//cas的样例
// casTest(unsafe);
}
private static void casTest(Unsafe unsafe) throws NoSuchFieldException {
//通过Unsafe实现CAS操作,普通的CAS采用的是compareAndSet方式来执行
//需要计算出偏移量
Field name = Student.class.getDeclaredField("name");
Field age = Student.class.getDeclaredField("age");
long nameOffset= unsafe.objectFieldOffset(name);
long ageOffset = unsafe.objectFieldOffset(age);
//根据偏移量对结果进行赋值,保证原子操作,可以用debug的setValue试一下
Student student = new Student();
while(true){
//采用setValue对其进行改造过后,初始就不再为null,便会死循环,执行不完
if(unsafe.compareAndSwapObject(student,nameOffset,null,"张三")
&& unsafe.compareAndSwapObject(student,ageOffset,null,20)){
log.debug("修改成功");
break;
}
}
}
}
@Data
class Student{
private String name;
private Integer age;
}
也可以通过Unsafe类来自己封装对应的Atomic类,这个其实就是配合CAS来进行使用,也不难。不过如果有其它的业务场景需要好好考虑。
package com.bo.threadstudy.six;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
import java.util.concurrent.CountDownLatch;
/**
* 自己编写一个AtomicInteger的底层功能
*/
public class UnsafeAtomicTest {
public static void main(String[] args) throws InterruptedException {
AtomicIntegerDemo atomicIntegerDemo = new AtomicIntegerDemo(10000);
CountDownLatch countDownLatch = new CountDownLatch(2);
new Thread(() -> {
for (int i = 0; i < 10000; i++) {
atomicIntegerDemo.decrease(2);
}
countDownLatch.countDown();
}).start();
new Thread(() -> {
for (int i = 0; i < 10000; i++) {
atomicIntegerDemo.decrease(2);
}
countDownLatch.countDown();
}).start();
countDownLatch.await();
System.out.println(atomicIntegerDemo.get());
}
}
class AtomicIntegerDemo{
private volatile int value;
private static Unsafe unsafe;
private static long OFFSET;
static{
try {
Field field= Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe)field.get(null);
//得到value的便宜量
OFFSET = unsafe.objectFieldOffset(AtomicIntegerDemo.class.getDeclaredField("value"));
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
public AtomicIntegerDemo(int value){
this.value = value;
}
public int get(){
return value;
}
public void decrease(int i){
while(true){
int curValue = get();
//这么写我个人感觉只是容易理解而已,假如上一步执行完成后,下一步如果发生变化则重新寻新欢
//如果这不用这个get方法,直接使用value,那么整个操作应该都是原子的,没有问题
//之所以这么写的原因估计是怕中间有什么用到curValue的操作,在操作完成后通过CAS判断,如果在操作过程发生了线程并发,导致值发生改变,那么CAS代入curValue这就有用了
if(unsafe.compareAndSwapInt(this,OFFSET,curValue,curValue-i)){
break;
}
}
}
}