多线程进阶篇

news2024/12/24 14:41:07

多线程进阶篇

文章目录

  • 多线程进阶篇
    • 1、常见的锁策略
      • 1) 乐观锁 vs 悲观锁
      • 2) 重量级锁 vs 轻量级锁
      • 3) 自旋锁 vs 挂起等待锁
      • 4) 读写锁 vs 互斥锁
      • 5) 公平锁 vs 非公平锁
      • 6) 可重入锁 vs 不可重入锁
    • 2、死锁
      • 1) 死锁的三种典型情况:
      • 2) 如何解决死锁问题
      • 3) 死锁产生的必要条件
    • 3、Synchronized 采用策略
      • 1) 偏向锁
      • 2) 轻量级锁
      • 3) 其他优化操作
        • 1.锁消除
        • 2.锁粗化
    • 4、CAS
      • 1) CAS的应用
        • 1. 实现原子类
        • 2. 实现自旋锁
      • 2) CAS 的 ABA 问题
      • 3) 解决方案
    • 5、Callable 接口
    • 6、JUC
      • 1) ReentrantLock 可重入锁
      • 2) 原子类的应用场景
      • 3) 信号量 Semaphore
      • 4) CountDownLatch
    • 7、集合类
      • 1) 多线程环境使用 ArrayList
      • 2) 多线程环境使用队列
      • 3) 多线程环境使用哈希表

1、常见的锁策略

这里讨论的锁策略不仅仅局限于 Java,此篇幅主要是认识几种常见的锁策略,能够知道概念。

接下来提及到的都不是某个具体的锁,而是抽象的概念。

描述的是锁的特性,描述的是“一类锁”。


1) 乐观锁 vs 悲观锁

二者都是对后续场景中的锁冲突现象进行一个预估。

乐观锁:预测后续的场景中,不会出现很多锁冲突的现象。(后续的工作会更少

悲观锁:预测后续的场景中,很容易出现锁冲突的现象。(后续会做出更多的工作来保证线程安全

锁冲突:两个线程尝试获取同一把锁,一个线程能获取成功,另一个线程阻塞等待。

锁冲突的概率大还是小,对后续的工作,是有一定影响的。


2) 重量级锁 vs 轻量级锁

重量级锁:加锁的开销是比较大的(花的时间多,占用系统资源多)

轻量级锁:加锁开销比较小的(花的时间少,占用系统资源少)

乐观悲观锁 vs 重量轻量锁

乐观悲观锁,是在加锁之前,对锁冲突概率的预测,决定工作的多少。

重量轻量,是在加锁之后,考量实际的锁的开销。

正是因为这样的概念存在重合,针对一个具体的锁,可能把它叫做乐观锁,也可能叫做轻量锁。但是此观点是不绝对的,反之也成立。


3) 自旋锁 vs 挂起等待锁

自旋锁:是轻量级锁的一种典型实现。

在用户态下,通过自旋的方式(while…循环),实现类似于加锁的效果。

这种锁,会消耗一定 cpu 资源,但是可以做到最快速度拿到锁

等待挂起锁:是重量级锁的一种典型实现。

通过内核态,借助系统提供的锁机制,当出现锁冲突的时候,会牵扯到内核对于线程的调度,是冲突的线程出现挂起(阻塞等待)

这种方式,消耗 cpu 资源更少.也就无法保证第一时间拿到锁


4) 读写锁 vs 互斥锁

互斥锁:就是简单的加锁(synchronized)解锁

读写锁:把读操作枷锁和写操作加锁分开了。

读锁:是在读的时候加锁。 写锁:是在写的时候加锁。

如果两个线程,都对读加锁加锁,则不会产生锁竞争。(多线程并发执行的效率就更高)

如果两个线程,一个线程写加锁,一个线程也是写加锁,则会产生锁竞争。

如果两个线程,一个线程写加锁,两一个线程读加锁,则也会产生锁竞争。

实际开发中,读操作的频率,往往比读操作,高更多。

java标准库中也提供了现成的读写锁。


