【无标题】javaEE初阶---多线程(面试常用)

news2025/1/11 22:52:07

这篇文章 , 我将主要介绍多线程进阶部分的内容 . 主要涉及到一些在面试中常考的内容。

一:常见的锁策略

1.1乐观锁和悲观锁

乐观锁 : 预测接下来发生锁冲突的可能性不大 , 而进行的一类操作;
悲观锁 : 预测接下来发生锁冲突的可能性很大 , 而进行的一类操作.
  • 乐观锁 : 假设数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则会返回用户错误的信息,让用户决定如何去做。乐观锁适用于多读的应用类型,这样可以提高吞吐量 .一般的实现乐观锁的方式就是记录数据版本(version)或者是使用时间戳,其中使用版本记录是最常用的。
  • 悲观锁 : 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁 . 可以理解为牺牲效率提高了安全性.

举例:

在这里插入图片描述

乐观锁的一个重要功能就是要检测出数据是否发生访问冲突. 我们可以引入一个 “版本号” 来解决.

在这里插入图片描述
在这里插入图片描述

假设我们需要多线程修改 “瑞士银行账户余额”.设当前余额为 1万亿. 引入一个版本号 version, 初始值为 1. 并且我们规定 “提交版本必须大于记录当前版本才能执行更新余额” .

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
说到乐观锁,就必须提到一个概念:CAS
什么是CAS呢?Compare-and-Swap,即比较并替换,也有叫做Compare-and-Set的,比较并设置。
1、比较:读取到了一个值A,在将其更新为B之前,检查原值是否仍为A(未被其他线程改动)。
2、设置:如果是,将A更新为B,结束。[1]如果不是,则什么都不做。
上面的两步操作是原子性的,可以简单地理解为瞬间完成,在CPU看来就是一步操作。
有了CAS,就可以实现一个乐观锁,允许多个线程同时读取(因为根本没有加锁操作),但是只有一个线程可以成功更新数据,并导致其他要更新数据的线程回滚重试。 CAS利用CPU指令,从硬件层面保证了操作的原子性,以达到类似于锁的效果。
ava中真正的CAS操作调用的native方法
因为整个过程中并没有“加锁”和“解锁”操作,因此乐观锁策略也被称为无锁编程。换句话说,乐观锁其实不是“锁”,它仅仅是一个循环重试CAS的算法而已,但是CAS有一个问题那就是会产生ABA问题,什么是ABA问题,以及如何解决呢?

ABA 问题:
如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。

ABA 问题解决:
我们需要加上一个版本号(Version),在每次提交的时候将版本号+1操作,那么下个线程去提交修改的时候,会带上版本号去判断,如果版本修改了,那么线程重试或者提示错误信息~

我们之前提到过的synchronized既是一个悲观锁 , 也是一个乐观锁 , 我们称之为== “自适应锁” == .
如果发现当前锁冲突概率不大 , 就会以乐观锁的方式运行 , 往往是纯用户态执行的 ; 一旦发现锁冲突的概率比较大了 , 就会以悲观锁的方式运行 , 往往要进入内核 , 对当前线程进行挂起等待.

挂起等待 : 线程的挂起操作实质上就是线程进入"非可执行"状态下,在这个状态下CPU不会分给线程时间片,进入这个状态可以用来暂停一个线程的运行。线程挂起后,可以通过重新唤醒线程来使之恢复运行。
挂起的原因可能有 :

  1. 通过调用sleep()方法使线程进入休眠状态,线程在指定时间内不会运行。
  2. 通过调用join()方法使线程挂起,自己等待另一个线程的结果,直到另一个线程执行完毕为止
  3. 通过调用wait()方法使线程挂起,直到线程得到了notify()和notifyAll()消息,线程才会进入“可执行”状态。

关于乐观锁和悲观锁的实现详情 , 我推荐大家阅读这篇文章 , 是基于数据库展开的 :悲观锁与乐观锁的实现(详情图解)

1.2普通的互斥锁和读写锁

多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需要进行互斥。如果两种场景下都用同一个锁,就会产生极大的性能损耗。所以读写锁因此而产生。
  • 读写锁 , 顾名思义 , 就是在执行加锁操作时要表明"读"还是"写" , 如果是读 , 读者之间并不互斥 ; 如果是写 , 那么要求与任何人互斥 .
  • 互斥锁 :每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
    在这里插入图片描述
    读写锁就是把读操作和写操作区分对待. Java 标准库提供了
    ReentrantReadWriteLock 类, 实现了读写锁.
  • ReentrantReadWriteLock.ReadLock 类表示一个读锁. 这个对象提供了 lock / unlock 方法进行加锁解锁.
  • ReentrantReadWriteLock.WriteLock 类表示一个写锁. 这个对象也提供了 lock / unlock 方法进行加锁解锁.

