JUC包中常用工具类的简单介绍和使用说明

本文针对JUC包中的常用类进行一个简单整理,讲解为什么用、是什么和怎么用,但不会涉及背后的原理。我一贯认为,先使用有了直观感受之后再探究原理会事半功倍。本文涉及到的类如下:

  • 线程局部变量,ThreadLocal
  • 并发随机数生成器,ThreadLocalRandom
  • 原子操作类,AtomicInteger等
  • 常用的锁
    • 可重入的独占锁,ReentrantLock
    • 可重入的读写锁,ReentrantReadWriteLock
  • 同步器
    • 和锁配合实现wait/notify模式,Condition
    • 控制并发线程数,Semaphore
    • 等待多线程完成,CountDownLatch
    • 同步屏障,CyclicBarrier
  • 并发List
    • 线程安全的ArrayList,CopyOnWriteArrayList
  • 并发队列
    • 线程安全的无界非阻塞队列,ConcurrentLinkedQueue
    • 独占锁方式实现的阻塞队列,LinkedBlockingQueue
    • 有界数组方式实现的阻塞队列,ArrayBlockingQueue
    • 带优先级的无界阻塞队列,PriorityBlockingQueue
    • 无界阻塞延迟队列,DelayQueue
  • 并发Map
    • 哈希表实现,ConcurrentHashMap
    • 跳表实现,ConcurrentSkipListMap
  • 线程池
    • ThreadPoolExecutor
    • ScheduledThreadPoolExecutor

1. ThreadLocal

ThreadLocal的设计原理是:对于多个线程并发访问的共享变量,ThreadLocal可以提供线程本地变量,访问ThreadLocal变量的每个线程都会有这个变量的一个本地副本,当线程操作这个变量时,实则是操作的自己本地内存里的变量,从而在根本上解决了线程安全问题。

ThreadLocal的常用方法有get()、set()和remove(),在下面的代码中,使用两个线程分别访问提供一个ThreadLocal变量,从输出结果可知两个线程中的变量是独立存在的。

public class ThreadLocalTest {
    private static ThreadLocal local = new ThreadLocal<>();
    public static void main(String[] args) {
        Thread one = new Thread(new Runnable() {
            @Override
            public void run() {
                local.set("Thread One Variable");
                System.out.println("threadOne: " + local.get());
                local.remove();
                System.out.println("threadOne: " + local.get());
            }
        });

        Thread two = new Thread(new Runnable() {
            @Override
            public void run() {
                local.set("Thread Two Variable");
                System.out.println("threadTwo: " + local.get());
                local.remove();
                System.out.println("threadTwo: " +local.get());
            }
        });

        one.start();
        two.start();
    }
}

程序输出:

threadOne: Thread One Variable
threadOne: null
threadTwo: Thread Two Variable
threadTwo: null

Process finished with exit code 0

2. ThreadLocalRandom

Random类随机数的产生是用一个种子变量计算出的,在多线程环境下对这个种子变量的竞争会降低并发性能,因此,ThreadLocalRandom应运而生。顾名思义,ThreadLocalRandom的原理是:让每一个线程都拥有自己的种子变量,则每个线程更新随机数时都根据自己老的种子计算新的种子,并使用新的种子计算随机数,这样就不会存在竞争问题了,可以极大提高并发性能。

使用ThreadLocalRandom时在每个线程里用ThreadLocalRandom.current()获取ThreadLocalRandom实例并使用即可,在下面的代码中,使用ThreadLocalRandom和Random各生成一千万个整数,ThreadLocalRandom耗时30ms,Random耗时507ms,显然ThreadLocalRandom在多线程环境下效率更高。

import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadLocalRandom;

public class TheadLocalRandomTest {
    private static Random r = new Random();
    private static CountDownLatch randomLatch = new CountDownLatch(10);
    private static CountDownLatch randomLocalLatch = new CountDownLatch(10);
    public static void main(String[] args) throws InterruptedException {
        useRandom();
        useThreadLocalRandom();
    }

    private static void useThreadLocalRandom() throws InterruptedException {

        long start = System.currentTimeMillis();
        ThreadLocalRandomTestThread[] rs = new ThreadLocalRandomTestThread[10];
        for (int i = 0; i < rs.length; i++) {
            rs[i] = new ThreadLocalRandomTestThread();
            new Thread(rs[i]).start();
        }
        randomLocalLatch.await();
        long end = System.currentTimeMillis();
        System.out.println("Use ThreadLocalRandom to generate 10000000 integers : " + (end - start) + " ms" );
    }

