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 包下的一个类,它主要包括两个常用的方法:
- acquire():获取一个许可证,如果没有许可证可用,则会阻塞直到有许可证为止。申请资源P
- 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 的关键操作上都带有 synchronized3.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