Java多线程编程(七)——并发工具类
HashMap、Hashtable
HashMap的用法
HashMap<String,String> hm = new HashMap<>();
Hashtable的用法
Hashtable<String,String> hm = new Hashtable<>();
Hashtable出现的原因
在集合类中HashMap是比较常用的集合对象,但是HashMap是线程不安全的(多线程环境下可能会存在问题)。为了保证数据的安全性我们可以使用Hashtable,但是Hashtable的效率低下。
详细案例
使用HashMap就会出现线程不安全的情况,代码样例与运行结果如下:
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("---------------");
Thread.sleep(1000);
for (int i = 0; i < 51; i++) {
System.out.println(hm.get(i+""));
}
}
}
运行结果
如上图所示,出现了很多了null,就是因为多线程异步所导致。
如果使用了Hashtable就不会出现这样的结果,这里我们可以看一下Hashtable的源码,Ctrl+B查看
显然,很容易发现,在Hashtable的源码中使用的方法都是用同步代码块的方式(即悲观锁)进行上锁,如下图只要有一个线程进入,就会将整个表锁起来!
这样也就导致了Hashtable效率十分低下!
问:有没有线程安全又高效的方法呢?
有!使用ConcurrentHashMap
ConcurrentHashMap
ConcurrentHashMap用法
ConcurrentHashMap<String,String> hm2 = new ConcurrentHashMap<>();
ConcurrentHashMap出现的原因
在集合类中HashMap是比较常用的集合对象,但是HashMap是线程不安全的(多线程环境下可能会存在问题)。为了保证数据的安全性我们可以使用Hashtable,但是Hashtable的效率低下。综合考虑之下,我们引入了ConcurrentHashMap。
体系结构
注意在ConcurrentHashMap的实现在JDK7和JDK8上是完全不同的。
JDK7上的ConcurrentHashMap
1.首先在创建对象的时候,先创建了一个16个单位的Segment[ ]的数组
2.再创建长度为2个单位的小数组HashEntey[ ],把HashEntry的地址值赋值给Segment的0号区域。
3.根据键的哈希值计算出Segment数组的索引
4.如果此处的Segment数组为null,就默认创建一个长度为2个单位的数组(即HashEntry)
5.再根据键的哈希值计算出在小数组应存入的索引值(二次哈希),如果为空直接添加。
6.如果此处Segment数组的值不为null,则会二次哈希,计算出该元素在小数组中的位置,如哈希值相同,则会形成Hash桶结构。
7. 若再添加的值,计算出的哈希值仍然为0.75,由于加载因子为0.75,所以要对其进行扩容(每次宽容2倍)
总上所述
JDK8上的ConcurrentHashMap
1,如果使用空参构造创建ConcurrentHashMap对象,则什么事情都不做。 在第一次添加元素的时候创建哈希表
2,计算当前元素应存入的索引。
3,如果该索引位置为null,则利用cas算法,将本结点添加到数组中。
4,如果该索引位置不为null,则利用volatile关键字获得当前位置最新的结点地址,挂在他下面,变成链表。
5,当链表的长度大于等于8时,自动转换成红黑树6,以链表或者红黑树头结点为锁对象,配合悲观锁保证多线程操作集合时数据的安全性
此处分享一个方法,先进入ConcurrentHashMap,选择空参构造,再Ctrl + Shift + Alt +U ,查看继承关系
CountDownLatch
常用方法
public CountDownLatch(int count) | 参数传递线程数,表示等待线程数量 |
---|---|
方法 | 解释 |
public void await() | 让线程等待 |
public void countDown() | 当前线程执行完毕 |
使用场景
让某一条线程等待其他线程执行完毕之后再执行
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();
}
}
public class ChileThread2 extends Thread {
private CountDownLatch countDownLatch;
public ChileThread2(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
//1.吃饺子
for (int i = 1; i <= 15; i++) {
System.out.println(getName() + "在吃第" + i + "个饺子");
}
//2.吃完说一声
//每一次countDown方法的时候,就让计数器-1
countDownLatch.countDown();
}
}
public class ChileThread3 extends Thread {
private CountDownLatch countDownLatch;
public ChileThread3(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
//1.吃饺子
for (int i = 1; i <= 20; i++) {
System.out.println(getName() + "在吃第" + i + "个饺子");
}
//2.吃完说一声
//每一次countDown方法的时候,就让计数器-1
countDownLatch.countDown();
}
}
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("妈妈在收拾碗筷");
}
}
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();
}
}
总结
1. CountDownLatch(int count):参数写等待线程的数量。并定义了一个计数器。
2. await():让线程等待,当计数器为0时,会唤醒等待的线程
3. countDown(): 线程执行完毕时调用,会将计数器-1。
Semaphore
使用场景
可以控制访问特定资源的线程数量。
案例实现
1,需要有人管理这个通道
2,当有车进来了,发通行许可证
3,当车出去了,收回通行许可证
4,如果通行许可证发完了,那么其他车辆只能等着
public class MyRunnable implements Runnable {
//1.获得管理员对象,
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();
}
}
}
public class MySemaphoreDemo {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();
for (int i = 0; i < 100; i++) {
new Thread(mr).start();
}
}
}