    private static void useRandom() throws InterruptedException {

        long start = System.currentTimeMillis();
        RandomTestThread[] rs = new RandomTestThread[10];
        for (int i = 0; i < rs.length; i++) {
            rs[i] = new RandomTestThread();
            new Thread(rs[i]).start();
        }
        randomLatch.await();
        long end = System.currentTimeMillis();
        System.out.println("Use Random to generate 10000000 integers : " + (end - start) + " ms" );
    }

    static class ThreadLocalRandomTestThread implements Runnable {

        @Override
        public void run() {
            ThreadLocalRandom localRandom = ThreadLocalRandom.current();
            for (int i = 0; i < 1000000; i++) {
                localRandom.nextInt();
            }
            randomLocalLatch.countDown();
        }
    }

    static class RandomTestThread implements Runnable {

        @Override
        public void run() {
            for (int i = 0; i < 1000000; i++) {
                r.nextInt();
            }
            randomLatch.countDown();
        }
    }
}

程序输出:

Use Random to generate 10000000 integers : 507 ms
Use ThreadLocalRandom to generate 10000000 integers : 30 ms

Process finished with exit code 0

3. 原子操作类

当多个线程同时更新同一个变量时,可能会得到期望之外的值,例如变量i = 1,A线程更新i = i + 1,B线程更新i = i + 1,经过A和B操作完后结果可能不是期望的3,而是可能为2,因为A和B线程在更新变量i的时候拿到的i可能都是1。为了解决此类线程不安全的更新操作,我们通常会用synchronized关键字,但其属于重量级锁,性能较低,JUC包中的Atomic包的原子操作类提供了一种用法简单、性能高效又线程安全的变量更新方式。

对于基本类型的原子方式更新,Atomic包提供了三个类:AtomicBoolean、AtomicInteger、AtomicLong。它们的接口方法几乎一致,我们以AtomicInteger为例。

AtomicInteger的常用方法有addAndGet等,这些API望文生义即可,无需过多解释。在下面的代码中,使用1000个线程对一个整数分别累加1000次,期望结果应当是100000,这个整数分别用普通整型变量、synchronized加锁保护和AtomicInteger变量实现,最后我们观察累加结果和时间消耗。由下面结果可见,使用普通整型变量的结果必定会小于1000000,但时间消耗是最小的;使用synchronized加锁保护和AtomicInteger变量的方式其累加结果是正确的,且synchronized比AtomicInteger变量更耗时。

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerTest {
    private static Integer a = new Integer(0);
    private static Integer b = new Integer(0);
    private static AtomicInteger c = new AtomicInteger(0);

    private static final int THREAD_NUM = 1000;
    private static CountDownLatch latchOne = new CountDownLatch(THREAD_NUM);
    private static CountDownLatch latchTwo = new CountDownLatch(THREAD_NUM);
    private static CountDownLatch latchThree = new CountDownLatch(THREAD_NUM);

    public static void main(String[] args) throws InterruptedException {
        useThreadTwo();
        useThreadOne();
        useThreadThree();
    }

    private static void useThreadOne() throws InterruptedException {
        long start = System.currentTimeMillis();
        ThreadOne threadOne = new ThreadOne();
        for (int i = 0; i < THREAD_NUM; i++) {
            new Thread(threadOne).start();
        }
        latchOne.await();
        long end = System.currentTimeMillis();
        System.out.println("normal: a => time = " + (end - start) + " ms" + ", result = " + a);
    }

    static class ThreadOne implements Runnable {

        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                a++;
            }
            latchOne.countDown();
        }
    }

    private static void useThreadTwo() throws InterruptedException {
        long start = System.currentTimeMillis();
        ThreadTwo threadTwo = new ThreadTwo();
        for (int i = 0; i < THREAD_NUM; i++) {
            new Thread(threadTwo).start();
        }
        latchTwo.await();
        long end = System.currentTimeMillis();
        System.out.println("synchronized: b => time = " + (end - start) + " ms" + ", result = " + b);
    }

    static class ThreadTwo implements Runnable {

        @Override
        public void run() {
            synchronized (this) {
                for (int i = 0; i < 1000; i++) {
                    b++;
                }
                latchTwo.countDown();
            }
        }
    }

    private static void useThreadThree() throws InterruptedException {
        long start = System.currentTimeMillis();
        ThreadThree threadThree = new ThreadThree();
        for (int i = 0; i < THREAD_NUM; i++) {
            new Thread(threadThree).start();
        }
        latchThree.await();
        long end = System.currentTimeMillis();
        System.out.println("AtomicInteger: c => time = " + (end - start) + " ms" + ", result = " + c);
    }

    static class ThreadThree implements Runnable {

        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                c.incrementAndGet();
            }

            latchThree.countDown();
        }
    }

}

