JUC中的同步工具类

0.主题

       在JDK1.5之前,我们只能通过Object的wait/notify/notifyAll来进行线程之间的协作,阻塞当前线程和唤醒阻塞在该Object方法上的线程。不过在JDK 1.5之后,JUC包中推出了很多工具类,来方便我们完成线程之间的协作,方便更加高效的低错误率的实现一些功能。这里介绍三个JUC中的工具类,CountDownLatch/Semaphore/Barrier。

  • CountDownLatch
  • Semaphore
  • Barrier

1.CountDownLatch

CountDownLatch,又称闭锁,这个是我个人最喜欢用的一个工具类。摘录《JAVA并发编程实战》中的一句话:

闭锁的作用相当于一扇门:在闭锁到达结束状态之前,这扇门一直是关闭的,并且没有任何线程能通过,当到达结束状态时,这扇门会打开并允许所有的线程通过。

首先我们来看一下他的构造方法,构造方法必须有一个int类型传参,并且不能小于0。

/**
 * Constructs a {@code CountDownLatch} initialized with the given count.
 *
 * @param count the number of times {@link #countDown} must be invoked
 *        before threads can pass through {@link #await}
 * @throws IllegalArgumentException if {@code count} is negative
 */
public CountDownLatch(int count) {
	if (count < 0) throw new IllegalArgumentException("count < 0");
	this.sync = new Sync(count);
}

再看一下它的方法,它主要有两个方法,一个叫做await()和countDown()。

public void await() throws InterruptedException;
public boolean await(long timeout, TimeUnit unit) throws InterruptedException;
public void countDown();

CountDownLatch的使用方法:初始化时会有一个count,调用await()方法时,如果count不为0,则会使线程阻塞。调用一次countDown()方法就会使count值减1,如果count减小到0就会开启大门,让所有在await()方法上阻塞的线程通过。

CountDownLatch的使用举例:如果两个线程分别执行A和B两个操作如果我们希望A执行完成之后过10秒再执行B,那么我们就可以使用CountDownLatch来实现,如下。

public class CountDownLatchDemo {


    public static void main(String[] args) throws IOException {
        CountDownLatch latch = new CountDownLatch(1);

        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("action A");
                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                latch.countDown();
            }
        });

        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    latch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("action B");
            }
        });

        threadA.start();
        threadB.start();

        //输入任意按键退出
        System.in.read();

    }
}

2.Semaphore

Semaphore,又称信号量,注重于资源池的控制,用于控制同一时间进行访问某个资源的数量。

同样的,我们来看下其构造方法:

/**
 * Creates a {@code Semaphore} with the given number of
 * permits and nonfair fairness setting.
 *
 * @param permits the initial number of permits available.
 *        This value may be negative, in which case releases
 *        must occur before any acquires will be granted.
 */
public Semaphore(int permits) {
	sync = new NonfairSync(permits);
}

/**
 * Creates a {@code Semaphore} with the given number of
 * permits and the given fairness setting.
 *
 * @param permits the initial number of permits available.
 *        This value may be negative, in which case releases
 *        must occur before any acquires will be granted.
 * @param fair {@code true} if this semaphore will guarantee
 *        first-in first-out granting of permits under contention,
 *        else {@code false}
 */
public Semaphore(int permits, boolean fair) {
	sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}

他有两个构造方法,相同的是两个都需要提供一个int值的permits也就是我们的资源池大小(访问资源的许可证个数);不同的是第二个构造方法多了一个boolean类型的参数fair,顾名思义fair代表是否是公平的,如果是公平的Semaphore,先阻塞的线程一定会先获取到许可证,否则,没有顺序保证,默认情况下是非公平的(和ReentrantLock一样,保证公平性必将损失部分性能)。我们再来看下方法,主要的就下面这些了。

public void acquire() throws InterruptedException {
	sync.acquireSharedInterruptibly(1);
}

public void acquireUninterruptibly() {
	sync.acquireShared(1);
}

public boolean tryAcquire() {
	return sync.nonfairTryAcquireShared(1) >= 0;
}

