四川做网站多少钱,提供邯郸移动网站建设,wordpress categories,广东省城乡和建设厅网站文章导读 引言考点1. CAS 指令#xff08;重点#xff09;一、什么是CAS二、CAS 的优点三、CAS 的缺点四、ABA问题五、相关面试题 考点2. 信号量#xff08;semaphore#xff09;一、基本概念二、信号量的主要操作三、信号量的应用四、相关面试题 考点3、CountDownLatch 类… 文章导读 引言考点1. CAS 指令重点一、什么是CAS二、CAS 的优点三、CAS 的缺点四、ABA问题五、相关面试题 考点2. 信号量semaphore一、基本概念二、信号量的主要操作三、信号量的应用四、相关面试题 考点3、CountDownLatch 类一、主要用途二、主要方法三、示例 考点4、Callable 接口Callable 与 Runnable 的主要区别使用场景示例相关面试题 考点5、多线程下的数据结构一、多线程环境使用ArrayList二、多线程环境下使用哈希表1、Hashtable2、ConcurrentHashMap重点相关面试题 考点五、其他常见面试题 引言
本篇文章总结了多线程中面试频率比较高的考点内容可能比较琐碎但是如果能够坚持看完注意总结积累相信对面试会有很大帮助。多线程内容较多用一篇文章写完可能篇幅过长我打算用两篇文章来总结本篇主要写的是多线程中辅助加锁的数据结构和指令下一篇主要讲的是锁策略。
考点1. CAS 指令重点
一、什么是CAS
CASCompare-and-Swap是一种用于实现多线程同步的原子指令。它涉及到三个操作数内存位置V、预期原值A和新值B。如果内存位置的值与预期原值相匹配那么处理器会自动将该位置值更新为新值这个操作是原子的。
CAS 操作包含三个关键动作
比较Compare将内存位置的值与预期原值进行比较。交换Swap如果比较相等那么处理器会自动将该内存位置的值更新为新值。原子性Atomicity上述整个比较和交换的操作是原子的即该操作在执行过程中不会被其他线程的操作打断。
CAS 指令一般是基于硬件实现的在 Intel 处理器中它可以通过 LOCK 前缀的 CMPXCHG 指令来实现。在 Java 中CAS 操作被广泛用于实现非阻塞算法如原子变量类java.util.concurrent.atomic 包下的类中的 getAndIncrement、compareAndSet 等方法都是基于 CAS 实现的。
二、CAS 的优点
非阻塞算法CAS 允许线程在不进入阻塞状态的情况下进行并发操作这有助于减少线程切换的开销提高系统的并发性能。无需使用锁在多个线程竞争同一个资源时传统的锁机制可能会导致线程阻塞而 CAS 可以在不依赖锁的情况下实现线程间的同步。
三、CAS 的缺点
循环时间长开销大如果 CAS 操作一直不成功那么线程会一直处于自旋状态返回失败并持续重试这会增加 CPU 的负担。只能保证一个共享变量的原子操作当需要对多个共享变量进行操作时CAS 就无法保证操作的原子性了。这种情况下需要使用锁或其他同步机制来保证操作的原子性。
四、ABA问题 情景 假设小明有存款1000元他去ATM机上取100元服务器产生了两个线程处理。线程1执行完存款变为900线程2CAS指令比较失败无法执行。 就在小明取钱的时候小明的爸爸给小明的银行账户转了100元产生了线程3。如果线程3是在线程1执行后才产生的那么就会出现存款从 1000 - 900 - 1000 的过程于是再执行线程2的CAS指令就会成功。本来小明只想取100元现在取钱操作执行了两次取出了200元 定义 在CAS操作中线程会首先读取某个内存位置的值我们称之为预期值A然后执行CAS操作尝试将该内存位置的值修改为新的值我们称之为B但前提是内存位置的值必须仍然是预期值A。如果在读取值和尝试修改值之间有其他线程修改了该内存位置的值比如从A改为了B然后又改回了A那么CAS操作会错误地认为该值没有变化从而成功执行这就会导致ABA问题。 在以上情景中存款从1000变900再变成1000的过程所导致的取钱两次的BUG就是ABA问题。 解决方法 给要修改的值, 引⼊版本号. 在 CAS ⽐较数据当前值和旧值的同时, 也要⽐较版本号是否符合预期。例如 给存款引入版本号每次执行线程时版本号加1. 版本号为1线程1执行扣款成功存款为900版本号1变为2线程3执行存入成功存款为1000版本号1变为3线程2执行版本号与之前读取的不同执行失败。
五、相关面试题 讲解下你⾃⼰理解的 CAS 机制。ABA问题怎么解决 忠告相关面试题的答案我不会给出读者应自己总结积累盲目背诵答案已经过时面试场上的八股文已被千变万化的情景题目所取代只有自己总结积累经验和知识才能应对变化才能让面试官青睐
考点2. 信号量semaphore
一、基本概念
定义信号量是一个非负整数用于表示某种资源的数量。它有两个主要操作P等待和V释放。作用实现任务之间的同步或临界资源的互斥访问常用于协助一组相互竞争的任务来访问临界资源。
二、信号量的主要操作 P等待操作 当一个进程或线程需要访问共享资源时它会尝试执行P操作。如果信号量的值大于0表示资源可用进程或线程可以继续访问资源并将信号量的值减1。如果信号量的值等于0表示资源已被占用进程或线程会被阻塞直到信号量的值变为正数。 V释放操作 当一个进程或线程完成对共享资源的访问时它会执行V操作将信号量的值加1。如果有其他等待进程被阻塞它们中的一个将被唤醒并获得对资源的访问权限。
代码示例 public static void main(String[] args) {// 信号量为4表明有四个资源待访问Semaphore semaphore new Semaphore(4);// 写一个线程访问访问资源Thread t new Thread(() - {try {// accquire方法表示P操作semaphore.acquire();// do something ...Thread.sleep(1000);// release方法表示V操作semaphore.release();} catch(InterruptedException e){e.printStackTrace();}});t.start();}三、信号量的应用
信号量在操作系统和并发编程中有着广泛的应用包括但不限于
进程同步控制多个进程的执行顺序保证数据的正确处理。临界资源的互斥访问保护共享资源防止数据竞争和冲突。生产者-消费者问题在生产者-消费者模型中通过信号量来控制资源的生产和消费。线程池管理控制线程池中的线程数量以控制系统的负载。顺序控制确保多个任务按照特定的顺序执行。
综上所述信号量是一种重要的同步机制通过合理地控制信号量的值可以实现对共享资源的互斥访问和同步操作从而避免并发编程中的常见问题。
四、相关面试题 简单解释一下什么是信号量什么场景下会使用到信号量 考点3、CountDownLatch 类
CountDownLatch 是 Java 并发包 java.util.concurrent 中的一个非常有用的同步辅助类它允许一个或多个线程等待一组其他线程完成操作。这个类通过让一个或多个线程等待其他线程完成一组操作来协调线程。CountDownLatch 初始化时设置一个计数器count这个计数器代表等待完成的操作的数量。
一、主要用途
等待多个线程完成CountDownLatch 允许一个或多个线程等待其他一组线程完成它们的任务。例如在启动多个线程进行并行计算时你可能希望等待所有线程都完成计算后再继续执行后续操作。性能优化通过并行处理可以提高应用程序的响应速度和吞吐量。CountDownLatch 可以帮助在并行处理完成后同步后续操作。
二、主要方法
CountDownLatch(int count)构造函数初始化计数器值为给定的 count 值。void await()使当前线程在锁存器倒计数至零之前一直处于等待状态除非线程被中断。void await(long timeout, TimeUnit unit)使当前线程在锁存器倒计数至零之前一直处于等待状态或者从当前时间起已经过了指定的等待时间或者线程被中断。void countDown()递减锁存器的计数如果计数到达零则释放所有等待的线程。
三、示例
下面是一个简单的示例展示了如何使用 CountDownLatch 来等待一组线程完成它们的任务。
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class CountDownLatchExample {public static void main(String[] args) throws InterruptedException {int taskCount 5;ExecutorService executor Executors.newFixedThreadPool(taskCount);CountDownLatch latch new CountDownLatch(taskCount);for (int i 0; i taskCount; i) {executor.submit(() - {try {// 模拟任务执行Thread.sleep(1000);} catch (InterruptedException e) {Thread.currentThread().interrupt();}latch.countDown(); // 完成任务减少计数器});}// 等待所有任务完成latch.await();System.out.println(所有任务完成);executor.shutdown();}
}在这个示例中我们创建了一个包含 5 个任务的线程池。每个任务完成后都会调用 countDown() 方法来减少 CountDownLatch 的计数器。主线程通过调用 await() 方法等待直到计数器的值达到 0即所有任务都已完成。然后主线程继续执行并打印出 “所有任务完成”。
考点4、Callable 接口
在Java中Callable 接口是Java并发API的一部分它位于 java.util.concurrent 包中。与 Runnable 接口不同Callable 接口可以返回一个结果并且可能抛出一个异常。这使得 Callable 接口非常适合用于那些需要返回值的并发任务。
Callable 接口的定义如下
FunctionalInterface
public interface CallableV {/*** Computes a result, or throws an exception if unable to do so.** return computed result* throws Exception if unable to compute a result*/V call() throws Exception;
}Callable 与 Runnable 的主要区别
返回值Runnable 接口的 run 方法没有返回值而 Callable 接口的 call 方法可以返回一个泛型类型的值。异常处理Runnable 的 run 方法不允许抛出受检查的异常checked exceptions而 Callable 的 call 方法可以。如果 call 方法抛出了一个异常这个异常将被封装在一个 ExecutionException 中这个异常是由 Future.get() 方法抛出的。
使用场景
当你需要执行一个任务并且这个任务完成后需要返回一个结果时就可以使用 Callable。例如你可能需要从远程服务器获取数据或者执行一些计算并返回结果。
示例
创建线程计算 1 2 3 … 1000,
使用 Run 版本 创建⼀个类 Result包含⼀个 sum 表示最终结果, lock 表⽰线程同步使⽤的锁对象。 main ⽅法中先创建 Result 实例, 然后创建⼀个线程 t. 在线程内部计算 1 2 3 … 1000. 主线程同时使⽤ wait 等待线程 t 计算结束. (注意, 如果执行到 wait 之前, 线程 t 已经计算完了, 就不必等待了)。 当线程 t 计算完毕后, 通过 notify 唤醒主线程, 主线程再打印结果.
public class Demo18 {static class Result {public int sum 0;public Object lock new Object();}public static void main(String[] args) throws InterruptedException {Result result new Result();Thread t new Thread() {Overridepublic void run() {int sum 0;for (int i 1; i 1000; i) {sum i;}synchronized (result.lock) {result.sum sum;result.lock.notify();}}};t.start();synchronized (result.lock) {while (result.sum 0) {result.lock.wait();}System.out.println(result.sum);}}
}可以看到上述代码需要⼀个辅助类 Result还需要使⽤⼀系列的加锁和 wait notify 操作代码复杂容易出错。
使用Callable版本
创建⼀个匿名内部类实现 Callable 接⼝。 Callable 带有泛型参数泛型参数表⽰返回值的类型。重写 Callable 的 call ⽅法完成累加的过程直接通过返回值返回计算结果.把 callable 实例使⽤ FutureTask 包装⼀下。线程的构造⽅法传⼊ FutureTask。 此时新线程就会执行 FutureTask 内部的 Callable 的 call 方法完成计算计算结果就放到了 FutureTask 对象中。在主线程中调⽤ futureTask.get() 能够阻塞等待新线程计算完毕. 并获取到 FutureTask 中的结果。
public class Demo18 {public static void main(String[] args) throws InterruptedException, ExecutionException {CallableInteger callable new CallableInteger() {Overridepublic Integer call() throws Exception {int sum 0;for (int i 1; i 1000; i) {sum i;}return sum;}};FutureTaskInteger futureTask new FutureTask(callable);Thread t new Thread(futureTask);t.start();int result futureTask.get();System.out.println(result);}
}可以看到使⽤ Callable 和 FutureTask 之后代码简化了很多也不必⼿动写线程同步代码了。
相关面试题 请你说说Callable 和 Runnable 的主要区别 考点5、多线程下的数据结构
一、多线程环境使用ArrayList
普通的 ArrayList 线程并不安全在使用时必须在可能发生冲突的地方加锁操作复杂且容易发生死锁。使用较多使用Collections.synchronizedList(new ArrayList)synchronizedList 是标准库提供的⼀个基于 synchronized 进行线程同步的方法它是 Collections 类的一个静态方法它的返回值是一个对关键方法加了锁的链表List。这样可以简化程序猿对代码的加锁操作降低死锁的风险。常考使⽤ CopyOnWriteArrayList这是一个写时复制的容器。 原理 当我们往⼀个容器添加元素的时候不直接往当前容器添加而是先将当前容器进行Copy复制出⼀个新的容器然后新的容器里添加元素添加完元素之后再将原容器的引用指向新的容器。 优点 线程可以对CopyOnWrite原容器进行并发的读而不需要加锁因为原容器不会添加任何元素读和写是不同的容器。在读多写少的场景下性能很高不需要加锁竞争。 缺点 占用内存较多且新写的数据不能被第⼀时间读取到。
二、多线程环境下使用哈希表
HashMap 本身是线程不安全的Java 又在 HashMap 的基础上封装了两个类
HashtableConcurrentHashMap
1、Hashtable
Hashtable 只是在 HashMap 的基础上把关键方法加上了锁synchronized如
public synchronized V get(Object key)
public synchronized V put(K key, V value)这无疑是给所有读写操作都加上了锁线程想要访问同一个 Hashtable 的任何数据都会直接造成锁竞争一把锁锁上整个Hash表如图一个每次只能有一个线程访问该表一旦触发扩容就只有在单个线程触发扩容的线程上进行涉及大量的数据拷贝效率非常低。
2、ConcurrentHashMap重点
ConcurrentHashMap 是 Java 并发包 java.util.concurrent 中的一个非常重要的类用于在并发环境下替代传统的 HashMap。它提供了比 Hashtable 更高的并发级别因为 Hashtable 是同步的这意味着在每一次访问时整个表都需要被锁定这大大降低了并发性能。ConcurrentHashMap 通过以下几个方面的优化和改进来提升并发性能 分段锁Segmentation Locking在 Java 8 之前: 在 Java 8 之前ConcurrentHashMap 使用分段锁的机制来减少锁的竞争。它将整个哈希表分为多个段Segment每个段都维护着自己的锁。这样在并发环境中只要多个线程访问的是不同的段它们就可以并行地执行从而减少了锁的争用。每个段内部都维护了一个哈希表用于存储键值对。当需要对某个键进行操作时首先通过哈希码确定该键属于哪个段然后只锁定该段进行操作而不是锁定整个表。 锁粒度细化Fine-grained Locking在 Java 8 及以后: 从 Java 8 开始ConcurrentHashMap 放弃了分段锁的设计转而采用了一种更为灵活的锁策略即使用 Node 数组加上链表或红黑树在链表过长时的方式来存储键值对并通过 synchronized 关键字或 CASCompare-And-Swap操作来确保线程安全。在 Java 8 的实现中锁被细化到了每个桶bucket上即每个数组元素。当多个线程访问不同的桶时它们可以并行地执行。这进一步减少了锁的竞争提高了并发性能。读操作没有加锁(但是使用了 volatile 保证从内存读取结果)只对写操作进行加锁。 使用 CASCompare-And-Swap操作: CAS 是一种无锁算法用于实现线程间的同步而不需要使用传统的锁。在 ConcurrentHashMap 的实现中当尝试修改某个桶或节点时会尝试使用 CAS 操作来更新该桶的状态。如果桶的状态在此期间没有被其他线程修改则 CAS 操作成功否则重试CAS与版本号结合。CAS 操作减少了锁的使用从而提高了性能但也可能导致更高的 CPU 使用率因为需要不断重试直到成功为止。 动态扩容: 定义 ConcurrentHashMap 支持动态扩容即当哈希表中的元素数量达到某个阈值时会自动进行扩容操作以避免哈希冲突和性能下降。与 HashMap 类似扩容操作涉及到重新计算每个元素的哈希码并将其放置到新的哈希表中。但 ConcurrentHashMap 的扩容操作是并发安全的可以在不阻塞读操作的情况下进行。原理 发现需要扩容的线程只需要创建⼀个新的数组同时只搬几个元素过去。扩容期间, 新老数组同时存在后续每个来操作 ConcurrentHashMap 的线程都会参与搬家的过程每个操作负责搬运一小部分元素搬完最后⼀个元素再把老数组删掉。这个期间插入的元素只往新数组里添加查找需要同时查新数组和⽼数组。 红黑树优化: 在 Java 8 及以后的版本中当某个桶中的链表长度超过一定阈值时默认为 8ConcurrentHashMap 会将该链表转换为红黑树以优化查找性能。这是因为红黑树在查找、插入和删除操作上的时间复杂度比链表更低在平均和最坏情况下都是 O(log n)可以进一步提高并发性能。
综上所述ConcurrentHashMap 通过分段锁在 Java 8 之前、锁粒度细化在 Java 8 及以后、CAS 操作、动态扩容和红黑树优化等多种机制来优化和改进并发性能使其成为 Java 中处理并发哈希表的首选数据结构。
相关面试题 ConcurrentHashMap的读是否要加锁为什么介绍下 ConcurrentHashMap的锁分段技术ConcurrentHashMap在jdk1.8做了哪些优化Hashtable和HashMap、ConcurrentHashMap 之间的区别 考点五、其他常见面试题
以下的面试题的答案都在我之前的文章中大家可以从中寻找答案这里我就不再一一赘述。 谈谈 volatile关键字的用法 参考文章 线程安全 Java多线程是如何实现数据共享的? JVM 把内存分成了这几个区域 ⽅法区, 堆区, 栈区, 程序计数器。 其中堆区这个内存区域是多个线程之间共享的。只要把某个数据放到堆内存中就可以让多个线程都能访问到。 Java创建线程池的接⼝是什么参数 LinkedBlockingQueue 的作用是什么 参考文章 线程池的认识和使用 Java线程共有几种状态状态之间怎么切换的 参考文章 线程安全 Thread类和线程的用法 Thread和Runnable的区别和联系 参考文章 Thread类和线程的用法