程序输出:

synchronized: b => time = 644 ms, result = 1000000
normal: a => time = 331 ms, result = 943204
AtomicInteger: c => time = 408 ms, result = 1000000

Process finished with exit code 0

4. 常用的锁

Java的synchronized关键字已经给我们提供了锁的功能,synchronized的特点是使用简单,一切交给JVM处理不需要显示释放锁;而Lock需要在finally里释放锁,否则会产生异常。Lock并不是对synchronized的替代,而是提供了更高级的功能,当你需要控制可重入、公平锁、可限时等功能。

4.1 ReentrantLock

ReentrantLock是可重入锁,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞。下面代码中,使用1000个线程对变量分别累加10,并通过ReentrantLock控制来确保输出正确的结果10000。

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.ReentrantLock;

public class LockTest {
    private static ReentrantLock lock = new ReentrantLock();
    private static int i = 0;
    private static final int THREAD_NUM = 1000;
    private static CountDownLatch cd = new CountDownLatch(THREAD_NUM);
    public static void main(String[] args) throws InterruptedException {
        for (int j = 0; j < THREAD_NUM; j++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    lock.lock();
                    try {
                        for (int k = 0; k < 10; k++) {
                            i++;
                        }
                    } finally {
                        lock.unlock();
                    }
                    cd.countDown();
                }
            }).start();
        }
        cd.await();
        System.out.println("i = " + i);
    }
}

程序输出:

i = 10000

Process finished with exit code 0

4.2 ReentrantReadWriteLock

ReentrantLock是独占锁,每时每刻只能有一个线程可以获取该锁,但实际场景中会有读多写少的场景,此时使用ReentrantLock就会影响性能,因为读线程和读线程之间不会引起线程安全问题,于是,ReentrantReadWriteLock应运而生,ReentrantReadWriteLock采取读写分离的策略,允许多个线程可以同时获取读锁。

通过ReentrantReadWriteLocks的实例对象可以获取ReadLock和WriteLock,在下面代码中,分别用ReentrantReadWriteLock和ReentrantLock实现了List,并各自启用1000个线程来执行查询操作,观察结果可发现ReentrantReadWriteLock的时间耗费远低于ReentrantLock。

import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReentrantReadWriteLockTest {
    private static ReentrantLockList lockList = new ReentrantLockList();
    private static ReentrantReadWriteLockList readWriteLockList = new ReentrantReadWriteLockList();
    private static final int THREAD_NUM = 1000;
    private static CountDownLatch lockDownLatch = new CountDownLatch(THREAD_NUM);
    private static CountDownLatch readWriteLockDownLatch = new CountDownLatch(THREAD_NUM);

    public static void main(String[] args) throws InterruptedException {
        lockList.add(0);
        readWriteLockList.add(0);
        useReentrantLockList();
        useReentrantReadWriteLockList();

    }

    private static void useReentrantReadWriteLockList() throws InterruptedException {
        long start = System.currentTimeMillis();
        for (int i = 0; i < THREAD_NUM; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    readWriteLockList.get(0);
                    readWriteLockDownLatch.countDown();
                }
            }).start();
        }
        readWriteLockDownLatch.await();
        long end = System.currentTimeMillis();
        System.out.println("ReentrantReadWriteLock => time = " + (end - start) + " ms");
    }

    private static void useReentrantLockList() throws InterruptedException {
        long start = System.currentTimeMillis();
        for (int i = 0; i < THREAD_NUM; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    lockList.get(0);
                    lockDownLatch.countDown();
                }
            }).start();
        }
        lockDownLatch.await();
        long end = System.currentTimeMillis();
        System.out.println("ReentrantLockList => time = " + (end - start) + " ms");
    }
}

class ReentrantLockList {
    private ArrayList array = new ArrayList<>();
    private ReentrantLock lock = new ReentrantLock();

    public void add(int e) {
        lock.lock();
        try {
            array.add(e);
        } finally {
            lock.unlock();
        }
    }

