Java线程安全集合

并发编程的本质:充分利用CPU资源。

目录

1.准备环境

2.回顾线程和进程

并发和并行

3.回顾多线程

4.Lock锁(重点)

Lock锁是一个接口

lock锁与synchronized的区别

传统生产者消费者问题以及如何解决虚假唤醒问题?

JUC版的生产者与消费者问题

Condition实现精准通知唤醒

5.集合类不安全解决方案

CopyOnWriteArrayList

CopyOnWrite比Vector好在哪里?

1.CopyOnWrite 思想

2.优点

3.缺点

CopyOnWriteSet

ConcurrentHashMap

关于concurrentHashMap的原理:

Segment 是什么呢?

ConcurrentHashMap 小总结:

6.Callable接口简介

7.常用辅助类(必会)

CountDownLatch--减法计数器

CyclicBarrier--加法计数器

Semaphore--信号量


1.准备环境

准备工作使用IDEA作为开发环境,新建一个Maven项目

1.添加一个包

<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.8</version>
    </dependency>
</dependencies>

2.配置好环境,使用jdk1.8,三步分别配置如下

环境准备完毕。

2.回顾线程和进程

一共三个包

普通的线程编码使用Thread,Runnable,效率都比callable低,实际企业里使用Callable比较多。

问题:Java真的可以开启线程吗?答案不可以。代码解释如下:

先看下Thread线程启动源码:

public synchronized void start() {
    /**
     * This method is not invoked for the main method thread or "system"
     * group threads created/set up by the VM. Any new functionality added
     * to this method in the future may have to also be added to the VM.
     *
     * A zero status value corresponds to state "NEW".
     */
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    /* Notify the group that this thread is about to be started
     * so that it can be added to the group's list of threads
     * and the group's unstarted count can be decremented. */
    group.add(this);

    boolean started = false;
    try {
        start0();  //这里实际调用的是底层C++的方法,因为Java干不了开启线程的事情
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
              it will be passed up the call stack */
        }
    }
}

private native void start0(); 
//只能通过本地方法去调,底层是C++,因为Java是运行在JVM上的,无法操作底层的硬件。

并发和并行

并发:指的是多线程操作同一个资源,即交替执行;

  • CPU一核,模拟出来多条线程,天下武功,唯快不破,快速交替

并行:多个人一起行走,同时执行

  • CPU多核,多个线程可以同时执行;

本机CPU核数多少如下:

或者任务管理器里的

或者通过代码来获取CPU核数:

System.out.println(Runtime.getRuntime().availableProcessors());

3.回顾多线程

线程的5大状态:直接贴源码,来自Thread.State

public enum State {
   //新生
    NEW,

    //运行
    RUNNABLE,

   //阻塞
    BLOCKED,

    //一直等
    WAITING,

   //超时等待,过期不候
    TIMED_WAITING,

    //线程终止
    TERMINATED;
}

wait和sleep区别:

  • 来自不同的类,wait->Object,sleep->Thread
  • wait会释放锁,sleep抱着锁睡,不会释放锁
  • wait必须在同步代码块里(synchronized),sleep可以在任何地方

4.Lock锁(重点)

基本写法
lock.lock();
try {
    //业务代码块
} catch (InterruptedException e) {
    e.printStackTrace();
} finally {
    lock.unlock();
}

Lock锁是一个接口

有三个实现类,如下

lock锁的语句写法如上

传统的synchronized写法如下,但这里并没有用到JUC的写法

public class Demo3 {
    public static void main(String[] args) {
        //并发:多线程操作同一个资源类,把资源类丢入线程
        Ticket ticket = new Ticket();
        new Thread(()->{ for (int i = 0; i < 40; i++) ticket.sale(); },"A").start();
        new Thread(()->{ for (int i = 0; i < 40; i++) ticket.sale(); },"B").start();
        new Thread(()->{ for (int i = 0; i < 40; i++) ticket.sale(); },"C").start();
    }
}

class Ticket {
    private int number = 50;
    public synchronized void sale(){ 
        if(number>0){
            System.out.println(Thread.currentThread().getName()+"卖出了第"+(number--)+"票,剩余"+number);
        }
    }
}

