Java并发编程学习四:CountDownLatch,CyclicBarrier,Semaphore以及原子类

上篇文章线程同步的关键字以及理解中介绍了一下多线程同步协作之间经常使用的关键字,今天这篇文章就介绍一下一些同步类以及原子类的使用吧。Java中提供了不少的同步类,如:CountDownLatch,CyclicBarrier,Semaphore等,下面就对每个类做个学习的记录。


CountDownLatch

CountDownLatch是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程的操作执行完后再执行。其拥有的方法如下:

public CountDownLatch(int count);  //参数count为计数值
public void await() throws InterruptedException;   //调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException;  //等待一定的时间后count值还没变为0的话就会继续执行
public void countDown();  //将count减1


CountDownLatch比较简单,这里写个Demo就应该都懂了:

val countDownLatch = CountDownLatch(3)

fun main(args: Array<String>) {
    val thread = Thread(Runnable {
        Thread.sleep(3000)
        countDownLatch.countDown()
        System.out.println(ThreadTest.timeStamp2Date() + " 线程一执行完毕")
    })
    val thread1 = Thread(Runnable {
        Thread.sleep(1000)
        countDownLatch.countDown()
        System.out.println(ThreadTest.timeStamp2Date() + " 线程二执行完毕")
    })
    val thread2 = Thread(Runnable {
        Thread.sleep(2000)
        countDownLatch.countDown()
        System.out.println(ThreadTest.timeStamp2Date() + " 线程三执行完毕")
    })
    thread2.start()
    thread.start()
    thread1.start()
    countDownLatch.await()
    System.out.print(ThreadTest.timeStamp2Date() + " 主线程继续执行")

}
//--------------------------------------
2018-11-20 14:09:37 线程二执行完毕
2018-11-20 14:09:38 线程三执行完毕
2018-11-20 14:09:39 线程一执行完毕
2018-11-20 14:09:39 主线程继续执行

由于CountDownLatch的存在,并且计数值为3,所以主线程需要等到CountDownLatch内部计数值减为0了再执行。


CyclicBarrier

CyclicBarrier可以称作同步屏障,其意义是让一组线程达到一个屏障时被阻塞,直到最后一个线程达到屏障时,所有被阻塞的线程才能继续执行。先看下他的主要方法:

    public CyclicBarrier(int parties);//计数值
    public CyclicBarrier(int parties, Runnable barrierAction);//barrierAction表示当最后线程都达到屏障后会执行的Runnable
	public int await();//阻塞直到最后一个线程遇到屏障
	public int await(long timeout, TimeUnit unit);//阻塞直到最后一个线程遇到屏障或者超出timeout时间

首先测试一下第一种构造方法的运行代码:


val barrier = CyclicBarrier(4)

fun main(args: Array<String>) {
    val thread1 = Thread(Runnable {
        System.out.println(ThreadTest.timeStamp2Date() + " 执行thread1代码")
        Thread.sleep(1000)
        barrier.await()
        System.out.println(ThreadTest.timeStamp2Date() + " 屏障过后执行thread1代码")
    })
    val thread2 = Thread(Runnable {
        System.out.println(ThreadTest.timeStamp2Date() + " 执行thread2代码")
        Thread.sleep(3000)
        barrier.await()
        System.out.println(ThreadTest.timeStamp2Date() + " 屏障过后执行thread2代码")
    })
    val thread3 = Thread(Runnable {
        System.out.println(ThreadTest.timeStamp2Date() + " 执行thread3代码")
        barrier.await()
        System.out.println(ThreadTest.timeStamp2Date() + " 屏障过后执行thread3代码")
    })
    val thread4 = Thread(Runnable {
        System.out.println(ThreadTest.timeStamp2Date() + " 执行thread4代码")
        Thread.sleep(800)
        barrier.await()
        System.out.println(ThreadTest.timeStamp2Date() + " 屏障过后执行thread4代码")
    })
    thread1.start()
    thread2.start()
    thread3.start()
    thread4.start()
}
//--------------------------------------
2018-11-20 14:32:18 执行thread3代码
2018-11-20 14:32:18 执行thread1代码
2018-11-20 14:32:18 执行thread4代码
2018-11-20 14:32:18 执行thread2代码
2018-11-20 14:32:21 屏障过后执行thread2代码
2018-11-20 14:32:21 屏障过后执行thread4代码
2018-11-20 14:32:21 屏障过后执行thread1代码
2018-11-20 14:32:21 屏障过后执行thread3代码

