【Java ee 初阶】多线程(9)上

一、信号量Semaphore

本质上就是一个计数器,描述了一种"可用资源"的个数

申请资源(P操作):使得计数器-1

释放资源(V操作):使得计数器+1

如果计数器为0了,继续申请资源,就会触发阻塞

上述+1 -1 这些操作,都是原子的

Java把操作系统提供的信号量进行封装

java 复制代码
package Thread;

import java.util.concurrent.Semaphore;

public class demo4 {

    public static void main(String[] args) throws InterruptedException {
        
        Semaphore semaphore = new Semaphore(3);

        semaphore.acquire();
        System.out.println("执行P操作");
        semaphore.acquire();
        System.out.println("执行P操作");
        semaphore.acquire();
        System.out.println("执行P操作");
        semaphore.acquire();
        System.out.println("执行P操作");

    }
}

最后输出:

java 复制代码
package Thread;

import java.util.concurrent.Semaphore;

public class demo4 {

    public static void main(String[] args) throws InterruptedException {
        
        Semaphore semaphore = new Semaphore(3);

        semaphore.acquire();
        System.out.println("执行P操作");
        semaphore.release();
        System.out.println("执行V操作");
    }
}

输出:

通过信号量来实现原子操作:

java 复制代码
package Thread;

import java.util.concurrent.Semaphore;

public class demo5 {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
    Semaphore semaphore = new Semaphore(1);
    Thread t1 = new Thread(()->{
        for(int i=0;i<5000;i++){
            try {
                semaphore.acquire();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            count++;
            semaphore.release();
        }  
    });
    
    Thread t2 = new Thread(()->{
        for(int i=0;i<5000;i++){
            try {
                semaphore.acquire();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            count++;
            semaphore.release();
        }
    });
    t1.start();
    t2.start();
    t1.join(); 
    t2.join();
    System.out.println(count);
}
}

输出:

信号量,相当于"锁"概念的进一步延申。锁,可以视为是"初始值为1"的特殊信号量。

小结:编写线程安全代码的时候:

1.加锁(最主要)

2.CAS/原子类

3.信号量

java 复制代码
package Thread;

import java.util.concurrent.CountDownLatch;

public class demo6 {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(8);
        for(int i=0;i<8;i++){
            int id =i;
            Thread t = new Thread(()->{
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();}
                    System.out.println("线程"+id+"执行完毕");
                    latch.countDown();//计数器减1
            });
           t.start();
                    }
                    //主线程通过await等待所有线程执行完毕
                    //awaur阻塞,直到countDownLatch调用latch.countDown()减为0
         latch.await();
    }

}

二、多线程环境使用ArrayList

三、多线程环境使用哈希表

HashMap线程不安全

解决方案:

1)自己加锁

2)Hashtable类似于Vector,在关键方法加了synchronized

java 复制代码
package Thread;

import java.util.Hashtable;

public class demo7 {

    public static void main(String[] args) {
        
        Hashtable<String,String> hashtable = new Hashtable<>();
        hashtable.put("k1", "v1");
        hashtable.put("k2", "v2");
        hashtable.put("k3", "v3");
        hashtable.get("v1");
    }
}

但是不推荐使用,因为这是个JDK即将要废弃的方案

3)ConcurrentHashMap

ConcurrentHashMap,最大的调整就是针对锁的粒度进行了优化

Hashtable来说,针对this加锁,任何一个线程,只要操作你这个hash表就可能触发锁竞争

两个线程,针对同一个变量进行修改,所以对于哈希表的操作来说,如果两个线程的修改,是在不同的链表上,本身就是线程安全的。只需要针对同一个链表的修改,才引入阻塞。

ConcurrentHashMap使用了锁桶方案,使竞争更小。实践中,一个hash表,桶的个数非常多,针对哈希表元素的操作,大概率是分布在不同的桶上真正触发锁竞争的情况是非常小的,几乎忽略不计。

那么问题来了,ConcurrentHashMap多引入这些锁,是否会有额外的"空间开销",Java任意对象都可以作为锁对象。实际上直接拿每个链表的头结点作为锁对象即可。

size随着put,remove触发++ 和--

ConcurrentHashMap采取了原子类的方案,基于CAS操作,针对size

ConcurrentHashMap扩容的时候,采取"化整为零的方案"

扩容:搞更大的数组,把原来数组的所有链表元素,重新hash到新数组的链表上。元素本身元素特别多,那么扩容开销就很大。

进行上述搬运的过程中,为了保证线程安全,当然是得加锁的。如果全部进行搬运,持有锁的的实践就会特别长,导致其他线程无法正常使用哈希表了。

因此,ConcurrentHashMap在扩容的时候,不会一股脑把所有的键值全部搬运过去,而是每次都只搬运一点点,以确保这单次搬运的速度足够快,持有锁的实践足够段,一旦触发搬运,每次进行get,put,remove...都会搬运一点。

*假如对size进行加锁,是不是相当于又对this整体加锁:整个哈希表就一个size变量,所有针对size操作的线程就都会引起锁竞争了。前面锁桶方案带来的提升,就被稀释掉了。

相关推荐
AI你一生一世2 小时前
不止于时序存储:DolphinDB 如何成为 AI 时代的工业智能底座
java·人工智能·struts·时序数据库·工业智能·dolphindb·ai基础设施
云-缩影2 小时前
拆解 AI 图像生成应用的后端架构:ECS、OSS 和大模型 API 怎么配合
java·spring boot·后端
瑞克-桑切斯2 小时前
7、MySQL 慢查询排查与优化实战指南
java·服务器·数据库
海的透彻6 小时前
dockerfile如何快速构建容器(java服务)
java·开发语言
code2cat7 小时前
Java工具篇之Gradle:用任务图组织构建与交付
java·开发语言·单元测试·gradle
后台模板学习7 小时前
大模型 Prompt 工程在电商订单系统中的踩坑记录
java·python·prompt
w_zero_one7 小时前
算法--构造
java·数据结构·算法
SL_staff8 小时前
制造业订单交付崩盘?用四层协同框架重建交付可信度(技术实践)
java·产品·全栈
SL_staff9 小时前
打破风控迭代死循环:用决策编排重构业务与技术协作链路
java·产品·全栈
Mikko79 小时前
Redis 连接数满了服务端有错误信息吗?连接数不增加却命令超时?6.2 实测连接池、超时与大 key 怎么判定
java·spring boot·redis·后端