文章目录
- 强化历程5-Java并发系列(2023.8.23)
- 1 Java多线程
- 1.1 Java中多线程有几种实现方式?
- 1.2 那么Runnable和Callable都可以实现多线程,他们有什么区别?
- 1.3 采用实现Runnable和Callable接口方式和采用继承Thread类方式各有什么好处?
- 1.4 Java如何停止一个正在运行的线程
- 1.5 上面你提到`volatile`关键字,这是什么?
- 1.6 所以`volatile`具不具有原子性?
- 1.7`volatile`可以保证有序性吗?
- 1.8 notify()和notifyAll()有什么区别?
- 1.9 sleep()和wait()有什么区别?
- 1.11 Thread类中的start()和run()方法有什么区别?
- 1.12 为什么wait, notify和notifyAll这些方法不在Thread类里?
- 1.13 为什么wait和notify方法要在同步块中调用?
- 1.14 Java中interrupted和isInterruptedd方法的区别?
- 1.15 Java中synchronized和ReentrantLock有什么不同?
- 1.16 Thread类中的yield方法有什么作用?
- 1.17 什么是线程池?
- 1.18 Java中有常用线程池?
- 1.19 自定义线程池参数?
- 1.20 线程池核心线程数怎么设置呢?
- 1.21 线程池拒绝策略
- 1.22 Java线程池中队列常用类型有哪些?
- 1.23 Java线程池中submit()和execute()方法有什么区别?
- 1.24 什么是 synchronized关键字?
- 1.25 说说自己是如何使用synchronized关键字?
- 1.26 什么是Callable和Future?
- 2 并发编程
- 2.1 有三个线程T1,T2,T3,如何保证顺序执行?
- 2.2 什么是线程安全?
- 2.3 Vector是一个线程安全类吗?
- 2.4 SynchronizedMap和ConcurrentHashMap有什么区别?
- 2.5 锁的优化机制了解吗?
- 2.6 什么是CAS?
- 2.7 CAS的问题?
- 2.8 什么是乐观锁什么是悲观锁?
- 2.9 产生死锁的四个必要条件?
- 2.10 如何避免死锁?
- 2.11 线程安全需要保证几个基本特征?
- 2.12 说一下线程之间是如何通信的?
- 2.13 引用类型有哪些?有什么区别?
- 2.14 说说ThreadLocal原理?
- 2.15 说说你对JMM内存模型的理解?为什么需要JMM?
- 2.16 说说CyclicBarrier和CountDownLatch的区别?
- 2.17 什么是AQS?
- 2.18 了解Semaphore吗?
- 2.19 什么是阻塞队列?阻塞队列的实现原理是什么?如何使用阻塞队列来实现生产者-消费者模型?
- 2.20 什么是多线程中的上下文切换?
- 2.21 什么是Daemon线程?它有什么意义?
- 2.22 乐观锁和悲观锁的理解及如何实现,有哪些实现方式?
强化历程5-Java并发系列(2023.8.23)
1 Java多线程
1.1 Java中多线程有几种实现方式?
答:四种,继承 Thread类,实现Runnable和Callable接口,线程池
1.2 那么Runnable和Callable都可以实现多线程,他们有什么区别?
答:① Runnable通过run()方法创建线程任务,而且该方法执行后无法返回结果,如果需要得到结果,需要声明全局变量获得。
Callable通过call()方法来创建线程任务,该方法执行后可以直接返回一个由泛型指定类型的结果
② Runnable中run()方法不会抛出异常
Callable中call()方法可以抛出异常
③Runnable可以直接通过Thread对象来执行线程任务
Callable定义线程任务则需要FutureTask(未来任务类)或ExecutorService来执行线程任务
1.3 采用实现Runnable和Callable接口方式和采用继承Thread类方式各有什么好处?
答:
①实现Runnable和Callable接口方式:
- 实现接口后还可以继承其他类,这种方式多个线程可以共享一个任务,适合多线程处理同一份资源的情况,从而可以将CPU,代码和数据分割开来,较好体现面向对象思想。
- 但是这种方式编程较复杂,想要访问当前线程使用
Thread.currentThread()
方法②继承Thread类方式:
- 编程简单,访问当前线程可以直接使用this
- 继承Thread类后,不能继承其他类
1.4 Java如何停止一个正在运行的线程
答:
① 使用
interrupt
方法中断线程,当线程在等待、睡眠或阻塞时,会抛出InterruptedException
。② 使用
stop
方法强行终止(过期方法)③ 通过设置标记终止线程
- 设置一个共享的
volatile boolean
变量,当需要停止线程时,将这个变量设置为true
。然后,线程在运行时需要定期检查这个变量,如果为true
,就退出运行。
1.5 上面你提到volatile
关键字,这是什么?
答:
volatile
是Java中的一个关键字,用于修饰变量(修饰共享变量可以保证共享变量同步)。当一个变量被volatile
修饰时,它会具有以下两个特性:① 可见性:当一个线程对
volatile
变量进行修改时,其他线程会立即看到修改后的值。每次读取该变量时都需要从主内存中获取,而不是从该变量所在的处理器的缓存中获取。这是因为
volatile
变量在写入时会立即同步到主内存,而在读取时也会从主内存中获取最新的值。这样可以保证不同线程之间的数据一致性,避免了数据不一致的问题。② 禁止指令重排序:
volatile
变量的读取和写入操作都是原子性的,也就是说,这些操作是不可分割的,不会被其他线程打断。这样可以保证多线程访问时的数据安全性,避免了数据竞争和不一致的问题。
1.6 所以volatile
具不具有原子性?
答:他可以保证单操作原子性,但是不保证复合操作的原子性,例如自增或者自减等操作,因为这些操作实际上是由多个步骤组成的。如果需要保证复合操作的原子性,需要使用更高级别的并发控制机制,比如
synchronized
关键字或者Lock
接口。
1.7volatile
可以保证有序性吗?
答:有序性:有序性是指指令的执行顺序,即程序在执行时,必须按照代码的顺序执行,不能随意改变指令的执行顺序。
他只保证部分有序性:当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
volatile boolean flag = false; int x = 0; // Thread 1 flag = true; // 操作1 x = 42; // 操作2 // Thread 2 if (flag) { // 操作3 int y = x; // 操作4 // do something with y }
- 线程1和线程2交替执行。当线程1执行操作1后,由于存在指令重排,处理器可能会将操作2放在操作1之后执行。然后,线程2执行操作3,检查
flag
的值为true
,但是此时x
的值可能还没有被线程1设置为42
。因此,在这种情况下,线程2可能会获取到x
的旧值,而不是线程1最新设置的值。
1.8 notify()和notifyAll()有什么区别?
答:两者都是Object类的方法,都可以进行线程唤醒,notify()可能导致死锁,因为只通知一个线程,如果这个线程无法完成任务,其他等待的线程也无法得到通知,此时可能会发生死锁。而notifyAll()则不会导致死锁,因为它会唤醒所有等待的线程,它们可以重新竞争。
任何时候只有一个线程可以获得锁,也就是说只有一个线程可以运行synchronized 中的代码
使用notifyall,可以唤醒 所有处于wait状态的线程,使其重新进入锁的争夺队列中,而notify只能唤醒一个。
wait() 应配合while循环使用,不应使用if,务必在wait()调用前后都检查条件,如果不满足,必须调用notify()唤醒另外的线程来处理,自己继续wait()直至条件满足再往下执行。
1.9 sleep()和wait()有什么区别?
答:
①所属类不同:sleep()是Thread类的静态方法,而wait()是Object类的普通方法。
②持有锁的状态不同:sleep()方法并不会释放锁,而wait()方法会释放锁。
③应用场景不同:sleep()方法可以在任何地方使用,而wait()方法通常在同步控制中使用。
④唤醒机制不同:sleep()方法唤醒后,线程会自动恢复到原来的状态,而wait()方法唤醒后,线程需要重新竞争以获取对象的锁。
⑤捕获异常不同:sleep()方法必须捕获异常,而wait()方法不需要捕获异常。
1.11 Thread类中的start()和run()方法有什么区别?
答:start()方法用于启动新线程并执行run()方法,而run()方法则用于定义线程的执行行为。因此,如果你想在Java中使用多线程,应该调用start()方法来启动新的线程。
1.12 为什么wait, notify和notifyAll这些方法不在Thread类里?
答:在Java中,锁是对象级别的,而不是线程级别的。这意味着,当一个线程需要获取一个对象的锁时,它必须通过该对象本身来获取。因此,将wait、notify和notifyAll方法定义在Object类中是有意义的,因为它们与对象锁的获取和释放紧密相关。
1.13 为什么wait和notify方法要在同步块中调用?
答: ① 只有在调用线程拥有某个对象的独占锁时,才能够调用该对象的wait(),notify()和notifyAll()方法。
②如果你不这么做,你的代码会抛出IllegalMonitorStateException异常。
③ 还有一个原因是为了避免wait和notify之间产生竞态条件。
1.14 Java中interrupted和isInterruptedd方法的区别?
答:interrupted() 和 isInterrupted()的主要区别是前者会将中断状态清除而后者不会。
简单来说前者是将线程中断后者是判断线程是否处于中断状态
1.15 Java中synchronized和ReentrantLock有什么不同?
答:
①synchronized是Java的关键字,是一种内建的锁机制,与对象关联,是JVM级别的锁。当一个方法被synchronized修饰时,只有获得对象锁的线程才能执行该方法,其他线程需要等待。
ReentrantLock是java.util.concurrent.locks包下的一个类,是一种显式的锁机制,与对象关联。使用ReentrantLock类需要手动加锁和解锁,通常通过try-finally块来确保解锁。
②等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。
③公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。
④锁绑定多个条件,一个ReentrantLock对象可以同时绑定对个对象。
1.16 Thread类中的yield方法有什么作用?
答:Thread类中的yield()方法是一种控制线程执行的机制,它用于让当前线程暂停执行,让其他线程有机会执行。
具体来说,yield()方法的作用是暂停当前正在执行的线程,将CPU的控制权让给其他线程。当一个线程调用yield()方法时,它会暂停当前的任务并让出CPU资源,这使得其他等待执行的线程有机会获得CPU资源并继续执行。
1.17 什么是线程池?
答:①降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
②提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
③提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
1.18 Java中有常用线程池?
答:①
newSingleThreadExecutor()
:创建单线程的线程对象②
newFixedThreadPool(int nThreads)
:创建固定线程数量的线程池对象③
newCachedThreadPool()
:创建一个带有缓冲区的线程池④
newScheduledThreadPool(int corePoolSize):
创建一个周期性线程池
1.19 自定义线程池参数?
答:①核心线程数
②最大线程数
③等待时间单位
④等待时间
⑤空闲等待队列
⑥线程工厂
⑦拒绝策略
1.20 线程池核心线程数怎么设置呢?
答:IO密集型:2*cpu内核数,系统IO比cpu速度慢
CPU密集型: cpu内核数+1,一个任务出现问题的话,避免资源浪费
1.21 线程池拒绝策略
答:
new ThreadPoolExecutor.AbortPolicy()
异常策略,当有新线程任务提交后会报拒绝异常(默认)
new ThreadPoolExecutor.CallerRunsPolicy()
谁提交谁执行策略,线程池已满,该策略将线程给提交者执行
new ThreadPoolExecutor.DiscardPolicy()
丢弃策略,当新任务提交后,线程池无法满足,直接将新提交任务丢弃
new ThreadPoolExecutor.DiscardOldestPolicy()
替换策略,替换等待时间最久的任务
1.22 Java线程池中队列常用类型有哪些?
答:
ArrayBlockingQueue
是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则 对元素进行排序。LinkedBlockingQueue
一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue
。SynchronousQueue
一个不存储元素的阻塞队列。PriorityBlockingQueue
一个具有优先级的无限阻塞队列。PriorityBlockingQueue
也是基于
最小二叉堆实现
1.23 Java线程池中submit()和execute()方法有什么区别?
答:两者都是线程池提交任务的方法,前者没有返回值,后者返回Future对象。
1.24 什么是 synchronized关键字?
答:synchronized是Java语言中的一个关键字,它用于控制多个线程对共享资源的访问顺序和互斥性。synchronized关键字可以用来修饰方法或代码块,它保证在同一时刻最多只有一个线程可以执行被synchronized修饰的方法或代码块。
1.25 说说自己是如何使用synchronized关键字?
答:① 同步方法:使用synchronized关键字修饰方法,可以保证同一时刻只有一个线程可以执行该方法。
② 同步代码块:使用synchronized关键字修饰代码块,可以指定在执行该代码块时需要获取某个对象的锁。
synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。
synchronized关键字加到实例方法上是给对象实例上锁。
尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能!
1.26 什么是Callable和Future?
答:
Callable被线程执行后,可以返回值,这个返回值可以被Future拿到,也就是说,Future可以拿到异步执行任务的返回值。
Future接口表示异步任务,是还没有完成的任务给出的未来结果。所以说Callable用于产生结果,Future用于获取结果。
2 并发编程
2.1 有三个线程T1,T2,T3,如何保证顺序执行?
答:① 利用线程优先级
②使用互斥锁:使用互斥锁来确保同一时刻只有一个线程可以访问共享资源。首先,线程T1获取互斥锁,执行完成后释放锁。然后,线程T2获取互斥锁并执行,完成后释放锁。最后,线程T3获取互斥锁并执行。这样可以确保按照设定的顺序执行。
③利用join()方法
2.2 什么是线程安全?
答:线程安全是指在多线程环境下,一个线程对共享资源的操作不会对其他线程造成影响。即在任何时刻,每个线程对共享资源的访问都是正确的,不会发生数据冲突和不安全的情况。
2.3 Vector是一个线程安全类吗?
答:
Vector是一个线程安全类。它的所有方法都是同步的,因此多个线程不能同时修改Vector的内容,从而保证了数据的一致性和完整性。然而,由于Vector的线程安全特性,其性能在多线程环境下可能比非线程安全的类(如ArrayList)要低。
2.4 SynchronizedMap和ConcurrentHashMap有什么区别?
答:
SynchronizedMap和ConcurrentHashMap主要有以下几点区别:
- 线程安全性:
- SynchronizedMap:在SynchronizedMap中,整个Map的所有操作都是同步的,所以它在多线程环境下的线程安全性有保证。
- ConcurrentHashMap:ConcurrentHashMap采用了分段锁的设计,使得多个线程在访问Map的不同段时可以获取不同的锁,这种设计提高了伸缩性,使得在高并发环境下其性能优于SynchronizedMap。
- 锁机制:
- SynchronizedMap:在SynchronizedMap中,只有一个锁,当一个线程访问Map时,其他线程必须等待。
- ConcurrentHashMap:ConcurrentHashMap使用了多个锁(通常与Map的大小相同),因此,当一个线程访问Map的一部分时,其他线程仍然可以访问Map的其他部分,这大大提高了并发性。
- 性能:
- 在高并发环境下,由于ConcurrentHashMap采用了分段锁的设计,使得它相对于SynchronizedMap有更好的并发性能。
- 当只有一个线程访问Map时,SynchronizedMap的性能较好,因为不需要获取锁。
- ConcurrentModificationException:在遍历Map时,如果其他线程试图对Map进行数据修改,SynchronizedMap可能会抛出ConcurrentModificationException,而ConcurrentHashMap则不会。
总的来说,选择哪种Map取决于你的具体需求。如果你的应用场景需要整个Map的所有操作都同步,那么SynchronizedMap是一个好的选择。如果你的应用场景需要更高的并发性,并且可以接受部分操作不同步,那么ConcurrentHashMap可能是一个更好的选择。
2.5 锁的优化机制了解吗?
答:
- 自旋锁(Spinlock):在短时间内的等待时,为了避免线程从用户态转为内核态以及上下文切换的开销,可以让当前线程进行忙等待,这就是自旋锁。当共享资源被其他线程使用时,当前线程会循环检查,直到能获取到锁。
- 自适应自旋锁(Adaptive spinlock):自适应自旋锁根据上一次在同一个锁的自旋时间和锁的持有者状态来决定自旋的次数。如果上一次等待的时间较长,或者锁的持有者状态不稳定(如在等待队列中),则可能会选择放弃自旋,从而避免长时间占用CPU资源。
- 锁消除(Lock elision):锁消除是指JVM检测到一些同步的代码块完全不存在数据竞争的场景,就会进行锁消除。这种方式可以提高并发性能并减少不必要的锁竞争。
- 偏向锁(Biased lock):这是一种针对非热点代码的优化方式。当线程进入同步代码块时,会在对象的对象头和栈帧中保存当前线程的id,再次进入同步块时都不需要CAS来加锁和解锁了。偏向锁会永远偏向第一个获得锁的线程,后面没有线程竞争的话,线程都不需要同步。
- 轻量级锁(Lightweight lock):轻量级锁基于CAS操作,代码进入同步块时,JVM将会使用CAS方式来尝试获取锁。如果更新成功则会把对象头中的状态位标记为轻量级锁,如果更新失败,当前线程就尝试自旋来获得锁。
- 锁粗化(Lock coarsening):如果一个程序有多个操作都对同一个对象加锁,可以将这些操作看作一个序列操作,将这些操作对同一个对象加锁的过程合并,从而减少加锁和解锁的次数。
2.6 什么是CAS?
答:
当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
2.7 CAS的问题?
答:
- ABA问题:CAS操作在比较值时,只能确定值没有被其他线程修改,但无法确定值没有被其他线程读取和修改。
- 因此,如果一个线程读取了某个值,进行了修改,然后又被其他线程修改了,那么该线程的修改就会被覆盖掉,这就是ABA问题。
- 自旋开销:CAS操作需要不断地尝试获取锁,如果获取不到就会一直自旋等待,这会导致CPU资源的浪费。
- 不适合高竞争的场景:在高竞争的场景下,CAS操作的效率会降低,因为大量的重试和失败会导致CPU资源的浪费。
2.8 什么是乐观锁什么是悲观锁?
答:
乐观锁:认为每次取数据别人都不会修改,所以不上锁,但是在修改数据时会判断在此期间该数据有无修改,实现方式:版本号机制,CAS
悲观锁:认为每次取数据别人都会去修改,所以每次操作都会上锁,实现方式:synchronized关键字
2.9 产生死锁的四个必要条件?
答:
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
- 环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。
2.10 如何避免死锁?
答:因为互斥条件是必须的,破坏其他三个条件即可。
2.11 线程安全需要保证几个基本特征?
答:
- 原子性,简单说就是相关操作不会中途被其他线程干扰,一般通过同步机制实现。
- 可见性,是一个线程修改了某个共享变量,其状态能够立即被其他线程知晓,通常被解释为将线程本地状态反映到主内存上,volatile 就是负责保证可见性的。
- 有序性,是保证线程内串行语义,避免指令重排等。
2.12 说一下线程之间是如何通信的?
答:
共享内存
线程 A 把本地内存 A 更新过得共享变量刷新到主内存中去。
线程 B 到主内存中去读取线程 A 之前更新过的共享变量。
消息传递
在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行 通信。在 Java 中典型的消息传递方式,就是 wait() 和 notify() ,或者 BlockingQueue 。
2.13 引用类型有哪些?有什么区别?
答:
- 强引用:我们经常使用的例如
A a =new A()
,只要强引用还存在,垃圾收集器永远不会回收被引用的对象。即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。- 弱引用:内存满后,直接gc可以回收
- 软引用:gc即可回收
- 虚引用:等于没有虚引用必须和ReferenceQueue一起使用,当发生GC的时候,虚引用也会被回收。虚引用的主要作用是跟踪对象被垃圾回收的活动。
2.14 说说ThreadLocal原理?
答:ThreadLocal可以理解为线程本地变量,他会在每个线程都创建一个副本,那么在线程之间访问内部副本变量就行了,做到了线程之间互相隔离,相比于synchronized的做法是用空间来换时间。
ThreadLocal原理是,ThreadLocal内部维护了一个Map,这个Map不是直接使用的HashMap,而是ThreadLocal实现的一个叫做ThreadLocalMap的静态内部类。ThreadLocal类中最主要的就是set/get方法,这些方法其实是调用了ThreadLocalMap类对应的set/get方法。最终的变量是放在了当前线程的ThreadLocalMap中,并不是存在ThreadLocal上,ThreadLocal可以理解为只是ThreadLocalMap的封装,传递了变量值。
2.15 说说你对JMM内存模型的理解?为什么需要JMM?
答:
随着CPU和内存的发展速度差异的问题,导致CPU的速度远快于内存,所以现在的CPU加入了高速缓存,高速缓存一般可以分为L1、L2、L3三级缓存。基于上面的例子我们知道了这导致了缓存一致性的问题,所以加入了缓存一致性协议,同时导致了内存可见性的问题,而编译器和CPU的重排序导致了原子性和有序性的问题,JMM内存模型正是对多线程操作下的一系列规范约束,
因为不可能让程序员的代码去兼容所有的CPU,通过JMM我们才屏蔽了不同硬件和操作系统内存的访问差异,这样保证了Java程序在不同的平台下达到一致的内存访问效果,同时也是保证在高效并发的时候程序能够正确执行。
2.16 说说CyclicBarrier和CountDownLatch的区别?
答:
①CyclicBarrier的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;CountDownLatch则不是,某线程运行到某个点上之后,只是给某个数值-1而已,该线程继续运行
②CyclicBarrier只能唤起一个任务,CountDownLatch可以唤起多个任务
③CyclicBarrier可重用,CountDownLatch不可重用,计数值为0该CountDownLatch就不可再用了
2.17 什么是AQS?
答:
AQS(AbstractQueuedSynchronizer)是Java并发包java.util.concurrent.locks中的一部分,是一个用于实现线程同步的抽象类。它提供了一个基于队列的、用于构建锁或其他同步组件的框架。
AQS使用一个int类型的成员变量state来表示同步状态,当state>0时表示已经获取了锁,当state=0时表示释放了锁。它提供了三个方法(getState()、setState(int newState)、compareAndSetState(int expect,int update))来对同步状态state进行操作,当然这些操作是原子的。
AQS本身是一个框架,具体的同步组件可以通过实现它的抽象方法来实现线程同步。例如,ReentrantLock和ReentrantReadWriteLock都实现了AQS的抽象方法,并使用了AQS的内部锁机制。
总之,AQS是一个用于实现线程同步的抽象类,它提供了一个基于队列的框架,使得实现线程同步变得更加方便和灵活。
2.18 了解Semaphore吗?
答:
- 信号量,Semaphore可以设定一个许可数量,当一个线程需要访问资源时,它会先尝试获取许可。如果许可数量已满,线程则会阻塞,直到有更多的许可被释放。同样,当一个线程释放许可时,如果有其他线程正在等待,那么这个释放的许可将会被传递给其中一个等待的线程。
2.19 什么是阻塞队列?阻塞队列的实现原理是什么?如何使用阻塞队列来实现生产者-消费者模型?
答:
阻塞队列(Blocking Queue)是一种特殊的队列,用于在多线程环境中共享数据。当队列为空时,从队列中获取元素的操作将会被阻塞,直到其他线程往队列中插入新的元素。同样,当队列已满时,往队列中插入元素的操作将会被阻塞,直到其他线程从队列中获取元素。
阻塞队列的实现原理通常基于数组或链表数据结构,并使用条件变量(Condition)来实现线程间的同步。当队列为空时,获取操作会通过条件变量等待插入操作完成;当队列已满时,插入操作会通过条件变量等待获取操作完成。
Java的
java.util.concurrent
包中提供了BlockingQueue
接口及其实现类,如ArrayBlockingQueue
、LinkedBlockingQueue
等。这些阻塞队列的实现都是线程安全的,因此可以直接在多线程环境中使用。使用阻塞队列来实现生产者-消费者模型可以按照以下步骤:
- 创建一个阻塞队列,用于存储共享数据。
- 创建一个生产者线程,用于生成数据并往阻塞队列中插入。
- 创建一个或多个消费者线程,用于从阻塞队列中获取数据并处理。
- 当阻塞队列为空时,消费者线程会被阻塞等待生产者线程生成新的数据;当阻塞队列已满时,生产者线程会被阻塞等待消费者线程处理已有的数据。
下面是一个简单的Java示例代码:
import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; public class ProducerConsumerExample { public static void main(String[] args) { // 创建一个阻塞队列 BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(10); // 创建一个生产者线程 Thread producer = new Thread(() -> { try { int value = 0; while (true) { queue.put(value); // 往队列中插入数据 System.out.println("Produced: " + value); value++; Thread.sleep(1000); // 模拟生成数据的时间 } } catch (InterruptedException e) { e.printStackTrace(); } }); // 创建一个消费者线程 Thread consumer = new Thread(() -> { try { while (true) { int value = queue.take(); // 从队列中获取数据 System.out.println("Consumed: " + value); Thread.sleep(1500); // 模拟处理数据的时间 } } catch (InterruptedException e) { e.printStackTrace(); } }); // 启动生产者线程和消费者线程 producer.start(); consumer.start(); } }
这个示例代码中,生产者线程会不断生成新的整数并往阻塞队列中插入,消费者线程会不断从阻塞队列中获取整数并处理。当队列为空时,消费者线程会被阻塞等待生产者线程生成新的数据;当队列已满时,生产者线程会被阻塞等待消费者线程处理已有的数据。
2.20 什么是多线程中的上下文切换?
答:上下文切换(Context Switching)是指在CPU中切换线程执行的过程。当一个线程正在执行时,CPU需要暂停当前线程的执行,并将其上下文(如程序计数器、寄存器内容、堆栈指针等)保存到内存中,然后加载另一个线程的上下文,使其继续执行。这个过程就是上下文切换。
2.21 什么是Daemon线程?它有什么意义?
答:所谓后台(daemon)线程,也叫守护线程,是指在程序运行的时候在后台提供一种通用服务的线程,并且这个线程并不属于程序中不可或缺的部分。因此,当所有的非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程。
反过来说, 只要有任何非后台线程还在运行,程序就不会终止。必须在线程启动之前调用setDaemon()方法,才能把它设置为后台线程。注意:后台进程在不执行finally子句的情况下就会终止其run()方法。
比如:JVM的垃圾回收线程就是Daemon线程,Finalizer也是守护线程。
2.22 乐观锁和悲观锁的理解及如何实现,有哪些实现方式?
答:
- 每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
- 每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。
对于Java来说,乐观锁通常通过使用版本号(如JPA中的版本字段)和数据库的UPDATE语句实现。悲观锁则可以通过数据库排他锁(如MySQL的FOR UPDATE)或Java的synchronized关键字实现。