java并发5--并发工具(辅助类|集合类)

一、Semaphore 信号量

1、定义

用来限制能同时访问共享资源的线程上限。
比喻为停车位,车位占用则不能使用,得等上一辆车开走,释放资源。(类似于能允许存在多个锁)
类似于之前的notify和wait。是经过封装的高级api,用起来更简单

2、应用

单机版、当许可数(资源线程上限)==资源数时,更适合用
在这里插入图片描述

3、原理

核心是继承了AQS.使用其中的states来存放允许的线程数。每消耗一个许可,则减一。释放一个许可则加一

在这里插入图片描述

4、示例代码

  1. 抢车位示例

    package com.jian8.juc.conditionThread;
    
    import java.util.concurrent.Semaphore;
    import java.util.concurrent.TimeUnit;
    
    public class SemaphoreDemo {
        public static void main(String[] args) {
            Semaphore semaphore = new Semaphore(3);//模拟三个停车位
            for (int i = 1; i <= 6; i++) {//模拟6部汽车
                new Thread(() -> {
                    try {
                        semaphore.acquire();
                        System.out.println(Thread.currentThread().getName() + "\t抢到车位");
                        try {
                            TimeUnit.SECONDS.sleep(3);//停车3s
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + "\t停车3s后离开车位");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        semaphore.release();
                    }
                }, "Car " + i).start();
            }
        }
    }
    
    

二、CountdownLatch

1、定义

用来进行线程同步协作,等待所有线程完成倒计时。
其中构造参数用来初始化等待计数值,countDown() 用来让计数减一,await() 用来等待计数归零(在归零前一直等待着)。
一般是主线程等待其他线程执行完,继续后面的操作。

2、原理

内部和信号量一样,有一个同步器Sync.也是根据state的值来判断,初始值时x,让所有应执行的线程都执行完,state=0时,才停止等待,进行之后的任务。

3、应用

和之前的join效果差不多,但是是高级api
1)同步等待多线程准备完毕
2)同步等待多个远程调用结束

4、代码示例:

public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
//        general();
        countDownLatchTest();
    }

    public static void general(){
        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName()+"\t上完自习,离开教室");
            }, "Thread-->"+i).start();
        }
        while (Thread.activeCount()>2){
            try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
        }
        System.out.println(Thread.currentThread().getName()+"\t=====班长最后关门走人");
    }

    public static void countDownLatchTest() throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName()+"\t被灭");
                countDownLatch.countDown();
            }, CountryEnum.forEach_CountryEnum(i).getRetMessage()).start();
        }
        countDownLatch.await();
        System.out.println(Thread.currentThread().getName()+"\t=====秦统一");
    }
}

三、CyclicBarrier

1、定义

循环栅栏,用来进行线程协作,等待线程满足某个计数。构造时设置『计数个数』,每个线程执
行到某个需要“同步”的时刻调用 await() 方法进行等待,当等待的线程数满足『计数个数』时,继续执行
CountdownLatch只能使用一次,CyclicBarrier可以循环使用多次。当计数减为0时,下一次线程数够时再调,又会从头开始

2、比较

CyclicBarrier 可以被比喻为『人满发车』,下一班满了再次发车;
而CountdownLatch是人下完之后,车开走了,并且不再回来。

3、代码示例:

package com.jian8.juc.conditionThread;

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

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        cyclicBarrierTest();
    }

    public static void cyclicBarrierTest() {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
            System.out.println("====召唤神龙=====");
        });
        for (int i = 1; i <= 7; i++) {
            final int tempInt = i;
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "\t收集到第" + tempInt + "颗龙珠");
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }, "" + i).start();
        }
    }
}

四、线程安全集合类概述

线程安全集合类不一定真的安全。因为他们只保证某个方法的原子性。
在多个线程中,一个方法同时get+计算+put 就会线程不安全。要考虑如何安全使用
在这里插入图片描述
1)前两类都是直接用sync关键字,效率不高。不推荐使用。
2)JUC安全集合
Blocking 大部分实现基于锁,并提供用来阻塞的方法
CopyOnWrite 之类容器修改开销相对较重
Concurrent 类型的容器