可以看到在thread2中调用到barrier.await()后,屏障消失,所有线程开始继续执行。接着我们再看下第二种构造方法,传入的barrierAction会从某个线程中随机选取一个来执行代码:


val barrier = CyclicBarrier(4, Runnable {
    System.out.println("当前线程=" + Thread.currentThread().name)
})

fun main(args: Array<String>) {
    val thread1 = Thread(Runnable {
        System.out.println(ThreadTest.timeStamp2Date() + " 执行thread1代码"+" name="+Thread.currentThread().name)
        Thread.sleep(1000)
        barrier.await()
        System.out.println(ThreadTest.timeStamp2Date() + " 屏障过后执行thread1代码"+" name="+Thread.currentThread().name)
    })
    val thread2 = Thread(Runnable {
        System.out.println(ThreadTest.timeStamp2Date() + " 执行thread2代码"+" name="+Thread.currentThread().name)
        Thread.sleep(3000)
        barrier.await()
        System.out.println(ThreadTest.timeStamp2Date() + " 屏障过后执行thread2代码"+" name="+Thread.currentThread().name)
    })
    val thread3 = Thread(Runnable {
        System.out.println(ThreadTest.timeStamp2Date() + " 执行thread3代码"+" name="+Thread.currentThread().name)
        barrier.await()
        System.out.println(ThreadTest.timeStamp2Date() + " 屏障过后执行thread3代码"+" name="+Thread.currentThread().name)
    })
    val thread4 = Thread(Runnable {
        System.out.println(ThreadTest.timeStamp2Date() + " 执行thread4代码"+" name="+Thread.currentThread().name)
        Thread.sleep(800)
        barrier.await()
        System.out.println(ThreadTest.timeStamp2Date() + " 屏障过后执行thread4代码"+" name="+Thread.currentThread().name)
    })
    thread1.start()
    thread2.start()
    thread3.start()
    thread4.start()
}
//-----------------------------------------------
2018-11-20 14:42:40 执行thread3代码 name=Thread-2
2018-11-20 14:42:40 执行thread2代码 name=Thread-1
2018-11-20 14:42:40 执行thread1代码 name=Thread-0
2018-11-20 14:42:40 执行thread4代码 name=Thread-3
当前线程=Thread-3
2018-11-20 14:42:40 屏障过后执行thread4代码 name=Thread-3
2018-11-20 14:42:40 屏障过后执行thread3代码 name=Thread-2
2018-11-20 14:42:40 屏障过后执行thread2代码 name=Thread-1
2018-11-20 14:42:40 屏障过后执行thread1代码 name=Thread-0


Semaphore

Semaphore即信号量的意思,通过Semaphore可以控同时访问的线程个数,通过acquire()获取一个许可,如果没有就等待,而release()释放一个许可。Semaphore使用的情景是资源有限,而线程消费又多的情景。

举个生活例子就是医院有5名医生(资源),而看病的有20个人(Thread)。

Semaphore主要方法如下:


 public Semaphore(int permits);//参数为许可数量
 public Semaphore(int permits, boolean fair);//fair表示是否是公平的,即等待时间越久的越先获取许可
 public void acquire() throws InterruptedException {  }     //获取一个许可
 public void acquire(int permits) throws InterruptedException { }    //获取permits个许可
 public void release() { }          //释放一个许可
 public void release(int permits) { }    //释放permits个许可
 public boolean tryAcquire() { };    //尝试获取一个许可,若获取成功,则立即返回true,若获取失败,则立即返回false
 public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException { };  //尝试获取一个许可,若在指定的时间内获取成功,则立即返回true,否则则立即返回false
 public boolean tryAcquire(int permits) { }; //尝试获取permits个许可,若获取成功,则立即返回true,若获取失败,则立即返回false
 public boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException { }; //尝试获取permits个许可,若在指定的时间内获取成功,则立即返回true,否则则立即返回false