读写锁特别适合于"频繁读, 不频繁写"的场景中.

synchronized就是普通的互斥锁 , 两个加锁操作之间会发生竞争 !

1.3重量级锁和轻量级锁

重量级锁 : 锁开销比较大 , 做的工作比较多 ;
轻量级锁 : 锁开销比较小 , 做的工作比较少 . 
悲观锁   : 经常会是重量级锁 ; 
乐观锁   : 经常会是轻量级锁 .
  • 重量级锁 : 主要是依赖了操作系统提供的锁 , 加锁机制重度依赖了 OS 提供的 mutex ~~ .使用这种锁 , 容易产生阻塞等待 ; 有大量的内核态用户态切换 , 很容易引发线程的调度 .
  • 轻量级锁 : 主要尽量的避免使用操作系统提供的锁 , 而在用户态完成功能~~ 使用这种锁 , 可以尽量避免用户态和内核态的切换 , 尽量避免挂起等待 ; 有少量的内核态用户态切换 , 不容易引发线程的调度 .
    在这里插入图片描述

注意, synchronized 并不仅仅是对 mutex 进行封装, 在 synchronized 内部还做了很多其他的工作 .

synchronized是自适应锁 , 既是轻量级锁 , 又是重量级锁 , 根据锁冲突的情况而定 .

  • 冲突不高 :轻量级锁
  • 冲突很高 :重量级锁

1.4自旋锁和挂起等待锁

自旋锁    : 是轻量级锁的具体实现 , 是乐观锁 ;
挂起等待锁 : 是重量级锁的具体实现 , 是悲观锁 .

举个栗子 , 帮我们理解自旋锁和挂起等待锁 :

想象一下, 去追求一个女神. 当男生向女神表白后, 女神说: 你是个好人, 但是我有男朋友了~~

挂起等待锁: 陷入沉沦不能自拔… 过了很久很久之后, 突然女神发来消息, “咱俩要不试试?” (注意,这个很长的时间间隔里, 女神可能已经换了好几个男票了).

自旋锁: 死皮赖脸坚韧不拔. 仍然每天持续的和女神说早安晚安. 一旦女神和上一任分手, 那么就能立刻抓住机会上位 . 我本人是强烈谴责这种行为的 !
在这里插入图片描述

synchronized是自适应锁 , 作为轻量级锁时 , 内部是自旋锁 ; 作为重量级锁时 , 内部是挂起等待锁 .

1.5公平锁和非公平锁

公平锁   : 遵循"先来后到"的规则 ; 
非公平锁 : 不遵循"先来后到"的规则 .

假设三个线程 A, B, C. A 先尝试获取锁, 获取成功. 然后 B 再尝试获取锁, 获取失败, 阻塞等待; 然后C 也尝试获取锁, C 也获取失败, 也阻塞等待.

当线程 A 释放锁的时候, 会发生啥呢?

  • 公平锁: 遵守 “先来后到”. B 比 C 先来的. 当 A 释放锁的之后, B 就能先于 C 获取到锁.
  • 非公平锁: 不遵守 “先来后到”. B 和 C 都有可能获取到锁.

sychronized是非公平锁 .

1.6可重入锁和不可重入锁

可重入锁   : "可以重新进入的锁",即允许同一个线程多次获取同一把锁。(可递归锁)
不可重入锁 : "不可以重新进入的锁",若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到 , 被阻塞。(非递归锁)

二者的区别是 : 同一个线程可以多次获取同一个递归锁,不会产生死锁。而如果一个线程多次获取同一个非递归锁,则会产生死锁。

这里谈到一个概念 : 死锁 . 什么是死锁 ?

当两个任务都在等待被对方持有的资源时,两个任务都无法再继续执行,这种情况就被称为死锁。

即一个线程没有释放锁 , 然后又尝试再次加锁 .

// 第一次加锁, 加锁成功
lock();
// 第二次加锁, 锁已经被占用, 阻塞等待.
lock();

按照之前对于锁的设定, 第二次加锁的时候, 就会阻塞等待. 直到第一次的锁被释放, 才能获取到第二个锁. 但是释放第一个锁也是由该线程来完成, 结果这个线程已经躺平了, 无法进行解锁操作. 这时候就会 死锁.