public boolean tryAcquire(long timeout, TimeUnit unit)
	throws InterruptedException {
	return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

public void release() {
	sync.releaseShared(1);
}

public void acquire(int permits) throws InterruptedException {
	if (permits < 0) throw new IllegalArgumentException();
	sync.acquireSharedInterruptibly(permits);
}

public void acquireUninterruptibly(int permits) {
	if (permits < 0) throw new IllegalArgumentException();
	sync.acquireShared(permits);
}

public boolean tryAcquire(int permits) {
	if (permits < 0) throw new IllegalArgumentException();
	return sync.nonfairTryAcquireShared(permits) >= 0;
}

public boolean tryAcquire(int permits, long timeout, TimeUnit unit)
	throws InterruptedException {
	if (permits < 0) throw new IllegalArgumentException();
	return sync.tryAcquireSharedNanos(permits, unit.toNanos(timeout));
}

public void release(int permits) {
	if (permits < 0) throw new IllegalArgumentException();
	sync.releaseShared(permits);
}

上面5个和下面5个其实是差不多的,上面5个是获取一个许可证,而下面5个是获取N个许可证,这里我们简单介绍下上面5个方法。acquire()和acquireUninterruptibly()都是获取许可证,如果没有则阻塞等待,唯一的区别就是第一个是可中断的,而第二个方法是不可中断的。tryAcquire()表示尝试获取许可证,有无参数的区别就是无参数的会立刻返回是否有许可证,而有参数的在没有许可证的情况下会阻塞当前线程直到有许可证或者时间到达,再返回是否有许可证。release()表示释放一个许可证。

Semaphore的使用方法:初始化时传入许可证数量,acquire()获取许可证,如果没有则阻塞当前线程等待直到有许可证或者被中断抛出InterruptedException ,release()释放一个许可证,同时就会唤醒一个acquire()阻塞的线程。

Semaphore的使用举例:我们可以使用Semaphore来实现一个简单的线程池。

public class SemaphoreDataSource implements DataSource {

    private Semaphore semaphore;

    private LinkedList<Connection> dataSourcePool;


    public SemaphoreDataSource(int count){
        semaphore = new Semaphore(count);
        //省略了数据库连接的初始化过程
        dataSourcePool = new LinkedList<>();
    }

    @Override
    public Connection getConnection() throws SQLException {
        try {
            semaphore.acquire();
            return dataSourcePool.pop();
        } catch (InterruptedException e) {
            e.printStackTrace();
            return null;
        }
    }

    public void release(Connection connection){
        dataSourcePool.push(connection);
        semaphore.release();
    }
......
}

3.Barrier

Barrier,又称栅栏。同样引用《JAVA并发编程实战》中的一段话。

栅栏类似于闭锁,它能阻塞一组线程直到某个事件发生。栅栏与闭锁的关键区别在于,所有的线程必须同时到达栅栏的位置,才能继续执行。闭锁用于等待事件,而栅栏用于等待其他线程。

栅栏有多种,这里我们只介绍CyclicBarrier,一种可以循环使用的栅栏。我们来看其构造函数:

/**
 * Creates a new {@code CyclicBarrier} that will trip when the
 * given number of parties (threads) are waiting upon it, and which
 * will execute the given barrier action when the barrier is tripped,
 * performed by the last thread entering the barrier.
 *
 * @param parties the number of threads that must invoke {@link #await}
 *        before the barrier is tripped
 * @param barrierAction the command to execute when the barrier is
 *        tripped, or {@code null} if there is no action
 * @throws IllegalArgumentException if {@code parties} is less than 1
 */
public CyclicBarrier(int parties, Runnable barrierAction) {
	if (parties <= 0) throw new IllegalArgumentException();
	this.parties = parties;
	this.count = parties;
	this.barrierCommand = barrierAction;
}

/**
 * Creates a new {@code CyclicBarrier} that will trip when the
 * given number of parties (threads) are waiting upon it, and
 * does not perform a predefined action when the barrier is tripped.
 *
 * @param parties the number of threads that must invoke {@link #await}
 *        before the barrier is tripped
 * @throws IllegalArgumentException if {@code parties} is less than 1
 */
public CyclicBarrier(int parties) {
	this(parties, null);
}

两个构造函数都有一个参数parties,代表栅栏前有多少线程,就会开启(不能小于1)。上面一个构造函数还有一个Runnable参数表示我们可以设置一个行为,这个行为会在栅栏开启的时候执行。我们再看其方法:

public int await() throws InterruptedException, BrokenBarrierException {
	try {
		return dowait(false, 0L);
	} catch (TimeoutException toe) {
		throw new Error(toe); // cannot happen
	}
}

public int await(long timeout, TimeUnit unit)
	throws InterruptedException,
		   BrokenBarrierException,
		   TimeoutException {
	return dowait(true, unit.toNanos(timeout));
}

public void reset() {
	final ReentrantLock lock = this.lock;
	lock.lock();
	try {
		breakBarrier();   // break the current generation
		nextGeneration(); // start a new generation
	} finally {
		lock.unlock();
	}
}

其方法相对闭锁和信号量少了类似countDown()和release()的方法。主要是两个await方法,无参的就是直接阻塞等待,直到栅栏开启,或者被中断抛出InterruptedException,或者发生了BrokenBarrierException 异常。带有参数的await方法比无参的方法多了一个,当时间到了栅栏还没有开启,那么就会抛出TimeoutException。

BrokenBarrierException 发生的场景:

1.如果在线程处于等待状态时barrier被reset()或者在调用await()时 barrier 被损坏,将抛出 BrokenBarrierException 异常。

2.如果任何线程在等待时被中断,则其他所有等待线程都将抛出 BrokenBarrierException 异常。

CyclicBarrier的使用方法:初始化时传入等待线程数量parties,调用await()方法,阻塞线程,当被阻塞的线程数等于parties时,栅栏开启,线程全部不再阻塞。

CyclicBarrier的使用示例:我们在并发测试的时候,如果我们需要测试一个方法,在并发的场景下是否还能正确的被执行,那么我们可以使用栅栏,而不是叫上一个同时两个人喊321一起点。示例是测试SimpleDateFormat在并发下的情况示例代码:

public class CyclicBarrierDemo {

    private static SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");


    public static void main(String[] args) throws IOException {

        CyclicBarrier barrier = new CyclicBarrier(10);

        for (int i = 0; i < 10; i++) {

            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        barrier.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                    try {
                        Date parse = format.parse("2018-01-01 01:01:01");
                        System.out.println(parse);
                    } catch (ParseException e) {
                        e.printStackTrace();
                    }

                }
            }).start();

        }

        System.in.read();

    }

}

结果肯定是报错的,原因读者自行百度。

最后,感谢您的阅读!

本文内容参考《JAVA并发编程实战》,如有侵权,请联系删除。

猜你喜欢

转载自blog.csdn.net/xt8469/article/details/81430393