1、并发编程三要素?
并发编程的三要素包括共享数据、互斥访问和同步机制。
1.1 共享数据
共享数据是指在多个线程之间共享的数据资源。多个线程可以同时读取和修改共享数据。共享数据在并发编程中可能引发以下问题:
数据竞争:当多个线程同时修改共享数据时,可能导致不可预期的结果,程序的行为可能不再是确定性的,依赖于线程的执行顺序和时间。
数据一致性:当多个线程同时读取和修改共享数据时,可能导致数据不一致的问题,例如一个线程读取了一个尚未被另一个线程修改的共享数据的旧值。
1.2 互斥访问
互斥访问是指在多个线程之间对共享数据的访问进行互斥控制,保证同一时间只有一个线程可以对共享数据进行读取或修改。通过互斥访问,可以避免数据竞争和数据一致性的问题。常用的互斥控制机制包括:
互斥锁:通过使用互斥锁,保证同一时间只有一个线程可以获得锁并访问共享数据,其他线程需要等待锁的释放才能访问共享数据。
读写锁:允许多个线程同时读取共享数据,但只允许一个线程进行写操作,这样可以提高读取操作的并发性能,同时保证写操作的原子性和线程安全性。
1.3 同步机制
同步机制是指在多个线程之间进行协调和同步操作,以保证共享数据的一致性和正确性。同步机制可以通过控制线程的执行顺序和时机来确保共享数据的正确访问。常用的同步机制包括:
条件变量:用于线程之间的通信和等待,一个线程可以等待某个条件变量的触发,而另一个线程可以在满足特定条件时触发条件变量,从而唤醒等待的线程继续执行。
信号量:用来实现线程的互斥和同步操作,通过控制信号量的值和使用wait和signal操作,实现线程的阻塞和唤醒。
来源链接 : 并发编程三要素:共享数据、互斥访问和同步机制
2、同步方法和同步块哪个是更好的选择?
同步块通常是更好的选择,因为它不会锁住整个对象,而是可以灵活控制锁的范围,从而提高并发性能并减少死锁的风险。具体来说:
锁的范围:
同步方法:会锁住整个对象,即使类中只有部分方法需要同步,也会导致整个对象被锁定,这可能导致性能下降,尤其是在多线程环境下12。
同步块:可以锁定代码块,而不是整个对象。这意味着只有需要同步的代码部分会被锁定,减少了锁的范围,从而提高了并发性能12。
性能和并发性能:
同步方法:由于锁的范围较大,可能会导致线程频繁等待锁的释放,影响并发性能12。
同步块:通过精确控制锁的范围,减少了线程等待的时间,提高了并发性能12。
使用场景:
同步方法:适合于整个方法都需要同步的情况,代码简洁明了3。
同步块:适合于只需要对部分代码进行同步的情况,或者需要对多个对象进行同步时更为灵活和高效3。
3、谈谈原子性?哪些使用到了?
原子性(Atomicity)是指一个操作在执行过程中不可分割,要么完全执行,要么完全不执行。在并发编程和多线程环境中,原子性操作不会被其他线程的操作打断,从而确保操作的完整性和一致性。
原子性的定义和重要性
在并发编程中,原子性确保了临界区域的代码在执行过程中不会被其他线程干扰,从而避免了数据竞争和不一致的问题。例如,在银行转账操作中,从账户A扣除金额和向账户B增加金额这两个操作必须作为一个整体执行,否则可能会导致资金错误12。
原子性的实现方式
synchronized关键字:在Java中,可以使用synchronized关键字来确保一段代码在同一时刻只能被一个线程执行,从而保证操作的原子性。例如:
javaCopy Code
synchronizedvoidincrease(){ i++;}
Lock接口:Java的java.util.concurrent包提供了Lock接口,通过显式锁定和解锁机制来实现同步,例如使用ReentrantLock:
javaCopy Code
Locklock=newReentrantLock();lock.lock();try{ i++;}finally{ lock.unlock();}
Atomic类:Java的java.util.concurrent.atomic包提供了一系列原子类,如AtomicInteger,可以确保对整数的操作是原子的:
javaCopy Code
AtomicIntegeratomicInteger=newAtomicInteger(0);atomicInteger.incrementAndGet();
内存屏障:在某些情况下,可以使用内存屏障(Memory Barrier)来确保指令的执行顺序,从而保证操作的原子性。
原子性在其他领域的应用
数据库事务:在数据库管理中,原子性是ACID属性之一,确保事务中的所有操作要么全部提交,要么全部回滚。数据库通过日志记录和恢复技术来实现原子性3。
多核处理器指令集:在一些处理器架构中,对某些类型的操作(如读取或写入特定大小的内存块)提供了原子性支持。例如,在32位系统上,对32位整数的读写通常是原子的4。
4、谈谈可见性?哪些使用到了?
在并发编程中,可见性是指多个线程访问共享变量时,一个线程修改的状态能够立即对其他线程可见。由于多核CPU的缓存机制,可能会导致数据不一致的问题。
解决可见性的方式包括使用volatile关键字、synchronized锁和Lock机制。volatile关键字确保变量修改后立即同步到主内存,禁止指令重排序;synchronized在锁定和解锁时同步缓存;Lock则是基于volatile并手动控制锁状态实现同步2。
参考:并发编程三大特性:2、可见性(什么是可见性、解决可见性的方式)
Volatile、Synchronized、ReentrantLock锁机制使用说明
5、强引用、弱引用、软引用、虚引用
6、ThreadLocal 实现原理 与 如何使用弱引用解决内存泄漏问题
参考:Java ThreadLocal 实现原理 与 如何使用弱引用解决内存泄漏问题
ThreadLocal 源码解析:巧用弱引用解决内存泄漏问题
7、谈谈有序性?举一个例子?
8、什么是线程池?
参考:线程池详解(建议收藏)
9、谈谈四种线程池的创建?
参考:java四种线程池创建
10、多线程的优缺点?
提高程序性能:多线程允许程序在多核处理器上并行执行多个任务,充分利用现代多核CPU的计算能力。通过将任务分割成可以并行执行的子任务,并将这些子任务分配给多个线程,程序可以显著缩短任务的执行时间12。
提高响应速度:在图形用户界面(GUI)应用中,多线程可以使主线程处理用户输入,而其他线程执行耗时的后台任务,从而提高程序的响应速度和用户体验23。
改善程序设计:多线程可以使程序更加灵活和可靠。例如,在服务器程序中,多线程可以处理多个客户端请求,保证程序的稳定性和可靠性4。
多线程的缺点包括:
内存占用增加:线程越多,占用的内存也越多,这可能会导致系统资源紧张5。
管理开销增加:线程的协调和管理需要额外的CPU开销,增加了程序的复杂性和潜在的错误率5。
资源共享问题:多线程环境中需要解决资源共享的问题,不当的资源共享可能导致数据不一致或其他异常情况2。
11、创建线程的有哪些方式?
继承Thread类:创建一个类继承Thread类,并重写run()方法来实现线程的执行逻辑。然后创建该类的实例并调用start()方法启动线程。这种方式简单直观,适合初学者学习线程的基本原理,但扩展性较差,因为Java不支持多继承12。
实现Runnable接口:创建一个类实现Runnable接口,实现run()方法来实现线程的执行逻辑。然后创建Thread类的实例,将实现了Runnable接口的类实例作为参数传递给Thread类的构造函数,最后调用start()方法启动线程。这种方式任务与代码分离,适合多线程共享执行相同任务的情况12。
使用匿名内部类:可以在创建线程的同时定义并实现线程的执行逻辑,使用匿名内部类的方式。这种方式代码简洁,适用于简单的线程创建1。
使用Lambda表达式:在Java 8及以上版本中,可以使用Lambda表达式来简化线程的创建。这种方式代码更加简洁,适用于简单的线程任务1。
实现Callable接口:通过实现Callable接口,实现call()方法来实现线程的执行逻辑。然后创建FutureTask对象,将Callable对象传递给FutureTask的构造函数,最后通过FutureTask对象的get()方法获取执行结果。这种方式可以获取线程的执行结果,适用于需要返回执行结果的场景34。
使用线程池:通过线程池来创建和管理线程。线程池提供了多种创建和管理线程的方式,可以提高资源利用率和减少线程创建的开销。适用于需要大量并发处理的任务4。
使用定时器工具类:通过定时器工具类(如Timer和ScheduledExecutorService)来创建定时执行的线程任务。这种方式适用于需要定时执行的任务4。
参考:线程创建方式
12、谈谈各种创建线程的优缺点?
13、Runnable和Callable的区别?
返回值:
Runnable:其run()方法没有返回值。适用于不需要返回结果的任务12。
Callable:其call()方法可以返回一个泛型指定的值,并且可以抛出受检异常(checked exception)。适用于需要返回结果或处理异常的复杂任务12。
异常处理:
Runnable:run()方法不能抛出受检异常,所有的异常需要在方法内部处理或通过其他机制传递12。
Callable:call()方法可以抛出受检异常,异常可以在外部被捕获和处理,或者通过Future对象的get()方法捕获12。
使用方式:
Runnable:通常与Thread类结合使用,通过Thread的构造函数传递Runnable实例来创建线程24。
Callable:通常与线程池(如ExecutorService)结合使用,线程池会返回一个Future对象,用于表示异步计算的结果,通过Future对象可以获取计算结果或捕获异常24。
适用场景:
Runnable:适用于不需要返回值且不会抛出受检异常的简单任务,如简单的打印输出、修改共享变量等24。
Callable:适用于需要返回值或需要处理异常的复杂任务,如计算结果处理、网络或IO操作等,常用于实现异步任务处理以提高系统吞吐量和响应速度24。
14、线程在执行过程中的状态是如何流转的?
15、LockSupport.park与LockSupport.unpark的底层原理
参考:Java并发基础:LockSupport.park与LockSupport.unpark的底层原理
16、互斥锁(mutex)的底层原理是什么? 操作系统具体是怎么实现的?
参考:互斥锁(mutex)的底层原理是什么? 操作系统具体是怎么实现的?
17、常用的并发集合类有哪些?
18、ConcurrentHashMap实现
参考:最爱问的高频ConcurrentHashMap原理,你会了吗?
19、ConcurrentHashMap 为何不能插入 null?HashMap 为何可以?
参考:为什么HashMap的键值可以为null,而ConcurrentHashMap不行?
ConcurrentHashMap 为何不能插入 null?HashMap 为何可以?
HashMap很美好,但线程不安全怎么办?ConcurrentHashMap告诉你答案
20、HashMap万字解析
参考:耗时3天写完的HashMap万字解析,争取一篇文章讲透它,面试官看了都直点头
面试官上来就让手撕HashMap的7种遍历方式,当场愣住,最后只写出了3种
21、CopyOnWriteArrayList实现
参考:深入解析 `CopyOnWriteArrayList` 的实现原理与应用
22、CopyOnWriteArraySet实现
参考:CopyOnWriteArrayList和 CopyOnWriteArraySet 怎么实现线程安全
23、常用的并发工具类有哪些
参考:CountDownLatch、Semaphore等4大并发工具类详解
24、CyclicBarrier和CountDownLatch的应用场景
CyclicBarrier的应用场景
并行任务同步:CyclicBarrier常用于需要多个线程或任务并行执行到某个点后同步进行的场景。例如,在跑步比赛中,所有参赛者需要热身完毕才能同时起跑1。
数据汇总和处理:在多线程计算数据后,CyclicBarrier可以用于合并计算结果。例如,计算5个人的月平均工资,每个线程计算一个人的工资,所有线程计算完毕后合并结果2。
重复执行的循环任务:CyclicBarrier的可重复使用特性使其适用于需要多次重复执行的循环任务。每次任务完成后,所有线程再次等待并重新执行,直到满足某些条件3。
CountDownLatch的应用场景
串行任务等待:CountDownLatch适用于需要等待一组操作按顺序完成的场景。例如,电商平台的订单处理系统,需要完成库存检查、支付验证、优惠券验证等多个独立任务后才能确认订单4。
初始化操作:在服务器或应用程序启动时,CountDownLatch可以用于确保某些初始化操作完成后再进行下一步。例如,服务器在接受请求前必须完成依赖服务的初始化5。
区别
可重复使用性:CyclicBarrier是可重复使用的,当所有线程到达屏障后,它会重置并允许再次使用;而CountDownLatch是一次性的,一旦计数达到零,就不能再次使用3。
参考:CountDownLatch 和 CyclicBarrier 使用场景详解
25、CyclicBarrier和CountDownLatch的区别
参考:CountDownLatch 和 CyclicBarrier的区别与详解
26、Semaphore的应用场景
27、synchronized的作用?底层如何实现?
52.说一下 synchronized 底层实现原理?_synchronized底层实现
28、synchronized和ReentrantLock的区别
synchronized和ReentrantLock的主要区别在于获取和释放锁的方式、锁的公平性、响应中断的能力以及底层实现机制等方面。
获取和释放锁的方式:
synchronized:synchronized是隐式锁,当进入synchronized修饰的代码块或方法时自动加锁,离开时自动释放锁。这种方式不需要手动管理锁的获取和释放12。
ReentrantLock:ReentrantLock是显式锁,需要手动调用lock()方法获取锁,并在使用完毕后调用unlock()方法释放锁。这种方式需要程序员显式管理锁的生命周期12。
锁的公平性:
synchronized:synchronized是非公平锁,不能保证等待时间最长的线程最先获取锁12。
ReentrantLock:ReentrantLock默认是非公平锁,但可以通过构造函数设置为公平锁。公平锁会按照线程请求锁的顺序来分配锁,保证等待时间最长的线程最先获取锁12。
响应中断的能力:
synchronized:不能响应中断,如果发生死锁,线程会一直等待下去23。
ReentrantLock:可以通过lockInterruptibly()方法获取锁并响应中断,如果在等待过程中线程被中断,则会抛出InterruptedException,从而允许线程提前退出等待状态23。
底层实现机制:
synchronized:是Java内置的关键字,通过JVM层面的监视器(Monitor)实现,采用悲观并发策略4。
ReentrantLock:基于AbstractQueuedSynchronizer(AQS)框架实现,是一个高级的同步工具类,采用乐观并发策略4。
其他特性:
ReentrantLock提供了更多的功能,如尝试非阻塞获取锁(tryLock())、设置超时时间、判断锁是否被其他线程持有等12。
参考:synchronized和ReentrantLock有什么区别
死磕 java同步系列之ReentrantLock VS synchronized——结果有点出乎意外
高并发时为什么推荐ReentrantLock而不是synchronized
29、jvm 虚拟线程
参考:[Java基础]虚拟线程
30、什么是CAS?底层如何实现
31、什么是Future?底层如何实现
32、什么是AQS?底层如何实现?
参考: AQS实现原理
万长文字 | 16张图解开AbstractQueuedSynchronizer
33、ReadWriteLock底层实现
34、死锁的常见原因有哪些
35、怎么唤醒一个阻塞的线程
36、什么是多线程的上下文切换
37、线程调度算法是什么?
38、什么是线程调度器和时间分片
参考:什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing)?
39、单例模式的线程安全性?
参考:线程安全的单例模式