死锁肯定不好 , 为了避免这个问题 , 就引入了"可重入锁" , 一个线程可以多次获取同一把锁 , 反复多次加锁 , 也没事 ! 因为"可重入锁" , 会在内部记录这个锁是哪个线程获取到的 . 如果发现当前加锁的线程和持有锁的线程是同一个 , 则不挂起等待 , 而是直接获取到锁 . 同时还会给锁内部加上计数器 , 记录当前是第几次加锁了 . (通过计数器来控制啥时候释放锁) .

Java里只要以 Reentrant 开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的。而 Linux 系统提供的 mutex 是不可重入锁.

注意 : synchronized属于"可重入锁".

二:CAS

2.1CAS简介

是操作系统/硬件,给JVM提供的一种更轻量的原子操作的机制.全称Compare and swap,字面意思:"比较并交换",是CPU提供的一个特殊指令 . 

一个CAS包含以下操作步骤 :

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

  1. 比较A与V是否相等 ;
  2. 如果比较相等 , 将B写入V;(交换)
  3. 返回操作是否成功.

在这里插入图片描述

注意:这一系列操作都是由一个CPU指令来完成的.

想到个死锁的例子,健康码(没码进不去,进不去修不了,修不了没码,没码进不去).

2.2 CAS应用场景

2.2.1 使用CAS实现原子类

2.2.1.1什么是原子类?

  1. 一个操作是不可中断的,即使是多线程的情况下也可以保证 .通常用于实现原子地进行++ , --等操作.
  2. 在Java中原子类都被保存在 java.util.concurrent.atomic包里 .

2.2.1.2具体实例

以count++为例 , 实际上是由1.读取 2.加一 3.写入 三步组成的,这是个复合类的操作(所以我们之前提到过的volatile是无法解决num++的原子性问题的) , 在并发环境下 , 如果不做任何同步处理,就会有线程安全问题.最直接的处理方式就是加锁 .

加锁操作意味着同一时刻只能有一个线程持有锁 , 其他线程则阻塞等待 , 线程的挂起恢复会带来很大的性能开销 .

AtomicInteger 类同样能够保证数据的同步性 , 我们来看看它是如何使用的 .

package Thread;

import java.util.concurrent.atomic.AtomicInteger;

public class Demo33 {
    public static AtomicInteger count = new AtomicInteger(0);
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                //这个操作相当于count++
                count.getAndIncrement();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                //这个操作相当于count++
                count.getAndIncrement();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = "+count);
    }
}

在这里插入图片描述

说明 :

AtomicInteger内部并没有加锁 , 而是基于CAS实现了原子类的操作 .那么 , 原子类是怎么基于CAS进行实现的呢 ?

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

通过"原子类" , 就可以在不加锁的情况下高效地完成多线程的自增操作 .

2.2.2 使用CAS实现自旋锁

在这里插入图片描述

2.3 CAS之ABA问题

什么是ABA问题?

在这里插入图片描述
一部手机 , 你无法确定它是新机还是翻新机 , 其实这就是ABA问题 .

在CAS中 , 你也无法确定数据一直是A , 还是从A -> B -> A 的.

现在有这样一个场景 , 滑稽老哥有100块存款 , 他想取50 , 取款机就创建了两个线程 , 并行地执行-50操作 . 期望结果是 : 一个线程-50成功 , 一个线程-50失败 .
在这里插入图片描述

正常过程 :

  1. 存款 100. 线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100, 期望更新为 50.
  2. 线程1 执行扣款成功, 存款被改成 50. 线程2 阻塞等待中.
  3. 轮到线程2 执行了, 发现当前存款为 50, 和之前读到的 100 不相同, 执行失败.

异常过程 :

  1. 存款 100. 线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100, 期望更新为 50.
  2. 线程1 执行扣款成功, 存款被改成 50. 线程2 阻塞等待中.
  3. 在线程2 执行之前, 滑稽的朋友正好给滑稽转账 50, 账户余额变成 100 !
  4. 轮到线程2 执行了, 发现当前存款为 100, 和之前读到的 100 相同, 再次执行扣款操作 !

此时扣款操作被执行了两次 , 这显然不是我们期望的结果 !

如何解决ABA问题呢 ?

上述CAS时是比较余额 , 因为余额可大可小 , 所以才会导致出现ABA问题 ; 如果能引入一个向唯一方向变化的值 , 就可解决ABA问题 .具体做法是 :

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

三.详解synchronized

3.1synchronized使用的锁策略

  1. 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁 ;
  2. 开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁 ;
  3. 轻量级锁大概率基于自旋锁实现 , 重量级锁大概率基于挂起等待锁实现 ;
  4. 是非公平锁 ;
  5. 是可重入锁 ;
  6. 不是读写锁 .