5) 公平锁 vs 非公平锁

公平锁:遵循先来后到。(通过一定的数据结构去实现)

非公平锁:一拥而上,抢占式。(原生)

操作系统自带的锁(pthread_mutex)属于是非公平锁。


6) 可重入锁 vs 不可重入锁

一个线程,针对同一把锁,连续加锁多次。如果产生了死锁,则是不可重入锁,如果没有产生死锁,就是可重入锁。

可以按照字面意思来理解,可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁

观察下面这串伪代码:

public synchronized void increase() {
    synchronized (this) {
        count ++;
    }
}

1.调用方法,先针对this加锁。 此时假设加锁成功了。
⒉接下来往下执行到代码块中的 synchronized ,此时,还是针对this来进行加锁。

在不可重入锁中

第二次 this 上的锁,得在 increase 方法执行完毕之后,才能释放。要想让代码继续往下执行,就需要把第二次加锁获取到,也就是把第一次加锁释放。要想把第一次加锁释放,又需要保证代码先继续执行。这就陷入了一个死锁状态,程序无法执行。(这个状态是非常不合理的,第二次尝试加锁的时候,该线程已经有了这个锁的权限了,这个时候不应该加锁失败,不应该阻塞等待


不可重入锁:如果是一个不可重入锁。这把锁不会保存,是哪个线程对它加的锁,只要它当前处于加锁状态之后,收到了"加锁”这样的请求,就会拒绝当前加锁。而不管当下的线程是哪个。就会产生死锁。

可重入锁:一把可重入锁,是会让这个锁保存,是哪个线程加上的锁。后续收到加锁请求之后,就会先对比一下,看看加锁的线程是不是当前持有自己这把锁的线程,这个时候就可以灵活判定了。

注:synchronized 实际上是一个可重入锁


可重入锁,是让锁记录了当前是哪个线程持有了锁,观察下面伪代码。

synchronized (this) {             //这个是真正加了锁,下面的锁只是虚晃一枪。
    synchronized (this) {         //判定了一下持有线程就是当前线程
        synchronized (this) {	  //同上
            …………
		}                 //执行到这个代码,出了代码块的时候,刚才加上的锁是否要释放??        答案是:不释放。
    }                     //如果在里层就释放了锁,意味着最外面的 synchronized 和次外层的代码,就没有处于锁的保护之中了
}

问题:如果加了 N 层锁,在遇到大括号时,JVM 咋知道当前这个大括号是最后一个(最外层的)呢??

答:让锁这里持有一个“计数器”就行了。让锁对象不光要记录是哪个线程持有的锁,同时再通过一个整形变量记录当前这个线程加了几次锁!!


2、死锁

什么是死锁??

死锁是一种严重的 BUG!!导致一个程序的线程 “卡死”, 无法正常工作!

死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。


1) 死锁的三种典型情况:

1. 一个线程,一把锁,但是是不可重入锁。该线程针对这个锁连续加锁两次,就会出现死锁。

public synchronized void increase() {
    synchronized (this) {
        count ++;
    }
}

2. 两个线程,两把锁。这两个线程先分别获取到一把锁,然后再同时尝试获取对方的锁。

public class Demo1 {
    private static Object locker1 = new Object();    	 //第一把锁
    private static Object locker2 = new Object();    	 //第二把锁

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (locker1) {                  	//获取第一把锁,成功获取。
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (locker2) {              //获取第二把锁,由于 locker2 被占用,获取失败。(死锁)
                    System.out.println("t1 两把锁加锁成功!");
                }
            }
        },"t1");
        
        Thread t2 = new Thread(() -> { 
            synchronized (locker2) {				    //获取第二把锁,成功获取。
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (locker1) {  			  //获取第一把锁,由于 locker1 被占用,获取失败。(死锁)
                    System.out.println("t2 两把锁加锁成功!");
                }
            }
        },"t2");

        t1.start();
        t2.start();
    }
}

从 jconsole 中可以看到程序中两个线程中(t1、t2)的死锁。死锁的线程就僵住了,无法正常工作,会对程序造成严重的影响。
在这里插入图片描述


