目录
6.Runnable ,future ,futuretask,和callable关系
1、分而治之(Fork/join)
1.规模为N的问题,N<阈值,直接解决,N>阈值,将N分解为K个小规模子问题,子问题互相对立,与原问题形式相同,将子问题的解合并得到原问题的解。
Fork/Join框架是Java 7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。Fork/Join框架要完成两件事情:
(1)任务分割:首先Fork/Join框架需要把大的任务分割成足够小的子任务,如果子任务比较大的话还要对子任务进行继续分割
(2)执行任务并合并结果:分割的子任务分别放到双端队列里,然后几个启动线程分别从双端队列里获取任务执行。子任务执行完的结果都放在另外一个队列里,启动一个线程从队列里取数据,然后合并这些数据。
2.fork/join使用的标准范式
标准范式的意思,就是按照这个模板样子来用forkjoin就是了。
ForkJoinTask有两个子类,RecursiveAction和RecursiveTask。他们之间的区别是,RecursiveAction没有返回值,RecursiveTask有返回值。
(1)使用 RecursiveTask拿到返回值
首先按照范式,新建我们的Mytask,来做求和功能 命名为MySumtask,这里我们继承RecursiveTask来拿到返回值。将求和分成一个个更小的求和计算,然后通过汇总其结果达到求和的目的。
public class MySumTask extends RecursiveTask<Integer> {
private final int THRESHOLD = MakeArray.ArrayLength / 10;// 门槛值,也就是分而治之的最小单位
private int[] src;
private int fromIndex;
private int toIndex;
MySumTask(int fromIndex, int toIndex, int[] src) {
this.src = src;
this.fromIndex = fromIndex;
this.toIndex = toIndex;
}
@Override
protected Integer compute() {
if (toIndex - fromIndex < THRESHOLD) {
int count = 0;
for (int i = fromIndex; i<=toIndex; i++) {
count += src[i];
}
return count;
} else {
int mid = (toIndex + fromIndex) / 2;
MySumTask sumtaskLeft = new MySumTask(fromIndex, mid, src);
MySumTask sumtaskRight = new MySumTask(mid + 1, toIndex, src);
invokeAll(sumtaskLeft, sumtaskRight);
return sumtaskLeft.join() + sumtaskRight.join();
}
}
public static void main(String[] args) {
ForkJoinPool pool= new ForkJoinPool();
int []src =MakeArray.makeArray();
MySumTask mytask=new MySumTask(0,src.length-1,src);
pool.invoke(mytask);
int count =mytask.join();
System.out.println("计算结果是:"+count);
}
}
工具类
public class MakeArray {
public static final int ArrayLength = 10000;
public static int[] makeArray() {
int[] arr = new int[ArrayLength];
for (int i = 0; i < ArrayLength; i++) {
arr[i] = i;
}
return arr;
}
}
测试结果如下:
计算结果是:49995000;
(2)使用RecursiveAction来遍历磁盘目录
public class FindTxt extends RecursiveAction {
private File path;// 当前任务需要搜寻的目录
FindTxt(File path) {
this.path = path;
}
@Override
protected void compute() {
File[] files = path.listFiles();
List<FindTxt> subTasks = new ArrayList<>();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
subTasks.add(new FindTxt(file));
} else {
// 遇到文件,检查
if (file.getAbsolutePath().endsWith("txt")) {
System.out.println("文件:" + file.getAbsolutePath());
}
}
}
}
invokeAll(subTasks);
for (FindTxt fd : subTasks) {
fd.join();
}
}
public static void main(String[] args) throws InterruptedException {
ForkJoinPool pool = new ForkJoinPool();
FindTxt fdf = new FindTxt(new File("D:/work/aboutWorkSoft"));
pool.invoke(fdf);//pool.execute(fdf)是异步调用
System.out.println("你看他是不是同步调用");
fdf.join();
}
}
测试结果
文件:D:\work\aboutWorkSoft\localization\draw.io\war\resources\dia_zh-tw.txt
文件:D:\work\aboutWorkSoft\com.polarion.alm.ui.diagrams.mxgraph_3.17.2\draw.io\war\resources\dia_zh.txt
你看他是不是同步调用
pool.invoke是同步调用,pool.execute是异步调用,这里的同步和异步是什么 意思呢
同步就是pool相关逻辑执行完,main线程后面的逻辑才执行。
异步就是pool相关逻辑和main后面的逻辑一起执行。
把pool.invoke改成pool.execute可以得到执行结果如下:
你看他是不是同步调用
文件:D:\work\aboutWorkSoft\localization\draw.io\war\resources\dia_zh-tw.txt
文件:D:\work\aboutWorkSoft\com.polarion.alm.ui.diagrams.mxgraph_3.17.2\draw.io\war\resources\dia_zh.txt
2、CountDownLatch
可以理解成并发编程中的顺序计数器,闭锁,可以用来确保某些活动直到其他活动都完成后才继续执行,例如:
- 确保某个计算在其需要的所有资源都被初始化之后才继续执行。
- 确保某个服务在其依赖的所有其他服务都已经启动之后才启动。
- 等到某个操作的所有参与者都就绪再继续执行。
先看看实例
public class CountDownLatchTest {
static CountDownLatch latch = new CountDownLatch(3);
static class Thread1 extends Thread {
@Override
public void run() {
try {
System.out.println("Thread1.......is....prepare");
latch.await();
System.out.println("Thread1.......is....running");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class Thread2 extends Thread {
@Override
public void run() {
System.out.println("Thread2 is running");
latch.countDown();
}
}
static class Thread3 extends Thread {
@Override
public void run() {
System.out.println("Thread3 is running");
latch.countDown();
}
}
public static void main(String[] args) {
System.out.println("main Thread is running");
latch.countDown();
Thread thread1 = new Thread1();
Thread thread2 = new Thread2();
Thread thread3 = new Thread3();
thread1.start();
thread2.start();
thread3.start();
}
}
测试结果如下:
main Thread is running
Thread1.......is....prepare
Thread2 is running
Thread3 is running
Thread1.......is....running
所有具有latch.await()的线程中的后续逻辑,必须在latch的countDown执行了规定次数3次的时候,才会执行。latch的countDown可以在一个线程中执行多次扣减。
使用CountDownLatch可以通过特定的使用,来达到调整线程执行的先后顺序
3.CyclicBarrirer的使用
栅栏类似于闭锁,它能阻塞一组线程直到某个事件发生。栅栏与闭锁的关键区别在于,所有线程必须同时到达栅栏位置,才能继续执行。闭锁用于等待事件,而栅栏用于等待其他线程。
CyclicBarrier可以使一定数量参与方反复地在栅栏位置汇集,它在并行迭代算法中非常有用:这种算法通常将一个问题拆分成一系列相互独立的子问题。当线程到达栅栏位置时,将调用await方法,这个方法将阻塞直到所有线程都到达栅栏位置。如果所有线程都到达了栅栏位置,那么栅栏将打开,此时所有线程都被释放,而栅栏将被重置以便下次使用。如果对await的调用超时,或者await阻塞的线程被中断,那么栅栏就被认为是打破了,所有的阻塞的await调用都将终止并抛出BrokenBarrierException,那么await将为每个线程返回一个唯一到达的索引号,我们可以利用这些索引选举一个领导线程,并在下一次迭代中由该领导线程执行一些特殊工作。
例如某个步骤中的计算可以并行执行,但是必须等到该步骤的所有计算都执行完毕才能进入下一个步骤。
public class CyclicBarrierTest {
static CyclicBarrier barrier = new CyclicBarrier(2, new Thread1());
static class Thread1 extends Thread {
@Override
public void run() {
System.out.println("Thread1.......is....running");
}
}
static class Thread2 extends Thread {
@Override
public void run() {
try {
System.out.println("Thread2 is wait");
barrier.await();
System.out.println("Thread2 is continue Running");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
}
static class Thread3 extends Thread {
@Override
public void run() {
try {
System.out.println("Thread3 is wait");
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println("Thread3 is continue Running");
}
}
public static void main(String[] args) {
Thread thread2 = new Thread2();
Thread thread3 = new Thread3();
thread2.start();
thread3.start();
}
}
测试结果如下:
Thread2 is wait
Thread3 is wait
Thread1.......is....running
Thread3 is continue Running
Thread2 is continue Running
从CyclicBarrirer的英文来看,是周期屏障的意思。在CyclicBarrirer实例化的时候,传入两个参数,一个是int 值2,一个线程Thread1。其中2是表示在CyclicBarrirer在2个线程Thread2,Thread3中调用await后,才会放开屏障,让其继续执行。并且是在Thread1执行完成之后,才执行Thread3,Thread2的后续逻辑。
CyclicBarrirer和CountDownLatch区别就是,CyclicBarrier的调用await线程数量必须和初始化定义的int值,一一对应,并且一个线程只能调用一次CyclicBarrirer.await(),否则就会一直等待的结果。
4、 信号量 Semaphore
Semaphore可以将任何一中容器变成有界阻塞容器。acquire将阻塞知道有许可。release将返回一个许可给信号量。
这里我们通过实现一个简单的连接池来使用Semaphore(读音:生 me four),
这里semaphore作为一种信号量,来实现获取数据库连接数的计数,
实现原理就是
定义两个Semaphore,一个useful(10)表示可用的数据库连接,useless(0)表示已用的数据库连接。
通过useful.getQueueLength(),得到当前请求获得连接的线程的数量(实际请求信号量)
useful.availablePermits()得到实际可用连接数(剩余信号量)。
其实最主要思想就是通过Semaphore的信号量来判断请求信号等待数,请求信号成功数,和剩余信号量。
useful.acquire():阻塞获取信号(代表线程请求获得数据库连接,useful的信号量-1)
useless.release():释放一个信号(代表线程请求获得数据库连接成功,useless信号量+1)
useless.acquire()释放一个信号(代表线程使用数据库连接完成,useless信号量-1)。
useful.release()返回信号量(代表线程使用数据库连接完成,useful的信号量+1)。
Semaphore的主要方法:
void acquire():从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断。
void release():释放一个许可,将其返回给信号量。
int availablePermits():返回此信号量中当前可用的许可数。
boolean hasQueuedThreads():查询是否有线程正在等待获取。
int getQueueLength :得到等待数量
1、创建数据库连接实体类(这里模拟实现,所以并不涉及到连接信息,并且并不重写任何接口方法,只添加静态获取连接方法 fetchConnection())
public class SqlConnectImpl implements Connection{
/*拿一个数据库连接*/
public static final Connection fetchConnection(){
return new SqlConnectImpl();
}
@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
// TODO Auto-generated method stub
return null;
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
// TODO Auto-generated method stub
return false;
}
.......................
}
2、新建一个类DBPoolSemaphore,通过静态代码块实现连接池的初始化。
public class DBPoolSemaphore {
private final static int POOL_SIZE = 10;
private final Semaphore useful,useless;//useful表示可用的数据库连接,useless表示已用的数据库连接
public DBPoolSemaphore() {
this. useful = new Semaphore(POOL_SIZE);
this.useless = new Semaphore(0);
}
//存放数据库连接的容器
private static LinkedList<Connection> pool = new LinkedList<Connection>();
//初始化池
static {
for (int i = 0; i < POOL_SIZE; i++) {
pool.addLast(SqlConnectImpl.fetchConnection());
}
}
/*归还连接*/
public void returnConnect(Connection connection) throws InterruptedException {
if(connection!=null) {
System.out.println("当前有"+useful.getQueueLength()+"个线程等待数据库连接!!"
+"可用连接数:"+useful.availablePermits()+",当前实际已用数据库连接"+useless.availablePermits()+"个");
useless.acquire();
synchronized (pool) {
pool.addLast(connection);
}
useful.release();
}
}
/*从池子拿连接*/
public Connection takeConnect() throws InterruptedException {
useful.acquire();
Connection conn;
synchronized (pool) {
conn = pool.removeFirst();
}
useless.release();
return conn;
}
}
3、新建测试类
public class AppTest {
private static DBPoolSemaphore dbPool = new DBPoolSemaphore();
//业务线程
private static class BusiThread extends Thread{
@Override
public void run() {
Random r = new Random();//让每个线程持有连接的时间不一样
long start = System.currentTimeMillis();
try {
Connection connect = dbPool.takeConnect();
System.out.println("Thread_"+Thread.currentThread().getId()
+"_获取数据库连接共耗时【"+(System.currentTimeMillis()-start)+"】ms.");
SleepTools.ms(100+r.nextInt(100));//模拟业务操作,线程持有连接查询数据
System.out.println("查询数据完成,归还连接!");
dbPool.returnConnect(connect);
} catch (InterruptedException e) {
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 50; i++) {
Thread thread = new BusiThread();
thread.start();
}
}
}
4、测试结果如下:
Thread_11_获取数据库连接共耗时【0】ms.
Thread_15_获取数据库连接共耗时【0】ms.
Thread_16_获取数据库连接共耗时【0】ms.
Thread_13_获取数据库连接共耗时【0】ms.
Thread_12_获取数据库连接共耗时【0】ms.
Thread_10_获取数据库连接共耗时【0】ms.
Thread_20_获取数据库连接共耗时【0】ms.
Thread_17_获取数据库连接共耗时【0】ms.
Thread_14_获取数据库连接共耗时【0】ms.
Thread_18_获取数据库连接共耗时【1】ms.
查询数据完成,归还连接!
当前有40个线程等待数据库连接!!可用连接数:0,当前实际已用数据库连接10个
Thread_19_获取数据库连接共耗时【102】ms.
查询数据完成,归还连接!
当前有39个线程等待数据库连接!!可用连接数:0,当前实际已用数据库连接10个
Thread_21_获取数据库连接共耗时【118】ms.
..............................................................
5、Exchange的使用
该类用于处理,偶数个线程之间的数据交换。比如有两个线程A和B,当A开始运行并准备就绪数据后,它就会等待B开始运行,直到B准备好数据,两个线程马上开始交互数据。如果有三个线程将导致死锁, 另外一种形式的栅栏是Exchanger,它是一种两方栅栏,各方在栅栏位置上交换数据。当两方执行不对称的操作时,Exchanger会非常有用。例如当一个线程向缓冲区写入数据,而另外一个线程从缓冲区读取数据。通过Exchanger,达到满的缓冲区与空的缓冲区交换,当两个线程通过Exchanger较好对象时,这种交换就把两个对象安全地发布给另一方。
测试类
public class UseExchange {
private static final Exchanger<Set<String>> exchange
= new Exchanger<Set<String>>();
public static void main(String[] args) {
//第一个线程
new Thread(new Runnable() {
@Override
public void run() {
Set<String> setA = new HashSet<String>();//存放数据的容器
try {
/*添加数据
* set.add(.....)
* */
setA.add("A的数据");
setA = exchange.exchange(setA);//交换set
System.out.println(setA.toArray()[0]+",这是A");
/*处理交换后的数据*/
} catch (InterruptedException e) {
}
}
}).start();
//第二个线程
new Thread(new Runnable() {
@Override
public void run() {
Set<String> setB = new HashSet<String>();//存放数据的容器
try {
/*添加数据
* set.add(.....)
* set.add(.....)
* */
setB.add("B的数据");
setB = exchange.exchange(setB);//交换set
System.out.println(setB.toArray()[0]+",这是B");
/*处理交换后的数据*/
} catch (InterruptedException e) {
}
}
}).start();
}
}
测试结果:
B的数据,这是A
A的数据,这是B
6.Runnable ,future ,futuretask,和callable关系
FutureTask中的 outcome存储 callable中的返回值,并且可以通过FutureTask的对象示例.get()得到。
isDone,结束,正常还是异常结束,或者自己取消,返回true;
isCancelled 任务完成前被取消,返回true;
cancel(boolean):
- 任务还没开始,返回false
- 任务已经启动,cancel(true),中断正在运行的任务,中断成功,返回true,cancel(false),不会去中断已经运行的任务
- 任务已经结束,返回false
实际应用:
包含图片和文字的文档的处理:图片(云上),可以用future去取图片,主线程继续解析文字。
7、原子操作
1.原子操作相关解释
"原子操作(atomic operation)是不需要synchronized",这是Java多线程编程的老生常谈了。所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切 换到另一个线程)。
2.原子操作可以通过锁机制和CAS来实现
3.CAS和synchronized的区别
synchronized基于阻塞的锁的机制,1、被阻塞的线程优先级很高,2、拿到锁的线程一直不释放锁怎么办?3、大量的竞争,消耗cpu,同时带来死锁或者其他安全。而原子操作则是通过指令级保证安全
4.CAS的基本实现思想
三个运算符:一个内存地址V,一个期望的值A,一个新值B
基本思路:如果地址V上的值和期望的值A相等,就给地址V赋给新值B,如果不是,不做任何操作。
循环(死循环,自旋)里不断的进行CAS操作
5.CAS的缺点
(1)ABA问题。因为CAS需要在操作值的时候,只检查值有没有发生变化,如果没有发生变化则更新,但是如果有一个值原来是A,变成了B,然后又变成了A,那么CAS进行检查时会发现它的值没有变化,但是实际上却变化了。
(2)循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销
(3)只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式保证原子操作,但是对多个个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子操作
6.原子操作相关java类
更新基本类型类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
更新数组类:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
更新引用类型:AtomicReference,AtomicMarkableReference,AtomicStampedReference
原子更新字段类: AtomicReferenceFieldUpdater,AtomicIntegerFieldUpdater,AtomicLongFieldUpdater
7.常用原子操作实战
AtomicInteger
public class UseAtomicInt {
static AtomicInteger ai = new AtomicInteger(10);
public static void main(String[] args) {
System.out.println(ai.getAndIncrement());//10--->11
System.out.println(ai.incrementAndGet());//11--->12--->out
System.out.println(ai.get());
}
}
测试结果:
10
12
12
AtomicArray
public class AtomicArray {
static int[] value = new int[] { 1, 2 };
static AtomicIntegerArray ai = new AtomicIntegerArray(value);
public static void main(String[] args) {
ai.getAndSet(0, 3);
System.out.println(ai.get(0));
System.out.println(value[0]);
}
}
测试结果
3
1
引用类型
public class UseAtomicReference {
static AtomicReference<UserInfo> userRef = new AtomicReference<UserInfo>();
public static void main(String[] args) {
UserInfo user = new UserInfo("erwan", 25);//
userRef.set(user);
UserInfo updateUser = new UserInfo("xuegao", 1);//我的宠物狗,叫雪糕
userRef.compareAndSet(user, updateUser);
System.out.println(userRef.get().getName()+":"+userRef.get().getAge());
System.out.println(user.getName()+":"+user.getAge());
}
//定义一个实体类
static class UserInfo {
private String name;
private int age;
public UserInfo(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
}
测试结果:
xuegao:1
erwan:25
通过版本号来解决原子操作的ABA问题的类
package com.erwan.chp2;
import java.util.concurrent.atomic.AtomicStampedReference;
public class AtomicStampedReferenceTest {
static AtomicStampedReference<String> asr = new AtomicStampedReference<>("未更新", 0);
static class Thread1 extends Thread {
@Override
public void run() {
boolean flag =asr.compareAndSet("未更新", "第一次更新", asr.getStamp(), asr.getStamp()+1);
System.out.println(asr.getReference() +",版本号:"+asr.getStamp()+",操作是否成功:"+flag);
}
}
static class Thread2 extends Thread {
@Override
public void run() {
boolean flag =asr.compareAndSet("第一次更新", "第二次更新", asr.getStamp(), asr.getStamp()+1);
System.out.println(asr.getReference() +",版本号:"+asr.getStamp()+",操作是否成功:"+flag);
}
}
static class Thread3 extends Thread {
@Override
public void run() {
boolean flag =asr.compareAndSet("第二次更新", "第三次更新", asr.getStamp()+1, asr.getStamp());
boolean flag2 =asr.compareAndSet("第一次更新", "第三次更新", asr.getStamp(), asr.getStamp());
System.out.println("错误使用,版本号不对:"+asr.getReference() +",版本号:"+asr.getStamp()+",操作是否成功:"+flag);
System.out.println("错误使用,原有值不对:"+asr.getReference() +",版本号:"+asr.getStamp()+",操作是否成功:"+flag2);
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1=new Thread1();
thread1.start();
thread1.join();
Thread thread2=new Thread2();
thread2.start();
thread2.join();
Thread thread3=new Thread3();
thread3.start();
}
}
测试结果如下:
第一次更新,版本号:1,操作是否成功:true
第二次更新,版本号:2,操作是否成功:true
错误使用,版本号不对:第二次更新,版本号:2,操作是否成功:false
错误使用,原有值不对:第二次更新,版本号:2,操作是否成功:false
解释:boolean flag =asr.compareAndSet("未更新", "第一次更新", asr.getStamp(), asr.getStamp()+1);
asr中的compareAndSet会进行比较和设值,如果设值成功,则返回true,设置失败则返回false
四个参数依次是 oldValue,replaceValue,oldStamp,newStamp。
Thread1和Thread2为什么会用join。
join的用处,Thread1.join会让Thread1执行完成之后,才执行Thread2,同理,Thread3是最后执行。
如果不加join(),则Thread1和Thread2不知道谁先执行,如果Thread2先执行,则在oldValue为“未更新”和Thread2传入的oldValue “第一次更新”不匹配,那么Thread2的值就设值失败,返回false
结果分析:
从Thread3的分析结果来看,AtomicStampedReference 通过版本号来防止cas操作出现ABA的问题的,如果在compareAndSet的操作时候,oldValue不存在,则设值失败,返回false。如果旧版本号不存在,则设值错误。
通过AtomicMarkableReference解决ABA问题
package com.erwan.chp2;
import java.util.concurrent.atomic.AtomicMarkableReference;
public class AtomicMarkableReferenceTest {
static AtomicMarkableReference<String> asr = new AtomicMarkableReference<>("未更新", false);
static class Thread1 extends Thread {
@Override
public void run() {
boolean [] booleans={true};
System.out.println("第一次操作前:操作前的值:"+asr.get(booleans)+",标记为"+booleans[0]);
boolean flag = asr.compareAndSet("未更新", "第一次更新", false, false);
System.out.println("第一次操作后:操作是否成功" + flag+",操作后的值:"+asr.get(booleans)+",标记为"+booleans[0]);
}
}
static class Thread2 extends Thread {
@Override
public void run() {
boolean [] booleans={true};
System.out.println("第二次操作前:操作前的值:"+asr.get(booleans)+",标记为"+booleans[0]);
boolean flag = asr.compareAndSet("第一次更新", "第二次更新", false, false);
System.out.println("第二次操作后:操作是否成功" + flag+",操作后的值:"+asr.get(booleans)+",标记为"+booleans[0]);
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread1();
Thread thread2 = new Thread2();
thread1.start();
thread1.join();
thread2.start();
thread2.join();
}
}
测试结果:
第一次操作前:操作前的值:未更新,标记为false
第一次操作后:操作是否成功true,操作后的值:第一次更新,标记为false
第二次操作前:操作前的值:第一次更新,标记为false
第二次操作后:操作是否成功true,操作后的值:第二次更新,标记为false
AtomicMarkableReference <V>方法介绍:
compareAndSet(V expectedReference, V newReference, boolean expectedMark, boolean newMark)
expectedReference 期望的值,newReference 更改后的值,expectedMark 期望的标记位,newMark 修改后的标记位。
V get(boolean []) 获得返回值,boolean[0],可以存储当前变量的标记位
AtomicMarkableReference,没有版本号,只是传递一个boolean,表示是否修改过值
AtomicStampedReference 通过版本号表示修改过几次值