然后使用JUC下的lock锁对他进行改造如下,首先会new一个ReentrantLock对象,
这个对象点进去看源码发现默认无参时使用非公平锁,

公平锁:十分公平,先来后到;
非公平锁:十分不公平,可以插队(默认)
为什么默认使用非公平锁?是因为如果两个线程,第一个要用3h执行完毕,第二个要用3s,如果使用公平锁会造成严重耗时问题.
非公平锁总体会比公平要好一些,它是根据每个线程对资源抢占能力来分配的,不需要严格的安装锁的请求顺序接入

非公平锁与公平锁的创建

  • 非公平锁:ReentrantLock()或ReentrantLock(false)
    final ReentrantLock lock = new ReentrantLock();
  • 公平锁:ReentrantLock(true)
    final ReentrantLock lock = new ReentrantLock(true)

改造代码如下:

public class Demo3 {
    public static void main(String[] args) {
        //并发:多线程操作同一个资源类,把资源类丢入线程
        Ticket ticket = new Ticket();
        new Thread(()->{ for (int i = 0; i < 40; i++) ticket.sale(); },"A").start();
        new Thread(()->{ for (int i = 0; i < 40; i++) ticket.sale(); },"B").start();
        new Thread(()->{ for (int i = 0; i < 40; i++) ticket.sale(); },"C").start();
    }
}

/*lock三部曲
  1.new ReentrantLock();
  2.lock.lock();  //加锁
  3.finally=> lock.unlock(); //解锁
 */
class Ticket {
    private int number = 50;
    Lock lock = new ReentrantLock();
    public void sale(){
        lock.lock();  //加锁
        try {
            //业务代码
            if(number>0){
                System.out.println(Thread.currentThread().getName()+"卖出了第"+(number--)+"票,剩余"+number);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();  //解锁
        }
    }
}

使用JUC下的lock锁可以保证多线程在并发编码下的操作安全性,

lock锁与synchronized的区别

  • Synchronized 内置的Java关键字 ,Lock 是一个java接口
  • Synchronized 无法判断获取锁的状态, Lock可以判断是否获取到了锁
  • Synchronized 会自动释放锁,lock必须要手动释放锁!如果不释放锁,否则造成死锁
  • Synchronized 线程1 ( 获得锁)、线程2(等待,如果线程1阻塞则线程2傻傻的等) ; Lock锁就不一定会等待下去 ;
  • Synchronized 可再入锁 ,不可以中断的,非公平; Lock ,可再入锁,可以判断锁,非公平(可以自己设置) ;
  • Synchronized 适合锁少量的代码同步问题, Lock适合锁大量的同步代码!

传统生产者消费者问题以及如何解决虚假唤醒问题?

代码如下,有四个线程,两个线程操作加方法,另外两个线程操作减方法,同时两个方法都用synchronized进行修饰,如下:

synchronized(wait,notify)

public class Demo3 {
    public static void main(String[] args) {
        //并发:多线程操作同一个资源类,把资源类丢入线程
        Data data = new Data();
        //线程1,执行加方法
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();

        //线程2,执行加方法
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"B").start();

        //线程3,执行减方法
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"C").start();

        //线程4,执行减方法
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"D").start();
    }
}

class Data {
    private int number = 0;
    public synchronized void increment() throws InterruptedException{
        if(number!=0){
            //等待
            this.wait();
        }
        number++;
        System.out.println(Thread.currentThread().getName()+"=>"+number);
        //通知其他线程,执行完毕
        this.notifyAll();
    }

    public synchronized void decrement() throws InterruptedException{
        if(number==0){
            //等待
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName()+"=>"+number);
        //通知其他线程,执行完毕
        this.notifyAll();
    }
}
/*
A=>2
B=>3
C=>2
C=>1
C=>0
其中一段输出结果
 */

发现似乎有两个以上的线程同时针对number进行了加操作,否则四个线程应该结果应该都是1或0,造成这个原因就是虚假唤醒,因为这里使用了if,

如jdk1.8文档里所说,wait方法应该用在while循环里,不能用if,所以上面的方法改为while即可.这也是一个面试点.

