ReentrantLock &信号量&CountDownLatch&多线程环境使用集合类ArrayList和ConcurrentHashMap

本文详细介绍了Java中ReentrantLock的特性、公平锁实现、与条件变量的配合,以及Semaphore、CountDownLatch和CopyOnWriteArrayList在多线程协作中的应用。此外,还对比了ConcurrentHashMap与传统哈希表在锁机制和性能优化上的差异。
摘要由CSDN通过智能技术生成

ReentrantLock 

ReentrantLock 也是一个可重入锁,使用了lock和unlock方式加锁解锁,使用效果上和 synchronized 是类似,
优势:

1. ReentrantLock,在加锁的时候,有两种方式. lock, tryLock.给了更多的可操作空间

lock(): 加锁, 如果获取不到锁就死等.

trylock(超时时间): 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁.

2. ReentrantLock, 提供了 公平锁 的实现.(默认情况下是非公平锁),通过构造方法传入一个 true 开启公平锁模式。

  ReentrantLock reentrantLock=new ReentrantLock(true);

3. ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程;synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是一个随机等待的 线程.

ReentranLock 容易忘记unlock释放锁

信号量Semaphore 

信号量, 用来表示 "可用资源的个数". 本质上就是一个计数器。它可以用来限制同时访问某些资源(例如,数据库连接,线程池等)的线程数量,从而避免资源被过度利用。

比如:停车场的剩余停车位就是信号量,车进-1,有车出+1 ,+1(V操作)-1(P操作)都是原子的。

在 Java 中,Semaphore 是在 java.util.concurrent 包下的一个类,它主要包括两个常用的方法:

  1. acquire():获取一个许可证,如果没有许可证可用,则会阻塞直到有许可证为止。申请资源P
  2. release():释放一个许可证,将其归还给信号量。V

初始10个信号量,每P操作一次数值-1,当进行10次P操作,数值就是0了,继续进行P操作就会进行阻塞等待,这里的阻塞等待就有锁的感觉,锁本质上就是特殊的信号量,是可用资源为1的信号量,加锁操作,P操作,1->0,解锁操作,V操作,0-》1,是二元信号量。

操作系统提供了信号量实现,提供了api,JVM封装了这样的api,就可以在java代码中使用了,遇到需要申请资源的场景就可以使用信号量。


        Semaphore semaphore = new Semaphore(3);
        semaphore.acquire();
        System.out.println("第一次P操作");

CountDownLatch

 适用于多个线程来完成一项任务时,衡量任务是否全部完成。同时等待 N 个任务执行结束。需要把一个大任务拆分成小任务让这些任务并发执行。可以使用countdownlatch判定这些任务是否全部完成。专业下载工具IDM就可以成倍下载,

其中await方法一调用就会阻塞,就会等待其他所有线程都完成任务,await才会返回继续向下走,countdown方法会告诉countDownLatch当前一个子任务已经完了,每个线程走到终点就调用一下countDown方法,使用 CountDownLatch,我们可以很方便地实现多个线程之间的协作,确保某个线程在其他线程完成操作后再执行


    public static void main(String[] args) throws InterruptedException {
        //10个任务,await会在10个任务都调用完countDown后才会执行,a=all
        //而countDown方法会在每个子任务结束后调用,通知当前任务执行完毕
        CountDownLatch countDownLatch=new CountDownLatch(10);
        Thread t=new Thread(()->{
            for(int i=0;i<10;i++){
                int id=i;
                System.out.println("thread:"+id);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                countDownLatch.countDown();
            }

        });
        t.start();
        countDownLatch.await();
        System.out.println("所有任务完成");
    }
    /**
     * thread:0
     * thread:1
     * thread:2
     * thread:3
     * thread:4
     * thread:5
     * thread:6
     * thread:7
     * thread:8
     * thread:9
     * 所有任务完成
     * **/
 

 多线程环境使用ArrayList

 Vector, Stack, HashTable, 这几个集合是线程安全的,关键方法带有synchronized,而其他的集合想要多个线程使用同一个集合类对象,就需要一定的方法,以ArrayList为例:

1.synchronized、Reentranlock

2.Collections.synchronizedList(new ArrayList);
synchronizedList 是标准库提供的一个基于 synchronized 进行线程同步的 List.
synchronizedList 的关键操作上都带有 synchronized

3.CopyOnWriteArrayList写时拷贝.读写分离
比如,两个线程使用同一个 ArrayList 可能会读,也可能会修改
如果要是两个线程读,就直接读就好了
如果某个线程需要进行修改,就把 ArrayList 复制出一份副本,修改线程就修改这个副本,与此同时,另一个线程仍然可以读取数据(从原来的数据上进行读取)一旦这边修改完毕,就会使用修改好的这份数据,替代掉原来的数据(往往就是一个引用赋值)。

