Java 常用的并发工具类介绍

Java 官方提供了一些比较实用的并发工具类,能够使我们很轻松的驾驭多线程,不用再担心线程安全问题。在工作中巧妙使用这些并发工具类,能够达到事半功倍的效果。下面我们就一起看看这些并发工具类吧。


一、Hashtable 和 ConcurrentHashMap

在 Map 类型的集合中,我们最常用的是 HashMap ,但是 HashMap 并不是线程安全的。为了确保线程安全,我们可以使用 Hashtable,但是 Hashtable 的性能效率相对比较低,主要原因是 Hashtable 是通过整表加锁来确保线程安全。为了在确保线程安全的前提下,同时兼顾性能效率,Java 在 1.5 以后提供了新的并发工具类 ConcurrentHashMap,其使用方法跟 HashMap 一样,非常简单。

代码演示:

import java.util.HashMap;
import java.util.Hashtable;
import java.util.concurrent.ConcurrentHashMap;

public class MyHashtableDemo {
    public static void main(String[] args) throws InterruptedException {
        /*
        这里分别使用 HashMap,Hashtable,ConcurrentHashMap 进行测试
        开启两个线程,向同一个 Map 集合中,添加 10000 条数据(key 不存在就新增,key 存在就更新)。
        最后打印出 Map 集合中的数据条数。
        */

        //HashMap<Integer, String> hm = new HashMap<>();
        //Hashtable<Integer, String> hm = new Hashtable<>();
        ConcurrentHashMap<Integer, String> hm = new ConcurrentHashMap<>();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                hm.put(i , i + "--线程1");
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                hm.put(i , i + "--线程2");
            }
        });

        t1.start();
        t2.start();

        //休眠 2 秒钟,确保两个线程都能够运行完毕。
        Thread.sleep(2000);

        /*
        从打印出的 hm 中数据条数可以发现:
        当 hm 是 HashMap 时,每次打印的条数不相同,这说明 HashMap 不是线程安全的。
        当 hm 是 Hashtable 和 ConcurrentHashMap 时,每次打印的条数都是 10000 ,符合预期。
        */
        System.out.println(hm.size());
    }
}

分别使用 HashMap,Hashtable,ConcurrentHashMap 来执行上面的代码,我们会发现 HashMap 每次打印出的数据条数具有随机性,这说明 HashMap 不是线程安全的。Hashtable 和 ConcurrentHashMap 每次打印出的数据条数都是 10000 ,符合预期,这说明它们是线程安全的。

有关 Hashtable 和 ConcurrentHashMap 的性能对比,以及它们的底层实现原理,网上资料也很多,限于篇幅,这里就不演示和介绍了。结论就是 ConcurrentHashMap 总体性能效率要比 Hashtable 高,大家在工作中有需要的情况下,使用 ConcurrentHashMap 就对了。


二、CountDownLatch

CoutDownLatch 的使用场景是:让一个线程等待其它线程执行完毕后再执行。其主要方法如下:

方法 说明
public CountDownLatch(int count) 构造方法中传递要等待的线程数量
public void await() 让当前线程等待
public void countDown() 要等待的目标线程执行完毕后,调用此方法

为了更形象的介绍 CoutDownLatch 的使用,我们假设一个案例场景:
一个家庭里面有 3 个孩子,妈妈做好了热腾腾的饺子给孩子们吃,等 3 个孩子都吃完饺子后,妈妈再进行打扫卫生,收拾碗筷,洗碗擦桌子。代码实现如下:

import java.util.concurrent.CountDownLatch;

//小孩线程
public class ChileThread extends Thread {

    private CountDownLatch countDownLatch;
    public ChileThread(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void run() {
        //每个小孩都吃 15 个饺子
        for (int i = 1; i <= 15; i++) {
            System.out.println(getName() + " 吃完了第 " + i + " 个饺子");
        }

        //吃完以后,告诉妈妈一声(每次执行 countDown 方法,就让其内部计数器减 1)
        countDownLatch.countDown();
    }
}

//妈妈线程
public class MotherThread extends Thread {
    private CountDownLatch countDownLatch;
    public MotherThread(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void run() {
        try {
            //让妈妈等待,等所有孩子吃完饺子后,自动唤醒。
            //当 countDownLatch 内部计数器变成 0 的时候,会自动唤醒这里等待的线程。
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("妈妈打扫卫生,收拾碗筷,洗碗擦桌子");
    }
}

//代码演示 CountDownLatch 的使用
public class MyCountDownLatchDemo {
    public static void main(String[] args) {
        //由于妈妈要等待 3 个小孩吃完饭,也就是要等待 3 个线程
        //所以这里创建 CountDownLatch 对象时,构造函数传入 3
        CountDownLatch countDownLatch = new CountDownLatch(3);

        //创建妈妈线程,传入 countDownLatch
        MotherThread motherThread = new MotherThread(countDownLatch);
        motherThread.start();

        //创建第 1 个小孩线程,传入 countDownLatch
        ChileThread t1 = new ChileThread(countDownLatch);
        t1.setName("小孩01");

        //创建第 2 个小孩线程,传入 countDownLatch
        ChileThread t2 = new ChileThread(countDownLatch);
        t2.setName("小孩02");

        //创建第 3 个小孩线程,传入 countDownLatch
        ChileThread t3 = new ChileThread(countDownLatch);
        t3.setName("小孩03");

        t1.start();
        t2.start();
        t3.start();
    }
}

/*
最后运行的结果就是:
妈妈线程刚开始会进行等待,当 3 个小孩的线程都执行完毕后,妈妈线程才会执行。
*/

三、Semaphore

Semaphore 的使用场景就是:控制并发执行的线程数量。其主要方法如下:

方法 说明
public void acquire() 获取许可,如果没有获取到,则进行线程阻塞,直到获取到为止
public void release() 释放许可,返还给 Semaphore 中

为了更形象的介绍 Semaphore 的使用,我们假设一个案例场景:
一群女生排队上卫生间,卫生间只有 3 个坑位,卫生间外面有个大妈进行协调管理,每次最多只有 3 个女生获得许可进入卫生间,等卫生间里面的某个或某些女生出来后,大妈再安排相应数量的其它女生进入卫生间。代码实现如下:

import java.util.concurrent.Semaphore;

public class MyRunnable implements Runnable {
    //卫生间管理员大妈,管理卫生间 3 个坑位
    private Semaphore semaphore = new Semaphore(3);

    @Override
    public void run() {
        try {
            //获得进入卫生间的许可
            semaphore.acquire();
            System.out.println(Thread.currentThread().getName() + " 进入了卫生间");

            Thread.sleep(2000); //两秒钟解决完内急,速度还是比较快的

            System.out.println(Thread.currentThread().getName() + " 离开了卫生间");
            //离开卫生间后,归还许可
            semaphore.release();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class MySemaphoreDemo {
    public static void main(String[] args) {
        //创建一个卫生间,所有女生都排队上这一个卫生间
        MyRunnable mr = new MyRunnable();

        for (int i = 0; i < 100; i++) {
            //创建出 100 个女生,排队上卫生间
            new Thread(mr).start();
        }
    }
}

就先介绍到这里吧,希望本篇博客的内容,能够对大家有用。



上一篇:168. Excel表列名称


下一篇:美团高级工程师面试168题汇总:并发+JVM+框架+分布式+数据库