【多线程】精品面试题61道
在多线程编程中,伪共享指的是多个线程在同时访问不同但是在同一缓存行中的变量,由于缓存行的一致性协议,这些变量的访问会相互影响,导致性能下降。在现代计算机体系结构中,内存系统一般被分为若干个缓存行,每个缓存行的大小通常为 64 字节。当多个线程并发访问不同但是在同一缓存行中的变量时,会导致缓存一致性协议的频繁通信,从而降低程序的性能。例如,如果两个变量a和b在同一个缓存行中,线程 1 修改了变量a的
【多线程】精品面试题61道
1.进程和线程什么区别?
2.进程间通信有哪几种方式?
3.操作系统什么情况下会死锁?
4.产生死锁的四个必要条件是什么?
5.如何理解分布式锁?
6.线程同步与阻塞的关系是怎样的?同步一定阻塞吗?阻塞一定同步吗?
7.同步和异步有什么区别?
8.为什么要使用线程池?
9.如何调用wait()方法?使用if块还是循环?为什么?
10.实现线程的几种方法?
11.什么是多线程环境下的伪共享(false sharing)?
12.如何指定多个线程的执行顺序?
13.线程池的7个核心参数是什么?
14.线程池的拒绝策略是怎样的?
15.ThreadLocal原理和实现是怎样的?
16.线程池的线程数量是怎么确定的?
17.进程的组成部分有哪些?
18.进程间五种通信方式有什么区别?
19.如何预防死锁?
20.synchronized和Lock的区别是什么?
21.什么是CAS?
22.什么是CAS操作的ABA问题?
23.AQS理论的数据结构是什么?
24.父子线程间怎么共享数据?
25.CountDownLatch和CyclicBarrier的区别是什么?
26.什么是总线风暴?
27.什么是内存屏障?
28.synchronized在JDK6做了哪些优化?
29.什么是happens before原则?
30.Java常见的锁有哪几种?
31.Java乐观锁如何实现?
32.如何实现阻塞队列?
33.CountDownLatch、CyclicBarrier、Semaphore区别和使用场景?
34.ConcurrentHashMap在JDK1.8前后线程安全实现方式有何不同?
35.协程和线程的区别是什么?
36.多线程的异步调用怎么实现的?
37.线程池的阻塞队列用的最多的是什么?
38.ArrayBlockingQuene和LinkdeBlockingQuene的区别是什么?
39.SingleThreadExecutor和CachedThreadPool为什么不推荐使用,会出现哪些问题?
40.有一个static变量,初始值是0,现在有2个程序同时修改这个值,每个程序都是进行自增操作,请问这个变量最后的取值可能有多少?如果这个变量加上volatile,取值可能有多少
41.是否了解synchronized关键词
42.Tomcat中打破双亲委派模型的方法有哪两种?
43.synchronized实现原理是什么?
44.死锁怎么查?
45.ReentrantLock原理是什么?
46.自旋锁升级到重量级锁条件是什么?
47.volatile的特性是什么?
48.敖丙遇到的问题多线程线程因抛错被中断了?
49.什么是公平锁和非公平锁?
50.在JVM中,对象在内存中分为哪三块区域?
51.那synchronized他自己本身又具有哪些特性呢?
52.synchronized底层实现是怎样的?
53.什么是用户态和内核态?
54.官方优化后的锁升级过程是怎样的?
55.可见性的解决方案有哪些?
56.volatile与synchronized的区别?
57.乐观锁在项目开发中的实践和ABA怎么保证?
58.讲一下悲观锁和悲观锁怎么保证只有一个线程进入临界区?
59.正常线程进程同步的机制有哪些?
60.分布式锁你了解过有哪些?
1.进程和线程什么区别?
区别:
1.颗粒大小
进程指的是运行中的程序,进程是操作系统资源分配的最小单位
线程是进程中的一个实体,指进程中的顺序执行流,是CPU任务调度的最小单位
一个进程可以包含多个线程
2.通信难度
进程之间是相互独立的,它们之间的通信需要通过进程间通信(IPC)机制来实现,
进程间通信难度大
线程则共享同一进程的内存和文件等资源,线
程间通信非常容易,而每个线程则拥有自己的运行栈和程序计数器
2.系统开销
创建和切换线程的开销相对较小
系统在运行时会为每个进程分配内存空间
除CPU外,系统不会为线程分配内存,线程所使用的资源来自所属进程的资源
2.进程间通信有哪几种方式?
进程间通信(IPC)是指不同进程之间传递信息或者协调工作的技术和机制。
常见的IPC方式有以下几种:
1.管道(Pipe):管道是一种半双工的通信方式,只能在具有父子关系的进程之间使用。管道可以用于单向数据传输,也可以通过创建两个管道实现双向通信。
2.FIFO命名管道(Named Pipe):命名管道也是一种半双工的通信方式,但不限于具有父子关系的进程,可以在不同进程之间使用。命名管道可以通过文件系统中的文件名进行访问。
3.信号(Signal):信号是一种异步通信方式,用于通知接收进程发生了某个事件,如进程结束、用户中断等。信号可以用于进程间的通信和同一进程中不同线程之间的通信。
4.共享内存(Shared Memory):共享内存是一种高效的IPC方式,多个进程可以访问同一块内存区域,实现数据共享。但需要注意的是,共享内存的并发控制和同步问题需要由应用程序自行处理。
5.信号量(Semaphore):信号量是一种计数器,用于控制多个进程对共享资源的访问。通过加锁和解锁操作,可以实现对共享资源的互斥访问和同步。
6.消息队列(Message Queue):消息队列是一种按照消息类型进行有序排列并具有特定读写权限的消息缓存区,多个进程可以通过消息队列进行通信和同步。
7.套接字(Socket):套接字是一种网络通信方式,不仅可以用于不同计算机之间的进程通信,也可以用于同一计算机内的进程间通信。套接字可以实现多种通信协议,如TCP和UDP等。
3.操作系统什么情况下会死锁?
操作系统在多进程并发执行的情况下,会出现死锁的情况。
死锁是指两个或多个进程相互等待对方释放资源而陷入无限等待的状态,从而导致系统无法正常运行。
具体来说,操作系统在以下情况下会出现死锁:
1.资源竞争:多个进程同时请求共享资源,但这些资源只能被一个进程占用,当它们相互等待对方释放资源时,就会陷入死锁状态。
2.进程间通信:多个进程之间需要通过信号量、消息队列等方式进行通信,但如果通信过程中出现意外情况(如阻塞、中断等),就可能导致死锁。
3.循环等待:多个进程之间形成了一个循环等待的环路,每个进程都在等待下一个进程释放资源,从而导致系统陷入死锁状态。
为了避免死锁的发生,操作系统通常采用一些预防和避免策略,
如资源分配策略、进程调度策略、剥夺策略等,以尽可能地减少死锁的风险。
4.产生死锁的四个必要条件是什么?
产生死锁的四个必要条件是:
1.互斥条件:一个资源每次只能被一个进程使用。
2.请求与保持条件:一个进程在等待申请的资源时,继续占有已经分配到的资源。
3.不剥夺条件:已经分配给进程的资源不能被强制性地抢占,只能在进程用完后自行释放。
4.循环等待条件:若干进程之间形成一种头尾相接的循环等待资源的关系。
当这四个条件同时成立时,就可能会导致死锁的发生。
在实际应用中,操作系统通常采取一些预防和避免策略,
比如破坏死锁的四个条件之一,以尽可能地减少死锁的风险。
例如,通过限制进程的最大资源使用量、强制释放部分资源等方式来破坏不剥夺条件;
通过资源分配序列化、资源优先级等方式来破坏循环等待条件。
5.如何理解分布式锁?
分布式锁解决分布式场景下数据一致性的问题,保证各个线程或进程之间的互斥性和同步性。
分布式锁具有以下特点:
1.互斥性:同一时刻只能有一个进程或线程持有该锁。
2.可重入性:同一个进程或线程可以重复获取该锁,避免死锁的发生。
3.容错性:在集群中的某些节点宕机或网络故障时,仍能保证锁的正确性。
4.高可用性:在高并发场景下,能够保证锁的高效和可用性。
分布式锁通常有两种实现方式:基于数据库和基于缓存。
在基于数据库的实现中,通过在数据库中创建一张锁表,将锁作为一条记录插入该表中,从而实现对共享资源的控制。
在基于缓存的实现中,通过在缓存中存储锁的状态信息,来协调多个进程或线程之间对共享资源的访问。
总之,分布式锁是一种重要的分布式系统协调机制,能够有效地解决多个进程或线程之间访问共享资源的问题,提高系统的可用性和性能。
6.线程同步与阻塞的关系是怎样的?同步一定阻塞吗?阻塞一定同步吗?
线程同步与否跟阻塞不阻塞没关系
同步是个过程,阻塞是线程的一种状态
线程同步是指多个线程之间协调工作,以便于它们能够按照既定的顺序执行,并且共享资源时能够保证数据的一致性和正确性。线程同步的实现方式通常有互斥锁、信号量、条件变量等。
线程阻塞是指线程在执行过程中,由于某些原因(如等待资源、等待I/O操作等),无法继续执行,从而被暂停。线程阻塞的实现方式通常有睡眠、等待信号等。
多个线程操作共享变量时可能会出现竞争,这时需要同步来防止两个以上的线程同时进入临界区内,在这个过程中后进入临界区的线程将阻塞,等待先进入的线程走出临界区
7.同步和异步有什么区别?
同步和异步最大的区别在于:同步需要等待,异步不需要等待
同步是指任务按照一定的顺序依次执行,前一个任务的执行结束后才会执行下一个任务。在同步执行的过程中,如果某个任务出现问题,则会阻塞后续所有任务的执行。
异步是指任务在执行过程中不需要等待前一个任务的完成,可以同时执行多个任务。在异步执行的过程中,如果某个任务出现问题,不会影响到其他任务的执行。
一般来说,同步适用于执行顺序有要求的任务
而异步适用于执行顺序没有要求且需要提高执行效率的任务。
8.为什么要使用线程池?
多线程程序在并发执行多个任务时,线程的创建和销毁会消耗大量的系统资源,降低程序的性能,并且可能导致系统崩溃。
线程池的主要目的是为了减少线程的创建和销毁过程,提高线程的复用率和执行效率。线程池将多个线程预先创建好,并且维护一个任务队列,接收需要执行的任务,将任务分配给空闲的线程执行。执行完任务后,线程不会立即销毁,而是返回线程池,等待下一个任务的到来,从而降低了创建和销毁线程的开销。
线程池可以避免线程数量的过多导致系统资源的浪费,
同时也可以避免线程数量不足导致任务无法及时执行的问题。
9.如何调用wait()方法?使用if块还是循环?为什么?
Java中使用wait()方法来使线程进入等待状态,直到其他线程调用notify()或notifyAll()方法来唤醒它。wait()方法一般需要与synchronized关键字一起使用,以确保线程安全。
在调用wait()方法时,一般都需要使用循环来判断是否满足唤醒条件。因为在多线程环境中,可能存在虚假唤醒的情况,即线程在没有接收到notify()或notifyAll()方法的唤醒信号的情况下,也会从wait()方法中返回。这种情况的出现可能会导致程序逻辑错误,因此需要使用循环来判断是否满足唤醒条件。
使用while循环的代码示例如下:
synchronized (lock) {
while (!condition) {
lock.wait();
}
// 执行任务
}
使用if块的代码示例如下:
synchronized (lock) {
if (!condition) {
lock.wait();
}
// 执行任务
}
可以看出,使用while循环和if块都可以实现wait()方法的调用,
但是while循环更为安全,能够避免虚假唤醒的情况
10.实现线程的几种方法?
在Java中,实现线程的几种方法如下:
1.继承Thread类:定义一个类继承Thread类并重写run()方法。
然后通过实例化该类并调用start()方法来启动线程。
2.实现Runnable接口:定义一个类实现Runnable接口并重写run()方法。
然后通过实例化该类并将其作为参数传递给Thread类的构造方法来启动线程。
3.实现Callable接口:定义一个类实现Callable接口并重写call()方法。
然后通过实例化该类并将其作为参数传递给ExecutorService的submit()方法来启动线程。
4.使用线程池:使用Java提供的线程池框架来管理线程的创建和销毁,从而实现线程的复用。
5.使用定时器:使用Java提供的Timer类和TimerTask类来实现定时任务,从而间接实现多线程。
无论使用哪种方法实现线程,都需要注意线程安全问题,避免出现数据不一致或者死锁等问题。
11.什么是多线程环境下的伪共享(false sharing)?
在多线程编程中,伪共享指的是多个线程在同时访问不同但是在同一缓存行中的变量,由于缓存行的一致性协议,这些变量的访问会相互影响,导致性能下降。
在现代计算机体系结构中,内存系统一般被分为若干个缓存行,每个缓存行的大小通常为64字节。当多个线程并发访问不同但是在同一缓存行中的变量时,会导致缓存一致性协议的频繁通信,从而降低程序的性能。
例如,如果两个变量a和b在同一个缓存行中,线程1修改了变量a的值,线程2修改了变量b的值,这会导致缓存一致性协议将缓存行的数据全部刷回到主内存中,从而影响程序的性能。
为了避免伪共享问题,可以通过以下两种方式来进行优化:
1.将不同的变量放置在不同的缓存行中,避免多个线程同时访问同一缓存行。
2.使用填充技术(Padding)来占据多余的缓存行,从而避免多个变量在同一缓存行中,从而避免伪共享问题。
总之,伪共享问题在多线程编程中是一个常见的性能瓶颈,需要特别注意。
12.如何指定多个线程的执行顺序?
指定多个线程的执行顺序可以通过以下方式实现:
1.使用 join() 方法。在一个线程中调用其他线程的 join() 方法,表示该线程需要等待其他线程执行完成之后再继续执行。
2.使用 wait() 和 notify() 方法。在一个线程中调用其他线程的 wait() 方法,表示该线程需要等待其他线程发出 notify() 信号之后再继续执行。
3.使用 Lock 和 Condition。使用 Lock 和 Condition 可以实现更加灵活的线程调度。
4.设定一个orderNum,要while先判断orderNum是否等于自己的要求值,不是则wait(),是则执行本线程
13.线程池的7个核心参数是什么?
线程池的7个核心参数如下:
1.corePoolSize:线程池的核心线程数,即线程池中一直保持的线程数量。
2.maximumPoolSize:线程池中允许的最大线程数,包括核心线程和非核心线程。
3.keepAliveTime:非核心线程的空闲时间,超过这个时间就会被回收,直到线程池中的线程数量不大于核心线程数。
4.unit:keepAliveTime 参数的时间单位,通常为秒或毫秒。
5.workQueue:任务队列,用于存放等待执行的任务。
6.threadFactory:线程工厂,用于创建新线程。
7.rejectedExecutionHandler:拒绝策略,
当任务队列已满且线程池中的线程数量达到最大值时,用于处理无法处理的任务。
14.线程池的拒绝策略是怎样的?
线程池在执行任务时,如果线程池已经满了,那么新的任务就需要进行一些处理,这个处理方式就是拒绝策略。
线程池的拒绝策略有以下几种:
1.CallerRunsPolicy:直接在调用者线程中运行被拒绝的任务。
2.AbortPolicy:直接抛出RejectedExecutionException异常。
3.DiscardPolicy:直接丢弃被拒绝的任务。
4.DiscardOldestPolicy:丢弃最早被放入线程池的任务,然后尝试重新提交被拒绝的任务。
其中,CallerRunsPolicy是最常用的一种拒绝策略,因为它可以保证任务一定会被执行,但是它也有可能会影响调用者线程的性能。其他的拒绝策略则需要根据实际情况来选择。
15.ThreadLocal原理和实现是怎样的?
ThreadLocal可以让我们在多线程环境中轻松地实现线程间数据的隔离。
ThreadLocal的作用主要是做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程而言是相
对隔离的,在多线程环境下,防止自己的变量被其它线程篡改
使用:
ThreadLocal<String> localName = new ThreadLocal();
localName.set("张三");
String name = localName.get();
localName.remove();
set源码:
public void set(T value) {
Thread t = Thread.currentThread();// 获取当前线程
ThreadLocalMap map = getMap(t);// 获取ThreadLocalMap对象
if (map != null) // 校验对象是否为空
map.set(this, value); // 不为空set
else
createMap(t, value); // 为空创建⼀个map对象
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
ThreadLocal数据隔离的真相了,每个线程Thread都维护了⾃⼰的
threadLocals变量,所以在每个线程创建ThreadLocal的时候,实际上数据是存在⾃⼰线程Thread的
threadLocals变量⾥⾯的,别⼈没办法拿到,从⽽实现了隔离
ThreadLocal原理:
ThreadLocal是通过每个线程内部维护一个Map,Map中存储了键值对,这个键值对中的键是ThreadLocal对象本身,而值则是我们通过set方法设置的值。所以,每个线程都有自己独立的Map,每个线程对ThreadLocal对象的get、set操作都只会影响到该线程内部的Map,而不会影响到其他线程中的Map。
ThreadLocal实现:
ThreadLocal的实现是通过Thread类中的ThreadLocalMap来实现的,ThreadLocalMap是Thread类的一个私有内部类,它维护了一个Entry数组,每个Entry对象都包含了一个ThreadLocal对象和一个对应的值对象。ThreadLocal对象作为键,值对象则是通过ThreadLocal的set方法来设置的,每个线程在进行get、set操作时,都会去访问它自己线程的ThreadLocalMap,从而实现线程间数据的隔离。
ThreadLocal的使用需要注意一些问题:
1.ThreadLocal对象需要在使用前先进行初始化。
2.ThreadLocal对象使用完后需要及时清理,否则可能会导致内存泄漏。
3.在使用ThreadLocal的时候,需要避免使用弱引用,
因为弱引用可能会导致ThreadLocal被垃圾回收,从而导致数据丢失。
4.在使用ThreadLocal的时候,需要注意线程安全问题,尤其是在多线程环境下进行数据的修改操作。
ThreadLocal常用场景:
Spring实现事务隔离级别的源码
Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,
同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,
通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。
Spring框架⾥⾯就是用的ThreadLocal来实现这种隔离,
主要是在 TransactionSynchronizationManager 这个类里面
代码:
private static final Log logger =
LogFactory.getLog(TransactionSynchronizationManager.class);
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
private static final ThreadLocal<Set<TransactionSynchronization>>
synchronizations =
new NamedThreadLocal<>("Transaction synchronizations");
private static final ThreadLocal<String> currentTransactionName =
new NamedThreadLocal<>("Current transaction name");
其他场景1:
之前我们上线后发现部分用户的日期居然不对了,排查下来是 SimpleDataFormat 的锅,当时我们使用
SimpleDataFormat的parse()方法,内部有一个Calendar对象,调用SimpleDataFormat的parse()方法会
先调用Calendar.clear(),然后调用Calendar.add(),如果一个线程先调用了add()然后另一个线程又调
用了clear(),这时候parse()方法解析的时间就不对了。
其实要解决这个问题很简单,让每个线程都new 一个自己的 SimpleDataFormat 就好了,
但是1000个线程难道new1000个 SimpleDataFormat ?
所以当时我们使用了线程池加上ThreadLocal包装 SimpleDataFormat ,再调用initialValue让每个线程有
一个 SimpleDataFormat 的副本,从而解决了线程安全的问题,也提高了性能
其他场景2:
在项目中存在一个线程经常遇到横跨若干方法调用,需要传递的对象,也就是上下文(Context),
它是一种状态,经常就是是用户身份、任务信息等,就会存在过渡传参的问题。
使用到类似责任链模式,给每个方法增加一个context参数非常麻烦,而且有些时候,
如果调用链有无法修改源码的第三方库,对象参数就传不进去了,所以我使用到了ThreadLocal去做了一下改造,
这样只需要在调用前在ThreadLocal中设置参数,其他地方get一下就好了
void work(User user) {
getInfo(user);
checkInfo(user);
setSomeThing(user);
log(user);
}
void work(User user) {
try{
threadLocalUser.set(user);
// 他们内部 User u = threadLocalUser.get(); 就好了
getInfo();
checkInfo();
setSomeThing();
log();
} finally {
threadLocalUser.remove();
}
}
其他场景3:很多场景的cookie,session等数据隔离都是通过ThreadLocal去做实现的
其他场景4:在Android中,Looper类就是利用了ThreadLocal的特性,保证
每个线程只存在一个Looper对象
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per
thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
能跟我说一下对象存放在哪里么?
在Java中,栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可
见,即栈内存可以理解成线程的私有内存,而堆内存中的对象对所有线程可见,堆内存中的对象可以被
所有线程访问。
那么是不是说ThreadLocal的实例以及其值存放在栈上呢?
其实不是的,因为ThreadLocal实例实际上也是被其创建的类持有(更顶端应该是被线程持有),而
ThreadLocal的值其实也是被线程实例持有,它们都是位于堆上,只是通过一些技巧将可见性修改成了
线程可见。
如果我想共享线程的ThreadLocal数据怎么办?
使用 InheritableThreadLocal 可以实现多个线程访问ThreadLocal的值,我们在主线程中创建一个 InheritableThreadLocal 的实例,然后在子线程中得到这个 InheritableThreadLocal 实例设置的值。
private void test() {
final ThreadLocal threadLocal = new InheritableThreadLocal();
threadLocal.set("一般帅");
Thread t = new Thread() {
@Override
public void run() {
super.run();
Log.i( "你帅吗 =" + threadLocal.get());
}
};
t.start();
}
在子线程中是能够正常输出那一行日志的,这也是之前提到过的父子线程数据传递的问
题
ThreadLocal的可能产生的问题是什么?
内存泄露
ThreadLocal在保存的时候会把自己当做Key存在ThreadLocalMap中,正常情况应该是key和value都应
该被外界强引用才对,但是现在key被设计成WeakReference弱引用了
这就导致了一个问题,ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal
的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
就比如线程池里面的线程,线程都是复用的,那么之前的线程实例处理完之后,出于复用的目的线程依
然存活,所以,ThreadLocal设定的value值被持有,导致内存泄露。
怎么解决:
在代码的最后使用remove就好了,我们只要记得在使用的最后用remove把值清空就好了
ThreadLocal<String> localName = new ThreadLocal();
try {
localName.set("张三");
……
} finally {
localName.remove();
}
remove的源码很简单,找到对应的值全部置空,这样在垃圾回收器回收的时候,会自动把他们回收掉
那为什么ThreadLocalMap的key要设计成弱引用
key不设置成弱引用的话就会造成和entry中value一样内存泄漏的场景
16.线程池的线程数量是怎么确定的?
线程池的线程数量的确定需要根据具体的应用场景和硬件环境等因素综合考虑,一般可以从以下几个方面进行考虑:
1.CPU的核心数:线程池中的线程数量最好不要超过CPU的核心数,这样可以避免过度竞争,提高CPU的利用率。
2.任务类型:如果是CPU密集型的任务,线程数可以设置为CPU核心数的1~2倍;如果是I/O密集型的任务,由于线程会被阻塞,因此线程数可以设置为CPU核心数的4~5倍。
3.内存限制:线程池中的线程数量过多,会占用较多的内存,因此需要考虑系统内存的限制。
4.平均任务等待时间和执行时间:如果任务的平均等待时间比执行时间长,线程池中的线程数量可以适当增加,提高任务的响应速度;如果任务的平均等待时间比执行时间短,线程池中的线程数量可以适当减少,节约系统资源。
5.CPU密集型应用,线程池大小设置为N+1
IO密集型应用,线程池大小设置为2N+1
估算公式:最佳线程数目=((线程等待时间+线程CPU时间)/线程CPU时间)*CPU数目
需要注意的是,线程池中的线程数量过多或过少都会影响到系统的性能,因此需要根据具体的应用场景和硬件环境等因素进行综合考虑,选择合适的线程数。此外,线程池的线程数量也可以动态调整,根据任务的数量和系统的负载情况动态地增加或减少线程的数量,从而提高系统的性能。
17.进程的组成部分有哪些?
进程包含以下组成部分:
1.程序代码:指进程所执行的指令集合,通常由二进制可执行文件或脚本语言编写的脚本文件组成。
2.数据区:指程序执行时所需的数据存储区域,包括静态数据、全局数据和堆栈数据等。
3.进程控制块(PCB):是操作系统用来管理进程的数据结构,包含了进程的状态、程序计数器、寄存器、内存资源等信息。
4.进程堆栈:指进程在运行时使用的堆栈,用于存储函数调用、返回地址、局部变量和参数等信息。
5.资源:指操作系统为进程分配的资源,如CPU时间片、内存空间、I/O设备等。
这些组成部分共同构成了进程。
18.进程间通信方式有什么区别?
进程间通信是指不同进程之间传递数据和共享资源的机制。
常见的进程间通信方式比较:
管道:适用于父子进程之间或兄弟进程之间的通信。管道是一种半双工通信方式,只能实现单向通信。管道具有简单易用、可靠性高的优点。缺点:速度慢,容量有限
FIFO命名管道:任何进程间都能通讯,缺点是速度慢
消息队列:适用于多个进程之间的通信。消息队列可以实现异步通信,具有高可靠性和高效性的优点。但是需要对消息格式进行规定,不支持大量数据传输。
需要注意的是第一次读的时候,要考虑上一次没有读完数据的问题
共享内存:适用于大量数据的高速传输和多个进程之间的共享数据。共享内存具有高效性和数据共享的优点。但是需要对共享内存进行同步处理,容易出现数据一致性问题。
套接字:适用于网络通信和不同主机之间的进程通信。套接字具有灵活性和通用性的优点,支持不同协议的通信。但是需要对网络协议进行了解和设置,实现起来相对复杂。
信号量:适用于多个进程之间对共享资源的访问控制。信号量可以实现对资源的互斥访问和同步操作。但是需要对信号量进行同步处理,容易出现死锁和饥饿问题,不能传递复杂消息
因此,选择进程间通信方式应根据实际需求来确定,综合考虑通信的性能、可靠性、易用性、数据共享和访问控制等方面的因素。
19.如何预防死锁?
一:破坏"请求和保持"条件:
让进程在申请资源时,一次性申请所有需要用到的资源,不要一次一次来申请,
当申请的资源有一些没空,那就让线程等待
不过这个方法比较浪费资源,进程可能经常处于饥饿状态
还有一种方法是,要求进程在申请资源前,要释放自己拥有的资源
二:破坏"不可抢占"条件:
允许进程进行抢占,方法一:如果去抢资源,被拒绝就立马释放自己的资源
方法二:操作系统允许抢,只要你优先级高,就可以抢到
三:破坏"循环等待"条件:
将系统中的所有资源统一编号,进程可在任何时刻提出资源申请,
但所有申请必须按照资源的编号顺序提出(指定获取锁的顺序,顺序加锁)
20.synchronized和Lock的区别是什么?
synchronized和Lock的区别是什么?
synchronized和Lock都是Java中用于实现线程同步的机制,它们的主要区别在以下几个方面:
1.使用方式不同:synchronized是Java中的关键字,可以用于修饰方法和代码块,它的使用方式相对简单;而Lock是Java中的接口,需要通过实例化Lock的实现类来使用,使用方式相对复杂。
2.性能效率不同:在低并发的情况下,synchronized的性能要优于Lock;但在高并发的情况下,Lock的性能要优于synchronized。
3.功能方面不同:synchronized是Java中的内置机制,可以自动释放锁,而且支持重入锁;而Lock需要手动获取和释放锁,并且支持更多的功能,如可中断锁、公平锁等。
4.可读性不同:synchronized的使用方式相对简单,代码可读性较高;而Lock代码相对复杂,可读性稍差。
综上所述,synchronized适用于简单的同步场景,而Lock适用于更复杂和高级的同步场景。
synchronized是关键字,是JVM层面的底层啥都帮我们做了,而Lock是一个接口,是JDK层面的有
丰富的API。
synchronized会⾃动释放锁,而Lock必须手动释放锁。
synchronized是不可中断的,Lock可以中断也可以不中断。
通过Lock可以知道线程有没有拿到锁,而synchronized不能。
synchronized能锁住⽅法和代码块,而Lock只能锁住代码块。
Lock可以使用读锁提高多线程读效率。
synchronized是非公平锁,ReentrantLock可以控制是否是公平锁。
两者一个是JDK层面的一个是JVM层面的,
我觉得最大的区别其实在,
是否需要丰富的api,还有就是具体的应用场景。
锁升级过程是不可逆的,过了高峰我们还是重量级的锁,那效率会大打折扣了,
这种时候你用Lock会更好
场景是一定要考虑的,因为脱离了业务,一切技术讨论都没有了价值
21.什么是CAS?
什么是CAS?
CAS是“Compare and Swap”的缩写,中文名为“比较并交换”。
它是一种多线程同步机制,常用于实现线程安全的计数器、缓存、队列等数据结构。
CAS(Compare And Swap 比较并且替换)是乐观锁的一种实现方式,是一种轻量级锁,JUC 中很多工具类的实现就是基于 CAS 的
CAS操作包括三个参数:内存地址V、当前预期值A、新值B。
当且仅当预期值A和内存地址V中的值相同时,才将内存地址V中的值更新为新值B。
CAS操作是原子性的,保证了多线程情况下的数据一致性和线程安全。
CAS 是怎么实现线程安全的?
线程在读取数据时不进行加锁,
在准备写回数据时,先去查询原值,操作的时候比较原值是否修改,
若未被其他线程修改则写回,若已被修改,则重新执行读取流程。
举例:现在一个线程要修改数据库的name,修改前先去数据库查name的值,发现name="王帅",
拿到值了,我们准备修改成name="卫云腾",
在修改之前我们判断一下,原来的name是不是等于"王帅",
如果被其他线程修改就会发现name不等于"王帅",我们就不进行操作,
如果原来的值还是"王帅",我们就把name修改为"卫云腾"
Tip:比较+更新 整体是一个原子操作,是乐观锁的一种实现,
认为数据总是不会被更改
存在什么问题呢?
有。
一:要是结果是值一直被改变就一直循环查询,CPU开销是个大问题
CAS操作长时间不成功的话,会导致一直自旋,相当于死循环了,CPU的压力会很大
二:ABA问题
三:只能保证一个共享变量原子操作的问题
CAS操作单个共享变量的时候可以保证原子的操作,多个变量就不⾏了,
JDK 5之后 AtomicReference可以用来保证对象之间的原子性,就可以把多个对象放入CAS中操作
你之前说在JUC包下的原子类也是通过这个实现的,能举例吗
拿AtomicInteger举例,他的自增函数incrementAndGet()就是这样实现的,
其中就有大量循环判断的过程,直到符合条件才成功
循环判断给定偏移量是否等于内存中的偏移量,直到成功才退出
22.什么是CAS操作的ABA问题?
CAS操作的ABA问题指的是在一些特定情况下,虽然CAS操作仍然成功执行,但是实际上CAS操作成功的条件已经不满足了。
具体来说,如果一个线程在执行CAS操作时,发现内存地址V中的值与预期值A相同,于是就将内存地址V中的值更新为新值B。
但是,在这个操作过程中,另外一个线程也访问了内存地址V,将它的值从B改为了C,
然后又将它的值从C改回了B,此时CAS操作仍然成功,因为内存地址V的值仍然是B。
这就是ABA问题。
为了解决ABA问题,可以使用版本号机制。每次对内存地址V进行更新时,不仅要更新其值,
还要将版本号加1。
这样,在执行CAS操作时,只有当版本号与预期值A都相同时,才进行更新操作。
如果此时内存地址V的值已经被修改过了,那么版本号就不会匹配,CAS操作就会失败。
这样就避免了ABA问题的发生。
23.AQS理论的数据结构是什么?
AQS(AbstractQueuedSynchronizer)是Java中用于实现锁和同步器的框架,
其核心原理是基于FIFO队列和状态位来实现线程的阻塞和唤醒。
AQS使用了一个FIFO的双向队列来保存被阻塞的线程,同时使用一个整型的状态位来表示共享资源的状态(例如锁的状态),并提供了一些操作这个状态位的方法。
线程在获取锁时,会首先尝试获取锁的状态位,如果可以获取到,说明锁是可用的,线程可以继续执行;
如果获取不到,说明锁已经被占用,线程就会被阻塞,并加入到等待队列中。当锁的状态位被释放时,AQS会自动唤醒等待队列中的第一个线程,并将线程从队列中移除。
AQS的数据结构是一个双向队列,每个节点(Node)代表一个被阻塞的线程。
Node中包含了一个等待状态(waitStatus)和一个指向前驱节点的指针(prev),以及一个指向后继节点的指针(next)。
当线程被阻塞时,它会创建一个节点并加入到等待队列中,当锁被释放时,AQS会自动将队列中的第一个节点唤醒并移出队列。AQS通过维护这个双向队列和状态位来实现锁和同步器的功能。
24.父子线程间怎么共享数据?
父子线程之间可以通过共享内存、全局变量、管道、消息队列等方式进行数据共享。
1.共享内存:可以通过在父进程中创建共享内存区域,然后让子进程通过映射到自己的地址空间中来实现数据共享。
2.全局变量:在进程中定义全局变量,父子进程都可以访问和修改这些全局变量。
3.管道:父子进程之间可以通过管道进行通信,父进程将需要共享的数据写入管道,子进程从管道中读取数据。
4.消息队列:父子进程之间可以通过消息队列进行通信,父进程将需要共享的数据发送到消息队列中,子进程从消息队列中读取数据。
以上是一些常见的数据共享方式,具体使用哪种方式需要根据具体情况进行选择。
25.CountDownLatch和CyclicBarrier的区别是什么?
CountDownLatch和CyclicBarrier都是Java中用于线程协作的类,它们的主要区别在于:
1.CountDownLatch是一个计数器,用于控制一个或多个线程等待多个线程完成任务后才能执行,而CyclicBarrier是一个栅栏,用于多个线程互相等待,直到所有线程都到达栅栏位置,然后继续执行。
2.CountDownLatch计数器只能使用一次,一旦计数器减为0就不能再次使用,而CyclicBarrier可以重复利用,当所有线程都到达栅栏位置后,栅栏会自动重置计数器。
3.CountDownLatch的计数器是递减的,而CyclicBarrier的计数器是递增的。
4.CountDownLatch只能让等待的线程继续执行,而CyclicBarrier可以让所有线程同时继续执行。
总之,CountDownLatch和CyclicBarrier都是用于线程协作的类,但是它们的适用场景和使用方法有所不同。
如果需要多个线程等待某个事件的发生,就可以使用CountDownLatch;
如果需要多个线程互相等待,并且需要多次重复执行某个任务,就可以使用CyclicBarrier。
26.什么是总线风暴?
总线风暴是指在计算机系统中,由于访问总线的请求过多或者某些设备频繁地请求总线,导致总线上的数据传输量过大,总线带宽被占满,从而导致系统性能下降的现象。
总线是计算机系统中不同设备之间通信的桥梁,它承载了大量的数据传输和控制信号。如果多个设备同时请求总线,或者某些设备频繁地请求总线,就会导致总线带宽被占满,从而导致系统性能下降。此时,系统可能会出现延迟、死机、数据丢失等问题,严重影响系统的稳定性和可靠性。
为了避免总线风暴,可以采取以下措施:
1.合理规划系统架构,避免过多设备集中在同一总线上。
2.优化设备的访问方式,降低对总线的请求频率和数据传输量。
3.使用高速总线、缓存、DMA等技术,提高总线带宽和数据传输效率。
4.采用分布式系统架构,避免单一总线成为系统瓶颈。
总线风暴是一个常见的计算机系统问题,需要在系统设计和优化中引起足够的重视。
27.什么是内存屏障?
内存屏障(Memory Barrier),也称内存栅栏,是一种CPU指令或者编程语言提供的关键字,用于控制内存访问的顺序和可见性。内存屏障在多线程编程中非常重要,可以保证多线程之间的内存访问顺序和数据的可见性,从而避免出现数据竞争和内存一致性问题。
内存屏障可以分为两种类型:
1.Load Barrier:用于确保一个线程读取的数据是最新的,它会阻塞当前线程,直到所有之前的读操作都已经完成。
2.Store Barrier:用于确保一个线程写入的数据对其他线程可见,它会阻塞当前线程,直到所有之前的写操作都已经完成。
内存屏障的作用可以总结为两点:
1.保证内存访问的顺序:内存屏障可以保证指令的执行顺序,避免出现指令重排等问题,从而2.保证线程之间的内存访问顺序。
保证数据的可见性:内存屏障可以保证数据的可见性,即一个线程对数据的修改对其他线程是可见的,从而避免出现数据竞争和内存一致性问题。
需要注意的是,内存屏障的使用需要谨慎,不当的使用可能会影响程序的性能和正确性。在使用内存屏障时,需要根据具体的情况和需求进行选择和配置。
28.synchronized在JDK6做了哪些优化?
在JDK6及之前的版本中,synchronized主要存在两个性能问题:
1.内置锁的竞争激烈:在并发量较高的情况下,线程竞争内置锁会导致性能下降。
2.重量级锁的高开销:synchronized在JDK6及之前的版本中采用的是重量级锁,每次对锁的获取和释放都需要进行内核态和用户态之间的切换,开销较高。
针对这两个问题,JDK6做了如下优化:
1.偏向锁:在JDK6中,synchronized引入了偏向锁机制,通过偏向锁来减少锁的竞争,提高并发性能。当只有一个线程访问同步块时,偏向锁会将锁定的对象标记为偏向状态,并将线程ID保存在对象头中,下次该线程访问时,就可以直接获取锁,避免了多次竞争锁的开销。
2.轻量级锁:当多个线程竞争同一把锁时,JDK6中的synchronized会尝试使用轻量级锁来避免进入重量级锁状态,从而减少了锁的竞争和开销。轻量级锁是通过在对象头中的Mark Word中存储锁信息,来判断锁的状态,避免了进入内核态和用户态之间的切换。
需要注意的是,偏向锁和轻量级锁的优化只适用于低竞争情况下的synchronized,当竞争激烈时,仍然会进入重量级锁状态,因此在高并发场景下,可以考虑使用基于CAS的锁(如AtomicInteger、AtomicLong等)或基于Lock接口的锁(如ReentrantLock)来替换synchronized。
29.什么是happens before原则?
Happens-before原则是Java内存模型中的一个重要概念,用于描述多线程并发执行时的语义关系。该原则规定了在一个线程中,某个操作对某个变量的修改一定会对另一个线程中访问该变量的操作产生影响,从而保证了多线程并发执行时的可见性和有序性。
具体来说,happens-before原则包括以下规则:
1.程序顺序规则(Program Order Rule):一个线程中的每个操作,happens-before于该线程中的任意后续操作。
2.锁定规则(Lock Rule):一个unlock操作happens-before于后续的lock操作,这个锁也可以是线程内部的锁或者是synchronized中的锁。
3.volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作,happens-before于后续的对该变量的读操作。
4.传递性规则(Transitivity Rule):如果操作A happens-before操作B,操作B happens-before操作C,那么操作A happens-before操作C。
5.start和join规则(Start Rule):线程的start()方法happens-before于该线程的任意操作,该线程的任意操作happens-before于其他线程的join()方法。
需要注意的是,在Java内存模型中,happens-before原则仅规定了操作之间的可见性和有序性,不涉及操作之间的执行顺序。因此,在多线程编程中,需谨慎使用happens-before原则,避免出现死锁、饥饿等问题。
30.Java常见的锁有哪几种?
在Java中,常见的锁包括以下几种:
1.synchronized锁:synchronized是Java中的内置锁,可以用于对对象或方法进行加锁,保证线程安全。synchronized锁是重量级锁,需要进入内核态和用户态之间的切换,开销较高。
2.ReentrantLock:ReentrantLock是Java中的可重入锁,也可以用于对对象或方法进行加锁,与synchronized相比,ReentrantLock提供了更多的锁定操作,如可中断、定时、公平或非公平锁等,适用于更加复杂的并发场景。
3.ReadWriteLock:ReadWriteLock是Java中的读写锁,允许多个线程同时读取共享资源,但只允许一个线程写入共享资源,可以提高系统的并发性能。
4.StampedLock:StampedLock是Java8中新增的一种锁类型,与ReadWriteLock类似,也是用于读写操作的并发控制,但提供了更高的并发性能和更多的功能,如乐观读、转换模式等。
5.Semaphore:Semaphore是Java中的信号量,用于控制同时访问某个资源的线程数量,可以用于限流、流量控制等场景。
6.CountDownLatch:CountDownLatch是Java中的倒计数器,用于控制线程的执行顺序,可以让某个线程等待其他线程执行完毕后再继续执行。
需要注意的是,不同类型的锁适用于不同的并发场景,应根据实际情况选择合适的锁类型,避免出现死锁、饥饿等问题。同时,在使用锁时,也需要注意锁的粒度和锁的性能开销,避免影响系统的并发性能。
31.Java乐观锁如何实现?
Java中的乐观锁通常是基于版本号(Version)实现的,也被称为CAS(Compare And Swap)操作。乐观锁通过CAS+自旋实现
乐观锁的实现方式可以分为以下几步:
1.在需要进行乐观锁控制的数据表中添加一个版本号字段,用于记录每次更新的版本号。初始值为1。
2.在更新数据时,先读取当前数据的版本号,然后根据业务逻辑进行数据的修改。
3.使用CAS操作更新数据时,先判断当前数据的版本号是否与之前读取的版本号一致,如果一致,则更新数据并将版本号加1;否则,表示数据已被其他线程修改,需要重新读取数据并重试。
乐观锁的优点是没有锁的开销,适用于并发量较高的场景,但同时也存在着ABA问题,需要使用AtomicStampedReference等带版本号的类来解决。需要注意的是,乐观锁适用于并发冲突较少的场景,如果并发冲突较多,建议使用悲观锁(如synchronized、ReentrantLock等)来保证数据的正确性。
32.如何实现阻塞队列?
实现阻塞队列可以使用Java中的ReentrantLock和Condition来实现,具体实现步骤如下:
1.定义一个循环数组作为队列,使用ReentrantLock作为互斥锁,使用两个Condition分别表示队列已满和队列已空的状态。
2.实现入队方法put(),在方法中先获取互斥锁,如果队列已满,则调用notFull.await()将当前线程阻塞,等待队列未满的信号;否则,将元素加入队列,并将notEmpty.signal()发送队列非空的信号,唤醒其他线程。
3.实现出队方法take(),在方法中先获取互斥锁,如果队列已空,则调用notEmpty.await()将当前线程阻塞,等待队列非空的信号;否则,将队头元素取出,并将notFull.signal()发送队列未满的信号,唤醒其他线程。
以下是一个示例代码实现:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class BlockingQueue<T> {
private Object[] items;
private int putIndex;
private int takeIndex;
private int count;
private ReentrantLock lock;
private Condition notFull;
private Condition notEmpty;
public BlockingQueue(int capacity) {
items = new Object[capacity];
lock = new ReentrantLock();
notFull = lock.newCondition();
notEmpty = lock.newCondition();
}
public void put(T item) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
notFull.await();
}
items[putIndex] = item;
if (++putIndex == items.length) {
putIndex = 0;
}
count++;
notEmpty.signal();
} finally {
lock.unlock();
}
}
public T take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await();
}
T item = (T) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length) {
takeIndex = 0;
}
count--;
notFull.signal();
return item;
} finally {
lock.unlock();
}
}
}
需要注意的是,在使用阻塞队列时,要根据实际情况选择合适的队列实现,如ArrayBlockingQueue、LinkedBlockingQueue等。同时,还需要注意队列的容量和阻塞策略,避免出现死锁、饥饿等问题。
33.CountDownLatch、CyclicBarrier、Semaphore区别和使用场景?
CountDownLatch、CyclicBarrier、Semaphore都是Java中用于多线程编程的同步工具类,但它们有不同的作用和使用场景。
一:CountDownLatch
CountDownLatch(倒计时门闩)是一个同步工具类,它允许一个或多个线程等待其他线程完成操作后再继续执行。CountDownLatch维护了一个计数器,初始值为线程数量,线程完成任务后计数器减1。当计数器为0时,等待线程继续执行。CountDownLatch的主要方法是await()和countDown()。
使用场景:CountDownLatch适用于一组线程等待另一组线程完成操作后再继续执行的场景。比如,主线程等待所有子线程完成初始化后再继续执行。
二:CyclicBarrier
CyclicBarrier(循环屏障)是一个同步工具类,它允许一组线程相互等待,直到所有线程都到达某个屏障点后再继续执行。CyclicBarrier的主要方法是await()。
使用场景:CyclicBarrier适用于需要多个线程协同完成某个任务的场景。比如,多个线程同时执行某个操作,需要等待所有线程都执行完后再进行下一步操作。
三:Semaphore
Semaphore(信号量)是一个同步工具类,它允许多个线程同时访问某个资源。Semaphore维护了一个许可证数量,线程需要先获取许可证才能访问资源,访问完后释放许可证。Semaphore的主要方法是acquire()和release()。
使用场景:Semaphore适用于需要限制线程数量访问某个资源的场景。比如,数据库连接池限制同时访问连接的线程数量。
34.ConcurrentHashMap在JDK1.8前后线程安全实现方式有何不同?
ConcurrentHashMap是Java中的线程安全的哈希表实现,它支持高并发的并发读写操作,能够比较好地解决多线程环境下的数据竞争问题。在JDK1.8之前和之后,ConcurrentHashMap的线程安全实现方式有所不同。
在JDK1.8之前,ConcurrentHashMap的线程安全实现方式是使用分段锁(Segment)来保证线程安全。它将整个哈希表分成了多个段(Segment),每个段都是一个独立的哈希表,每个段都有一个独立的锁,不同的线程可以同时访问不同的段,从而实现并发读写操作。
在JDK1.8之后,ConcurrentHashMap的线程安全实现方式改为了使用CAS和synchronized来保证线程安全。它使用了一个数组+链表+红黑树的数据结构,将整个哈希表分成了多个桶(Node数组),每个桶里面可以放置多个键值对,不同的线程可以同时访问不同的桶,从而实现并发读写操作。在put、get等操作时,通过CAS来保证对桶的操作的线程安全,如果发生哈希冲突,会使用synchronized来保证对桶里的链表或红黑树的操作的线程安全。
总的来说,在JDK1.8之前和之后,ConcurrentHashMap的线程安全实现方式有所不同,但都是基于分段锁或CAS和synchronized来实现并发读写操作的。在JDK1.8之后,ConcurrentHashMap的性能有了进一步的提升,特别是在高并发情况下的性能表现更为优异。
35.协程和线程的区别是什么?
协程和线程都是用于实现多任务并发的技术,但它们的实现机制和使用方式有所不同,主要区别如下:
一:调度方式不同
线程是由操作系统进行调度的,操作系统通过时间片轮转或优先级调度等方式来分配CPU时间片给不同的线程执行。而协程则是由程序员进行调度的,程序员需要在代码中显式地进行协程的切换。
二:上下文切换成本不同
线程的上下文切换开销较大,因为需要保存和恢复线程的执行状态,包括CPU寄存器、栈、堆栈等。而协程的上下文切换开销较小,因为协程只需要保存和恢复少量的执行状态,如变量值、函数调用栈等。
三:并发量不同
线程的并发量受限于CPU的核数和操作系统的调度算法,通常在1000个线程左右就会出现性能瓶颈。而协程的并发量则受限于内存和代码逻辑的复杂度,通常可以支持上万个协程。
四:数据共享方式不同
线程之间可以共享内存数据,但需要通过锁等机制来保证数据的一致性和线程安全。而协程之间则通过消息传递等方式来共享数据,可以避免锁的问题,但也需要注意数据的一致性和协程安全。
五:编程模型不同
线程的编程模型通常是基于多线程的,需要考虑锁、线程安全等问题。而协程的编程模型通常是基于事件驱动的,需要考虑协程的切换、消息传递等问题。
总的来说,协程和线程都是并发编程的重要技术,在不同的场景下都有其优势和限制。需要根据具体的需求选择合适的技术来实现多任务并发。
36.多线程的异步调用怎么实现的?
多线程异步调用可以通过以下几种方式实现:
1.Future和Callable:Callable接口可以在call()方法中返回结果,而Future接口可以在get()方法中获取结果。通过将Callable对象提交给ExecutorService线程池,可以异步执行Callable任务,并通过Future接口获取执行结果。
2.CompletableFuture:CompletableFuture类是Java 8中新增的异步编程API,可以更方便地实现多线程异步调用。它支持链式操作和回调函数,并提供了一系列的方法来处理异步任务返回结果。
3.线程池和FutureTask:可以使用线程池提交FutureTask对象,FutureTask可以在其中执行异步任务,并返回结果。可以通过Future接口获取任务执行结果。
4.回调函数:可以通过回调函数实现多线程异步调用。在任务执行完成后,线程会回调指定的函数并传递执行结果。
总之,多线程异步调用的实现方式很多,选择不同的方式取决于具体的应用场景和需求。
37.线程池的阻塞队列用的最多的是什么
线程池的阻塞队列最常用的是基于链表实现的无界阻塞队列LinkedBlockingQueue。
它的特点是可以无限制地添加新的元素,而不会受到容量的限制,因此非常适合于任务数量不可预知的情况。同时,它还可以指定队列的容量,以防止系统资源被耗尽。在线程池中,任务会先被添加到阻塞队列中,等待线程池中的空闲线程来执行。如果线程池中没有空闲线程,任务就会一直阻塞在队列中,直到有线程空闲出来才会被执行。
38.ArrayBlockingQuene和LinkdeBlockingQuene的区别是什么
ArrayBlockingQueue和LinkedBlockingQueue都是Java中的阻塞队列,它们的主要区别如下:
1.实现方式不同:ArrayBlockingQueue是基于数组实现的有界阻塞队列,而LinkedBlockingQueue是基于链表实现的可选有界阻塞队列。因此,ArrayBlockingQueue的容量是固定的,而LinkedBlockingQueue的容量可以选择是否有上限。
2.插入和删除操作性能不同:由于ArrayBlockingQueue是基于数组实现的,因此插入和删除操作的性能通常比LinkedBlockingQueue更高。而LinkedBlockingQueue则具有更高的吞吐量。
3.迭代器支持不同:ArrayBlockingQueue不支持迭代器,而LinkedBlockingQueue可以通过迭代器遍历队列中的元素。
4.线程安全性质不同:两种队列都是线程安全的,但是它们的实现方式不同。ArrayBlockingQueue使用一个锁来控制对队列的访问,而LinkedBlockingQueue使用两个锁来控制对队列头尾的访问。
通常情况下,如果需要使用有界队列,可以选择使用ArrayBlockingQueue;如果需要使用无界队列,可以选择使用LinkedBlockingQueue。
39.SingleThreadExecutor和CachedThreadPool为什么不推荐使用,会出现哪些问题?
SingleThreadExecutor适用于需要保证任务按顺序执行的场景,它只会创建一个线程来执行任务。CachedThreadPool则会创建可缓存的线程池,线程数量不固定,会根据任务的数量动态调整线程池的大小。这两种线程池都存在以下问题:
1.SingleThreadExecutor只有一个线程,如果该线程在执行任务时发生了异常,线程池会停止工作,后续任务将无法执行。
2.CachedThreadPool创建的线程都是非核心线程,如果任务数量非常大,会导致大量线程被创建,从而导致系统资源不足。
3.CachedThreadPool的线程存活时间为60秒,如果60秒内该线程没有执行新的任务,线程就会被回收,如果后续有新任务到来,需要重新创建线程,线程创建的过程会消耗一定的系统资源。
4.如果任务执行时间过长,CachedThreadPool会创建过多的线程,这样会占用大量的系统资源,从而导致系统崩溃。
因此,如果应用程序需要执行大量的短期任务,建议使用FixedThreadPool线程池,它可以控制线程的数量,避免线程数过多导致系统崩溃。如果应用程序需要执行大量的长期任务,建议使用ScheduledThreadPool线程池,它可以控制线程的执行时间,避免线程执行时间过长导致系统资源不足。
40.有一个static变量,初始值是0,现在有2个程序同时修改这个值,每个程序都是进行自增操作,请问这个变量最后的取值可能有多少?如果这个变量加上volatile,取值可能有多少?
如果没有加上volatile关键字,这个变量的最后取值可能是任何一个大于等于2的数字,因为两个程序同时对其进行自增操作,而没有对该变量的访问进行同步和互斥。
如果加上了volatile关键字,那么这个变量的最后取值仍然可能是任何一个大于等于2的数字,因为volatile只能保证变量的可见性和顺序性,不能保证原子性。因此,在多线程环境下,仍然需要使用同步和互斥机制来保证变量的原子性和一致性。
41.是否了解synchronized关键词?
synchronized关键字在使用层面的理解
synchronized关键字是Java中用来实现线程同步的关键字,可以修饰方法和代码块。当线程访问被synchronized修饰的方法或代码块时,需要获取对象的锁,如果该锁已被其他线程获取,则该线程会进入阻塞状态,直到获取到锁为止。synchronized关键字可以保证同一时刻只有一个线程能够访问被锁定的方法或代码块,从而避免了多线程并发访问时的数据竞争和一致性问题。
synchronized关键字在字节码中的体现
在Java代码编译成字节码后,synchronized关键字会被编译成monitorenter和monitorexit指令来实现。monitorenter指令对应获取锁操作,monitorexit指令对应释放锁操作。
synchronized关键字在JVM中的实现
在JVM中,每个对象都有一个监视器(monitor),用来实现对象锁。当一个线程获取对象锁后,就进入了对象的监视器中,其他线程只能等待该线程释放锁后再去竞争锁。
synchronized关键字的实现涉及到对象头中的标志位,包括锁标志位和重量级锁标志位等。当一个线程获取锁后,锁标志位被设置为1,其他线程再去获取锁时,会进入自旋等待或者阻塞等待状态,直到锁标志位被设置为0,即锁被释放后才能获取锁。
synchronized关键字在硬件方面的实现
在硬件层面,锁的实现需要通过CPU指令和总线锁来实现。当一个线程获取锁时,CPU会向总线发送一个锁请求信号,其他CPU收到该信号后会进入自旋等待状态,直到锁被释放后才能获取锁。总线锁可以保证多个CPU之间的原子操作,从而保证锁的正确性和一致性。
42.Tomcat中打破双亲委派模型的方法有哪两种?
Tomcat在启动的时候采用了Java的标准类加载机制,即双亲委派模型。在这种模型下,当一个类加载器收到一个类加载请求时,它首先会将请求委派给其父类加载器去处理,只有在父类加载器无法找到对应的类时,才会由该类加载器自己去加载该类。
然而,有时候我们需要在Tomcat中引入一些自己编写的类或第三方库,这些类和库可能和Tomcat中已有的类或库产生冲突,这时候就需要打破双亲委派模型,让Tomcat中的类加载器能够优先加载我们自己编写的类或库。
Tomcat中打破双亲委派模型的方法有两种:
1.在Tomcat中使用WebAppClassLoader的addTransformer()方法,将自定义的类加载器传递给它,然后使用自定义的类加载器加载指定的类。这种方法可以在不修改Tomcat源代码的情况下打破双亲委派模型。
2.在Tomcat启动脚本中修改CATALINA_OPTS环境变量,
添加“-Dcatalina.loader. searchDefaultJar=false”的参数。
这个参数会告诉Tomcat在加载类时不再搜索Tomcat的默认jar包,而是直接由当前类加载器加载指定的类。
需要注意的是,打破双亲委派模型可能会导致类加载器之间产生冲突,从而引发一些问题。因此,在使用这种方法时,需要谨慎考虑,并测试其是否能够正常工作。
43.synchronized实现原理是什么?
contentionList(请求锁线程队列) entryList(有资格的候选者队列) waitSet(wait⽅法后阻塞队列)
onDeck(竞争候选者) ower(竞争到锁线程) !ower(执行成功释放锁后状态); Synchronized 是非公平锁。
Synchronized 在线程进入 ContentionList 时,等待的线程会先尝试自旋获取锁,如果获取不到就进入
ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁
的线程还可能直接抢占 OnDeck 线程的锁资源。
底层是由一对monitorenter和monitorexit指令实现的(监视器锁)
每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter
指令时尝试获取monitor的所有权,过程:
如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所
有者。
如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试
获取monitor的所有权。
44.死锁怎么查?
死锁是多线程编程中常见的问题,通常可以通过以下步骤来查找和解决死锁问题:
一:查看线程堆栈:在死锁发生时,可以使用JDK提供的工具(如jstack)查看线程堆栈,确定哪些线程正在等待锁,哪些线程已经获得了锁。
二:分析代码逻辑:查看代码中涉及到的锁,确定哪些锁是互斥的、哪些锁是共享的,哪些锁的获取和释放顺序可能导致死锁。
三:模拟重现:如果无法在开发环境中重现死锁问题,可以使用性能测试工具模拟高并发场景,加大并发量,尝试重现死锁问题。
四:解决死锁:一旦确定了死锁原因,可以采取以下措施来解决死锁问题:
1.调整锁的获取和释放顺序,避免出现相互等待的情况。
2.使用tryLock()方法来尝试获取锁,在获取失败时及时释放已经获得的锁,避免死锁。
3.使用锁超时等待机制,避免某个线程一直占用锁而导致死锁。
4.缩小锁的范围,避免在一个锁上进行过多的操作,从而降低死锁的概率。
5.使用并发包中的工具类,如ConcurrentHashMap、CopyOnWriteArrayList等,避免手动管理锁带来的问题。
五:使用命令:-XX:+PrintGCDetails
-XX:+PrintGCDetails是JVM的一个参数,用于在进行垃圾回收时输出详细的垃圾回收日志信息,包括垃圾回收器的名称、回收前后内存占用情况、回收时间等信息,方便开发人员进行性能调优。
使用-XX:+PrintGCDetails参数可以输出详细的GC日志信息,包括每次GC的开始时间、结束时间、持续时间、GC前后内存占用情况、GC算法等信息。示例如下:
[GC (Allocation Failure) [PSYoungGen: 262144K->43519K(305664K)] 262144K->43519K(1005056K), 0.0176064 secs] [Times: user=0.00 sys=0.00, real=0.02 secs]
[Full GC (Ergonomics) [PSYoungGen: 43519K->0K(305664K)] [ParOldGen: 0K->43026K(699392K)] 43519K->43026K(1005056K), [Metaspace: 2646K->2646K(1056768K)], 0.0076659 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
从上面的日志中可以看出,第一行是Young GC的日志信息,第二行是Full GC的日志信息。其中,[PSYoungGen: 262144K->43519K(305664K)]表示Young GC前后内存占用情况,[Metaspace: 2646K->2646K(1056768K)]表示元空间内存占用情况,0.0176064 secs表示GC持续时间,user=0.00 sys=0.00, real=0.02 secs表示GC的CPU时间和实际时间。
需要注意的是,使用-XX:+PrintGCDetails参数会对程序的性能产生一定的影响,因此只在调试和性能调优时开启。另外,如果需要更加详细的GC日志信息,可以使用-XX:+PrintHeapAtGC参数输出堆内存的详细信息。
需要注意的是,死锁问题通常比较复杂,需要耐心地分析和排查,才能找到最好的解决方案。
45.ReentrantLock原理是什么?
ReentrantLock靠CAS+AQS队列来实现
(1):先通过CAS尝试获取锁, 如果此时已经有线程占据了锁,那就加入AQS队列并且被挂起;
(2):当锁被释放之后, 排在队首的线程会被唤醒CAS再次尝试获取锁,
(3):如果是非公平锁, 同时还有另一个线程进来尝试获取可能会让这个线程抢到锁;
(4):如果是公平锁, 会排到队尾,由队首的线程获取到锁。
AQS 原理
Node内部类构成的一个双向链表结构的同步队列,通过控制(volatile的int类型)state状态来判断锁的
状态,对于非可重入锁状态不是0则去阻塞;
对于可重入锁如果是0则执行,非0则判断当前线程是否是获取到这个锁的线程,是的话把state状态+
1,⽐如重入5次,那么state=5。 而在释放锁的时候,同样需要释放5次直到state=0其他线程才有资格
获得锁
AQS两种资源共享方式
Exclusive:独占,只有一个线程能执行,如ReentrantLock
Share:共享,多个线程可以同时执行,如Semaphore、CountDownLatch、ReadWriteLock,
CyclicBarrier
CAS原理
内存值V,旧的预期值A,要修改的新值B,当A=V时,将内存值修改为B,否则什么都不做;
CAS的缺点:
(1):ABA问题; (2):如果CAS失败,⾃旋会给CPU带来压⼒; (3):只能保证对一个变量的原
⼦性操作,i++这种是不能保证的
CAS在java中的应用:
(1):Atomic系列
46.自旋锁升级到重量级锁条件是什么?
47.volatile的特性是什么?
保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,
这新值对其他线程来说是立即可见的。(实现可见性)
禁止进行指令重排序。(实现有序性)
volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。
48.敖丙遇到的问题多线程线程因抛错被中断了?
线程池分配线程执行任务
// 线程⼯⼚,⽤于为线程池中的每条线程命名
ThreadFactory namedThreadFactory = new
ThreadFactoryBuilder().setNameFormat("stats-pool-%d").build();
// 创建线程池,使⽤有界阻塞队列防⽌内存溢出
ExecutorService statsThreadPool = new ThreadPoolExecutor(5, 10,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(100), namedThreadFactory);
// 遍历所有中⼼,为每⼀个centerId提交⼀条任务到线程池
statsThreadPool.submit(new StatsJob(centerId));
对于 submit() 形式提交的任务,我们直接看源码:
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
// 被包装成 RunnableFuture 对象,然后准备添加到⼯作队列
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
原因:
在 FutureTask 对象的 run() 方法中,该任务抛出的异常被捕获,然后在setException(ex); 方法中,抛出
的异常会被放到 outcome 对象中,这个对象就是 submit() 方法会返回的 FutureTask 对象执行 get() 方
法得到的结果。
但是在线程池中,并没有获取执行子线程的结果,所以异常也就没有被抛出来,即被“吞掉”了。
这就是线程池的 submit() 方法提交任务没有异常抛出的原因
线程池⾃定义异常处理方法
在定义 ThreadFactory 的时候调用 setUncaughtExceptionHandler 方法,自定义异常处理方法。 例如:
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
.setNameFormat("judge-pool-%d")
.setUncaughtExceptionHandler((thread, throwable)->
logger.error("ThreadPool {} got exception", thread,throwable))
.build();
这样,对于线程池中每条线程抛出的异常都会打下 error 日志,就不会看不到了。
49.什么是公平锁和非公平锁?
公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
优点:所有的线程都能得到资源,不会饿死在队列中。
缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很⼤。
非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会⾼点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。
公平锁排队,非公平锁插队,但有可能失败
具体实现:ReentrantLock中就有相关公平锁,非公平锁的实现
50.在JVM中,对象在内存中分为哪三块区域?
一:有序性
在Volatile会为优化我们的代码,会对我们程序进行重排序。
as-if-serial
不管编译器和CPU如何重排序,必须保证在单线程情况下程序的结果是正确的,还有就是有数据依赖的
也是不能重排序的。
就比如:
这两段是怎么都不能重排序的,b的值依赖a的值,a如果不先赋值,那就为空了。
二:可见性
int a = 1;
int b = a;
同样在Volatile章节我介绍到了现代计算机的内存结构,以及JMM(Java内存模型),这里我需要说明
一下就是JMM并不是实际存在的,而是一套规范,这个规范描述了很多java程序中各种变量(线程共享
变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节,Java内存模
型是对共享数据的可见性、有序性、和原子性的规则和保障。
三:原子性
其实他保证原子性很简单,确保同一时间只有一个线程能拿到锁,能够进入代码块这就够了。
这几个是我们使用锁经常用到的特性,
51.那synchronized他自己本身又具有哪些特性呢?
一:可重入性
synchronized锁对象的时候有个计数器,他会记录下线程获取锁的次数,在执行完对应的代码块之后,
计数器就会-1,直到计数器清零,就释放锁了。
那可重入有什么好处呢?
可以避免一些死锁的情况,也可以让我们更好封装我们的代码。
二:不可中断性
不可中断就是指,一个线程获取锁之后,另外一个线程处于阻塞或者等待状态,前一个不释放,后一个也一直会阻塞或者等待,不可以被中断。
值得一提的是,Lock的tryLock方法是可以被中断的。
52.synchronized底层实现是怎样的?
同步代码
对象头会关联到一个monitor对象。
当我们进入一个⼈方法的时候,执行monitorenter,就会获取当前对象的一个所有权,
这个时候monitor进入数为1,当前的这个线程就是这个monitor的owner。
如果你已经是这个monitor的owner了,你再次进入,就会把进入数+1.
同理,当他执行完monitorexit,对应的进入数就-1,直到为0,才可以被其他线程持有。
所有的互斥,其实在这里,就是看你能否获得monitor的所有权,一旦你成为owner就是获得者。
同步方法
一个特殊标志位没,ACC_SYNCHRONIZED。
同步方法的时候,一旦执行到这个方法,就会先判断是否有标志位,
然后,ACC_SYNCHRONIZED会去隐式调用刚才的两个指令:monitorenter和monitorexit。
所以归根究底,还是monitor对象的争夺。
什么是monitor?
monitor监视器源码是C++写
的,在虚拟机的ObjectMonitor.hpp⽂件中。
锁升级过程,其实就是在源码里面,调用了不同的实现去获取获取锁,失败就调用更高级
的实现,最后升级完成
53.什么是用户态和内核态?
看ObjectMonitor源码的时候,会发现Atomic::cmpxchg_ptr,Atomic::inc_ptr等内核函数,对应
的线程就是park()和upark()。
这个操作涉及用户态和内核态的转换了,这种切换是很耗资源的,所以知道为啥有自旋锁这样的操作了
吧
Linux系统的体系结构分为用户空间(应用程序的活动空间)和内核。
我们所有的程序都在用户空间运行,进入用户运行状态也就是(用户态),但是很多操作可能涉及内核
运行,比如I/O,我们就会进入内核运行状态(内核态)
概括下流程:
1. 用户态把一些数据放到寄存器,或者创建对应的堆栈,表明需要操作系统提供的服务。
2. 用户态执行系统调用(系统调用是操作系统的最小功能单位)。
3. CPU切换到内核态,跳到对应的内存指定的位置执行指令。
4. 系统调用处理器去读取我们先前放到内存的数据参数,执行程序的请求。
5. 调用完成,操作系统重置CPU为用户态返回结果,并执行下个指令。
所以一直说,1.6之前是重量级锁,没错,但是他重量的本质,是ObjectMonitor调用的过程,以及
Linux内核的复杂运行机制决定的,大量的系统资源消耗,所以效率才低。
还有两种情况也会发生内核态和用户态的切换:异常事件和外围设备的中断
54.官方优化后的锁升级过程是怎样的?
1.线程进入
2.判断是否同一个线程
3.不是则CAS轻量级
4.判断CAS获取是否成功
成功则拿到锁
5.不是则自旋:防止线程挂起理解为短暂死循环
6.锁升级为更高级锁
升级方向:
1.无锁
2.偏向锁
3.轻量级锁
4.重量级锁
切记这个升级过程是不可逆的
偏向锁
对象头是由Mark Word和Klass pointer 组成,锁争夺也就是对象头指向的Monitor对
象的争夺,一旦有线程持有了这个对象,标志位修改为1,就进入偏向模式,同时会把这个线程的ID记录在对象的Mark Word中。
这个过程是采用了CAS乐观锁操作的,每次同一线程进入,虚拟机就不进行任何同步的操作了,对标志位+1就好了,不同线程过来,CAS会失败,也就意味着获取锁失败。
偏向锁在1.6之后是默认开启的,1.5中是关闭的,需要手动开启参数是xx:-UseBiasedLocking=false。
轻量级锁
还是跟Mark Work 相关,如果这个对象是⽆锁的,jvm就会在当前线程的栈帧中建立一个叫锁记录(Lock Record)的空间,用来存储锁对象的Mark Word 拷贝,然后把Lock Record中的owner指向当前
对象。
JVM接下来会利用CAS尝试把对象原本的Mark Word 更新会Lock Record的指针,成功就说明加锁成
功,改变锁标志位,执行相关同步操作。
如果失败了,就会判断当前对象的Mark Word是否指向了当前线程的栈帧,是则表示当前的程已经持有了这个对象的锁,否则说明被其他线程持有了,继续锁升级,修改锁的状态,之后等待的线程也阻塞
自旋锁
我不是在上面提到了Linux系统的用户态和内核态的切换很耗资源,其实就是线程的等待唤起过程,那
怎么才能减少这种消耗呢?
自旋,过来的现在就不断自旋,防⽌线程被挂起,一旦可以获取资源,就直接尝试成功,直到超出阈值,自旋锁的默认大小是10次,-XX:PreBlockSpin可以修改。
自旋都失败了,那就升级为重量级的锁,像1.5的一样,等待唤起。
55.可见性的解决方案有哪些?
一:加锁
为什么加锁可以解决可见性问题?
因为某一个线程进入synchronized代码块前后,线程会获得锁,清空工作内存,从主内存拷贝共享变量
最新的值到工作内存成为副本,执行代码,将修改后的副本的值刷新回主内存中,线程释放锁。
而获取不到锁的线程会阻塞等待,所以变量的值肯定一直都是最新的
二:Volatile修饰共享变量
每个线程操作数据的时候会把数据从主内存读取到自己的工作内存,如果他操作了数据并且写会了,他
其他已经读取的线程的变量副本就会失效了,需要都数据进行操作又要再次去主内存中读取了。
volatile保证不同线程对共享变量操作的可见性,也就是说一个线程修改了volatile修饰的变量,当修改写
回主内存时,另外一个线程⽴即看到最新的值
为了解决一致性的问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操
作,这类协议有MSI、 MESI(IllinoisProtocol) 、MOSI、Synapse、Firefly及DragonProtocol等
什么是MESI(缓存一致性协议)?
当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号
通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中
缓存该变量的缓存行是无效的,那么它就会从内存重新读取
怎么发现数据是否失效?
嗅探
每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行
对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操
作的时候,会重新从系统内存中把数据读到处理器缓存里
嗅探的缺点
总线风暴
由于Volatile的MESI缓存一致性协议,需要不断的从主内存嗅探和cas不断循环,无效交互会导致总线带
宽达到峰值。
所以不要大量使用Volatile,至于什么时候去使用Volatile什么时候使用锁,根据场景区分
三:禁止指令重排序
什么是重排序?
为了提高性能,编译器和处理器常常会对既定的代码执行顺序进行指令重排序。
重排序的类型有哪些呢?源码到最终执行会经过哪些重排序呢?
源代码
=》编译器优化重排序
=》指令级并行重排序
=》内存系统重排序
=》最终执行指令序列
一般重排序可以分为如下三种:
编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;
指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据
依赖性,处理器可以改变语句对应机器指令的执行顺序;
内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱
序执行的。
什么是as-if-serial?
不管怎么重排序,单线程下的执行结果不能被改变。
编译器、runtime和处理器都必须遵守as-if-serial语义
那Volatile是怎么保证不会被执行重排序的呢?
内存屏障
java编译器会在生成指令系列时在适当的位置会插入 内存屏障 指令来禁止特定类型的处理器重排序。
为了实现volatile的内存语义,JMM会限制特定类型的编译器和处理器重排序
需要注意的是:volatile写是在前面和后面分别插入内存屏障,而volatile读操作是在后面插入两个内存屏
障
为了提高处理速度,JVM会对代码进行编译优化,也就是指令重排序优化,
并发编程下指令重排序会带来一些安全隐患:如指令重排序导致的多个线程操作之间的不可见性
从JDK5开始,提出了 happens-before 的概念,通过这个概念来阐述操作之间的内存可见性
happens-before
如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。
volatile域规则:对一个volatile域的写操作,happens-before于任意线程后续对这个volatile域的读。
如果现在我的变了falg变成了false,那么后面的那个操作,一定要知道我变了。
聊了这么多,我们要知道Volatile是没办法保证原子性的,一定要保证原子性,可以使用其他方法
无法保证原子性
就是一次操作,要么完全成功,要么完全失败。
假设现在有N个线程对同一个变量进行累加也是没办法保证结果是对的,因为读写这个过程并不是原子
性的。
要解决也简单,要么用原子类,⽐如AtomicInteger,要么加锁( 记得关注Atomic的底层 )
可见性怎么保证的?
因为可见性,线程A在自己的内存初始化了对象,还没来得及写回主内存,B线程也这么做了,那就创建
了多个对象,不是真正意义上的单例了
56.volatile与synchronized的区别?
volatile只能修饰实例变量和类变量,而synchronized可以修饰方法,以及代码块。
volatile保证数据的可见性,但是不保证原子性(多线程进行写操作,不保证线程安全);而synchronized是
一种排他(互斥)的机制。 volatile用于禁⽌指令重排序:可以解决单例双重检查对象初始化代码执行乱序
问题。
volatile可以看做是轻量版的synchronized,volatile不保证原子性,但是如果是对一个共享变量进行多个
线程的赋值,而没有其他的操作,那么就可以用volatile来代替synchronized,因为赋值本身是有原子性
的,而volatile又保证了可见性,所以就可以保证线程安全了。
1. volatile修饰符适用于以下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线
程可以立即得到修改后的值,比如booleanflag;或者作为触发器,实现轻量级同步。
2. volatile属性的读写操作都是无锁的,它不能替代synchronized,因为它没有提供原子性和互斥性。
因为无锁,不需要花费时间在获取锁和释放锁_上,所以说它是低成本的。
3. volatile只能作用于属性,我们用volatile修饰属性,这样compilers就不会对这个属性做指令重排
序。
4. volatile提供了可见性,任何一个线程对其的修改将立马对其他线程可见,volatile属性不会被线程缓
存,始终从主 存中读取。
5. volatile提供了happens-before保证,对volatile变量v的写入happens-before所有其他线程后续对v的
读操作。
6. volatile可以使得long和double的赋值是原子的。
7. volatile可以在单例双重检查中实现可见性和禁止指令重排序,从而保证安全性
57.乐观锁在项目开发中的实践和ABA怎么保证?
项目实践:
比如我们在很多订单表,流水表,
为了防止并发问题,就会加⼊CAS的校验过程,保证了线程的安全,
但是看场景使用,并不是适合所有场景,他的优点缺点都很明显
开发过程中ABA问题怎么保证的?
加标志位,例如搞个自增的字段,操作一次就⾃增加一,或者搞个时间戳,比较时间戳的值。
举例:现在我们去要求操作数据库,根据CAS的原则我们本来只需要查询原本的值就好了,现在我
们一同查出他的标志位版本字段vision。
之前不能防止ABA的正常修改:
update table set value = newValue where value = #{oldValue}
//oldValue就是我们执行前查询出来的值
带版本号能防止ABA的修改:
update table set value = newValue ,vision = vision + 1 where value = #
{oldValue} and vision = #{vision}
// 判断原来的值和版本号是否匹配,中间有别的线程修改,值可能相等,但是版本号100%不一样
除了版本号,像什么时间戳,还有JUC工具包里面也提供了这样的类
58.讲一下悲观锁和悲观锁怎么保证只有一个线程进入临界区?
顾名思义,就是比较悲观的锁,总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)
具体实践:
synchronized加锁,synchronized 是最常用的线程同步手段之一,
CAS是乐观锁的实现,
synchronized是悲观锁的实现
怎么保证只有一个线程进入临界区?
synchronized,代表这个方法加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检
查有没有其它线程B(或者C、 D等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用
synchronized方法的线程B(或者C 、D)运行完这个方法后再运行此线程A,没有的话,锁定调用者,
然后直接运行。
分别从他对对象、方法和代码块三方面加锁,去介绍他怎么保证线程安全的:
synchronized 对对象进行加锁,在 JVM 中,对象在内存中分为三块区域:对象头(Header)、实
例数据(Instance Data)和对齐填充(Padding)。
对象头:我们以Hotspot虚拟机为例,Hotspot的对象头主要包括两部分数据:Mark Word(标
记字段)、Klass Pointer(类型指针)。
Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。它会根据对象的状
态复用自⼰的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的
变化而变化。
Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个
类的实例。
你可以看到在对象头中保存了锁标志位和指向 monitor 对象的起始地址,如下图所示,右侧就是对
象对应的 Monitor 对象。
当 Monitor 被某个线程持有后,就会处于锁定状态,如图中的 Owner 部分,会指向持有 Monitor 对
象的线程。
另外 Monitor 中还有两个队列分别是EntryList和WaitList,主要是用来存放进入及等待获取锁的线
程。
如果线程进入,则得到当前对象锁,那么别的线程在该类所有对象上的任何操作都不能进行。
在对象级使用锁通常是一种比较粗糙的方法,为什么要将整个对象都上锁,而不允许其他
线程短暂地使用对象中其他同步方法来访问共享资源?
如果一个对象拥有多个资源,就不需要只为了让一个线程使用其中一部分资源,就将所有线程都锁
在外面
由于每个对象都有锁,可以如下所示使用虚拟对象来上锁:
class FineGrainLock{
MyMemberClassx,y;
Object xlock = new Object(), ylock = newObject();
public void foo(){
synchronized(xlock){
//accessxhere
}
//dosomethinghere-butdon'tusesharedresources
synchronized(ylock){
//accessyhere
}
}
public void bar(){
synchronized(this){
//accessbothxandyhere
}
//dosomethinghere-butdon'tusesharedresources
}
}
synchronized 应用在方法上时,在字节码中是通过方法的 ACC_SYNCHRONIZED 标志来实现的。
synchronized 应用在同步块上时,在字节码中是通过 monitorenter 和 monitorexit 实现的。
每个对象都会与一个monitor相关联,当某个monitor被拥有之后就会被锁住,当线程执行到
monitorenter指令时,就会去尝试获得对应的monitor。
步骤如下:
1. 每个monitor维护着一个记录着拥有次数的计数器。未被拥有的monitor的该计数器为0,当一个
线程获得monitor(执行monitorenter)后,该计数器自增变为 1 。
当同一个线程再次获得该monitor的时候,计数器再次自增;
当不同线程想要获得该monitor的时候,就会被阻塞。
2. 当同一个线程释放 monitor(执行monitorexit指令)的时候,计数器再自减。
当计数器为0的时候,monitor将被释放,其他线程便可以获得monitor。
同步方法和同步代码块底层都是通过monitor来实现同步的。
两者的区别:同步方式是通过方法中的access_flags中设置ACC_SYNCHRONIZED标志来实现,同步代
码块是通过monitorenter和monitorexit来实现。
我们知道了每个对象都与一个monitor相关联,而monitor可以被线程拥有或释放。
以前一直提synchronized是重量级的锁,为啥现在都不提了?
在多线程并发编程中 synchronized 一直是元老级角色,很多人都会称呼它为重量级锁。
但是,随着 Java SE 1.6 对 synchronized 进行了各种优化之后,有些情况下它就并不那么重,Java SE
1.6 中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁。
针对 synchronized 获取锁的方式,JVM 使用了锁升级的优化方式,就是先使用偏向锁优先同一线程然
后再次获取锁,如果失败,就升级为 CAS 轻量级锁,如果失败就会短暂自旋,防止线程被系统挂起。
最后如果以上都失败就升级为重量级锁
还有其他的同步手段么?
先介绍
AQS(AbstractQueuedSynchronizer)。
AQS:也就是队列同步器,这是实现 ReentrantLock 的基础。
AQS 有一个 state 标记位,值为1 时表示有线程占用,其他线程需要进入到同步队列等待,同步队列是
一个双向链表。
当获得锁的线程需要等待某个条件时,会进入 condition 的等待队列,等待队列可以有多个。
当 condition 条件满足时,线程会从等待队列重新进入同步队列进行获取锁的竞争。
ReentrantLock 就是基于 AQS 实现的,如下图所示,ReentrantLock 内部有公平锁和非公平锁两种实
现,差别就在于新来的线程是否比已经在同步队列中的等待线程更早获得锁。
和 ReentrantLock 实现方式类似,Semaphore 也是基于 AQS 的,差别在于 ReentrantLock 是独占锁,
Semaphore 是共享锁。
从图中可以看到,ReentrantLock里面有一个内部类Sync,Sync继承
AQS(AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实际上都是在Sync中实现的。
它有公平锁FairSync和非公平锁NonfairSync两个子类。
ReentrantLock默认使用非公平锁,也可以通过构造器来显示的指定使用公平锁
59.正常线程进程同步的机制有哪些?
正常线程进程同步的机制有哪些
互斥:互斥的机制,保证同一时间只有一个线程可以操作共享资源 synchronized,Lock等。
临界值:让多线程串行话去访问资源
事件通知:通过事件的通知去保证大家都有序访问共享资源
信号量:多个任务同时访问,同时限制数量,比如发令枪CDL,Semaphore等
60.分布式锁你了解过有哪些?
分布式锁你了解过有哪些
分布式锁实现主要以Zookeeper(以下简称zk)、Redis、MySQL这三种为主
61.事务的四大属性是什么?
事务就是一系列操作,要么同时成功,要么同时失败
保证操作的一致性和完整性
事务应该具有4个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为ACID特性。
原子性(atomicity):一个事务是一个不可分割的工作单位,事务中包括的操作要么都做,
要么都不做。
一致性(consistency):事务必须是使数据库从一个一致性状态变到另一个一致性状态。一
致性与原子性是密切相关的。
隔离性(isolation):一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用
的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
持久性(durability):持久性也称永久性(permanence),指一个事务一旦提交,它对数
据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响
更多推荐


所有评论(0)