JUC基本使用——并发集合和同步工具类

╄→尐↘猪︶ㄣ 提交于 2019-11-27 16:21:33

并发集合

普通的集合中有List(ArrayList|LinkedList)、Set(HashSet|TreeSet)和Map(HashMap|TreeMap)集合,这些集合只适合在单线程情况下使用。
在Collecionts工具类中有synchronized开头方法可以把单线程集合转成支持并发的集合,但是效率不高,很少使用。

为了更好的实现集合的高并发访问处理,JUC创建了一组新的集合工具类,其使用和一般的集合一样,使用方法参照 “Java之集合” 的分类。

1. List和Set集合

  • CopyOnWriteArrayList相当于线程安全的ArrayList,实现了List接口,支持高并发
  • CopyOnWriteArraySet相当于线程安全的HashSet,它继承了AbstractSet类,内部是通过CopyOnWriteArrayList实现的,所以是有序的 set 集合

2.Map集合

  • ConcurrentHashMap是线程安全的哈希表(相当于线程安全的HashMap);它继承于AbstractMap类,并且实现ConcurrentMap接口。
    该集合在jdk1.7之前是采用的分段锁机制,将哈希表分为16段,每段是一个锁,每个段又都是哈希表,写入扩容锁定,读取共享,类似于读写锁机制。
    在jdk1.8之后,取消分段锁,改用CAS无锁算法,提高写入小路,另外put方法有锁,但是get方法没锁,使读取速度与普通HashMap一致。
    如果哈希表位置已经有链表,则默认将链表的表头采用synchronized锁住,没有则直接通过算法添加元素。
  • ConcurrentSkipListMap是线程安全的有序的哈希表(相当于线程安全的TreeMap);它继承于AbstactMap类,并且实现ConcurrentNavigableMap接口。它是通过“跳表”来实现的,它支持并发;
  • ConcurrentSkipListSet是线程安全的有序的集合(相当于线程安全的TreeSet);它继承于AbstractSet,并实现了NavigableSet接口,是通过ConcurrentSkipListMap实现的,它也支持并发。

3.Queue队列

  • ArrayBlockingQueue是数组实现的线程安全的有界的阻塞队列
  • LinkedBlockingQueue是单向链表实现的(指定大小)阻塞队列,该队列按FIFO(先进先出)排序元素
  • LinkedBlockingDeque是双向链表实现的(指定大小)双向并发阻塞队列,该阻塞队列同时支持FIFO和FILO两种操作方式;
  • ConcurrentLinkedQueue是单向链表实现的无界队列,该队列按FIFO(先进先出)排序元素。
  • ConcurrentLinkedDeque是双向链表实现的无界队列,该队列同时支持FIFO和FILO两种操作方式。

4.使用并发实现生产者和消费者模式

并发集合本身具有高并发特性,且ArrayBlockingQueue队列满足先进先出的规则,不需要wait和notify机制,使用很方便,但是要保证双方必须使用同一个集合。

//生产者类
public class Producer extends Thread{
   private ArrayBlockingQueue<String> abq;

    public Producer(ArrayBlockingQueue<String> abq) {
        this.abq = abq;
    }