内部很多操作使用 cas 优化,一般可以提供较高吞吐量
弱一致性
遍历时弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍历,这时内容是旧的
求大小弱一致性,size 操作未必是 100% 准确
读取弱一致性
3) fail-fast
遍历时如果发生了修改,对于非安全容器来讲,使用 fail-fast 机制也就是让遍历立刻失败,抛出
ConcurrentModificationException,不再继续遍历

五、currentHashMap

1、hashMap并发死链

1.7中用的是头插法,链表中的数据新来的都插在头部
1.8 是尾插法
在这里插入图片描述
具体代码见(并发原理 P81)

2、jdk8(重点)

2.1重要属性和内部类

// 默认为 0
// 当初始化时, 为 -1(懒加载)
// 当扩容时, 为 -(1 + 扩容线程数)
// 当初始化或扩容完成后,为 下一次的扩容的阈值大小
private transient volatile int sizeCtl;
// 静态内部类,单个节点。
static class Node<K,V> implements Map.Entry<K,V> {}
//整个ConcurrentHashMap 就是一个 Node[]。 hash 表
transient volatile Node<K,V>[] table;
// 扩容时的 新 hash 表
private transient volatile Node<K,V>[] nextTable;
// 扩容时如果某个 bin 迁移完毕, 用 ForwardingNode 作为旧 table bin 的头结点
static final class ForwardingNode<K,V> extends Node<K,V> {}
// 用在 compute 以及 computeIfAbsent 时, 用来占位, 计算完成后替换为普通 Node
static final class ReservationNode<K,V> extends Node<K,V> {}
// 作为 treebin 的头节点, 存储 root 和 first
static final class TreeBin<K,V> extends Node<K,V> {}
// 作为 treebin 的节点, 存储 parent, left, right
static final class TreeNode<K,V> extends Node<K,V> {}

2.2重要方法

// 获取 Node[] 中第 i 个 Node
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i)
// cas 修改 Node[] 中第 i 个 Node 的值, c 为旧值, v 为新值
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v)
// 直接修改 Node[] 中第 i 个 Node 的值, v 为新值
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v)

2.3构造器

public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
	if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
	throw new IllegalArgumentException();
	if (initialCapacity < concurrencyLevel) // Use at least as many bins
	initialCapacity = concurrencyLevel; // as estimated threads
	long size = (long)(1.0 + (long)initialCapacity / loadFactor);
	// tableSizeFor 仍然是保证计算的大小是 2^n, 即 16,32,64 ...
	int cap = (size >= (long)MAXIMUM_CAPACITY) ?
	MAXIMUM_CAPACITY : tableSizeFor((int)size);
	this.sizeCtl = cap;
}

2.4 get

由于数组被volatile关键字修饰,因此不用担心数组的可见性问题。
同时每个元素是一个Node实例,它的Key值和hash值都由final修饰,不可变更,无须关心它们被修改后的可见性问题。而其Value及对下一个元素的引用由volatile修饰,可见性也有保障。–无需加锁
在这里插入图片描述

2.5 put

put
1)如果放入时表还未创建,则建表;
2)如果Key对应的数组元素为null,则通过CAS操作将其设置为当前值;
3)如果放入时正在扩容,则帮忙扩容;
4)如果Key对应的数组元素不为null(发生了hash冲突),这时才对头元素进行sync加锁

	如果是普通链表:
		如果找到相同key,则更新当前节点
		如果没找到 ,则在链表尾部追加
	如果是tree,调用tree的方法

5)如果该put操作使得当前链表长度超过一定阈值,则将该链表转换为树,从而提高寻址效率。

2.5 transfer

入参是当前tab和新的nexttab
1)容量x2创建nexttab
2)节点搬迁,以单个链表为单位

	处理完的链表状态为fnode
	链表头有元素,按链表和tree不同的方法去移动

2.6总结