3. N个线程M把锁,哲学家就餐问题。

可以通过一个抽象的图来进行理解。有五个哲学家(五个线程),五根筷子(五把锁)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

每个哲学家,主要要做两件事:

  1. 思考人生。(此时会放下筷子)
  2. 吃面,会拿起左手和右手的筷子,再去夹面条吃。(拿起筷子)

其他设定:

  1. 每个哲学家,啥时候思考人生,啥时候吃面条,都很随机。
  2. 每个哲学家一旦想吃面条了,就会非常固执的完成吃面条的操作。如果此时,他的筷子被别人使用了,就会阻塞等待,而且等待过程中不会放下手里已经拿着的筷子。

基于上述的模型设定,绝大部分情况下,这些哲学家都是可以很好的工作。

但是,如果出现了极端情况,就会出现死锁

比如,同一时刻,五个哲学家都想吃面,并且同时伸出左手拿起左边的筷子。再尝试伸右手拿右边的筷子。

2) 如何解决死锁问题

解决方法:针对锁进行编号,并且规定加锁的顺序。每个线程如果要获取多把锁,必须先获取编号小的锁,后获取编号大的锁

利用上述办法,1 2 3 4 号哲学家分别获取到 1 2 3 4 号筷子。当 5 号哲学家开始获取筷子时,只能去获取 4 号筷子,但是 4 号筷子已经被 4 号哲学家获取到了,因此只能阻塞等待 4 号哲学家用完后释放,才能获取到。当 1 号哲学家用完 1 5 两根筷子时,1 5 均被释放,2 号就可以获取到 1 号筷子……以此类推,当 4 号哲学家释放 4 号筷子时,5号哲学家才能开始动筷。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传


3) 死锁产生的必要条件

  1. 互斥使用:当一个线程获取到一把锁后,别的线程不能获取到着吧锁。(锁的基本特性)
  2. 不可抢占:锁只能是被持有者主动释放,而不能是被其他线程直接抢走。(锁的基本特性)
  3. 请求和保持:这一个线程去尝试获取多把锁,在获取第二把锁的过程中,会保持对第一把锁的获取状态。
  4. 循环等待:t1 尝试获取 locker2,需要 t2 执行完,释放 locker2; t2尝试获取 locker1,需要 t1 执行完,释放 locker1。

3、Synchronized 采用策略

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

synchronized 加锁过程:代码中写了一个 synchronized 之后,这里可能会产生一系列的"自适应的过程",锁升级(锁膨胀)

无锁→偏向锁→轻量级锁→重量级锁


1) 偏向锁

偏向锁,不是真的加锁,而只是做了一个"标记"。如果有别的线程来竞争锁了,才会真的加锁。如果没有别的线程竞争,就自始至终都不会真的加锁了。

加锁本身,有一定开销。能不加,就不加。非得是有人来竞争了,才会真的加锁~

偏向锁在没有其他人竞争的时候,就仅仅是一个简单的标记(非常轻量)。一旦有别的线程尝试进行加锁,就会立即把偏向锁,升级成真正加锁的状态,让别人只能阻塞等待。


2) 轻量级锁

synchronized 通过自旋锁的方式来实现轻量级锁。

当一个线程把锁占用时,其它线程就会按照自旋的方式,来反复查询当前的锁的状态是不是被释放了。

但是,后续如果竞争这把锁的线程越来越多了(锁冲突更激烈了),从轻量锁升级成重量级锁。


3) 其他优化操作

1.锁消除

编译器,会智能的判定当前代码是否有必要加锁

如果你写了加锁,但是实际上没有必要加锁,就会把加锁操作自动删除掉。

有些应用程序的代码中, 用到了 synchronized, 但其实没有在多线程环境下. (例如 StringBuffer)

StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
sb.append("d");

此时每个 append 的调用都会涉及加锁和解锁.。但如果只是在单线程中执行这个代码,那么这些加锁解锁操作是没有必要的,白白浪费了一些资源开销。