可以看到Semaphore的方法跟ReetrantLock的方法很像,其作用也是相似的,也容易理解,下面就简单写个例子测试一下部分api,有兴趣的同学可以自行测试其余的。


val semaphore = Semaphore(3)

fun main(args: Array<String>) {

    var count = 0
    val random = Random()
    while (count < 20) {
        count++
        val thread = Thread(Runnable {
            semaphore.acquire()
            System.out.println(Thread.currentThread().name + " 获取许可,正在看病中")
            synchronized(random) {
                Thread.sleep((random.nextInt(2000) + 1000).toLong())
            }
            System.out.println(Thread.currentThread().name + " 看病完毕,释放许可")
            semaphore.release()
        })
        thread.start()
    }
}

//-------------------------------------------------------
Thread-0 获取许可,正在看病中
Thread-1 获取许可,正在看病中
Thread-2 获取许可,正在看病中
Thread-0 看病完毕,释放许可
Thread-3 获取许可,正在看病中
Thread-2 看病完毕,释放许可
Thread-4 获取许可,正在看病中
Thread-1 看病完毕,释放许可
Thread-5 获取许可,正在看病中
Thread-4 看病完毕,释放许可
Thread-6 获取许可,正在看病中
Thread-3 看病完毕,释放许可
Thread-7 获取许可,正在看病中
Thread-6 看病完毕,释放许可
Thread-8 获取许可,正在看病中
Thread-5 看病完毕,释放许可
Thread-9 获取许可,正在看病中
Thread-8 看病完毕,释放许可
Thread-10 获取许可,正在看病中
Thread-7 看病完毕,释放许可
Thread-11 获取许可,正在看病中
Thread-10 看病完毕,释放许可
Thread-12 获取许可,正在看病中
Thread-9 看病完毕,释放许可
Thread-13 获取许可,正在看病中
Thread-12 看病完毕,释放许可
Thread-14 获取许可,正在看病中
Thread-11 看病完毕,释放许可
Thread-15 获取许可,正在看病中
Thread-14 看病完毕,释放许可
Thread-16 获取许可,正在看病中
Thread-13 看病完毕,释放许可
Thread-17 获取许可,正在看病中
Thread-16 看病完毕,释放许可
Thread-18 获取许可,正在看病中
Thread-15 看病完毕,释放许可
Thread-19 获取许可,正在看病中
Thread-18 看病完毕,释放许可
Thread-17 看病完毕,释放许可
Thread-19 看病完毕,释放许可

Ok,关于同步类的介绍就讲那么多,除了这些Java中还提供了更多其他的同步类,像Phaser这个类,这里就不介绍了,详细的可仔细查阅博客,也可以看我推荐的这两篇博客:

https://segmentfault.com/a/1190000015979879

https://blog.csdn.net/u010739551/article/details/51083004


原子类

原子类具有原子性,也就是意味着在执行的过程中是不能被打断的,那么更新原子类的值在多线程环境下就是安全的。Java中提供了许多的原子类:

  1. 基本类型原子类:AtomicBoolean,AtomicInteger,AtomicLong
  2. 数组类型原子类:AtomicLongArray,AtomicIntegerArray,AtomicReferenceArray
  3. 引用类型原子类:AtomicReference,AtomicReferenceUpdater,AtomicMarkReference

基本类型的原子类分别对应基本类型的boolean,int以及long类型,使用方式基本一样,在Java线程的带来的问题与内存模型(JMM)说过基本变量的i++其实并不是一个原子操作,那么通过使用AtomicInteger我们就能保证原子操作了,主要方法如下:

  public final int get();//获取当前值
  public final void set(int newValue);//设置一个新值
  public final int getAndDecrement();//先获取当前值,然后执行减一
   public final int getAndIncrement();//获取当前值,然后加一
   ...