    public int get(int index) {
        lock.lock();
        try {
            Thread.sleep(10);
            return array.get(index);
        } catch (InterruptedException e) {
            e.printStackTrace();
            return 0;
        } finally {
            lock.unlock();
        }
    }
}

class ReentrantReadWriteLockList {
    private ArrayList array = new ArrayList<>();
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private Lock readLock = lock.readLock();
    private Lock writeLock = lock.writeLock();

    public void add(int e) {
        writeLock.lock();
        try {
            array.add(e);
        } finally {
            writeLock.unlock();
        }
    }

    public int get(int index) {
        readLock.lock();
        try {
            Thread.sleep(10);
            return array.get(index);
        } catch (InterruptedException e) {
            e.printStackTrace();
            return 0;
        } finally {
            readLock.unlock();
        }
    }
}

程序输出:

ReentrantLockList => time = 10951 ms
ReentrantReadWriteLock => time = 184 ms

Process finished with exit code 0

5. 同步器

5.1 Condition

使用Lock可以实现临界区的互斥访问,但想实现线程间的等待通知模式,还需要Condition的辅助。Condition对象由Lock对象创建,Condition对象定义了await和signal两种方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁。当调用await方法时,当前线程会释放锁并在此等待,而其它线程调用Condition的signal方法,当前线程才从await方法返回,并在返回前已经获取了锁。