2.锁粗化

关于"锁的粒度",如果加锁操作里包含的实际要执行的代码越多,就认为锁的粒度越大。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

有的时候,希望锁的粒度小比较好,并发程度更高

有的时候,也希望锁的粒度大比较好 (因为加锁解锁本身也有开销).


4、CAS

CAS: 全称Compare and swap,字面意思:”比较并交换“。

能够比较和交换某个寄存器中的值和内存中的值,看是否相等。如果相等,则把另外一个寄存器中的值和内存进行交换。

我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。

  1. 比较 address 与 expectValue 是否相等。(比较)
  2. 如果比较相等,将 swapValue写入 address。(交换)
  3. 返回操作是否成功。

CAS 伪代码

boolean CAS(address, expectValue, swapValue) {  //判断 address 与 expectValue 是否相等,若相等则将 swapValue                                                   写入 address
 if (&address == expectedValue) {
   &address = swapValue;
        return true;
   }
    return false;
}

1) CAS的应用

1. 实现原子类

比如,多线程针对一个 count 变量进行 ++。在 java 标准库中,已经提供了一组原子类。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

基于CAS又能衍生出一套"无锁编程",进一步提高代码运行效率。

这里面提供了 自增/自减/自增任意值/自减任意值,这些操作,就可以基于 CAS 无锁编程的方式来实现。

上述的原子类,就是基于 CAS 来实现的。

//伪代码实现
class AtomicInteger {
    private int value;
    public int getAndIncrement() {
        int oldValue = value;
        while ( CAS(value, oldValue, oldValue+1) != true) {
            oldValue = value;
       }
        return oldValue;
   }
}

假设两个线程同时调用 getAndIncrement ,同时假设 value 是 0.

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在 CAS 中比较 value 和 oldValue 是否相等时,其实就是在检查当前 value 的值是不是变了。是不是被别的线程穿插进来做出修改了!!进一步就发现了当前的 ++ 操作不是一气呵成的原子操作了,一旦发现出现其他线程穿插的情况,立即重新读取内存的值准备下一次尝试~~

当两个线程并发的去执行++操作的时候,如果不加任何限制,就意味着,有时候,这俩++是串行的,能计算正确的。有的时候这俩++操作是穿插的,这个时候是会出现问题的。可以通过加锁保证线程安全:通过锁,强制避免出现穿插~~
原子类/CAS保证线程安全:借助CAS来识别当前是否出现“穿插"的情况,如果没穿插,此时直接修改,就是安全的。如果出现穿插了,就重新读取内存中的最新的值,再次尝试修改。

2. 实现自旋锁

基于 CAS 实现更灵活的锁, 获取到更多的控制权.

public class SpinLock {
    private Thread owner = null;     //用owner表示当前线程持有的锁,null为解锁状态。
    public void lock(){
        // 通过 CAS 看当前锁是否被某个线程持有. 
        // 如果这个锁已经被别的线程持有, 那么就自旋等待. 
        // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程. 
        while(!CAS(this.owner, null, Thread.currentThread())){
        //获取当前线程引用.哪个线程调用lock,这里得到的结果就是哪个线程的引用!
       }
   }
   
    public void unlock (){
        //当该锁已经处于加锁状态,这里就会返回false, cas不会进行实际的交换操作.接下来循环条件成立,继续进入下一轮循环.
        this.owner = null;
   }
}

2) CAS 的 ABA 问题

上面讲到了,CAS 的关键要点,是比较 寄存器1 和 内存 中的值,通过这里的是否相等,来判定内存的值是否发生变化。

如果内存的值发生变化,则存在其他线程进行了修改。如果内存的值没有发生变化,则没有别的线程修改,接下来进行的修改就是安全的。

但是我们要想到一个问题,如果这里的值没变,就一定没有别的线程进行修改吗?

ABA 问题就描述了另一个线程,把变量的值从A->B,又从B->A。此时本线程区分不了,这个值是始终没变,还是出现变化又回来了的情况。

