JUC并发编程三:ABA&阻塞队列&计数器

463 阅读8分钟
ABA问题

CAS会导致"ABA问题",CAS算法实现的一个重要前提是需要取出内存中某时刻的数据并在当下时刻进行比较并替换,那么在这个时间差内会导致数据的变化。

比如一个线程①从内存位置V中取出A,这时另一个线程②也从内存中取出A,并且线程②进行了一些操作将值变成了B,然后线程②又将V位置上的数据变成了A,这个时候线程①进行CAS操作发现内存中仍然是A,然后线程①操作成功。这就导致了ABA问题的出现。

一:ABA问题的解决方式是通过时间戳来进行CAS操作

public class AbaDemo {

 static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);  //用于演示ABA问题
 static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100,1);//加时间戳
 public static void main(String[] args) {
     new Thread(()->{
         atomicReference.compareAndSet(100,101);
         atomicReference.compareAndSet(101,100);
     },"t1").start();
     new Thread(()->{

         try{
             TimeUnit.SECONDS.sleep(1);//等待线程1执行完成
             atomicReference.compareAndSet(100,102);//T2这个线程存在问题
             System.out.println(atomicReference.get());//输出为102
         }catch (Exception e){
             e.printStackTrace();
         }
     },"t2").start();

     //主线程等待
     try{
         TimeUnit.SECONDS.sleep(3);
     }catch (Exception e){
         e.printStackTrace();
     }

     System.out.println("aba问题解决--------------------");
     new Thread(()->{
         int stamp = atomicStampedReference.getStamp();
         System.out.println(Thread.currentThread().getName()+"第一次版本号"+stamp);
         try{
             TimeUnit.SECONDS.sleep(1);
             atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
             System.out.println(Thread.currentThread().getName()+"第二次版本号"+atomicStampedReference.getStamp());
             atomicStampedReference.compareAndSet(101,102,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
             System.out.println(Thread.currentThread().getName()+"第三次版本号"+atomicStampedReference.getStamp());
         }catch (Exception e){
             e.printStackTrace();
         }
     },"t3").start();

     new Thread(()->{
         int stamp = atomicStampedReference.getStamp();
         System.out.println(Thread.currentThread().getName()+"第一次版本号"+stamp);
         try{
             TimeUnit.SECONDS.sleep(3);//等待t3线程执行完成
         }catch (Exception e){
             e.printStackTrace();
         }
         boolean  flag = atomicStampedReference.compareAndSet(100,209,stamp,stamp+1);
         System.out.println(Thread.currentThread().getName()+"第二次版本号"+atomicStampedReference.getStamp()+"执行结果"+flag);
     },"t4").start();
 }
}

二:通过AtomicReference进行原子读写对象引用来解决ABA问题,通过包装的元组来对对象标记版本戳stamp,从而避免ABA问题。

AtomicStampedReference 本质是通过一个int值作为版本号,每次更改前先取到这个int值的版本号,等到修改的时候比较当前版本号与线程持有的版本号是否一致,如果一致则进行修改,并将版本号+1,(注,到底是加1还是减1还是多少都有自己定义)并且值得一提的是在后面的zookeeper中保持数据一致性的处理也是用的这种方法。