AtomicInteger的api非常容易理解,还有一些别的api可自行查阅,接下来就是测试的事了,下面是使用的Demo:

val atomicInteger = AtomicInteger(0)
var value = 0;
fun main(args: Array<String>) {
    var count = 0
    while (count < 100) {
        count++
        val thread = Thread(Runnable {
            atomicInteger.incrementAndGet()
            atomicInteger.incrementAndGet()
            atomicInteger.incrementAndGet()
            atomicInteger.incrementAndGet()
            atomicInteger.incrementAndGet()
            atomicInteger.incrementAndGet()
        })
        thread.start()
    }
    count = 0
    Thread.sleep(3000)
    System.out.println("原子类型值=$atomicInteger")
    while (count < 100) {
        count++
        val thread = Thread(Runnable {
            value++
            value++
            value++
            value++
            value++
            value++
        })
        thread.start()
    }
    Thread.sleep(3000)
    System.out.println("基本类型值=$value")
}
//输出结果
原子类型值=600
基本类型值=561


数组类型原子类能够在多线程环境下正确的更新对应数组中的值,其主要的方法如下:

 public final int length();//返回当前数组长度
 public final void set(int i, int newValue);//设置对应索引的值
 public final int getAndSet(int i, int newValue);//先获取值,然后设置新值
 public final int getAndIncrement(int i);//获取对应索引当前值,然后加一
 ...

主要是的方法就这些,下面看下测试效果:


   private int[] values = {1, 2, 0};
   private AtomicIntegerArray mIntegerArray = new AtomicIntegerArray(values);

   public void testArray() {
       mIntegerArray.getAndIncrement(2);
       mIntegerArray.getAndIncrement(1);
       mIntegerArray.getAndIncrement(0);
   }

   public String getFinalValues() {
       return mIntegerArray.get(0) + "  " + mIntegerArray.get(1) + "  " +  mIntegerArray.get(2);
   }
	//-------
	fun main(args: Array<String>) {
		var count = 0
		val threadTest = ThreadTest()
		while (count < 100) {
			count++
			val thread = Thread(Runnable {
				threadTest.testArray()
			})
			thread.start()
		}
		Thread.sleep(3000)
		System.out.println(threadTest.finalValues)
	}
//测试结果
101  102  100

引用类型原子类提供了一种读和写都是原子性的对象引用变量,在大多数情况下,我们需要处理的状态变量不止一个,那么我们可以把这些变量临时放在一个原子的引用里面。以AtomicReference为例,其中提供的主要方法有:

public final V getAndSet(V newValue);//获取值再设置值
public final void set(V newValue);//设置值
public final V get() ;//获取值
public final boolean compareAndSet(V expect, V update);//比较设置,设置成功返回true,否则返回false,这里使用的是CAS
...

简单例子如下:

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


   public static void main(String[] args) {

       User user = new User("conan", 15);

       atomicUserRef.set(user);

       User updateUser = new User("Shinichi", 17);

       boolean flag = atomicUserRef.compareAndSet(user, user);

       System.out.println(atomicUserRef.get().getName() + "  " + flag);

       System.out.println(atomicUserRef.get().getOld());

   }


   static class User {

       private String name;

       private int old;


       public User(String name, int old) {

           this.name = name;

           this.old = old;

       }


       public String getName() {

           return name;

       }


       public int getOld() {

           return old;

       }

   }


相比于使用锁机制来处理并发的问题,使用原子类能够在代码的可伸缩性以及活跃性上拥有非常的优势,缘由就是原子类由非阻塞算法实现,而锁机制由阻塞算法实现,非阻塞算法可以使得多个线程在竞争相同的数据的时候不会发生阻塞的情况,因而能在粒度更细的层次上进行协调,并且极大的减少调度的开销。原子类的内部实现使用了CAS的操作,关于CAS的介绍,放在下一章进行学习总结,这里就不介绍了。


参考资料

猜你喜欢

转载自my.oschina.net/u/3863980/blog/2878955