大部分情况下,就算是出现 ABA 问题,也没啥太大影响。但是在一些比较极端情况下,还是会出现问题。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

虽然上述操作,概率比较小,也需要去考虑。

ABA 问题,CAS 基本的思路是 ok 的,但是主要是修改操作能够进行反复横跳,就容易让咱们 CAS 的判定失效。


3) 解决方案

我们也有相应的解决办法,可以给上述案例中的账户余额安排一个隔壁邻居 ——— 版本号

给要修改的值,引入版本号。 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期。

  • CAS 操作在读取旧值的同时, 也要读取版本号.
  • 真正修改时
    • 如果当前版本号和读到的版本号相同,则修改数据,并把版本号 + 1。
    • 如果当前版本号高于读到的版本号。就操作失败(认为数据已经被修改过了)。

在 Java 标准库中提供了 AtomicStampedReference<E> 类。这个类可以对某个类进行包装,在内部就提供了上面描述的版本管理功能。

5、Callable 接口

Callable 是一个 interface。相当于把线程封装了一个 “返回值”。方便程序猿借助多线程的方式计算结果。

Callable interface 也是创建线程的一种方式。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如果进行多线程操作,如果你只是关心多线程执行的过程,使用 Runnable 即可。(只关心过程)

如果是关心多线程的计算结果,使用Callable更合适。(比如说通过多线程,计算一个公式,返回结果)

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Demo1 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 1; i <= 1000; i++) {
                    sum += i;
                }
                return sum;
            }
        };

        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread t = new Thread(futureTask);
        t.start();

        Integer result = futureTask.get();
        System.out.println(result);
    }
}

使用 Callable 不能直接作为 Thread 的构造方法参数。而是需要用到 FutureTask 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

上面 Callable 线程结果啥时候能算出来??这是最关心的一点。使用 futureTask 就可以帮助咱们解决这个问题。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

获取 call 方法的返回结果。get 类似于 join 一样,如果 call 方法没执行完,会阻塞等待。

6、JUC

Juc (java.util.concurrent) 的常见类也是并发编程。

1) ReentrantLock 可重入锁

这个锁,没有 synchronized 那么常用,但是也是一个可选的加锁的组件。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

ReentrantLock 具有一些特点,是 synchronized 不具备的功能。

  1. 提供了一个tryLock方法进行加锁。对于lock操作,如果加锁不成功,就会阻塞等待(死等)。对于tryLock,如果加锁失败,直接返回false/也可以设定等待时间。tryLock给加锁操作提供了更多的可操作空间~~
  2. ReentrantLock有两种模式。可以工作在公平锁状态下,也可以工作在非公平锁的状态下。构造方法中通过参数设定的公平/非公平模式
  3. ReentrantLock 也有等待通知机制。搭配Condition 这样的类来完成。这里的等待通知要比 wait notify功能更强。这几个是ReentrantLock的优势~~

synchronized 锁对象是任意对象。ReentrantLock 锁对象就是自己本身。如果你多个线程针对不同的 ReentrantLock 调用 lock 方法,此时是不会产生锁竞争的。


2) 原子类的应用场景

  1. 计数需求

    播放量、点赞量、投币量、转发量、收藏量等……

    同一个视频,有很多人同时播放、点赞、收藏……

  2. 统计效果

    统计出现错误的请求数量。使用原子类,记录出错的请求的数目

3) 信号量 Semaphore

semaphore 是并发编程中的一个重要组件。它可以用来控制同时访问某个资源的线程数量。Semaphore维护了一个许可证集合,线程在访问资源前必须先获取许可证,如果许可证已经全部被占用,则线程必须等待其他线程释放许可证后才能获取许可证并访问资源。

准确来说,Semaphore 是一个计数器(变量),描述了“可用资源的个数”。

描述当前线程,是否“有临界资源可以使用”。(多个线程修改同一个变量,这个变量就可以认为是临界资源)

acquire 方法表示申请资源(P操作),release 方法表示释放资源(V操作)。

import java.util.concurrent.Semaphore;