final int stamp = ATOMIC_REFERENCE.getStamp();
 atomicStampedRef.compareAndSet(100, 101, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
              atomicStampedRef.compareAndSet(101, 100, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);

AtomicMarkableReference 则是通过一个Boolean值作为是否有更改的标记,本质就是它只有两个版本号,true和false,修改的时候在这两个版本号之间来回切换,并不能从根本上解决ABA问题,只会降低ABA问题发生的几率。

阻塞队列

阻塞队列顾名思义首先他是一个队列,而一个阻塞队列从数据结构中所起到的作用大致如下

消费者阻塞:当队列为空时获取元素的操作(Thread2)会被阻塞,此为消费者阻塞

​ 从空的阻塞中获取元素线程阻塞后,直到等到其他线程往队列中插入元素,才会解除阻塞。

生产者阻塞:当队列为满时往队列中添加操作(Thread1)会被阻塞,此为生产者阻塞

​ 往已经满的阻塞队列中添加新的元素,线程会被阻塞,直到其他线程从队列中移除一个元素或多个元素乃至完全清空队列,使队列重新空闲起来并后续增加。

在多线程领域,所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足被挂起的线程就会自动被唤醒。

这就是为什么我们需要blockingqueue,我们不需要关心什么时候阻塞线程,什么时候需要唤醒线程,blockingqueue都给我们一手包办了。

BlockingQueue为接口,下面简单介绍相应实现类

  • ArrayBlockingQueue 一个由数组结构组成的有界阻塞队列

  • LinkedBlockingQueue 一个由链表结构组成的有界阻塞队列

  • PriorityBlockingQueue 一个支持优先级排序的无界阻塞队列

  • DelayQueue一个使用优先级队列实现的无界阻塞队列

  • SynchronousQueue 一个不存储元素的阻塞队列

  • LinkedTransferQueue 一个由链表结构组成的无界阻塞队列

  • LinkedBlockingDeque 一个由链表结构组成的双向阻塞队列

    常用方法

方法类型 抛出异常 特殊值 阻塞 超时
插入 add(e) offer(e) put(e) offer(e,time,unit)
移除 remove() poll() take() poll(time,unit)
检查 element() peek() 不可用 不可用
public static void main(String[] args) throws InterruptedException {
   //List list = null;
   BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<String>(3);// 有界队列
   System.out.println(blockingQueue.add("a"));   //true
   System.out.println(blockingQueue.add("b"));   //true
   System.out.println(blockingQueue.add("c"));   //true
   //System.out.println(blockingQueue.add("d")); 报错抛出异常  java.lang.IllegalStateException: Queue full

   System.out.println(blockingQueue.element());//获取第一排队列  a
   System.out.println(blockingQueue.remove());//移除元素    a

   System.out.println(blockingQueue.element());//获取第一排队列  b
   System.out.println(blockingQueue.remove());//移除元素    b

   System.out.println(blockingQueue.element());//获取第一排队列  c
   System.out.println(blockingQueue.remove());//移除元素   c

  // System.out.println(blockingQueue.element());//获取第一排队列   java.util.NoSuchElementException
   //System.out.println(blockingQueue.remove());//移除元素   java.util.NoSuchElementException

   System.out.println("=================================");
   BlockingQueue<String> blflagQueue = new ArrayBlockingQueue<String>(3);
   System.out.println(blflagQueue.offer("a"));  //输出  true
   System.out.println(blflagQueue.offer("b"));  //输出  true
   System.out.println(blflagQueue.offer("c"));  //输出  true
   System.out.println(blflagQueue.offer("d"));  //输出  false

   System.out.println(blflagQueue.peek());  //输出  a
   System.out.println(blflagQueue.poll());  //输出  a

   System.out.println(blflagQueue.peek());  //输出  b
   System.out.println(blflagQueue.poll());  //输出  b

   System.out.println(blflagQueue.peek());  //输出  c
   System.out.println(blflagQueue.poll());  //输出 c

   System.out.println(blflagQueue.peek());  //输出 null
   System.out.println(blflagQueue.poll());  //输出 null

   System.out.println("====================put he take =============");
   BlockingQueue<String> threeQueue = new ArrayBlockingQueue<String>(3);
   threeQueue.put("a");
   threeQueue.put("b");
   threeQueue.put("c");
   //threeQueue.put("c");   程序卡死,会一直等待队列中数据释放
   System.out.println(threeQueue.take());//  a
   System.out.println(threeQueue.take());//  b
   System.out.println(threeQueue.take());//  c

   System.out.println("====================有时间得等待始放 =============");

   BlockingQueue<String> fourQueue = new ArrayBlockingQueue<String>(3);
   System.out.println(fourQueue.offer("a",2L, TimeUnit.SECONDS));//等待两秒,如果不能加入队列就会放弃  true
   System.out.println(fourQueue.offer("b",2L, TimeUnit.SECONDS));//等待两秒,如果不能加入队列就会放弃  true
   System.out.println(fourQueue.offer("c",2L, TimeUnit.SECONDS));//等待两秒,如果不能加入队列就会放弃  true
   System.out.println(fourQueue.offer("d",2L, TimeUnit.SECONDS));//等待两秒,如果不能加入队列就会放弃  false

   System.out.println(fourQueue.poll(2L,TimeUnit.SECONDS));//等待两秒取数据如果等不到就放弃  a
   System.out.println(fourQueue.poll(2L,TimeUnit.SECONDS));//等待两秒取数据如果等不到就放弃  b
   System.out.println(fourQueue.poll(2L,TimeUnit.SECONDS));//等待两秒取数据如果等不到就放弃  c
   System.out.println(fourQueue.poll(2L,TimeUnit.SECONDS));//等待两秒取数据如果等不到就放弃  null
}

同步队列

public static void main(String[] args) {
   BlockingQueue<String> blockingQueue = new SynchronousQueue<>();
   new Thread(()->{
       try{
           System.out.println(Thread.currentThread().getName()+"添加一条数据");
           blockingQueue.put("1");
           System.out.println(Thread.currentThread().getName()+"添加两条数据");
           blockingQueue.put("2");
       }catch (Exception e){
           e.printStackTrace();
       }
   },"AA").start();

   new Thread(()->{
       try{
           System.out.println(Thread.currentThread().getName()+"取一条数据");
           TimeUnit.SECONDS.sleep(3);
           blockingQueue.take();
           System.out.println(Thread.currentThread().getName()+"取二条数据");
           TimeUnit.SECONDS.sleep(3);
           blockingQueue.take();
       }catch (Exception e){
           e.printStackTrace();
       }
   },"BB").start();
}
计数器

说了线程,CAS,阻塞,在开始线程前得说一下计数器。也称之为同步计数器。

CountDownLatch

在完成一组正在其他线程中执行的操作之前,允许一个活多个线程一直等待,在计数器到达0之前await方法会一直阻塞,之后释放所有等待线程,await的后续调用都会返回。也有人称之为减数上门闩,就是人都走了就关门不迎客了。

方法:countDown()计数减一

​ awaot()阻塞线程

CountDownLatch countObj = new CountDownLatch(6);
     for(int i=1;i<=6;i++){
         new Thread(()->{
             System.out.println(Thread.currentThread().getName()+"上完自习离开教室");
             countObj.countDown();
         },String.valueOf(i)).start();
     }
     try{
         countObj.await();
         System.out.println(Thread.currentThread().getName()+"班长走人,锁上自习室");
     }catch (Exception e){
         e.printStackTrace();  
     }

在了解这个计数器时,我得到一个很好的举例就是秦灭六国

CountDownLatch countObj = new CountDownLatch(6);
     for(int i=1;i<=6;i++){
         new Thread(()->{
             System.out.println(Thread.currentThread().getName()+"被灭");
             countObj.countDown();
         },CountryEnum.getCountryEnum(i).getMessage()).start();
     }
     try{
         countObj.await();
         System.out.println(Thread.currentThread().getName()+"秦国一统华夏");
     }catch (Exception e){
         e.printStackTrace();
     }
     System.out.println(CountryEnum.One);
     System.out.println(CountryEnum.Two.getCode());
     System.out.println(CountryEnum.Three.getMessage());

//在CountryEnum 枚举中存放六国
public enum CountryEnum {
 One(1,"齐国"),Two(2,"楚国"),Three(3,"燕国"),Four(4,"赵国"),Five(5,"魏国"),Six(6,"韩国");

 private Integer code;
 private String message;

 CountryEnum(Integer code,String message){
     this.code = code;
     this.message = message;
 }

 /**
     * 根据code得到国家的枚举
     * @param code
     * @return
     */
    public static CountryEnum getCountryEnum(int code){
        CountryEnum[] countrys =  CountryEnum.values();
        for(CountryEnum el : countrys){
            if(code == el.getCode()){
                return el;
            }
        }
        return null;
    }

有了做减法的计数器,相对应的也就有了做加法的计数器

CyclicBarrier

原理是将一组线程在达到一个屏障之前被阻塞,只有当所有的线程都达到阻拦位置时,才会打开被拦截的线程。

主线程会等待执行线程执行完成

举例:七龙珠,召唤神龙

public static void main(String[] args) {
     CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
        System.out.println("召唤神龙");
     });
     for(int i=1;i<=7;i++){
         new Thread(()->{
             System.out.println(Thread.currentThread().getName()+"颗龙珠收集到了");
             //跑下代码注意,此地为无序输出。
             try {
                 cyclicBarrier.await();//阻塞
             } catch (InterruptedException e) {
                 e.printStackTrace();
             } catch (BrokenBarrierException e) {
                 e.printStackTrace();
             }
         },String.valueOf(i)).start();
     }
 }
