Java并发学习(八)并发工具类与原子操作类

并发工具类

   在Java并发包中提供了很多有用的并发工具类,并发工具类可以使任何一种对象,只要该对象可以根据自身的状态来协调控制线程的控制流。

阻塞队列可以作为同步工具类,其他类型的同步工具类还包括:信号量(Semaphore)、栅栏(Barrier)、闭锁(Latch)等。

 闭锁

闭锁是一种同步工具类,可以延迟线程的进度直到其达到终止状态。闭锁作用相当于一扇门:在闭锁到达某一状态之前,这扇门一直是关闭的,所有的线程都会在这扇门前等待(阻塞);只有门打开后,所有的线程才会同时通过并继续运行。

当闭锁到达终止状态后,将不会再改变状态,因此这扇门一旦打开将永久不会关闭。闭锁可以用来确保某些活动直到其它活动都完成后才继续执行,例如:

  1. 确保某个计算在其所有资源都被初始化之后才继续执行。二元闭锁(只有两个状态)可以用来表示“资源R已经被初始化”,而所有需要R操作都必须先在这个闭锁上等待。
  2. 确保某个服务在其依赖的所有其他服务都已经启动之后才启动。这时就需要多个闭锁。让S在每个闭锁上等待,只有所有的闭锁都打开后才会继续运行。
  3. 等待直到某个操作的参与者(例如,多玩家游戏中的玩家)都就绪再继续执行。在这种情况下,当所有玩家都准备就绪时,闭锁将到达结束状态。

1、CountDownLatch 

CountDownLatch 就是一种灵活的闭锁实现,可以用在上述各种情况中使用。闭锁状态包含一个计数器,初始化为一个正数,表示要等待的事件数量。countDown() 方法会递减计数器,表示等待的事件中发生了一件。await() 方法则阻塞,直到计数器值变为0,表示所有等待的事件都已经发生。

CountDownLatch的常见用法

下面的代码使用闭锁实现了在主线程中计算多个子线程运行时间的功能,具体逻辑是使用两个闭锁,“起始门”用来控制子线程同时运行,“结束门”用来标识子线程是否都结束。

public class Test {
    private static final int nThread = 5;//挡住的线程数
    public static void main(String[] args) throws InterruptedException{
        //起始门,控制多个子线程同时运行
        final CountDownLatch startGate = new CountDownLatch(1);
        //结束门,标识子线程是否都结束    
        final CountDownLatch endGate = new CountDownLatch(nThread);
        for(int i =0;i<nThread;i++){
            new Thread(){
                @Override
                public void run(){
                    try{
                        startGate.await();
                        Thread.sleep(300);}
                    catch(InterruptedException e){
                    }
                    finally{
                        System.out.println(Thread.currentThread().getName()+"ended");
                        endGate.countDown();
                    }
                }
            }.start();
        }
        long start = System.nanoTime();
        startGate.countDown();//打开起始门,同时放行所有线程
        endGate.await();      //把持结束门,直到所有线程已经结束,计数器值为0
        long end = System.nanoTime();
        System.out.println("Total time :"+(end - start)/Math.pow(10,9)+"秒");
    }
}

利用上述算法可以计算出,n个线程并发的执行某个任务所需要的时间,阻塞起始门直到所有线程就绪然后同时开放,把持结束门直到最后一个线程也完成了任务。

2、FutureTask

FutureTask也可以作为一种闭锁。可取消的异步计算。 该类提供了一个Future的基本实现 ,具有启动和取消计算的方法,查询计算是否完整,并检索计算结果。 结果只能在计算完成后才能检索; 如果计算尚未完成,则get方法将阻止。 一旦计算完成,则无法重新启动或取消计算(除非使用runAndReset()调用计算)。

FutureTask是通过 Callable 来实现的,相当于一种可生成结果的 Runnable ,并且可处于以下三种状态:等待运行,正在运行,运行完成。当FutureTask进入完成状态后,它会永远停留在这个状态上。

Future.get 用来获取计算结果,如果FutureTask还未运行完成,则会阻塞。FutureTask 将计算结果从执行计算的线程传递到获取这个结果的线程,而FutureTask 的规范确保了这种传递过程能实现结果的安全发布。

FutureTask主要用于Executor框架中表示异步任务,还可以用来表示一些时间较长的计算,这些计算可以在使用计算结果之前启动。详细的原理会在线程池的学习中介绍,具体可见这篇文章

栅栏

栅栏Bariier 类似于闭锁,它能阻塞一组线程直到某个事件发生。栅栏与闭锁的关键区别在于,所有的线程必须同时到达栅栏位置,才能继续执行。闭锁用于等待事件而栅栏用于等待线程且闭锁是一次性对象,一旦进入终止状态,将不能重置