获取一个Condition必须通过Lock的newCondition()方法。下面使用Lock和Condition实现生产者消费者模式:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionTest {
    private static final int SIZE = 10;
    private static List items = new ArrayList<>();
    private static ReentrantLock lock = new ReentrantLock();
    private static Condition notEmpty = lock.newCondition();
    private static Condition notFull = lock.newCondition();

    public static void main(String[] args) {
        Thread producer = new Thread(new Producer());
        Thread consumer = new Thread(new Consumer());
        producer.start();
        consumer.start();
    }

    static class Producer implements Runnable {

        private void produce() {
            lock.lock();
            try {
                if (items.size() == SIZE) {
                    System.out.println("Producer start await ...");
                    notFull.await();
                }
                items.add(ThreadLocalRandom.current().nextInt(100));
                notEmpty.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }

        @Override
        public void run() {
            while (true) {
                produce();
            }
        }
    }

    static class Consumer implements Runnable {

        private void consume() {
            lock.lock();
            try {
                if (items.size() == 0) {
                    System.out.println("Consumer start await ...");
                    notEmpty.await();
                }
                items.remove(0);
                notFull.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }

        @Override
        public void run() {
            while (true) {
                consume();
            }
        }
    }
}

5.2 Semaphore

Semaphore(信号量)是用来控制同时访问特定资源的线程数量,为多线程协作提供了更为强大的控制方法,是对锁的一种扩展。锁每一次只允许一个线程访问某一个资源,而信号量却可以指定多个线程同时访问某一个资源。

信号量在初始化时指定信号量的准入数,并通过acquire和release两个方法来控制线程的数量,示例见下面代码:


import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

public class SemaphoreTest implements Runnable{
    private Semaphore semaphore = new Semaphore(5);
    public static void main(String[] args) {
        ExecutorService exec = Executors.newFixedThreadPool(20);
        SemaphoreTest demo = new SemaphoreTest();
        for (int i = 0; i < 20; i++) {
            exec.submit(demo);
        }
        exec.shutdown();
    }

    @Override
    public void run() {
        try {
            semaphore.acquire();
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getId() + ":done!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            semaphore.release();
        }
    }
}

5.3 CountDownLatch

CountDownLatch适用于这样的场景:当需要在主线程中开启多个线程去并行执行任务,并且主线程需要等待所有子线程执行完毕后再进行汇总的场景。

CountDownLatch的常用方法为:初始化时设置计数器的大小,countDown方法使计数器减一,await方法使得当前线程在计数器不为0时等待。前文中的代码已多次使用过CountDownLatch,故不再赘述代码。

5.4 CyclicBarrier

CountDownLatch的缺点在于其计数器是一次性的,也就是当其计数器变为0后,再调用await和countDown方法都会立即返回,从而失去了线程同步的作用。CyclicBarrier提供了计数器可重置的功能,它的作用就是会让所有线程都等待完成后才会继续下一步行动。

CyclicBarrier的常用方法为:初始化时设置计数器大小,调用await方法时使得计数器减1并当计数器不为0时进行等待,当计数器为0时,放行所有线程并重置计数器,示例代码如下:

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierTest {
    private static final int NUM = 3;
    private static CyclicBarrier barrier = new CyclicBarrier(NUM);
    public static void main(String[] args) {
        for (int i = 0; i < NUM; i++) {
            new Thread(new Task()).start();
        }
    }

    static class Task implements Runnable {

        @Override
        public void run() {
            try {
                System.out.println(Thread.currentThread().getId() + " reach barrier A");
                barrier.await();
                System.out.println(Thread.currentThread().getId() + " leave barrier A");

                Thread.sleep(1000);

                System.out.println(Thread.currentThread().getId() + " reach barrier B");
                barrier.await();
                System.out.println(Thread.currentThread().getId() + " leave barrier B");

            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        }
    }
}

程序输出:

10 reach barrier A
11 reach barrier A
12 reach barrier A
12 leave barrier A
11 leave barrier A
10 leave barrier A
12 reach barrier B
11 reach barrier B
10 reach barrier B
10 leave barrier B
12 leave barrier B
11 leave barrier B

Process finished with exit code 0

6. 并发List

并发包中的并发List只有CopyOnWriteArrayList,顾名思义,当对其修改时,会使用写时复制策略,即对其进行的修改操作都是在底层一个复制的数组(快照)上进行的。

显然,写时复制策略会导致弱一致性问题,也就是说,对List的增删改是需要在一个新数组上执行的,而对List的查是在老数组上执行的。当返回List的迭代器进行遍历时,List的增删改都将对该迭代器不可见,这就是弱一致性问题。

下面这段代码中,在主线程中对CopyOnWriteArrayList添加了五个数,并启用了一个新线程删除了CopyOnWriteArrayList的三个数,但最后的结果输出仍然是五个数,这就说明了弱一致性问题。

package juc;

import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;

public class CopyOnWriteArrayListTest {
    public static void main(String[] args) {
        CopyOnWriteArrayList list = new CopyOnWriteArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);

        new Thread(new Runnable() {
            @Override
            public void run() {
                list.remove(0);
                list.remove(1);
                list.remove(2);
            }
        }).start();

        Iterator iterator = list.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
}

程序输出:

1
2
3
4
5

Process finished with exit code 0

7. 并发队列

总的来说,并发队列可分为阻塞队列和非阻塞队列,前者使用锁实现,后者使用CAS非阻塞算法实现;也可以分为有界和无界,前者使用数组实现,后者使用链表实现。

由于并发队列使用方法和普通队列一样,如无需要验证的特殊性质,将不再提供代码。

7.1 ConcurrentLinkedQueue

无界非阻塞队列,底层数据结构使用单向链表实现,对于入队和出队操作使用CAS来实现线程安全。由于使用非阻塞CAS算法,没有加锁,所以在计算size时有可能进行了offer、poll等操作,导致计算的元素个数不精确,所以在并发情况下size函数不是很有用。

7.2 LinkedBlockingQueue

无界阻塞队列,底层数据结构使用单向链表实现,对于入队和出队操作使用独占锁来实现线程安全。

7.3 ArrayBlockingQueue

有界阻塞队列,底层数据结构使用数组实现,对于入队和出队操作使用独占锁来实现线程安全。

7.4 PriorityBlockingQueue

无界阻塞队列,带优先级,底层使用堆实现,但数组可扩容,对于入队和出队操作使用独占锁来实现线程安全。

7.5 DelayQueue

无界阻塞队列,元素带过期时间,队列头元素是最快要过期的元素,当从队列获取元素时,只有过期元素才会出队列。其内部使用PriorityQueue存放数据,使用ReentrantLock实现线程同步。

8. 并发Map

8.1 ConcurrentHashMap

HashMap不是并发安全的,在多线程环境下容易造成死循环问题;并发安全的HashTable又是效率低下的,因此,ConcurrentHashMap应运而生。ConcurrentHashMap采用锁分段技术,在保证安全的情况下也提高了效率。

参考文献

【1】《Java并发编程之美》翟陆续等著,电子工业出版社。

【2】《Java并发编程的艺术》方腾飞等著,机械工业出版社。

【3】《实战Java高并发程序设计(第2版)》葛一鸣著,电子工业出版社。


每日学习笔记,写于2020-04-18 星期六

你可能感兴趣的:(JUC包中常用工具类的简单介绍和使用说明)