这里仍然有个问题,四个线程并没有按照顺序交替执行,四个线程仍然在没有顺序的争抢CPU时间片,所以解决方法见下方JUC写法.

JUC版的生产者与消费者问题

Lock(await,sinal),需要使用到condition实例

现在使用lock锁来改造传统的消费者与生产者写法如下:

 

改造传统的生产者与消费者,只改上方的Data外部类即可,其余不变:
class Data {
    private int number = 0;
    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
    public void increment() throws InterruptedException{
        lock.lock();
        //将代码块包起来
        try {
            while (number!=0){
                //等待
                //this.wait();
                condition.await();
            }
            number++;
            System.out.println(Thread.currentThread().getName()+"=>"+number);
            //通知其他线程,执行完毕
            //this.notifyAll();
            condition.signalAll(); //唤醒全部
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public synchronized void decrement() throws InterruptedException{
        lock.lock();
        try {
            while(number==0){
                //等待
                condition.await();
            }
            number--;
            System.out.println(Thread.currentThread().getName()+"=>"+number);
            //通知其他线程,执行完毕
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

效果与传统的写法一致,然后任何一种新的技术的出现绝不仅仅只是覆盖原先的写法,contidion可以做到让线程精准通知并按照顺序来执行,所以这解决了上方传统写法遗留的问题

Condition实现精准通知唤醒

紧接上文,实现A执行完调用B,B执行完调用C...

public class Demo3 {
    public static void main(String[] args) {
        //并发:多线程操作同一个资源类,把资源类丢入线程
        Data data = new Data();
        //线程1,执行加方法
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                data.printA();
            }
        },"A").start();

        //线程2,执行加方法
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                data.printB();
            }
        },"B").start();

        //线程3,执行减方法
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                data.printC();
            }
        },"C").start();

    }
}

class Data {
    private int number = 1;
    Lock lock = new ReentrantLock();
    Condition condition1 = lock.newCondition();
    Condition condition2 = lock.newCondition();
    Condition condition3 = lock.newCondition();

