目录
[1.信号量 Semaphore](#1.信号量 Semaphore)
[3.Hashtable,HashMap, ConcurrentHashMap](#3.Hashtable,HashMap, ConcurrentHashMap)
1.信号量 Semaphore
**Semaphone 是 java.util.concurrent包下用于控制并发访问资源数量的工具类,可以限制同时访问资源的的线程数量。**在Semaphone里面有一个计数器用于统计数量。
在Semaphone类下面提供了acquire()和release()方法,分别用于加减的操作。
代码演示:
java
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(2);
//创建五个线程
for (int i = 0; i < 5; i++) {
final int id = i;
new Thread(()->{
try{
//申请资源
System.out.println("申请资源。");
semaphore.acquire();
Thread.sleep(2000);
System.out.println("执行代码。");
semaphore.release();
System.out.println("释放资源。");
}catch(InterruptedException e){
e.printStackTrace();
}
}).start();
}
}
执行结果:
上述代码中我们创建了五个线程,五个线程同时执行代码,我们观察运行结果,我们发现先打印了五个"申请资源",之后呢打印了两个"执行代码"。那是因为我们创建的Semaphone初始量是2个,所以呢我们必要要等到里面存储的两个进行释放后,在进行执行其他线程阻塞等待的代码。
Semaphone里面的availablePermits() 方法是返回当前可用的许可数量。
2.CountDownLatch
CountDownLatch是JAVA并发包 java.util.concurrent 中的一个工具类,用于执行多个线程之间的执行顺序。我们可以将其想象成一个倒计电闸门,每当有一个线程完成任务就将计数器减1,当计数器的数值为0的时候,门闸就会打开,其他的线程就会继续执行。
代码展示:
java
public static void main(String[] args) throws InterruptedException{
//计数器初始值为10
CountDownLatch latch = new CountDownLatch(10);
Object lock = new Object();
//创建10个线程
for (int i = 0; i < 10; i++) {
final int id = i;
new Thread(()->{
try{
System.out.println("执行前:" + id);
Thread.sleep(1000);
System.out.println("执行后:" + id);
}catch(InterruptedException e){
e.printStackTrace();
}finally {
latch.countDown();
}
}).start();
}
latch.await();
System.out.println("执行结束。");
}
上述代码中我们可以看到创建了10个线程,每次执行了之后都会使用 latch.countDown() 进行计数器-1,主线程中使用的是latch.await(),阻塞等待所有的任务执行完毕。
3.Hashtable,HashMap, ConcurrentHashMap
Hashtable,HashMap, ConcurrentHashMap都是用于存储键值对的数据结构,不过它们三个还是有很大差别的。
1.HashMap
我们在学习数据结构的时候肯定是接触到了hashmap,这是一个效率很高的数据结构进行查找的时候。但是呢它是线程不安全的。我们在学习的时候都知道是在每一个数组后面都链着一个链表,当我们在多线程的情况下要不进行修改的时候,就会很明显的发生线程不安全的情况;如果多个线程同时对 HashMap 进行读写操作,可能会导致数据不一致、死循环等问题。
在性能方面的话由于不需要进行同步操作,在单线程环境下性能较高,操作速度快。
2.Hashtable
**在对于Hashtable来说,hashmap的情况下进行了改动,使其成为了线程安全的,其方法使用了synchronized锁进行同步,保证了在多线程情况操作的原子性。**但是它存在一个很明显的缺点,就是开销很大,同一时刻只有一个线程能访问hashtable的方法。
这就相当于对数组的整个整体进行了加锁,就只有一个,当两个线程同时访问的时候,就会发生锁竞争的情况。
3.ConcurrentHashMap
ConcurrentHashMp相对于Hashtable来说的话就是在其加锁的基础上面再次进行优化和改进。
**对于读操作来说的话,没有进行加锁;只对于写操作进行了加锁,加锁的方式依旧是synchronized,但是不是对于整个数组进行的加锁,而是对于每个链表的头结点进行的加锁,和hashmap相比的话,就大大降低了锁冲突的概率。**这是优化的一点,**对于size属性来说通过CAS来进行更新,尽量避免出现重量级锁的情况。**还有就是在进行扩容的时候,进行了化整为零的操作,就是当发现需要进行扩容的时候,只需要创建一个新的数组,同时只搬运几个元素过去,再扩容的过程中,新旧数组是同时存在的,在后续进行ConcurrentHashMap操作的时候,在进行搬运,每次只搬运一点点。
4.死锁
4.1死锁是什么。
在 Java 里,死锁是多线程编程中一种严重的问题。当两个或多个线程互相持有对方所需的资源,并且都在等待对方释放资源才能继续执行,从而陷入无限等待的状态,就形成了死锁。简单来说,就是每个线程都在等待其他线程释放资源,导致所有线程都无法继续执行。
下面给大家举一个例子:
就相当于我们想尽自己的家门,然后呢我们找不到钥匙;想到了我们的钥匙在车里面,当我们找车钥匙的时候发现钥匙放到了家里面,这就形成了一个死循环。我们就没有办法进到车里面去
下面展示一下死锁的代码。
代码展示:
java
private static Object lock1 = new Object();
private static Object lock2 = new Object();
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(()->{
synchronized (lock1){
System.out.println("执行锁1,对象1.");
try{
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println("执行锁1,对象2");
synchronized (lock2){
System.out.println("具体执行4");
}
}
});
Thread t2 = new Thread(()->{
synchronized (lock2){
System.out.println("执行锁2,对象1");
try{
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println("执行锁2,对象2");
synchronized (lock1){
System.out.println("具体执行3");
}
}
});
t1.start();
t2.start();
}
运行结果展示:

我们观察上述代码的运行结果,发现了代码处于僵持的阶段,一直没有结束。下面是我们更改之后的锁,这个不会产生死锁。
代码展示:
java
private static Object lock1 = new Object();
private static Object lock2 = new Object();
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(()->{
synchronized (lock1){
System.out.println("执行锁1,对象1.");
try{
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println("执行锁1,对象2");
synchronized (lock2){
System.out.println("具体执行4");
}
}
});
Thread t2 = new Thread(()->{
synchronized (lock1){
System.out.println("执行锁2,对象1");
try{
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println("执行锁2,对象2");
synchronized (lock2){
System.out.println("具体执行3");
}
}
});
t1.start();
t2.start();
}
运行结果:

4.2死锁产生的条件。
死锁的产生需要同时满足以下四个条件:
互斥条件:资源在同一时刻只能被一个线程使用。
请求与保持条件 :线程已经持有了至少一个资源,又在请求其他线程持有的资源,并且在等待过程中不释放已持有的资源。
不剥夺条件:线程持有的资源在使用完之前,不能被其他线程强行剥夺,只能由持有线程自己释放。
4**. 循环等待条件** :多个线程之间形成一种头尾相连的循环等待资源的关系。
4.3如何避免产生死锁。
1.破坏请求与保持条件。
我们可以采用一次性分配所有资源的策略,即线程在执行前一次性请求所需的所有资源,如果无法获取全部资源,即释放已持有的资源并等待。
2.破坏不剥夺条件。
使用可剥夺的锁,例如:reentrantlock 的trylock 方法,线程在尝试获取资源失败的时候,可以主动释放已持有的资源。
3.检测和恢复。
可以通过算法定期检测系统中是否存在死锁,如果发现死锁,则采取措施恢复,例如终止某些线程或剥夺某些线程的资源。不过 Java 本身没有直接提供死锁检测的机制,需要开发者自己实现。
4.破坏循环等待条件。
**(这个是最容易实现的)**对资源进行排序,所有线程按照相同的顺序请求资源,这样就不会形成循环等待。
这个代码我在前面说明死锁的时候就已经进行了代码演示,大家可以在前面看。