Java基础快速入门: 并发工具类详解之Hashtable与ConcurrentHashMap原理、安全机制及代码实践

本文纲要

  1. HashMap线程不安全演示
  2. Hashtable线程安全与原理
  3. ConcurrentHashMap基本使用
  4. ConcurrentHashMap 1.7原理(分段锁)
  5. ConcurrentHashMap 1.8原理(CAS + synchronized
  6. CountDownLatch使用场景与原理
  7. 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

锁住

等待

释放锁

线程1

整张表

线程2

线程2操作

小结: Hashtable 底层是哈希表结构(数组+链表),默认长度16,加载因子0.75。因为它对所有访问方法使用 synchronized,一旦有线程访问,整张表被锁,其他线程只能等待,效率低下。

ConcurrentHashMap基本使用

JDK1.5 提供了 ConcurrentHashMap,它既保证线程安全,又兼顾效率。它实现了 Map 接口,可以使用 putkeySetentrySet 等方法。

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 ) 添加元素流程

  1. 根据键的哈希值计算在 Segment 数组中的索引(第一次哈希)。
  2. 若该索引为 null,则按照模板创建新的 HashEntry 数组(默认长度2),并赋值给该索引。
  3. 进行二次哈希,计算元素在 HashEntry 数组中的位置。
  4. 若该位置为 null,直接存入;若不为 null,则调用 equals 比较,相同则覆盖,不同则形成链表(哈希桶)。
  5. HashEntry 数组元素达到阈值(长度×0.75)时,会扩容为原来的2倍(Segment 数组本身不扩容)。

3 ) 线程安全机制

  • 每个 Segment 对象都有独立的锁,ConcurrentHashMap 通过对 Segment 加锁实现并发控制。
  • 默认16个 Segment,最多允许16个线程同时操作不同的 Segment

锁机制

ConcurrentHashMap1.7

锁住

锁住

等待S1

Segment数组

Segment0: HashEntry数组

Segment1: null

...

HashEntry

链表节点

链表节点

线程1

线程2

线程3

小结:

  • 大数组(Segment)创建后不可扩容,小数组(HashEntry)可扩容2倍。
  • 通过分段锁(ReentrantLock)保证线程安全,不同 Segment 可并发操作。

ConcurrentHashMap 1.8原理

JDK1.8改进了 ConcurrentHashMap,底层采用数组+链表+红黑树结构,与 HashMap 1.8类似,但使用 CAS + synchronized 保证线程安全。

1 ) 初始化

  • 空参构造什么也不做,延迟初始化,在第一次 put 时创建数组(默认容量16,加载因子0.75)。

2) 添加元素流程

  1. 根据键的哈希值计算数组索引。
  2. 若该索引为 null,使用 CAS 算法直接将节点放入数组。
  3. 若不为 null,则使用 volatile 关键字获取该位置的最新节点,并采用 synchronized 锁住该节点(链表头节点或红黑树根节点)。
  4. 若是链表,遍历并比较 equals,相同则替换,不同则插入链表尾部;若链表长度 ≥ 8,转换为红黑树。
  5. 若是红黑树,按树的方式插入。

3 )线程安全机制

  • 不锁整张表,只锁当前操作的链表或红黑树头节点,其他线程可以同时操作其他桶。
  • 使用 synchronized 代码块锁住头节点对象。

锁机制

ConcurrentHashMap1.8

synchronized

synchronized

等待D

数组

桶0: 链表

桶1: 红黑树

节点1

节点2

根节点

子节点

线程1

线程2

线程3

小结:

  • 空参构造延迟初始化,首次 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 ) 执行流程

小刚小红小明妈妈线程小刚小红小明妈妈线程await() 等待吃饺子(10个)countDown() 计数器2吃饺子(15个)countDown() 计数器1吃饺子(20个)countDown() 计数器0被唤醒,收拾碗筷

小结: 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 ) 执行效果

100个线程

Semaphore允许2个通过

线程1执行

线程2执行

release

release

唤醒等待线程

新线程进入

运行结果:最多同时有2个线程打印“获得了通行证开始行驶”,执行完归还后,下一个线程才能进入。

总结

Semaphore 通过许可证数量控制并发数,使用时先 acquire() 获取许可,执行完毕 release() 归还,保证同时访问资源的线程数不超过指定值。

以上是 Java 并发工具类中 HashtableConcurrentHashMapCountDownLatchSemaphore 的核心知识点,通过原理图解和代码示例帮助快速入门。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Wang's Blog

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

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

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

打赏作者

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

抵扣说明:

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

余额充值