3.2synchronized在加锁时会经历几个阶段

在这里插入图片描述

  1. 无锁状态 : 没什么好说的 .
  2. 偏向锁:不是"真正加锁",只是给对象头标记一个状态,表示"这个锁是我的了",直到其他线程来竞争锁之前,都保持这个状态;当有其他线程来竞争锁时,才真正加锁.类似于单例模式中的"懒汉模式",必要时才加锁.
  3. 轻量级锁 :一旦有其他线程参与了竞争 , 那么偏向锁状态就被消除 , 进入"轻量级锁".(自旋锁) .其具体行为是 :
  • 通过 CAS 检查并更新一块内存;
  • 如果更新成功, 则认为加锁成功;
  • 如果更新失败, 则认为锁被占用, 继续自旋式的等待(并不放弃 CPU).
  1. 重量级锁 : 如果竞争进一步激烈 , 那么"轻量级锁"就会膨胀为"重量级锁" .此处的重量级锁就是指用到内核提供的 mutex.其具体行为是 :
  • 执行加锁操作, 先进入内核态;
  • 在内核态判定当前锁是否已经被占用;
  • 如果该锁没有占用, 则加锁成功, 并切换回用户态;
  • 如果该锁被占用, 则加锁失败. 此时线程进入锁的等待队列, 挂起. 等待被操作系统唤醒;
  • 直到该锁被其他线程释放了, 操作系统就唤醒这个线程, 并尝试重新获取锁.

3.3其他的优化操作

synchronized除了锁升级 , 还有其他的优化操作 .

3.3.1锁消除

编译器+JVM自动判定,认为当前代码没必要加锁,就会自动进行锁消除.

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

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

此时每个 append 的调用都会涉及加锁和解锁 . 但如果只是在单线程中执行这个代码 , 那么这些加锁解锁操作是没有必要的 , 白白浪费了一些资源开销 . 所以在这种情况下 , JVM+编译器通过判定 , 就把这些锁消除了 .

3.3.2 锁粗化

一段代码逻辑中多次出现加锁解锁操作,编译器+JVM会自动进行锁粗化,即让synchronized包含的代码范围更大一些.

在这里插入图片描述
实际开发过程中 , 使用细粒度锁 , 是期望释放锁的时候其他线程能使用锁.但是实际上可能并没有其他线程来抢占这个锁 . 这种情况 JVM 就会自动把锁粗化 , 避免频繁申请释放锁.

对于synchronized , 要求 :

1.能够理解synchronized基本执行过程 , 理解锁对象,理解锁竞争;
2.能够知道synchronized的基本策略;
3.能够理解synchronized 内部的一些锁优化的过程 (锁升级,锁消除,锁粗化)

四:Callable 接口

Callable接口与Runnable接口有类似之处,都可以在创建线程的时候指定一个具体的任务.
区别:Callable指定的任务是带有返回值的,Runnable指定的任务是不带返回值的.

4.1 代码示例

创建线程 , 计算1+2+3+4+…+1000.

package Thread;

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

public class Demo34 {
    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 = 0; i <= 1000; i++) {
                    sum += i;
                }
                return sum;
            }
        };

        /**
         * Thread t = new Thread(callable);
         * 这种写法是错误的!不能把callable加到Thread的构造方法中,而是需要套娃.
         */

        //套上一层, 目的是为了获取到后续的结果.
        FutureTask<Integer> task = new FutureTask<>(callable);
        Thread t = new Thread(task);
        t.start();

        // 在线程 t 执行结束之前, get 会阻塞. 直到 t 执行完,
        // get 才能返回. 返回值就是 call 方法 return 的内容.
        System.out.println(task.get());

    }
}

在这里插入图片描述

4.2 Callable总结

  1. Callable 和 Runnable 相对, 都是描述一个 “任务”. Callable 描述的是带有返回值的任务,Runnable 描述的是不带返回值的任务.
  2. Callable 通常需要搭配 FutureTask 来使用. FutureTask 用来保存 Callable 的返回结果. 因为Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定 . FutureTask 就可以负责这个等待结果出来的工作.
  3. 类似于你去学校食堂买麻辣烫 , 人家就会给你一个牌子 , 到时候叫到你的号 ,你去取就行了.这个"牌子"就是FutureTask.

五:JUC相关类

5.1 ReentrantLock

是一个可重入锁.

5.1.1 ReentrantLock的特点