虽然这个不用加锁就可以实现并发读,但是这个存在缺陷  ,1ArrayList不能太大拷贝成本不能过高;2更适合一个线程去修改多个线程读而不是多个线程进行修改。

在读多写少的场景下, 性能很高, 不需要加锁竞争
缺点:
1. 占用内存较多.
2. 新写的数据不能被第一时间读取到.

这种场景适合服务器的配置更新,通过配置文件描述配置更新内容,配置文件的内容会被读取到内存中,修改这个配置文件内容往往只有一个线程。

ConcurrentHashMap

HashMap是线程不安全的,HashTable是线程安全的,关键方法加锁了.我们更推荐的是ConcurrentHashMap ,更优化的线程安全哈希表。Hashtable 保证线程安全,主要就是给关键方法,加上 synchronized.直接加到方法上的.(相当于给 this加锁)
只要两个线程,在操作同一个 Hashtable 就会出现锁冲突~~但是,实际上,对于哈希表来说,锁不一定非得这么加, 有些情况,其实是不涉及到线程安全问题的~~ 

锁的粒度

HashMap是线程不安全的,HashTable是线程安全的,关键方法加锁了.我们更推荐的是ConcurrentHashMap ,更优化的线程安全哈希表。Hashtable 保证线程安全,主要就是给关键方法,加上 synchronized.直接加到方法上的.(相当于给 this加锁)
只要两个线程,在操作同一个 Hashtable 就会出现锁冲突~~但是,实际上,对于哈希表来说,锁不一定非得这么加, 有些情况,其实是不涉及到线程安全问题的~~l链地址法解决哈希冲突, ConcurrentHashMap 将大锁变为小锁,每个哈希桶都有一把锁(每个链表都是独立的锁),只有两个线程访问的恰好是同一个哈希桶上的数据才出现锁冲突,降低锁冲突概率。

把每个链表的头结点当做锁对象。

在java8前ConcurrentHashMap使用分段锁实现,java8之后,就变成了直接在链表头结点加锁的形式将原来 数组 + 链表 的实现方式改进成 数组 + 链表 / 红黑树 的方式. 当链表较长的时候(大于等于 8 个元素)就转换成红黑树.

目的都是降低锁冲突,分段锁是多个链表共用一把锁。

锁分段:这个是 Java1.7 中采取的技术 . Java1.8 中已经不再使用了 . 简单的说就是把若干个哈希桶分成一个 "段 " (Segment), 针对每个段分别加锁 . 目的也是为了降低锁竞争的概率. 当两个线程访问的数据恰好在同一个段上的时候 , 才触发锁竞争

充分利用CAS: 

减小不必要的加锁操作,在维护元素个数中使用。

读不加锁,写加锁 :

读操作没有加锁. 目的是为了进一步降低锁冲突的概率. 为了保证读到刚修改的数据, 搭配了

volatile 关键字(ConcurrentHashMap使用了volatile+原子的写操作维护线程安全)

读和读之间没有冲突,读和写之间也没有冲突(不会读到修改到一半的值)写和写之间有冲突,...其实很多场景下,读写之间不加锁控制,可能会读到写了一半的数据,相当于脏读了.写操作不是原子的。

加锁的方式仍然 是用 synchronized, 但是不是锁整个对象, 而是 "锁桶" (用每个链表的头结点作为锁对象), 大大降 低了锁冲突的概率

优化扩容:

本身 Hashtable 或者 HashMap 在扩容的时候,都是需要把所有的元素都拷贝一遍的(如果元素很多,拷贝就比较耗时)
用户访问 1000 次,999 次都很流畅,其中有一次就 卡了.(正好这一次触发扩容, 导致出现卡顿)
化整为零
一旦需要扩容,确实需要搬运,不是在一次操作中搬运完成,而是分成多次,来搬运.每次只搬运一部分数据避免这单次操作过于卡顿 

HashMap: 线程不安全. key 允许为 null,(如果 key 不允许为 null,那么在比较 key 值的时候,需要判断两个 key 是否相等。而判断两个 key 是否相等的过程,通常是通过调用 key 的 equals() 方法来实现的。如果 key 为 null,则在调用 equals() 方法时可能会出现空指针异常。因此,为了避免这种异常,HashMap 允许 key 为 null。这样,如果一个键的哈希码与其他键冲突且该键为 null,则可以直接将该键值对添加到对应的链表中,而不必进行额外的比较。)

Hashtable: 线程安全. 使用 synchronized 锁 Hashtable 对象, 效率较低. key 不允许为 null.

ConcurrentHashMap: 线程安全. 使用 synchronized 锁每个链表头结点, 锁冲突概率低, 充分利用CAS 机制. 优化了扩容方式. key 不允许为 null

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sqyaa.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值