Semaphore

信号灯线程处理,主要用于多个共享资源互斥的使用,用于并发线程数的控制

Semaphore obj = new Semaphore(3): 申明资源数据

new Thread 多线程占用资源

obj.acquire() 线程抢占资源

obj.release() 线程释放资源

public static void main(String[] args) {
     Semaphore obj = new Semaphore(3);//一共有三个位置
 //启动6个线程抢占位置,当三个抢占完成位置后,后面的线程只能等待线程释放。
     for(int i=1;i<=6;i++){
         new Thread(()->{
             try {
                 obj.acquire();//抢占资源
                 System.out.println(Thread.currentThread().getName()+"抢到位置");
                 TimeUnit.SECONDS.sleep(3);
                 System.out.println(Thread.currentThread().getName()+"释放位置");
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }finally {
                 obj.release();//释放资源
             }
         },String.valueOf(i)).start();
     }
 }

今天给大家分享了JUC并发编程中的ABA问题处理,阻塞队列,计数器,至此并发问题基础部分介绍完成,明天给大家介绍最后一篇java线程池的使用,会从源码角度讲解为什么阿里不推荐使用java自带的线程池。如果您对今天的分享感兴趣,请走之前点亮右下角的在看,如果您对内容有什么建议欢迎发邮件cqp1116@sina.com。并发线程非常枯燥,不如后面的中间件来的舒服和成就感强烈,但还是希望大家能敲一下文中的代码,因为这是解决后面所有技术栈的基础。是道,是思想,大道至简,一生二,二生三,三生万物。随着中间件研究的深入,其实对应用越来越没有兴趣,那就是一个配置。