读书笔记: Java并发编程实战(2)

本文深入探讨线程安全容器,如ConcurrentHashMap和CopyOnWriteArrayList,及其在高并发环境下的优势。同时,介绍了同步工具类,包括闭锁CountDownLatch、FutureTask、信号量Semaphore和循环栅栏CyclicBarrier,用于协调多线程操作,提高程序的并发性和效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

2.1 线程安全容器类

线程安全容器有同步容器类和并发容器类。

同步容器有早期的Vector和HashTable,以及通过Collections.synchronizedXxx等工厂方法创建的容器。这些类实现线程安全的方式是:将它们的状态封装起来,并对每个公有方法都进行同步,使得每次只有一个线程能访问容器的状态。

同步容器类的问题

同步容器将所有对容器状态的访问都串行化,以实现线程安全性,但这种方法的代价是严重降低并发性,当多个线程竞争容器的锁时,吞吐量将严重降低。

并发容器

并发容器是Java5.0新增的,用来改进同步容器性能的。

ConcurrentHashMap用来替代同步Map;CopyOnWriteArrayList用于在遍历操作为主要操作的情况下代替同步的List。另外还增加了两种新的容器类型:Queue和BlockingQueue。

Queue是非阻塞的队列,如果队列元素为空,则获取元素的操作会返回空值。ConcurrentLinkedQueue是queue的一个实现。BlockingQueue扩展了Queue,增加了可阻塞的插入和获取等操作。

关于并发容器,之前的blog中有过简单介绍——线程安全对象简介

需要注意的是,并发容器的线程安全性指的容器类的方法具有线程安全性,但在使用并发容器类并涉及到复合操作时仍然是线程不安全的

举个例子,HashMap的put操作是线程不安全的,因为put操作包含了很多底层逻辑,如计算散列值,判断散列位置上是否已经存在元素,是否需要扩容,是否插入新元素等等。多线程环境下put时这些底层逻辑可能会穿插进行,造成线程不安全。而并发HashMap的put操作则是一个原子操作,从而是线程安全的。但在复合操作时,如读取某个key对应的value,然后对value进行更改并写入。这个操作就不具有原子性了,在多线程情况下就是线程不安全的。

2.2 同步工具类

2.2.1 闭锁

闭锁是一种同步工具类,可以延迟线程的进度直到其到达终止状态,用来确保某些活动直到其他活动都完成后才继续执行。

CountDownLatch

CountDownLatch是一种灵活的闭锁实现,它可以使一个或多个线程等待一组事件发生。闭锁状态包括一个计数器,被初始化为一个正数,表示需要等待的事件数量。每发生一个事件就调用一次countDown方法来递减计数器。主线程调用await方法进行阻塞等待,直到计数器为0或等待中的线程中断或等待超时。

对于倒计时器,一种典型的场景就是火箭发射。在发射之前需要做各项设备、仪器的检查,只有当所有检查完毕后,引擎才能点火。使用CountDownLatch模拟这个场景如下所示:

public static void main(String[] args) throws Exception {
    CountDownLatch countDownLatch = new CountDownLatch(10);

    ExecutorService executorService = Executors.newFixedThreadPool(10);
    for (int i = 0; i < 10; i++) {
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(new Random().nextInt(10) * 1000L); // 模拟检查任务
                    System.out.println(Thread.currentThread().getName() + " check complete");
                    countDownLatch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }
    countDownLatch.await();
    System.out.println("Fire");
}

2.2.2 FutureTask

FutureTask也可以用做闭锁。
FutureTask是Future的实现类,实现了Future的语义,表示一种抽象的可生成结果的计算。

在线程任务中,Runnable接口实例可以实现无返回的线程任务,而Callable接口实例可以实现有返回值的线程任务,其返回值就放在Future对象中。直接看demo:

public static void main(String[] args) throws Exception {
        // 通过5个线程执行任务,并输出最终结果
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        List<Future<String>> futureList = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            Future<String> future = executorService.submit(new Callable<String>() {

                @Override
                public String call() throws Exception {
                    Thread.sleep(2000);
                    return Thread.currentThread().getName();
                }
            });
            futureList.add(future);
        }

//        for(int i = 0; i < futureList.size();) {
//            if(futureList.get(i).isDone()) {
//                // 如果执行完成,就删除当前元素,后续元素会往前移;如果未执行完成,当前元素保持不变继续等待循环处理
//                System.out.println(futureList.get(i).get());
//                futureList.remove(futureList.get(i));
//            }
//        }

        boolean flag = true;
        while(flag) {
            Iterator<Future<String>> iterator = futureList.iterator();
            while(iterator.hasNext()) {
                Future<String> future = iterator.next();
                if(future.isDone()) {
                    System.out.println(future.get());
                    iterator.remove();
                }
            }
            if (futureList.isEmpty()) {
                flag = false;
            }
        }
		executorService.shutdown();
}