Java 8 数组(Node) +( 链表 Node | 红黑树 TreeNode ) 以下数组简称(table),链表简称(bin)
1)初始化,使用 cas 来保证并发安全,懒惰初始化 table树化,当 table.length < 64 时,先尝试扩容,超过 64 时,并且 bin.length > 8 时,会将链表树化,树化过程会用 synchronized 锁住链表头
2)put,如果该 bin 尚未创建,只需要使用 cas 创建 bin;如果已经有了,锁住链表头进行后续 put 操作,元素添加至 bin 的尾部
3)get,无锁操作仅需要保证可见性,扩容过程中 get 操作拿到的是 ForwardingNode 它会让 get 操作在新table 进行搜索
4)扩容,扩容时以 bin 为单位进行,需要对 bin 进行 synchronized,但这时妙的是其它竞争线程也不是无事可做,它们会帮助把其它 bin 进行扩容,扩容时平均只有 1/6 的节点会把复制到新 table 中size,元素个数保存在 baseCount 中,并发时的个数变动保存在 CounterCell[] 当中。最后统计数量时累加即可

3、JDK7

它维护了一个 segment 数组,每个 segment 对应一把锁
优点:如果多个线程访问不同的 segment,实际是没有冲突的,这与 jdk8 中是类似的
缺点:Segments 数组默认大小为16,这个容量初始化指定后就不能改变了,并且不是懒惰初始化
详情见P92
在这里插入图片描述

4、ConcurrentHashMap变化

为何JDK8要放弃分段锁?

  • 加入多个分段锁 浪费了内存空间
  • 生产环境中,map在放入时 竞争同一个锁的概率非常小,分段锁反而会造成更新等操作的长时间等待
  • 为了提高GC的效率

为什么是用Synchronized 而不是 ReentrantLock?

  • 减少内存开销
    • 假设使用可重入锁来获得同步支持,那么每个节点都需要通过继承AQS来获得同步支持。但并不是每个节点都需要同步支持,只有链表的头结点(红黑树的根节点)需要同步,这无疑带来了巨大的浪费
  • 获得JVM支持
    • 可重入锁毕竟是API这个级别的,后续的性能优化空间 很小
    • Synchronized则是由JVM直接支持,JVM能够在运行时做出对应的优化措施:锁粗化,锁消除,锁自旋等。这就是使得Synchronized能够随着JDK版本的升级而无需改动代码的前提下获得性能上的提升。

六、LinkedBlockingQueue(阻塞)

1、比较 推荐linked
Linked 支持有界(默认无界),Array 强制有界
Linked 实现是链表,Array 实现是数组
Linked 是懒惰的,而 Array 需要提前初始化 Node 数组
Linked 每次入队会生成新 Node,而 Array 的 Node 是提前创建好的
Linked 两把锁,Array 一把锁
在这里插入图片描述

七、ConcurrentLinkedQueue(并发)

1、比较

ConcurrentLinkedQueue 的设计与 LinkedBlockingQueue 非常像
1)两把【锁】,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行
2)dummy 节点的引入让两把【锁】将来锁住的是不同对象,避免竞争
3)不同:只是这【锁】使用了 cas 来实现

2、应用

例如之前讲的 Tomcat 的 Connector 结构时,Acceptor 作为生产者向 Poller 消费者传递事件信息时,正是采用了
ConcurrentLinkedQueue 将 SocketChannel 给 Poller 使用

八、CopyOnWriteArrayList

1、特点

CopyOnWriteArraySet 是它的马甲 底层实现采用了 写入时拷贝 的思想,增删改操作会将底层数组拷贝一份,更
改操作在新数组上执行,这时不影响其它线程的并发读,读写分离
在这里插入图片描述

2、应用

适合『读多写少』的应用场景,用空间换取线程安全。

3、弱一致性

1)get | 迭代器弱一致性
2)不要觉得弱一致性就不好
数据库的 MVCC(多版本并发控制) 都是弱一致性的表现
并发高和一致性是矛盾的,需要权衡

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值