    @Override
    public void run() {
        for (int i = 0; i < 30; i++) {
            try {
                abq.put(Thread.currentThread().getName()+"生产"+(i+1)+"号产品");
                System.out.println(Thread.currentThread().getName()+"生产"+(i+1)+"号产品");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

//消费者类
public class Consumer extends Thread{
    private ArrayBlockingQueue<String> abq;

    public Consumer(ArrayBlockingQueue<String> abq) {
        this.abq = abq;
    }

    @Override
    public void run() {
        for (int i = 0; i < 30; i++) {
            try {
                String take = abq.take();
                System.out.println(Thread.currentThread().getName()+"消费了"+take);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

//main方法
public class Test {
    public static void main(String[] args) {
        //生产者生产满5停止,即队列满了就停止,然后消费者消费,队列特点就是先进先出,取出并删除元素
        ArrayBlockingQueue<String> arr = new ArrayBlockingQueue<>(5);

        new Producer(arr).start();
        new Consumer(arr).start();
    }
}

同步工具类

1.CountDownLatch类

CountDownLatch(闭锁)是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。

闭锁可以延迟线程的进度直到其到达终止状态,可以用来确保某些活动直到其他活动都完成才能继续执行:
(1)确保某个计算在其需要的所有资源都被初始化后才能继续执行。
(2)确保某个服务在其依赖的所有其他服务都已经启动之后才启动。
(3)等待直到某个操作所有参与者都执行完毕其他线程才能继续执行。
有点类似于join()方法,只不过是让更多的线程统一阻塞某一线程,当这些线程一个个执行完毕,最后再执行被阻塞线程,而不是join某一个线程。

相关方法

//构造方法,构造一个用给定计数初始化的 CountDownLatch
//对10个线程进行处理,当10个线程全部执行完毕,再执行被阻塞线程
CountDownLatch countDownLatch = new CountDownLatch(10);

//使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断
//该语句写在哪里就是阻塞哪个线程
void await();

//递减锁存器的计数,如果计数到达零,则释放所有等待的线程
void countDown();

//返回当前计数
long getCount();

计算整个程序执行的时间
main线程和其他子线程都是同级关系,所以,会出现main线程执行完毕,计算的时间结果仅仅是main线程自己的,而非整个程序的,所以,需要进行CountDownLatch。

public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(5);
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10; i++) {
            new MyThread(countDownLatch).start();
        }
        countDownLatch.await(); //相当于将10个子线程都join,等待执行的子线程数量到0,join结束,主线程开始,这里放到了main方法,就是将main阻塞
        long end = System.currentTimeMillis();  //只有让子线程join,才能让主线程最后结束,才能计算毫秒值
        System.out.println(end - start);
    }
    
	//静态内部类
    static class MyThread extends Thread {
        private CountDownLatch countDownLatch;
        public MyThread(CountDownLatch countDownLatch) {
            this.countDownLatch = countDownLatch;
        }
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "开始执行了。。。");
            int c = 0;
            for (int i = -999999; i < 999999999; i++) {
                for (int j = -999999; j < 99999999; j++) {
                    c = i + j;
                }
            }
            System.out.println(Thread.currentThread().getName() + "结束了。。。");
            countDownLatch.countDown(); //递减
        }
    }
}

注意
1. 如果CountDownLatch初始的线程数量小于实际创建的线程数量,则计时到0的时候,主线程阻塞结束,开始执行,剩余的子线程随后继续执行。
2. 如果大于实际的线程数量,则程序会无法退出,主线程一直被阻塞。
3.循环写在线程创建的外面,则main线程直接连续创建多个线程,然后这些线程各自抢到CPU执行;循环写到线程run()方法里面,则每个线程抢到CPU后自己执行多次后才让出时间片。

2.CyclicBarrier类

CyclicBarrier类似(屏障),表面意思理解为可循环使用的屏障,作用是让一组线程在到达一个屏障时被阻塞,等到最后一个线程到达屏障点,才会让所有被拦截的线程一起继续运行。

相关方法

//创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动
CyclicBarrier cyclicBarrier = new CyclicBarrier(10);

//创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,并在启动 barrier 时执行给定的屏障操作
CyclicBarrier(int parties, Runnable barrierAction);

//在所有参与者都已经在此 barrier 上调用 await 方法之前,将一直等待
int await();

运动员起跑

public class Demo5 {
    public static void main(String[] args) throws InterruptedException {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(10, new Runnable() {
            @Override
            public void run() {
                for (int i = 10; i > -1; i--) {
                    try {
                        Thread.sleep(1000);
                        System.out.println(i);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("比赛开始!!");
            }
        });
        for (int i = 0; i < 10; i++) {
            Thread.sleep(2000);
            new SportMan(i + "号", cyclicBarrier).start();
        }
    }
	//静态内部类
    static class SportMan extends Thread {
        CyclicBarrier cyclicBarrier;
        String name;

        public SportMan(String name, CyclicBarrier cyclicBarrier) {
            this.name = name;
            this.cyclicBarrier = cyclicBarrier;
        }
        @Override
        public void run() {
            try {
                System.out.println(this.name + "运动员就绪");
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        }
    }
}

注意: 如果CyclicBarrier的初始数量少于实际创建的线程数量,则会循环执行,直到所有线程都运行完毕;反之,程序不退出,死锁。

3.Semaphore信号量

Semaphore 是 synchronized 的加强版,作用是控制线程的并发数量。
当正在并发的线程数量低于阈值,则增加线程,高于阈值,则以阈值数量为准,多出的线程进行等待,总之,就是保证正在并发的线程数量始终是阈值。

相关方法

//创建具有给定的许可数和非公平的公平设置的 Semaphore
Semaphore(int permits);

//创建具有给定的许可数和给定的公平设置的 Semaphore
Semaphore(int permits, boolean fair);

//从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断。
void acquire();

//释放一个许可,将其返回给信号量,用在finally中
void release()

案例演示

public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(2);
        for (int i = 0; i < 20; i++) {
            Thread.sleep(2000);
            new MyThread(semaphore).start();
        }
    }
	//内部类
    static class MyThread extends Thread {
        private Semaphore semaphore;

        public MyThread(Semaphore semaphore) {
            this.semaphore = semaphore;
        }
        @Override
        public void run() {
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

            try {
                semaphore.acquire();//获取锁
                System.out.println(Thread.currentThread().getName() + "开始执行" + simpleDateFormat.format(new Date()));
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println(Thread.currentThread().getName() + "结束了" + simpleDateFormat.format(new Date()));
                semaphore.release();
            }
        }
    }
}

//打印结果,每次都保证有两个执行:
Thread-0开始执行2019-08-17 19:01:06
Thread-1开始执行2019-08-17 19:01:08
Thread-0结束了2019-08-17 19:01:08
Thread-1结束了2019-08-17 19:01:10
Thread-2开始执行2019-08-17 19:01:10
Thread-2结束了2019-08-17 19:01:12
Thread-3开始执行2019-08-17 19:01:12
Thread-3结束了2019-08-17 19:01:14
Thread-4开始执行2019-08-17 19:01:14
Thread-5开始执行2019-08-17 19:01:16
Thread-4结束了2019-08-17 19:01:16
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!