1、同步屏障CyclicBarrier

同步屏障CyclicBarrier 可以使一定数量的参与方反复的在栅栏位置(公共屏障点 common barrier point也叫同步点)汇聚,它在并行迭代算法中非常有用:将一个问题拆成一系列相互独立的子问题当线程到达栅栏位置(同步点)时,调用await() 方法,这个方法是阻塞方法,直到所有线程到达了栅栏位置,那么栅栏被打开,此时所有线程被释放,而栅栏将被重置以便下次使用。

CyclicBarrier 的主要方法,它可以将计数器通过使用reset()方法重置。

CyclicBarrier的简单使用:

public class CyclicBarrierTest {
    //初始化屏障拦截的线程数量
    static CyclicBarrier c = new CyclicBarrier(2);
    public static void main(String[] args) throws Exception {
        new Thread(new Runnable(){
            @Override
            public void run() {
                try {
                    c.await();//子线程等待直到足够的线程数调用此方法
                }catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println(2);
            }
        }).start();
        System.out.println(1);
        c.await();//达到屏障拦截的线程数量,释放屏障
        System.out.println(3);
    }
}

需要注意的,无论是主线程还是子线程谁先到达屏障同步点,都需要等待其他线程到达直到屏障的拦截极限数屏障的释放是对所有线程而言是同时的,意思就是说,即使是最后一个到达屏障的线程也可能抢先执行。以上述代码为例,输出结果可能是1,2,3也可能是1,3,2。另外,如果将构造参数改为3,那么主线程和子线程会永远等待,因为永远也没有第三个线程调用await方法

CyclicBarrier 还提供了一个更高级的构造方法CyclicBarrier(int parties, Runnable barrierAction):当所有线程到达同步点之后,优先执行barrierAction,等待该线程执行完之后,再继续执行await后面的方法,类似于一个拦截器。

public class CyclicBarrierTest {
    static CyclicBarrier c = new CyclicBarrier(2, new A());