// 信号量
public class Demo2 {
    public static void main(String[] args) throws InterruptedException {
        // 构造方法中, 就可以用来指定计数器的初始值.
        Semaphore semaphore = new Semaphore(4);   //申请 4 个资源
        semaphore.acquire();            // 计数器 -1
        System.out.println("执行 P 操作 1");
        
        semaphore.acquire();            // 计数器 -1
        System.out.println("执行 P 操作 2");
        
        semaphore.acquire();            // 计数器 -1
        System.out.println("执行 P 操作 3");
        
        semaphore.acquire(); 			// 计数器 -1
        System.out.println("执行 P 操作 4");        //到此为止所有资源已占用完,如果再申请资源则阻塞等待。
        
        semaphore.release();			// 计数器 +1
        System.out.println("执行 V 操作 1");
        
        semaphore.acquire(); 			// 计数器 -1
        System.out.println("执行 P 操作 5");
    }
}

打印结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

4) CountDownLatch

同时等待 N 个任务执行结束。

好像跑步比赛,10个选手依次就位,哨声响才同时出发;所有选手都通过终点,才能公布成绩。

当需要把一个任务拆成多个任务,如何衡量现在是把多个任务都搞定了呢?这时候就需要用到 CountDownLatch.

import java.util.concurrent.CountDownLatch;

public class Demo3 {
    public static void main(String[] args) throws InterruptedException {
        // 构造方法中, 指定创建几个任务.
        CountDownLatch countDownLatch = new CountDownLatch(10);

        for (int i = 0; i < 10; i++) {
            int id = i;
            Thread t = new Thread(() -> {
                System.out.println("线程" + id + "开始工作!");
                try {
                    // 使用 sleep 代指某些耗时操作, 比如下载.
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程" + id + "结束工作!");
                // 每个任务执行结束这里, 调用一下方法
                // 把 10 个线程想象成短跑比赛的 10 个运动员. countDown 就是运动员撞线了.
                countDownLatch.countDown();
            });
            t.start();
        }

        // 主线程如何知道上述所有的任务都完成了呢??
        // 难道要在主线程中调用 10 次 join 嘛?
        // 万一要是任务结束, 但是线程不需要结束, join 不就也不行了嘛?
        // 主线程中可以使用 countDownLatch 负责等待任务结束.
        // a => all 等待所有任务结束. 当调用 countDown 次数 < 初始设置的次数, await 就会阻塞.
        countDownLatch.await();
        System.out.println("多个线程的所有任务都执行完毕了!!");
    }
}

7、集合类

原来的集合类, 大部分都不是线程安全的。

Vector,Stack,HashTable,是线程安全的(不建议用),其他的集合类不是线程安全的。

1) 多线程环境使用 ArrayList

  1. 自己使用同步机制 (synchronized 或者 ReentrantLock)

  2. Collections.synchronizedList(new ArrayList);

  3. 使用 CopyOnWriteArrayList

2) 多线程环境使用队列

  1. ArrayBlockingQueue

    基于数组实现的阻塞队列

  2. LinkedBlockingQueue

    基于链表实现的阻塞队列

  3. PriorityBlockingQueue

    基于堆实现的带优先级的阻塞队列

  4. TransferQueue

    最多只包含一个元素的阻塞队列

3) 多线程环境使用哈希表

HashMap 本身不是线程安全的.

在多线程环境下使用哈希表可以使用:

  • Hashtable
  • ConcurrentHashMap

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1110565.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据结构:一篇拿捏十大排序(超详细版)

排序的概念&#xff1a; 排序&#xff1a;所谓排序&#xff0c;就是使一串记录&#xff0c;按照其中的某个或某些关键字的大小&#xff0c;递增或递减的排列起来的操作。稳定性&#xff1a;假定在待排序的记录序列中&#xff0c;存在多个具有相同的关键字的记录&#xff0c;若经…

浅谈AI人体姿态识别技术的先进性及安防视频监控应用场景