这里使用遍历的方式解决多任务结果,但是不是最优的效果,FutureTask正是为此而存在,它有一个回调函数protected void done(),当任务结束时,该回调函数会被触发,在该函数中直接调用get方法即可获取到执行结果。因此,只需重载该函数,即可实现在线程刚结束时就做一些事情,demo如下:

static class MyFutureTask extends FutureTask<String> {

    public MyFutureTask(Callable<String> callable) {
        super(callable);
    }

    @Override
    protected void done() {
        System.out.println(get() + "执行完毕!");
    }
}

public static void main(String[] args) throws Exception {
    // 通过5个线程执行任务,并输出最终结果
    ExecutorService executorService = Executors.newFixedThreadPool(5);
    for (int i = 0; i < 5; i++) {
        MyFutureTask myFutureTask = new MyFutureTask(new Callable<String>() {
            @Override
            public String call() throws Exception {
                Thread.sleep(2000);
                return Thread.currentThread().getName();
            }
        });
        executorService.submit(myFutureTask);
    }
    executorService.shutdown();
}

2.2.3 信号量

计数信号量用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量。Semaphore本质是一个有计数功能的共享锁。

Semaphore维护了一个信号量许可集。线程可以通过acquire()来获取信号量的许可并执行操作,当没有许可时,acquire方法将阻塞等待直到有许可(或被中断或操作超时)为止。release操作将创建一个许可。

许可与线程没有关联关系,因此在一个线程中获得的许可可以在另一个线程中释放。可以将acquire操作视为消费一个许可,而release操作可理解为释放一个许可,但实际上是创建一个许可(也就是说许可数是可以增加的,并不受限于Semaphore创建时的初始许可数量)。

Semaphore可用于实现资源池,如构造一个固定长度的资源池,当池为空时请求资源将会阻塞,直到资源池不为空。同样,Semaphore也可以将任何一种容器变成有界阻塞容器,通过信号量许可数指定容器界限,添加元素时获取许可,移除元素时释放许可。demo如下:

public class BoundHashSet<T> {
    private final Set<T> set;
    private final Semaphore semaphore;

    public BoundHashSet(int capacity) {
        this.set = Collections.synchronizedSet(new HashSet<>());
        this.semaphore = new Semaphore(capacity);
    }

    public boolean add(T o) throws InterruptedException {
        semaphore.acquire();
        boolean wasAdded = false;
        try {
            wasAdded = set.add(o);
            return wasAdded;
        } finally {
            if (!wasAdded) {
                semaphore.release();
            }
        }
    }

    public boolean remove(Object o) {
        boolean wasRemoved = set.remove(o);
        if (wasRemoved) {
            semaphore.release();
        }
        return wasRemoved;
    }
}

2.2.4 CyclicBarrier

循环栅栏类似于倒计时器CountDownLatch,用于阻塞一组线程直到某个事件发生。但相比于倒计时器,循环栅栏的功能更为强大,也更复杂。

众所周知,在玩游戏时需要先加载资源,然后才能进入游戏。假设现在有10个人玩一个多人对战游戏,其流程如demo所示:

public class CyclicBarrierDemo {

    static class Task implements Runnable {

        @Override
        public void run() {
            try {
                Thread.sleep(new Random().nextInt(1000));
                System.out.println(Thread.currentThread().getName() + "加载完资源");
                Thread.sleep(new Random().nextInt(1000) * 2);
                System.out.println(Thread.currentThread().getName() + "进入了游戏");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 10; i++) {
            Runnable task = new Task();
            Thread t = new Thread(task);
            t.start();
        }
    }
}

结果如下:
在这里插入图片描述

可以看到,部分人很快就加载完并进入了游戏,另一些人则加载得比较慢,进入游戏时间晚。
玩过游戏的人都知道,实际是不可能这样的。无论某个人加载资源多快,他都得等所有人全部加载完才能进入游戏。

通过循环栅栏可以实现这种需求:

public class CyclicBarrierDemo {

    static class Task implements Runnable {

        CyclicBarrier cyclicBarrier = null;

        public Task(CyclicBarrier cyclicBarrier) {
            this.cyclicBarrier = cyclicBarrier;
        }

        @Override
        public void run() {
            try {
                Thread.sleep(new Random().nextInt(1000) * 2);
                System.out.println(Thread.currentThread().getName() + "加载完资源");
                cyclicBarrier.await();
                Thread.sleep(new Random().nextInt(1000));
                System.out.println(Thread.currentThread().getName() + "进入了游戏");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws Exception {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(10);
        for (int i = 0; i < 10; i++) {
            Runnable task = new Task(cyclicBarrier);
            Thread t = new Thread(task);
            t.start();
        }
    }
}

运行结果如下:
在这里插入图片描述

需要注意的是,CountDownLatch倒计时器是独立于线程任务的,作为一个计数器工具使用。而CyclicBarrier循环栅栏是线程任务的一部分,作为任务对象的属性使用。

闭锁CountDownLatch是一次性对象,一旦进入终止状态就不能被重置,而循环栅栏是可以循环使用的。闭锁用于等待一组相关的操作结束,而循环栅栏用于等待线程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值