    public static void main(String[] args) throws Exception {
        new Thread(new Runnable(){
            @Override
            public void run() {
                try {
                    System.out.println(0);
                    c.await();
                    System.out.println(1);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
        System.out.println(-1);
        c.await();
        System.out.println(2);
    }
    static class A implements Runnable{
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(3);
        }
    }
}
//执行输出为-1 0 3 1 2

上述代码中,CyclicBarrier 设置拦截线程数为2,所以必须等代码中的主线程和其子线程都调用了await方法(即都到达屏障同步点)后,再优先执行A线程,最后打开屏障,释放被拦截的线程。

CyclicBarrier 和CountDownLatch的区别:

官方的解释为:

  • CountDownLatch是一个同步的辅助类,允许一个或多个线程,等待其他一组它所依赖的线程完成操作,再继续执行。
  • CyclicBarrier是一个同步的辅助类,允许一组线程相互之间等待,达到一个共同点,再继续执行。

CountDownBatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置。因此CyclicBarrier可以实现更加复杂的功能。例如:处理计算错误,可以重置计数器,让线程重新执行一次。

可以通过一个场景来理解,CountDownLatch可以理解成倒计时锁,例如在一场考试中,共有n个人参加考试,提前交卷一个人则减去1,交完卷的人并不会等待这里,可以去继续干自己的事,而直到最后一个人交完卷老师才关门回家。

这个场景同样可以用来描述CyclicBarrier,只是条件变了,不允许提前交卷,即使做完了也必须等待,直到所有人都做完了卷子(到达同步点)后统一交卷,只要有一个人未交卷则都必须等待。

总结,CountDownLatch : 一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行。   CyclicBarrier  : N个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。线程在countDown()之后,会继续执行自己的任务,而CyclicBarrier会在所有线程任务到达之后,才会进行后续任务。

 

2、线程间交换数据Exchanger

Exchanger(交换者)是另一种形式的栅栏。它是一种两方(Two-Party)栅栏,各方在栅栏位置上交换数据。例如当一个线程想缓冲区写入数据,而另一个线程从缓冲区中读取数据。这些线程可以使用 Exchanger 来汇合,并将慢的缓冲区与空的缓冲区交换。当两个线程通过 Exchanger 交换对象时,这种交换就把这两个对象安全的发布给另一方。

Exchanger也具有栅栏的同步点性质,它的同步点作用是:在这个同步点上,两个线程可以互相交换数据

第一个线程先执行exchange()方法,第二个线程也执行exchange()方法,当两个线程同时到达同步点,这两个线程就可以交换数据。如果第二个线程一直没有执行exchange()方法,那么第一个线程会一直等下去,如果担心特殊情况,可以使用exchange(V v,longtimeout, TimeUnit unit)设置最大等待时间。

class Producer extends Thread {
    List<Integer> list = new ArrayList<>();
    Exchanger<List<Integer>> exchanger = null;
    public Producer(Exchanger<List<Integer>> exchanger) {
        super();
        this.exchanger = exchanger;
    }
    @Override
    public void run() {
        Random rand = new Random();
        for(int i=0; i<5; i++) {
            list.clear();
            list.add(rand.nextInt(10000));
            try {
                list = exchanger.exchange(list);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
class Consumer extends Thread {
    List<Integer> list = new ArrayList<>();
    Exchanger<List<Integer>> exchanger = null;
    public Consumer(Exchanger<List<Integer>> exchanger) {
        super();
        this.exchanger = exchanger;
    }
    @Override
    public void run() {
        for(int i=0; i<5; i++) {
            try {
                list = exchanger.exchange(list);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(list.get(0));
        }
    }
}
public class ExchangerTest {
    public static void main(String[] args) {
        Exchanger<List<Integer>> exchanger = new Exchanger<>();
        new Consumer(exchanger).start();
        new Producer(exchanger).start();
    }
}

Exchanger还可以用于遗传算法、校对工作等。

信号量

Semaphore信号量,用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用公共资源。

以闭锁为比较,闭锁是通过控制访问的时间,而信号量这是控制访问的空间(线程数),而且闭锁只能够减少,一次性使用,而信号量则申请可释放,可增可减。 计数信号量还可以用来实现某种资源池,或者对容器施加边界。

Semaphone 管理着一组许可(permit),可通过构造函数指定。同时提供了阻塞方法acquire,用来获取许可。同时提供了release方法表示释放一个许可。可将信号量想象为一个红路灯,若某条路最多允许一百辆车行驶,则未到达一百时,信号量为路灯,每进入一辆车增加1;若到达一百,信号量为红灯,阻塞后续的进入,直到有车离开了这条路,信号量减1,重新变为路灯

Semaphone 可以将任何一种容器变为有界阻塞容器,如用于实现资源池。例如数据库连接池。我们可以构造一个固定长度的连接池,使用阻塞方法 acquire和release获取释放连接,而不是获取不到便失败。(当然,一开始设计时就使用BlockingQueue来保存连接池的资源是一种更简单的方法)。

信号量的主要方法:

构造方法Semaphore(int permits, boolean fair) 
          创建具有给定的许可数和给定的公平设置的 Semaphore

 void

acquire() 
          从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断
 intavailablePermits() 
          返回此信号量中当前可用的许可数。
 intgetQueueLength() 
          返回正在等待获取的线程的估计数目。
 boolean

hasQueuedThreads() 
          查询是否有线程正在等待获取。

 

 booleanisFair() 
          如果此信号量的公平设置为 true,则返回 true
 void

release() 
          释放一个许可,将其返回给信号量。 

protected  Collection<Thread>getQueuedThreads() 
          返回一个 collection,包含可能等待获取的线程。
 booleantryAcquire() 
          尝试获取一个信号量的可用许可,获取一个许可(如果提供了一个)并立即返回,其值为 true,将可用的许可数减 1;如果没有可用的许可,则此方法立即返回并且值为 false

可以看出,Semaphone 信号量是非常适合进行流量控制的,特别是公共资源有限的时候,如数据库连接。

public class TestSemaphore extends Thread {
    //控制某资源同时被访问的个数的类 控制同一时间最多只能有10个访问
    private static Semaphore semaphore = new Semaphore(10);
    private static final int Thread_Count = 50;
    public static void main(String[] args) {
        for (int i = 0;i < Thread_Count;i++) {
            new TestSemaphore().start();
        }
    }
    public void run() {
        Object conn = null;
        try {
            if (semaphore.tryAcquire(Thread_Count, TimeUnit.MILLISECONDS)) {
                conn = UUID.randomUUID().toString();
            }
            if (conn != null) {
                System.out.println("获得一个连接" + conn);
                Thread.sleep(300);
                semaphore.release();
                System.out.println("释放一个连接" + conn);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

原子操作类

    当程序更新一个变量的时候,如果出现多线程数据争用,则可能出现所意想不到的情况。这时的一般策略是使用synchronized解决,因为synchronized能够保证多个线程不会同时更新该变量,但因此也导致并发性能问题。

从JDK5之后,Java提供了粒度更细、量级更轻,并且在多核处理器具有高性能的原子操作类。这些原子操作类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式。因为原子操作类把竞争的范围缩小到单个变量上,这可以算是粒度最细的情况了。
 原子操作类共有13个类,在java.util.concurrent.atomic包下,可以分为四种类型的原子更新类:原子更新基本类型、原子更新数组类型、原子更新引用和原子更新属性。所有这些类都支持CAS。

1、原子更新基本类型

使用原子方式更新基本类型,共包括3个类:

  • AtomicBoolean:原子更新布尔变量
  • AtomicInteger:原子更新整型变量
  • AtomicLong:原子更新长整型变量

以上三个类提供的方法基本相同,以AtomicInteger为例,AtomicInteger提供的部分方法如下:

  • int addAndGet(int delta):以原子方式将输入的数值与实例中的数值(value)相加,并返回结果
  • boolean compareAndSet(int expect,int update):如果输入的数值等于预期值,则以原子方式将该值设置为输入的值
  • int getAndIncrement():以原子方式将当前值加1,但是,返回的是自增前的值
  • void lazySet(int newValue):最终会设置成newValue,使用lazySet设置值后,可能导致其线程在之后的一小段时间内还是可以读到旧的值。
  • int getAndSet(int newValue):以原子方式设置为newValue的值,并返回旧值。

通过getAndIncrement方法的源码来分析其实现原理:

    public final int getAndIncrement() {
        for (;;) {
            int current = get();//先取得AtomicInteger存储的值
            int next = current + 1;//对当前值加1
            //CAS操作更新
            if (compareAndSet(current, next))
                return current;//返回更新前的值
        }
    }

    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

for循环先获取AtomicInteger存储的值,然后对当前数值进行加1操作,最后调用compareAndSet方法进行原子更新(先检查当前数值是否等于current,如果是则将AtomicInteger的当前值更新成next;如果不是,则返回false,重新循环更新)。

2、原子更新数组

通过原子更新数组里的某个元素,共有3个类:

  • AtomicIntegerArray:原子更新整型数组的某个元素
  • AtomicLongArray:原子更新长整型数组的某个元素
  • AtomicReferenceArray:原子更新引用类型数组的某个元素

以上类的方法基本一样,以AtomicIntegerArray为例常用的方法有:

  • int addAndSet(int i, int delta):以原子方式将输入值与数组中索引为i的元素相加
  • boolean compareAndSet(int i, int expect, int update):如果当前值等于预期值,则以原子方式更新数组中索引为i的值为update
     
public class AtomicIntegerArrayDemo {

    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,数组value通过构造的方式传入AtomicIntegerArray中,实际上AtomicIntegerArray会将当前数组拷贝一份,所以在数组拷贝的操作不影响原数组的值。

3、原子更新引用类型

更新引用类型往往涉及多个变量,就需要使用原子更新引用类型的类,Atomic包提供了三个类:

  • AtomicReference:原子更新引用类型
  • AtomicReferenceFieldUpdater:原子更新引用类型里的字段
  • AtomicMarkableReference:原子更新带有标记位的引用类型。

以上类的方法基本一样,以AtomicReference为例:

public class AtomicReferenceDemo {

    static class User{
        private String name;
        private int id;

        public User(String name, int id) {
            this.name = name;
            this.id = id;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getId() {
            return id;
        }

        public void setId(int id) {
            this.id = id;
        }
    }

    public static AtomicReference<User> ar = new AtomicReference<User>();

    public static void main(String[] args){
        User user = new User("aa",11);
        ar.set(user);
        User newUser = new User("bb",22);
        ar.compareAndSet(user,newUser);
        System.out.println(ar.get().getName());
        System.out.println(ar.get().getId());
    }
}

4、原子更新字段类

如果需要原子更新某个类的某个字段,就需要用到原子更新字段类,可以使用以下几个类:

  • AtomicIntegerFieldUpdater:原子更新整型字段
  • AtomicLongFieldUpdater:原子更新长整型字段
  • AtomicStampedReference:原子更新带有版本号引用类型。该类将整型值与引用关联起来,可用于原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时可能出现的ABA问题。
     

要想原子更新字段,需要两个步骤:

  1. 每次必须使用newUpdater创建一个更新器,并且需要设置想要更新的类的字段
  2. 更新类的字段(属性)必须为public volatile
public class AtomicIntegerFieldUpdaterDemo {
    //创建一个原子更新器
    private static AtomicIntegerFieldUpdater<User> atomicIntegerFieldUpdater =
            AtomicIntegerFieldUpdater.newUpdater(User.class,"old");

    public static void main(String[] args){
        User user = new User("Tom",15);
        //原来的年龄
        System.out.println(atomicIntegerFieldUpdater.getAndIncrement(user));
        //现在的年龄
        System.out.println(atomicIntegerFieldUpdater.get(user));
    }

    static class User{
        private String name;
        public volatile int old;

        public User(String name, int old) {
            this.name = name;
            this.old = old;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getOld() {
            return old;
        }

        public void setOld(int old) {
            this.old = old;
        }
    }
}

 

 

参考文章:

《Java并发编程实战》

《Java并发编程的艺术》

  https://blog.csdn.net/zq602316498/article/details/41779431

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值