随着计算机视觉技术和安防监控技术的不断发展&#xff0c;基于AI算法的人体姿态识别技术也得到了广泛的应用。然而&#xff0c;传统的安防监控系统通常只局限于简单的视频监控等功能&#xff0c;无法准确地识别人体的姿态&#xff0c;使得一些安防监控存在着一定的漏洞和不足之…

中国人民大学与加拿大女王大学金融硕士项目——开启全球金融视野,锻造未来领袖

你是否曾梦想过站在国际金融的巅峰&#xff0c;洞察全球经济的脉搏&#xff0c;引领企业的创新与发展&#xff1f;如果你对金融充满热情&#xff0c;渴望在全球化的环境中锻炼自己&#xff0c;那么&#xff0c;中国人民大学与加拿大女王大学金融硕士项目便是你的转折点。 中国…

Confluence使用教程

1、如何创建空间 可以把空间理解成一个gitlab仓库&#xff0c;空间之间相互独立&#xff0c;一般建议按照部门&#xff08;小组的人太少&#xff0c;没必要创建空间&#xff09;或者按照项目分别创建空 2、confluence可以创建两种类型的文档&#xff1a;页面和博文 从内容上来…

SICP-- 元语言抽象--Scheme的变形--惰性求值

正则序和应用序 应用序&#xff1a;在过程应用时&#xff0c;提供给Scheme的所有参数都需要完成求值 正则序&#xff1a;将把对过程参数的求值延后到需要这些实际参数的值的时候。 将过程参数的求值拖延到最后的可能时刻被称为 惰性求值 如果在某个参数还没有完成求值之前就…

Apache Jmeter测压工具快速入门

Jmeter测压工具快速入门 一、Jmeter介绍二、Jmeter On Mac2.1 下载2.2 安装2.2.1 环境配置2.2.2 初始化设置 2.3 测试2.3.1 创建JDBC Connection Configuration2.3.2 创建线程组2.3.3 创建JDBC Request2.3.4 创建结果监控2.3.4 运行结果 2.4 问题记录2.4.1 VM option UseG1GC异…

查询企业信息的四种方法

在工作中或者对于找工作的求职人来说&#xff0c;怎么查看企业的信息呢&#xff1f;可能很多人会想到各种查查类软件&#xff0c;但是这类软件需要会员或者付费才能查看&#xff0c;对于没有会员的人来说&#xff0c;有没有其他查询企业的方法呢&#xff1f;答案肯定是有的&…

AI也可以​算命和占卜?一定要试试这个模型

01 模型介绍 Mistral Trismegistus 7B&#xff1a;专为玄学、神秘学、超自然和灵异感兴趣的人设计的模型。 专门用于处理与神秘学、灵性、超自然、占卜、炼金术、宗教、冥想等相关的问题和任务。该数据集包含大约35000个指令响应对&#xff0c;覆盖了数百个与神秘学有关的子…

安装visual studio报错“无法安装msodbcsql“

在安装visual studio2022时安装完成后提示无法安装msodbcsql, 查看日志文件详细信息提示&#xff1a;指定账户已存在。 未能安装包“msodbcsql,version17.2.30929.1,chipx64,languagezh-CN”。 搜索 URL https://aka.ms/VSSetupErrorReports?qPackageIdmsodbcsql;PackageActi…

[ Windows ] ping IP + Port 测试 ip 和 端口是否通畅

开发过程中经常会黑窗口中手动测试一下计划请求的目标ip和端口是否通畅&#xff0c;测试方式如下&#xff1a; 一、单纯测试ip是否能够 ping 通&#xff0c;这个比较熟悉了&#xff0c;运行 cmd 打开黑窗口 输入如下指令&#xff0c;能够如下提示信息&#xff0c;表示端口是通…

五分钟学会搭建悟空CRM内网穿透,实现公网访问企业内网,提升工作效率!

文章目录 前言1. 无需公网IP&#xff0c;使用cpolar实现悟空CRM远程访问2. 通过公网来访问公司内网悟空CRM3. 设置固定连接公网地址 前言 悟空CRM是一款开源的客户关系管理系统&#xff0c;以"客户关系一对一理论"为基础&#xff0c;通过对企业业务流程的重组来整合…