    public void printA() {
        lock.lock();
        //将代码块包起来
        try {
            while (number!=1){
                condition1.await();
            }
            System.out.println(Thread.currentThread().getName()+"=>AAA");
            number=2;
            condition2.signal(); //唤醒指定的B,也不是用了很高级的技术,只是指定唤醒了
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printB() {
        lock.lock();
        //将代码块包起来
        try {
            while (number!=2){
                condition2.await();
            }
            System.out.println(Thread.currentThread().getName()+"=>BBB");
            number=3;
            condition3.signal(); //唤醒指定的C
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printC() {
        lock.lock();
        //将代码块包起来
        try {
            while (number!=3){
                condition3.await();
            }
            System.out.println(Thread.currentThread().getName()+"=>CCC");
            number=1;
            condition1.signal(); //唤醒指定的A
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

5.集合类不安全解决方案

集合不安全指的是集合内部的多个方法不能保证原子性操作.

CopyOnWriteArrayList

public class Demo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        for (int i = 1; i <=30; i++) {
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(list);
            },String.valueOf(i)).start();
        }
    }
}

上述代码会报java.util.ConcurrentModificationException异常,这是并发修改异常!

因为并发下ArrayList不安全的,有以下解决方案:

  • 使用Vector,即List<String> list = new Vector<>();
  • List<String> list = Collections.synchronizedList(new ArrayList<>());这种是用工具类把ArrayList转成synchronized的.
  • 使用JUC, List<String> list = new CopyOnWriteArrayList<>();  写入时复制,写入的时候避免覆盖,造成数据问题,相比上面两种,这个更好.

CopyOnWrite比Vector好在哪里?

如上,Vector的源码时用synchroinzed修饰,只要有这个字段的方法所以效率很低,来看看copyonwrite源码如下:

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

我们先来看看 CopyOnWriteArrayList 的 add() 方法,其实也非常简单,就是在访问的时候加锁,拷贝出来一个副本,先操作这个副本,再把现有的数据替换为这个副本。

CopyOnWriteArrayList 的 get(int index) 方法就是普通的无锁访问。

private E get(Object[] a, int index) {
    return (E) a[index];
}
public E get(int index) {
    return get(getArray(), index);
}

1.CopyOnWrite 思想

写入时复制(CopyOnWrite,简称COW)思想是计算机程序设计领域中的一种通用优化策略。其核心思想是,如果有多个调用者(Callers)同时访问相同的资源(如内存或者是磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者修改资源内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此做法主要的优点是如果调用者没有修改资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。

通俗易懂的讲,写入时复制技术就是不同进程在访问同一资源的时候,只有更新操作,才会去复制一份新的数据并更新替换,否则都是访问同一个资源。

JDK 的 CopyOnWriteArrayList/CopyOnWriteArraySet 容器正是采用了 COW 思想,它是如何工作的呢?简单来说,就是平时查询的时候,都不需要加锁,随便访问,只有在更新的时候,才会从原来的数据复制一个副本出来,然后修改这个副本,最后把原数据替换成当前的副本。修改操作的同时,读操作不会被阻塞,而是继续读取旧的数据。这点要跟读写锁区分一下

2.优点

对于一些读多写少的数据,写入时复制的做法就很不错,例如配置、黑名单、物流地址等变化非常少的数据,这是一种无锁的实现。可以帮我们实现程序更高的并发。

CopyOnWriteArrayList 并发安全且性能比 Vector 好。Vector 是增删改查方法都加了synchronized 来保证同步,但是每个方法执行的时候都要去获得锁,性能就会大大下降,而 CopyOnWriteArrayList 只是在增删改上加锁,但是读不加锁,在读方面的性能就好于 Vector。

3.缺点

数据一致性问题。这种实现只是保证数据的最终一致性,在添加到拷贝数据而还没进行替换的时候,读到的仍然是旧数据。

内存占用问题。如果对象比较大,频繁地进行替换会消耗内存,从而引发 Java 的 GC 问题,这个时候,我们应该考虑其他的容器,例如 ConcurrentHashMap。

CopyOnWriteSet

set与list在写法上与上方类似,如下

public class Demo {
    public static void main(String[] args) {
        Set<String> set = new HashSet<>();
        for (int i = 1; i <=30; i++) {
            new Thread(()->{
                set.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(set);
            },String.valueOf(i)).start();
        }
    }
}

这个代码同样也会报并发修改异常问题,即java.util.ConcurrentModificationException,
解决方案如下:

  • Set<String> set = Collections.synchronizedSet(new HashSet<>());  这是用工具类转成synchronized的写法
  • Set<String> set = new CopyOnWriteArraySet<>();  这是JUC的写法,推荐使用

HashSet底层是什么?

底层源码如下:

public HashSet() {
    map = new HashMap<>();
}
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

就是new了一个HashMap.

ConcurrentHashMap

ConcurrentHashMap是一个线程安全的.

public class Demo {
    public static void main(String[] args) {
        Map<String,String> map = new HashMap<>();
        for (int i = 1; i <=30; i++) {
            new Thread(()->{
                map.put(Thread.currentThread().getName(),UUID.randomUUID().toString().substring(0,5));
                System.out.println(map);
            },String.valueOf(i)).start();
        }
    }
}

这个代码也会提示java.util.ConcurrentModificationException异常,解决如下:

  • Map<String,String> map = Collections.synchronizedMap(new HashMap<>());
  • Map<String,String> map = new ConcurrentHashMap<>();

关于concurrentHashMap的原理:

分段锁技术:ConcurrentHashMap 相比 HashTable 对锁的处理不同的点在于:前者是分段部分数据锁定
每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,后者是全部锁定。
HashMap 不是线程安全的,因此多线程操作需要注意,通常使用 HashTable 或者 Collections.synchronizedMap() 来返回线程安全的 HashMap ,但是这两种方法都是对所有方法实现同步,即使用synchronized修饰,导致读写性能比较低,而 ConcurrentHashMap 引入“分段锁”的概念,可以理解为把一个大的 Map 差分成小的 HashTable ,根据 key.hashCode() 来决定把 key 放到哪个 HashTable 中去。
就是把 Map 分成了N个 Segment , put 和 get 的时候,都是现根据 key.hashCode() 算出放到哪个Segment中.

ConcurrentHashMap (简称 CHM )是在 Java 1.5作为 Hashtable 的替代选择新引入的,是 concurrent 包的重要成员。在 Java 1.5之前,如果想要实现一个可以在多线程和并发的程序中安全使用的 Map ,只能在 HashTable 和 synchronizedMap 中选择,因为 HashMap 并不是线程安全的。但再引入了 CHM 之后,我们有了更好的选择。 CHM 不但是线程安全的,而且比 HashTable 和 synchronizedMap 的性能要好。相对于 HashTable 和 synchronizedMap 锁住了整个 Map , CHM 只锁住部分 Map 。 CHM 允许并发的读操作,同时通过同步锁在写操作时保持数据完整性。

Segment 是什么呢?

Segment 本身就相当于一个 HashMap 对象。每个Segment都持有自己的锁 ,Segment 是一种可重入锁(继承ReentrantLock)
同 HashMap 一样, Segment 包含一个 HashEntry 数组,数组中的每一个 HashEntry 既是一个键值对,也是一个链表的头节点。

这里写图片描述

从图中可以看出来ConcurrentHashMap的主干是个Segment数组。

在了解以上的功能 ,之后我们继续看一下ConcurrentHashMap核心构造方法代码。

// 跟HashMap结构有点类似
Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
            this.loadFactor = lf;//负载因子
            this.threshold = threshold;//阈值
            this.table = tab;//主干数组即HashEntry数组
        }

从以上代码可以看出ConcurrentHashMap有比较重要的三个参数:

  1. loadFactor 负载因子 0.75
  2. threshold 初始 容量 16
  3. concurrencyLevel 实际上是Segment的实际数量 默认16。

ConcurrentHashMap如何发生ReHash?
ConcurrentLevel 一旦设定的话,就不会改变。ConcurrentHashMap当元素个数大于临界值的时候,就会发生扩容。但是ConcurrentHashMap与其他的HashMap不同的是,它不会对Segment 数量增大,只会增加Segment 后面的链表容量的大小。即对每个Segment 的元素进行的ReHash操作。

ConcurrentHashMap 小总结:

  • CHM 允许并发的读和线程安全的更新操作
  • 在执行写操作时,CHM 只锁住部分的 Map 
  • 并发的更新是通过内部根据并发级别将 Map 分割成小部分实现的
  • 高的并发级别会造成时间和空间的浪费,低的并发级别在写线程多时会引起线程间的竞争
  • CHM 的所有操作都是线程安全
  • CHM 返回的迭代器是弱一致性, fail-safe 并且不会抛出 ConcurrentModificationException 异常
  • CHM 不允许 null 的键值
  • 可以使用 CHM 代替 HashTable,但要记住 CHM 不会锁住整个 Map

有待补漏,参深入理解ConcurrentHashMap原理分析以及线程安全性问题_猴凉凉的博客-CSDN博客_concurrenthashmap内存泄漏

6.Callable接口简介

有两种创建线程的方法-一种是通过创建Thread类,另一种是通过使用Runnable创建线程。但是,Runnable缺少的一项功能是,当线程终止时(即run()完成时),我们无法使线程返回结果。为了支持此功能,Java中提供了Callable接口。

底层是一个泛型函数式接口,V决定方法的返回类型,可以自主设置.

callable和runnable区别:

  • 为了实现Runnable,需要实现不返回任何内容的run()方法,而对于Callable,需要实现在完成时返回结果的call()方法。请注意,不能使用Callable创建线程,只能使用Runnable创建线程。
  • 另一个区别是call()方法可以引发异常,而run()则不能。
  • 为实现Callable而必须重写call方法。

要创建线程,需要Runnable。为了获得结果,需要future。

Java库具有具体的FutureTask类型,该类型实现Runnable和Future,并方便地将两种功能组合在一起。
可以通过为其构造函数提供Callable来创建FutureTask,然后,将FutureTask对象提供给Thread的构造函数以创建Thread对象。
因此,间接地使用Callable创建线程。

public class Demo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable myc = new MyCallable();
        FutureTask futureTask = new FutureTask(myc); //适配类
        new Thread(futureTask,"A").start();
        new Thread(futureTask,"B").start();  //这里开两个线程跑call发现结果只会执行一次,是因为第一次结果会被缓存,效率高
        Integer o = (Integer) futureTask.get(); //获取Callable的返回结果,这里的get方法可能会产生阻塞
        System.out.println(o);
    }
}

class MyCallable implements Callable<Integer>{
    @Override
    public Integer call() {
        System.out.println("你好");
        return 123456;
    }
}

细节:

