本文纲要
HashMap线程不安全演示Hashtable线程安全与原理ConcurrentHashMap基本使用ConcurrentHashMap 1.7原理(分段锁)ConcurrentHashMap 1.8原理(CAS+synchronized)CountDownLatch使用场景与原理Semaphore使用场景与原理
HashMap线程不安全演示
1 ) 项目结构
src/
└── com/
└── wb/
├── mymap/
│ ├── MyHashMapDemo.java
│ ├── MyHashtableDemo.java
│ └── MyConcurrentHashMapDemo.java
├── mycountdownlatch/
│ ├── MotherThread.java
│ ├── ChileThread1.java
│ ├── ChileThread2.java
│ ├── ChileThread3.java
│ └── MyCountDownLatchDemo.java
└── mysemaphore/
├── MyRunnable.java
└── MySemaphoreDemo.java
在多线程环境下,HashMap 因为线程抢夺CPU的随机性,会出现数据安全问题。
例如两个线程同时 put 数据,可能导致某个键的值为 null。
package com.wb.mymap;
import java.util.HashMap;
public class MyHashMapDemo {
public static void main(String[] args) throws InterruptedException {
HashMap<String, String> hm = new HashMap<>();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 25; i++) {
hm.put(i + "", i + "");
}
});
Thread t2 = new Thread(() -> {
for (int i = 25; i < 51; i++) {
hm.put(i + "", i + "");
}
});
t1.start();
t2.start();
System.out.println("----------------------------");
// 为了t1和t2能把数据全部添加完毕
Thread.sleep(1000);
// 预期输出: 0 1 2 3 ... 50
for (int i = 0; i < 51; i++) {
System.out.println(hm.get(i + ""));
}
}
}
运行结果可能在某处打印 null,说明 HashMap 在多线程下不安全。
Hashtable线程安全与原理
Hashtable 可以保证线程安全,但效率极低,因为它在几乎所有方法上都加了 synchronized 关键字,采用悲观锁,操作时会将整张表锁住。
package com.wb.mymap;
import java.util.Hashtable;
public class MyHashtableDemo {
public static void main(String[] args) throws InterruptedException {
Hashtable<String, String> hm = new Hashtable<>();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 25; i++) {
hm.put(i + "", i + "");
}
});
Thread t2 = new Thread(() -> {
for (int i = 25; i < 51; i++) {
hm.put(i + "", i + "");
}
});
t1.start();
t2.start();
System.out.println("----------------------------");
Thread.sleep(1000);
for (int i = 0; i < 51; i++) {
System.out.println(hm.get(i + ""));
}
}
}
多次运行结果均完整,不会出现 null。
Hashtable锁机制示意
小结: Hashtable 底层是哈希表结构(数组+链表),默认长度16,加载因子0.75。因为它对所有访问方法使用 synchronized,一旦有线程访问,整张表被锁,其他线程只能等待,效率低下。
ConcurrentHashMap基本使用
JDK1.5 提供了 ConcurrentHashMap,它既保证线程安全,又兼顾效率。它实现了 Map 接口,可以使用 put、keySet、entrySet 等方法。
package com.wb.mymap;
import java.util.concurrent.ConcurrentHashMap;
public class MyConcurrentHashMapDemo {
public static void main(String[] args) throws InterruptedException {
ConcurrentHashMap<String, String> hm = new ConcurrentHashMap<>();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 25; i++) {
hm.put(i + "", i + "");
}
});
Thread t2 = new Thread(() -> {
for (int i = 25; i < 51; i++) {
hm.put(i + "", i + "");
}
});
t1.start();
t2.start();
System.out.println("----------------------------");
Thread.sleep(1000);
for (int i = 0; i < 51; i++) {
System.out.println(hm.get(i + ""));
}
}
}
运行结果数据完整,无 null,同时效率高于 Hashtable。
ConcurrentHashMap 1.7原理
在JDK1.7中,ConcurrentHashMap 采用分段锁机制,内部由 Segment 数组和 HashEntry 数组组成。
1 ) 创建对象
- 创建一个长度为16的
Segment数组,加载因子0.75。 - 同时创建一个长度为2的小数组(
HashEntry),并将其地址赋给Segment[0],其他索引为null。该小数组作为模板。
2 ) 添加元素流程
- 根据键的哈希值计算在
Segment数组中的索引(第一次哈希)。 - 若该索引为
null,则按照模板创建新的HashEntry数组(默认长度2),并赋值给该索引。 - 进行二次哈希,计算元素在
HashEntry数组中的位置。 - 若该位置为
null,直接存入;若不为null,则调用equals比较,相同则覆盖,不同则形成链表(哈希桶)。 - 当
HashEntry数组元素达到阈值(长度×0.75)时,会扩容为原来的2倍(Segment数组本身不扩容)。
3 ) 线程安全机制
- 每个
Segment对象都有独立的锁,ConcurrentHashMap通过对Segment加锁实现并发控制。 - 默认16个
Segment,最多允许16个线程同时操作不同的Segment。
小结:
- 大数组(
Segment)创建后不可扩容,小数组(HashEntry)可扩容2倍。 - 通过分段锁(
ReentrantLock)保证线程安全,不同Segment可并发操作。
ConcurrentHashMap 1.8原理
JDK1.8改进了 ConcurrentHashMap,底层采用数组+链表+红黑树结构,与 HashMap 1.8类似,但使用 CAS + synchronized 保证线程安全。
1 ) 初始化
- 空参构造什么也不做,延迟初始化,在第一次
put时创建数组(默认容量16,加载因子0.75)。
2) 添加元素流程
- 根据键的哈希值计算数组索引。
- 若该索引为
null,使用CAS算法直接将节点放入数组。 - 若不为
null,则使用volatile关键字获取该位置的最新节点,并采用synchronized锁住该节点(链表头节点或红黑树根节点)。 - 若是链表,遍历并比较
equals,相同则替换,不同则插入链表尾部;若链表长度 ≥ 8,转换为红黑树。 - 若是红黑树,按树的方式插入。
3 )线程安全机制
- 不锁整张表,只锁当前操作的链表或红黑树头节点,其他线程可以同时操作其他桶。
- 使用
synchronized代码块锁住头节点对象。
小结:
- 空参构造延迟初始化,首次
put创建数组。 - 通过
CAS操作向空桶添加节点。 - 通过
synchronized锁住桶头节点,实现细粒度并发控制。 - 链表长度 ≥ 8 时转为红黑树,提升查询效率。
CountDownLatch使用场景与原理
CountDownLatch 允许一条线程等待其他多条线程执行完毕后再执行。
经典场景:妈妈等待三个孩子吃完饺子后收拾碗筷。
1 )核心方法
| 方法 | 说明 |
|---|---|
CountDownLatch(int count) | 构造方法,count 为等待线程数量(底层定义计数器) |
await() | 让当前线程等待,直到计数器变为0 |
countDown() | 每调用一次,计数器减1;当计数器为0时,唤醒等待线程 |
2 ) 代码实现
package com.wb.mycountdownlatch;
public class MotherThread extends Thread {
private CountDownLatch countDownLatch;
public MotherThread(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
// 1.等待
try {
// 当计数器变成0时,会自动唤醒这里等待的线程
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 2.收拾碗筷
System.out.println("妈妈在收拾碗筷");
}
}
```java
package com.wb.mycountdownlatch;
public class ChileThread1 extends Thread {
private CountDownLatch countDownLatch;
public ChileThread1(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
// 1.吃饺子
for (int i = 1; i <= 10; i++) {
System.out.println(getName() + "在吃第" + i + "个饺子");
}
// 2.吃完说一声
// 每一次countDown方法的时候,就让计数器-1
countDownLatch.countDown();
}
}
其余孩子线程(ChileThread2、ChileThread3)类似,分别吃15个和20个饺子。
package com.wb.mycountdownlatch;
import java.util.concurrent.CountDownLatch;
public class MyCountDownLatchDemo {
public static void main(String[] args) {
// 1.创建CountDownLatch对象,需要传递给四个线程。
// 在底层定义了一个计数器,此时计数器的值为3
CountDownLatch countDownLatch = new CountDownLatch(3);
// 2.创建四个线程对象并开启
MotherThread motherThread = new MotherThread(countDownLatch);
motherThread.start();
ChileThread1 t1 = new ChileThread1(countDownLatch);
t1.setName("小明");
ChileThread2 t2 = new ChileThread2(countDownLatch);
t2.setName("小红");
ChileThread3 t3 = new ChileThread3(countDownLatch);
t3.setName("小刚");
t1.start();
t2.start();
t3.start();
}
}
3 ) 执行流程
小结: CountDownLatch 通过内部计数器实现线程等待,一次递减,计数器为0时唤醒等待线程,常用于主线程等待子线程完成的场景。
Semaphore使用场景与原理
Semaphore 用于控制同时访问特定资源的线程数量,如同单行道最多允许2辆车同时通行,发通行证的管理员。
1 ) 核心方法
| 方法 | 说明 |
|---|---|
Semaphore(int permits) | 构造方法,permits 为最大通行证数量(允许同时执行的线程数) |
acquire() | 获得通行证,若无则线程等待 |
release() | 归还通行证,唤醒等待线程 |
2 ) 代码实现
package com.wb.mysemaphore;
import java.util.concurrent.Semaphore;
public class MyRunnable implements Runnable {
// 1.获得管理员对象,最多允许2个线程同时执行
private Semaphore semaphore = new Semaphore(2);
@Override
public void run() {
// 2.获得通行证
try {
semaphore.acquire();
// 3.开始行驶
System.out.println("获得了通行证开始行驶");
Thread.sleep(2000);
System.out.println("归还通行证");
// 4.归还通行证
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
java
package com.wb.mysemaphore;
public class MySemaphoreDemo {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();
for (int i = 0; i < 100; i++) {
new Thread(mr).start();
}
}
}
3 ) 执行效果
运行结果:最多同时有2个线程打印“获得了通行证开始行驶”,执行完归还后,下一个线程才能进入。
总结
Semaphore 通过许可证数量控制并发数,使用时先 acquire() 获取许可,执行完毕 release() 归还,保证同时访问资源的线程数不超过指定值。
以上是 Java 并发工具类中 Hashtable、ConcurrentHashMap、CountDownLatch 和 Semaphore 的核心知识点,通过原理图解和代码示例帮助快速入门。

827

被折叠的 条评论
为什么被折叠?