不会写代码同学的福音——AI 代码生成器 Amazon CodeWhisperer(通过注释写代码)

Amazon CodeWhisperer 是一个以机器学习为动力的代码生成器&#xff0c;直接在集成开发环境&#xff08;IDE&#xff09;中为开发者提供实时代码建议。它是一个通用的工具&#xff0c;可以用于 IDE 支持的任何编程语言。 大家可以通过下面的链接进入注册并使用&#xff1a; AI …

智能水电表对于普通居民来说有哪些好处?

随着科技的发展&#xff0c;智能水电表已经逐渐成为家庭生活中不可或缺的一部分。智能水电表的改造不仅可以提高居民的生活质量&#xff0c;还能为环保事业做出贡献。接下来&#xff0c;小编来为大家详细的介绍下智能水电表对于普通居民来说有哪些好处吧&#xff01; 一、节能减…

RISC-V声名鹊起,究竟为何?

近期&#xff0c;高通、恩智浦、Nordic、博世和英飞凌等宣布联手组建一家芯片新公司&#xff0c;目标是通过支持下一代硬件开发而推动RISC-V在全球范围内实现。该公司将支持基于RISC-V的兼容产品、提供参考架构并帮助建立行业广泛使用的解决方案&#xff0c;应用重点包括汽车、…

我用低代码平台,简单搭建了一套管理系统

目录 一、什么是低代码 应用场景分析&#xff1a; a. 帮助成熟的软件产品&#xff0c;低成本的支持个性化需求&#xff08;协助乙方快速解决甲方需求&#xff09; b. 帮助甲方企业&#xff0c;低成本的快速搭建全新的应用系统&#xff0c;让业务人员也能自主搭建数字化工具&…

arthas线上问题检查思路

&#x1f4e2;&#x1f4e2;&#x1f4e2;&#x1f4e3;&#x1f4e3;&#x1f4e3; 哈喽&#xff01;大家好&#xff0c;我是「奇点」&#xff0c;江湖人称 singularity。刚工作几年&#xff0c;想和大家一同进步&#x1f91d;&#x1f91d; 一位上进心十足的【Java ToB端大厂…

MySQL -- 环境安装(CentOS7)

MySQL – 环境安装&#xff08;CentOS7&#xff09; 文章目录 MySQL -- 环境安装&#xff08;CentOS7&#xff09;一、环境安装1.卸载不必要的环境2.检查系统安装包3.卸载默认安装包4.获取MySQL官方yum源6.看看yum源能不能正常工作7.安装mysql服务 二、MySQL登录与配置1.启动My…

SSO系统设计框架搭建

SSO系统设计 目录概述需求&#xff1a; 设计思路实现思路分析1.SSO 框架的Spring&#xff0c;Spring boot 适配2.Spring&#xff0c;SpringBoot 关键字寻找3.maven helper 的使用4.异常排查 参考资料和推荐阅读 Survive by day and develop by night. talk for import biz , sh…

【广州华锐互动】利用VR开展施工现场安全培训,提高员工安全意识水平

随着科技的不断发展&#xff0c;虚拟现实&#xff08;VR&#xff09;技术已经逐渐渗透到各个领域&#xff0c;为我们带来了前所未有的沉浸式体验。在建筑施工行业&#xff0c;VR技术的应用也日益广泛&#xff0c;从设计、施工到管理&#xff0c;都可以看到VR技术的身影。而在这…

斯坦福发布 最新 GPT 模型排行榜 AlpacaEval

文章目录 &#x1f4cc;提炼❓什么是 AlpacaEval&#x1f50e;AlpacaEval 排行榜 包含的 测试 模型 和数据&#x1f4af;在不同的测试集上各个大模型的能力评分&#x1f680;AlpacaEval Leaderboard 大模型的能力综合评分&#x1f4bc; 普遍国内白领 如何快速应用 大模型&#…