ReentrantLock和synchronized的区别 :
在这里插入图片描述

  • 大部分情况下 , 还是使用synchronized为主 .
  • 锁竞争不激烈的时候, 使用 synchronized, 效率更高, 自动释放更方便.
  • 锁竞争激烈的时候, 使用 ReentrantLock, 搭配 trylock ,可以更灵活控制加锁的行为.
  • 如果需要使用公平锁, 使用 ReentrantLock.

5.1.2 代码示例

package Thread;

import java.util.concurrent.locks.ReentrantLock;

public class Demo35 {
    public static void main(String[] args) {
        ReentrantLock locker = new ReentrantLock(true);
        try{
            //加锁
            locker.tryLock();
        } finally {
            //解锁
            locker.unlock();
        }
    }
}

5.2 原子类

其内部使用CAS实现,性能远高于使用加锁操作实现i++.

通常包括:

  • AtomicBoolean
  • AtomicInteger
  • AtomicInterArray
  • AtomicLong
  • AtomicReference
  • AtomicAtampedReference

5.3线程池

虽然创建销毁线程比创建销毁进程更轻量, 但是频繁创建销毁线程,还是比较低效.线程池就是为了解决这个问题.如果某个线程不再使用了,并不是真正把线程释放,而是放到一个 "池子"中, 下次如果需要用到线程就直接从池子中取,不必通过系统来创建了.

标准库中最核心的线程池类 , 就是ThreadPoolExecutor .我们需要了解其构造方法 . 打开Java官方文档 , 可以发现ThreadPoolExecutor类提供了很多种构造方法 , 我们主要来看其参数最多的构造方法 .

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

如果把创建线程类比为开一家公司 , 每个员工相当于一个线程 , 那么 :
在这里插入图片描述
在这里插入图片描述

Q : 当我们使用线程池的时候 , 线程数目设置多少比较合适 ?
A :
一种明显的错误回答 : 设置XXX(具体数字).
另一种明显的错误回答 : 设置为CPU核心数 XXX(具体数字) .
漏 , 大漏特漏 !
不同场景 , 不同的程序 , 不同的主机配置 , 都会有差异 ! 一种比较合适的回答是 , 我们有找到合适线程数的方法 . 就是压测(性能测试).
针对当前程序进行性能测试 , 设置不同的线程数目 , 分别测试 , 在测试过程中 , 记录程序的时间 ,CPU占用 , 内存占用等等 …根据压测结果 , 来选择最适合当前场景的线程数目 .

程序:

  1. CPU密集型(线程数最多也就是CPU核心数)
  2. IO密集型(线程数可以超过CPU核心数,等待IO过程不吃CPU)

实际开发中,一个程序既需要CPU也需要等待IO , 此时就根据这二者不同的时间比例 , 结合压测 , 得出线程数设置多少合适即可 .

线程池的工作流程 :

在这里插入图片描述

5.4 信号量Semaphore

5.4.1 基本概念

信号量,用于标识可用资源的个数,本质上是一个计数器.

Semaphore : /ˈseməfɔː®/

申请一个可用资源 , 信号量就 -= 1 , 称为P操作 .
释放一个可用资源 , 信号量就 += 1, 称为V操作 .

  • Semaphore可以直接用于多线程线程安全的控制 ;
  • 可以把信号量视为一个更广义的锁 , 当信号量取值仅为0或1时 , 就退化为了一个普通的锁.

在这里插入图片描述

5.4.2代码示例

package Thread;

import java.util.concurrent.Semaphore;

public class Demo36 {
    public static void main(String[] args) throws InterruptedException {
        //构造方法传入有效资源的个数
        Semaphore semaphore = new Semaphore(5);

        //P操作,申请资源
        semaphore.acquire();
        System.out.println("申请资源1");
        semaphore.acquire();
        System.out.println("申请资源2");
        semaphore.acquire();
        System.out.println("申请资源3");
        semaphore.acquire();
        System.out.println("申请资源4");
        semaphore.acquire();
        System.out.println("申请资源5");
        semaphore.acquire();
        System.out.println("申请资源6");
        //V操作,释放资源
        semaphore.release();
    }
}

在这里插入图片描述

构造方法传入有效资源的个数为5 , 所以申请第6个资源时 , 会阻塞等待 .

package Thread;

import java.util.concurrent.Semaphore;

public class Demo36 {
    public static void main(String[] args) throws InterruptedException {
        //构造方法传入有效资源的个数
        Semaphore semaphore = new Semaphore(5);

        //P操作,申请资源
        semaphore.acquire();
        System.out.println("申请资源1");
        semaphore.acquire();
        System.out.println("申请资源2");
        semaphore.acquire();
        System.out.println("申请资源3");
        semaphore.acquire();
        System.out.println("申请资源4");
        semaphore.acquire();
        System.out.println("申请资源5");
        //V操作,释放资源
        semaphore.release();
        semaphore.acquire();
        System.out.println("申请资源6");

    }
}