  • 有缓存
  • 结果可能需要等待,会阻塞

7.常用辅助类(必会)

CountDownLatch--减法计数器
 

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        //总数是6,必须要执行任务的时候再使用
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 1; i <= 6; i++) {
            int temp = i;
            new Thread(()->{
                System.out.println(temp); //这里会报错,会提示 从lambda 表达式引用的本地变量必须是最终变量或实际上的最终变量,所以在new Thread外面再写一个
                System.out.println(Thread.currentThread().getName()+" Go Out");
                countDownLatch.countDown();  //数量-1
            },String.valueOf(i)).start();
        }
        countDownLatch.await(); //等待计数器归零,然后再向下执行
        System.out.println("Close Door");
    }
}
/*
1 Go Out
2 Go Out
3 Go Out
4 Go Out
5 Go Out
6 Go Out
Close Door
 */

等所有任务执行完毕再关门
原理:
countDownLatch.countDown(); //数量-1
countDownLatch.await(); //等待计数器归零,然后再向下执行
每次有线程调用coundDown()数量-1,假设计数器变为0,countDownLatch.await()就会被唤醒,继续往下执行!

CyclicBarrier--加法计数器

在JUC包中为我们提供了一个同步工具类能够很好的模拟这类场景,它就是CyclicBarrier类。
利用CyclicBarrier类可以实现一组线程相互等待,当所有线程都到达某个屏障点后再进行后续的操作。下图演示了这一过程。
在CyclicBarrier类的内部有一个计数器,每个线程在到达屏障点的时候都会调用await方法将自己阻塞,此时计数器会减1,
当计数器减为0的时候所有因调用await方法而被阻塞的线程将被唤醒。

public class Demo {
    public static void main(String[] args) throws InterruptedException {
       //召唤龙珠的线程
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
            System.out.println("召唤神龙成功!");
        });

        for (int i = 1; i <= 7; i++) {
            int temp=i;
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"收集了"+temp+"个龙珠");
                try {
                    cyclicBarrier.await();  //等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}
/*
Thread-0收集了1个龙珠
Thread-1收集了2个龙珠
Thread-2收集了3个龙珠
Thread-3收集了4个龙珠
Thread-5收集了6个龙珠
Thread-4收集了5个龙珠
Thread-6收集了7个龙珠
召唤神龙成功!
 */

Semaphore--信号量

经常用,Semaphore也是一个线程同步的辅助类,可以维护当前访问自身的线程个数,并提供了同步机制。使用Semaphore可以控制同时访问资源的线程个数,
例如,实现一个文件允许的并发访问数。

public class Demo {
    public static void main(String[] args) {
      //线程数量:停车位!限流,一次只能停三辆
        Semaphore semaphore = new Semaphore(3);  //创建Semaphore信号量,初始化许可大小为3
        for (int i = 1; i <= 6; i++) {
            new Thread(()->{
                try {
                    semaphore.acquire(); 请求获得许可,如果有可获得的许可则继续往下执行,许可数减1。否则进入阻塞状态
                    System.out.println(Thread.currentThread().getName()+"抢到车位");
                    TimeUnit.SECONDS.sleep(2);
                    System.out.println(Thread.currentThread().getName()+"离开车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    semaphore.release();  //释放许可,许可数加1
                }
            },String.valueOf(i)).start();
        }
    }
}
/*
1抢到车位
3抢到车位
2抢到车位
3离开车位
2离开车位
4抢到车位
5抢到车位
1离开车位
6抢到车位
5离开车位
6离开车位
4离开车位
 */

原理:

  • semaphore.acquire();  获得,假设如果已经满了,等待,等待有资源被释放,-1;
  • semaphore.release();  获得,会将当前的信息量释放+1,然后唤醒等待的线程!

作用:多个共享资源互斥的使用,并发限流,控制最大的线程数!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值