在这里插入图片描述

构造方法传入有效资源的个数为5 , 先释放一个资源后 , 再次申请 , 可以成功 .

5.5CountDownLatch

5.5.1基本概念

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

进行一次多线程下载 , CountDownLatch描述当前所有线程都下载完毕 .

进行一场跑步比赛 , CountDownLatch描述当前所以选手都到达终点 .

在这里插入图片描述

5.4.2代码示例

package Thread;

import java.util.concurrent.CountDownLatch;

public class Demo37 {
    public static void main(String[] args) throws InterruptedException {
        //模拟跑步比赛
        //构造方法中设定参赛选手的个数
        CountDownLatch latch = new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(()->{
                try {
                    Thread.sleep(3000);
                    System.out.println("到达终点");
                    //countDown相当于撞线
                    latch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            t.start();
        }
        /**
         *await等待所有的线程"撞线"
         * 调用countDown的次数达到初始化时候设定的值,await就返回
         * 否则await就阻塞等待!
         */
        latch.await();
        System.out.println("比赛结束!");
    }


}

在这里插入图片描述

相关面试题?

Q:线程同步的方式有哪些?
A:synchronized, ReentrantLock, Semaphore 等都可以用于线程同步.

Q:为什么有了 synchronized 还需要 juc 下的 lock?
A:以 juc 的 ReentrantLock 为例,

  • synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活,
  • synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃.
  • synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个true 开启公平锁模式.
  • synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是一个随机等待的线程. ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程.

Q: AtomicInteger 的实现原理是什么?
A:基于CAS实现 . 伪代码如下:

class AtomicInteger{
  private int value;  
  public int getAndIncrement(){
     int oldValue = value;
     while(CAS(value,oldValue,oldValue+1) != true) {
          oldValue = value;
     } 
    return oldValue;
  }
}

Q:信号量听说过么?之前都用在过哪些场景下?
A:信号量, 用来表示 “可用资源的个数”. 本质上就是一个计数器.
使用信号量可以实现 “共享锁”, 比如某个资源允许 3 个线程同时使用, 那么就可以使用 P 操作作为加锁, V 操作作为解锁, 前三个线程的 P 操作都能顺利返回, 后续线程再进行 P 操作就会阻塞等待, 直到前面的线程执行了 V 操作 .

六 : 线程安全的集合类

我们学过的集合类 , 大部分是线程不安全的 , 例如 :

ArrayList

LinkedeList

TreeSet

TreeMap

HashSet

HahsMap

Queue

当然也有线程安全的 :

Vector : 上古时期Java内置的顺序表
Stack : 继承自Vector , 巧了 , 才线程安全的
HashTable : 不推荐使用 , 就是无脑加synchronized
ConcurrentHashMap : 推荐使用

6.1 多线程环境使用ArrayList

  1. 自己使用同步机制 (synchronized 或者 ReentrantLock)
  2. Collections.synchronizedList(new ArrayList) ; synchronizedList 是标准库提供的一个基于 synchronized 进行线程同步的 List.
    synchronizedList 的关键操作上都带有 synchronized
  3. 使用 CopyOnWriteArrayList . CopyOnWrite容器即写时复制的容器。当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器 . 这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素 , 所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器 .
    在这里插入图片描述

6.2 多线程环境使用队列

  1. ArrayBlockingQueue
    基于数组实现的阻塞队列
  2. LinkedBlockingQueue
    基于链表实现的阻塞队列
  3. PriorityBlockingQueue
    基于堆实现的带优先级的阻塞队列
  4. TransferQueue
    最多只包含一个元素的阻塞队列

6.3 多线程环境使用哈希表

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

经典面试题 : 谈谈HashMap,HashTable,ConcurrentHashMap之间的区别?

七 : 死锁

7.1 死锁的类型

死锁:尝试加锁的时候发现上次锁没有及时释放(bug),导致加锁加不上.

在这里插入图片描述
哲学家就餐问题 :

由Dijkstra提出并解决的哲学家就餐问题是典型的死锁问题。该问题描述的是五个哲学家共用一张圆桌,分别坐在周围的五张椅子上,在圆桌上有五个碗和五只筷子,他们的生活方式是交替的进行思考和进餐。平时,一个哲学家进行思考,饥饿时便试图取用其左右最靠近他的筷子,只有在他拿到两只筷子时才能进餐。进餐完毕,放下筷子继续思考。

解决方案:

  1. 哲学家要进餐时,要么同时拿起两支筷子,要么一支筷子都不拿.
  2. 对筷子进行编号 , 并约定一种拿筷子的规则 , 比如每次都从编号较小的筷子开始拿 .

7.2 死锁产生的必要条件

在这里插入图片描述
打破循环等待的方法 , 可以针对多把锁进行编号 . 约定在获取多把锁时 ,要明确获取锁的顺序 , 比如从小到大获取 . 只要所有线程都遵守这个顺序 , 就不会出现死锁 !

Q : 常考面试题 : 什么是死锁 ?

A : 按照如下思路回答:
1.一句话概括什么是死锁 .
2.产生死锁的三个典型场景:

  • 1个线程1把锁
  • 2个线程2把锁
  • N个线程M把锁

3.死锁产生的必要条件 .
4.从循环等待的角度切入 , 对锁进行编号 , 并按顺序加锁 , 就可以破坏循环等待的条件, 进而打破死锁 !

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

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

相关文章

ClassIn:如何打造更稳定的Zabbix监控系统

作者简介&#xff1a;罗呈祥。现就职于北京翼鸥教育科技有限公司&#xff0c;负责数据库相关的运维管理和技术支持工作&#xff0c;擅长故障处理和性能优化&#xff0c;对分布式数据库也有深入研究。 近期&#xff0c;OceanBase 社区发布了一篇关于我们公司选型分布式数据库的文…

bilateral_filter 双边滤波器详细用法

一、双边滤波&#xff08;Bilateral filter&#xff09;是一种可以保边去噪的滤波器。其输出像素的值依赖于邻域像素的值的加权组合。 从效果来说&#xff0c;双边滤波可产生类似美肤的效果。皮肤上的皱纹和斑&#xff0c;与正常皮肤的差异&#xff0c;远小于黑白眼珠之间的差异…

13种Shell逻辑与算术,能写出5种算你赢!

相较于最初的 Bourne shell&#xff0c;现代 bash 版本的最大改进之一体现在算术方面。早期的 shell 版本没有内建的算术功能&#xff0c;哪怕是给变量加1&#xff0c;也得调用单独的程序来完成。 1、算术方法一&#xff1a; $(( )) 只要都是整数运算&#xff0c;就可以在 $(…

DHT11温湿度传感器初识

目录 一、产品概述 1、接线方式 2、特点 3、数据传送逻辑 二、发送时序检测模块是否存在 1、C51单片机&#xff08;主机&#xff09;时序分析 2、编写代码检测模块是否存在 3、读取DHT11数据的时序分析 三、温湿度通过串口传到PC显示 四、温湿度检测小系统——使数据…

Discrete Opinion Tree Induction for Aspect-based Sentiment Analysis 论文阅读笔记

一、作者 Chenhua Chen、Zhiyang Teng、Zhongqing Wang、Yue Zhang School of Engineering, Westlake University, China Institute of Advanced Technology, Westlake Institute for Advanced Study Soochow University 二、背景 如何为每一个方面词定位相应的意见上下文…

反补码运算之 “1 - 1 = - 1 ” ?

我们在研究数据的二进制表示时遇到这样一个问题&#xff1a; 由于计算中的CPU只有加法器&#xff0c;没有减法器&#xff0c;所以在计算机采用原码做减法时对于&#xff1a;1 - 1 0 相当于1 &#xff08;-1&#xff09;&#xff0c;用二进制表示为&#xff1a;000110011001。…

儿童感染新冠病毒后发高烧如何应对?专家来支招

儿童感染新冠病毒后发热较多&#xff0c;但肺炎发展较少儿童感染新冠病毒后发高烧&#xff0c;是重症吗&#xff1f;徐红梅&#xff1a;从目前的接待情况来看&#xff0c;儿童感染新冠病毒后比成人发烧。超过一半的儿童在感染后有发烧症状&#xff0c;伴有咳嗽、鼻塞、流鼻涕、…

JavaWeb的一些学习总结

Web系统就是&#xff1a;前端负责貌美如花&#xff0c;后端负责坚如磐石 企业在做Web系统时&#xff0c;成本与收益的对比是不可忽视的&#xff0c;企业做项目一定是要盈利的&#xff01; 企业做项目最求的不是技术的新和潮&#xff0c;追求的是低成本和稳还有好用度&#xff…

函数的设计

一、默认参数 C允许在函数定义或声明时&#xff0c;为形参指定默认值&#xff0c;即默认参数&#xff08;default argument&#xff09;。 &#xff08;1&#xff09;函数定义与函数声明只能设置一次默认参数。 &#xff08;2&#xff09;可以设置多个默认参数&#xff0c;设…

【精华】搞定JVM调优学习

JVM 介绍 1. 什么是 JVM JVM 是 Java Virtual Machine&#xff08;Java 虚拟机&#xff09;的缩写。一台执行 Java 程序的机器。 2 .JAVA 语言的执行原理 计算机语言&#xff1a; 计算机能够直接执行的指令。这种指令和系统及硬件有关。 计算机高级语言&#xff1a; 在遵循…

「数据」驱动行业拐点,毫末智行冲刺自动驾驶3.0时代

“毫末预计&#xff0c;到2025年中国高阶辅助驾驶搭载率将达到70%。而在汽车新消费领域&#xff0c;中国汽车市场增换购消费比例将达到60%&#xff0c;智能驾驶功能成为必选因素&#xff0c;并迎来商业化的加速发展。”1月5日&#xff0c;第七届HAOMO AI DAY上&#xff0c;毫末…

黑马学ElasticSearch(五)

目录&#xff1a; &#xff08;1&#xff09;DSL查询语法-DSL查询分类和基本语法 &#xff08;2&#xff09;DSL查询语法-全文检索查询 &#xff08;3&#xff09;DSL查询语法-精确查询 &#xff08;4&#xff09;DSL查询语法-地理查询 &#xff08;5&#xff09;DSL查询语…

计算机网络(一)

计算机网络1 概述1.1 计算机网络的作用1.2 因特网概述1.2.1 网络的网络1.2.2 Internet和internet的区别1.2.3 因特网发展的三个阶段1.2.4 ISP介绍1.2.5 ISP分类1.2.5.1 主干ISP1.2.5.2 地区ISP1.2.5.3 本地ISP1.2.6 因特网交换点 IXP1.3 因特网的组成1.3.1 因特网的边缘部分1.3…

资产管理4大难点,如何破解?

随着企业业务扩大、人员增多&#xff0c;固定资产的数量和种类也会随着增加。此时&#xff0c;如何高效管理企业资产就成为很多企业亟待解决的一大难题。 传统资产管理4大难点 01.资产管理部门需要联系采购部门、使用部门、财务部门等收集数据&#xff0c;汇总难且工作量大&…

vacuum移除不必要的CLOG文件

迫切模式弥补了惰性模式的缺陷。它会扫描所有页面&#xff0c;检查表中的所有元组&#xff0c;更新相关的系统视图&#xff0c;并在可能时删除不必要的CLOG文件与页面。当满足以下条件时&#xff0c;会执行迫切模式。pg_database.datfrozenxid<(OldestXmin-vacuum_freeze_ta…

【多线程】【C++ 知识点】pthread_join学习

目录pthread_join进程id和线程idpthread_join pthread_join() 主线程会进入阻塞装题&#xff0c;pthread_join()之后的代码&#xff0c;只有等待子进程退出之后才能执行。 代码块A pthread_create(&id, NULL, Fn, NULL);pthread_create(&id_1, NULL, Fn, NULL);pthre…

jdk1.8 更替为 oepnJdk8遇到的坑

背景&#xff1a;客户服务器因为说jdk要收费&#xff0c;所以要求将jdk1.8替换为openJdk&#xff0c;本地测试ok&#xff0c;则将服务器的jdk替换为openJdk8&#xff0c;出现一个登录异常&#xff0c;调查发现是一个sso登录的问题&#xff08;单点登录&#xff09;&#xff0c;…

Rhce第一次作业

chrony服务部署&#xff1a;两台机器a: 第一台机器从阿里云同步时间&#xff0c;第二台机器从第一台机器同步时间1.查看防火墙是否关闭&#xff0c;若未关闭&#xff0c;关闭防火墙2.打开chrony配置文件3.向配置文件中写入阿里云时间服务器&#xff0c;并允许两台机器所在的网段…

从0到1完成一个Vue后台管理项目(十八、基础地图绘制)

往期 从0到1完成一个Vue后台管理项目&#xff08;一、创建项目&#xff09; 从0到1完成一个Vue后台管理项目&#xff08;二、使用element-ui&#xff09; 从0到1完成一个Vue后台管理项目&#xff08;三、使用SCSS/LESS&#xff0c;安装图标库&#xff09; 从0到1完成一个Vu…

spring restTemplate的坑----会对String类型的url中的特殊字符进行转义

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