Bootstrap

java多线程

1. 详细阐述Java进程和线程的区别 ?

进程:指在系统中正在运行的一个应用程序;程序一旦运行就是进程;或者更专业化来说:进程是指程序执行时的一个实例,即它是程序已经执行到课中程度的数据结构的汇集。从内核的观点看,进程的目的就是担当分配系统资源(CPU时间、内存等)的基本单位。

线程:系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元执行流。进程——资源分配的最小单位,线程——程序执行的最小单位。

线程进程的区别体现在4个方面:

1、因为进程拥有独立的堆栈空间和数据段,所以每当启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这对于多进程来说十分“奢侈”,系统开销比较大,而线程不一样,线程拥有独立的堆栈空间,但是共享数据段,它们彼此之间使用相同的地址空间,共享大部分数据,比进程更节俭,开销比较小,切换速度也比进程快,效率高,但是正由于进程之间独立的特点,使得进程安全性比较高,也因为进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。一个线程死掉就等于整个进程死掉。
2、体现在通信机制上面,正因为进程之间互不干扰,相互独立,进程的通信机制相对很复杂,譬如管道,信号,消息队列,共享内存,套接字等通信机制,而线程由于共享数据段所以通信机制很方便。
3、体现在CPU系统上面,线程使得CPU系统更加有效,因为操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
4、体现在程序结构上,举一个简明易懂的列子:当我们使用进程的时候,我们不自主的使用if else嵌套来判断pid,使得程序结构繁琐,但是当我们使用线程的时候,基本上可以甩掉它,当然程序内部执行功能单元需要使用的时候还是要使用,所以线程对程序结构的改善有很大帮助。

2. Java语言创建线程有几种不同的方式?

1.继承Thread类
总结:通过继承 Thread 类,重写 run() 方法,而不是 start() 方法
Thread 类底层实现 Runnable 接口
类只能单继承
接口可以多继承

2.实现Runnable接口
总结:通过实现 Runnable 接口,实现 run() 方法,依然要用到 Thread 类

3.实现Callable接口
通过实现 Callable 接口,实现 call() 方法,使用Thread+FutureTask配合可以得到异步线程的执行结果

4.利用线程池来创建线程
用 ExecutorService 创建线程
注意:不建议用 Executors 创建线程池,建议用 ThreadPoolExecutor 定义线程池。
用的无界队列,可能造成 OOM ;不能自定义线程名字,不利于排查问题。

以上四种方式底层都是基于 Runnable

3. 概括的解释下Java线程的几种可用状态?

线程在执行过程中,可以处于下面几种状态:
1 就绪(Runnable):线程准备运行,不一定立马就能开始执行。
2 运行中(Running):进程正在执行线程的代码。
3 等待中(Waiting):线程处于阻塞的状态,等待外部的处理结束。
4 睡眠中(Sleeping):线程被强制睡眠。
5 I/O阻塞(Blocked on I/O):等待I/O操作完成。
6 同步阻塞(Blocked on Synchronization):等待获取锁。
7 死亡(Dead):线程完成了执行。"

4. 简述Java同步方法和同步代码块的区别 ?

Java同步方法和同步代码块的区别 :
同步方法默认用this或者当前类class对象作为锁;
同步代码块可以选择以什么来加锁,比同步方法要更细颗粒度,我们可以选择只同步会发生同步问题的部分代码而不是整个方法;
同步方法使用关键字 synchronized修饰方法,而同步代码块主要是修饰需要进行同步的代码,用 synchronized(object){代码内容}进行修饰;

【为何使用同步?】
java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(增删改查),将会导致数据的不准确,相互之间产生冲突。类似于在atm取钱,银行数据确没有变,这是不行的,要存在于一个事务中。因此加入了同步锁,以避免在该线程没有结束前,调用其他线程。从而保证了变量的唯一性,准确性。
1.同步方法:
即有synchronized (同步,美 ['sɪŋkrənaɪzd] ) 修饰符修饰的方法。
由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用给方法前,要获取内置锁,否则处于阻塞状态。
例:public synchronized getMoney(){}
注:synchronized修饰静态方法,如果调用该静态方法,将锁住整个类。

2.同步代码块
即有synchronized修饰符修饰的语句块,被该关键词修饰的语句块,将加上内置锁。实现同步。
例:synchronized(Object o ){}

同步是高开销的操作,因此尽量减少同步的内容。通常没有必要同步整个方法,同步部分代码块即可。
同步方法默认用this或者当前类class对象作为锁。
同步代码块可以选择以什么来加锁,比同步方法要更颗粒化,我们可以选择只同步会发生问题的部分代码而不是整个方法

5. 在监视器(Monitor)内部,是如何做线程同步的?

在Java虚拟机中,每个对象(object和class)通过某种逻辑关联监视器,每个监视器和一个对象引用相关联,为了实现监视器的互斥功能,每个对象都关联着一把锁一旦方法或者代码块被synchronized修饰,那么这个部分就放入了监视器的监视区域,确保一次只能有一个线程执行该部分的代码,线程在获取锁之前不允许执行该部分的代码

6. 解释什么是死锁( deadlock )?

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去;此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

1:死锁的概念是什么?
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

2:解决方法:
在系统中已经出现死锁后,应该及时检测到死锁的发生,并采取适当的措施来解除死锁。

3:死锁预防:
这是一种较简单和直观的事先预防的方法。方法是通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或者几个,来预防发生死锁。预防死锁是一种较易实现的方法,已被广泛使用。但是由于所施加的限制条件往往太严格,可能会导致系统资源利用率和系统吞吐量降低。

4:死锁避免:
系统对进程发出的每一个系统能够满足的资源申请进行动态检查,并根据检查结果决定是否分配资源;如果分配后系统可能发生死锁,则不予分配,否则予以分配。这是一种保证系统不进入死锁状态的动态策略。

5:死锁检测和解除:
先检测:这种方法并不须事先采取任何限制性措施,也不必检查系统是否已经进入不安全区,此方法允许系统在运行过程中发生死锁。但可通过系统所设置的检测机构,及时地检测出死锁的发生,并精确地确定与死锁有关的进程和资源。检测方法包括定时检测、效率低时检测、进程等待时检测等。然后解除死锁:采取适当措施,从系统中将已发生的死锁清除掉。
这是与检测死锁相配套的一种措施。当检测到系统中已发生死锁时,须将进程从死锁状态中解脱出来。常用的实施方法是撤销或挂起一些进程,以便回收一些资源,再将这些资源分配给已处于阻塞状态的进程,使之转为就绪状态,以继续运行。死锁的检测和解除措施,有可能使系统获得较好的资源利用率和吞吐量,但在实现上难度也最大。

7. 如何确保N个线程可以访问N个资源同时又不导致死锁?

使用多线程的时候,一种非常简单的避免死锁的方式就是:指定获取锁的顺序,并强制线程按照指定的顺序获取锁。因此,如果所有的线程都是以同样的顺序加锁和释放锁,就不会出现死锁了

多线程产生死锁需要四个条件,分别是互斥性,保持和请求,不可剥夺性还有要形成闭环,这四个条件缺一不可,只要破坏了其中一个条件就可以破坏死锁,其中最简单的方法就是线程都是以同样的顺序加锁和释放锁,也就是破坏了第四个条件。

8. 请问Java方法可以同时即是static又是synchronized的吗?

可以。如果这样做的话,JVM会获取和这个对象关联的java.lang.Class实例上的锁。这样做等于:
synchronized(XYZ.class) {
}

9. 怎么理解什么是Java多线程同步?

1:线程同步
线程同步:当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态,实现线程同步的方法有很多。

2:为什么要创建多线程?
在一般情况下,创建一个线程是不能提高程序的执行效率的,所以要创建多个线程。

3:为什么要线程同步
多个线程同时运行的时候可能调用线程函数,在多个线程同时对同一个内存地址进行写入,由于CPU时间调度上的问题,写入数据会被多次的覆盖,所以就要使线程同步。
例如:我们去银行存钱,那肯定是我们银行卡里原本的钱加上要存入的钱。但是在你存钱的同时你的朋友在给你的银行卡转钱,这是两个线程,这两个线程同时拿到了银行卡的本金,那么这两个线程最后都会返回一个总金额,那这两个总金额都是不正确的,只有这两次交易有一个先后顺序才行,这就是线程同步的一个原因。

4:线程同步是意思
同步就是协同步调,按预定的先后次序进行运行。如:你做完,我再做。
错误理解:“同”字从字面上容易理解为一起动作,其实不是,“同”字应是指协同、协助、互相配合。
正确理解: 所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回,同时其它线程也不能调用这个方法。按照这个定义,其实绝大多数函数都是同步调用。但是一般而言,我们在说同步、异步的时候,特指那些需要其他部件协作或者需要一定时间完成的任务。
在多线程编程里面,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何时刻,最多有一个线程访问,以保证数据的完整性。

5:线程同步作用
线程有可能和其他线程共享一些资源,比如,内存,文件,数据库等。
当多个线程同时读写同一份共享资源的时候,可能会引起冲突。这时候,我们需要引入线程“同步”机制,即各位线程之间要有个先来后到,不能一窝蜂挤上去抢作一团。
线程同步的真实意思和字面意思恰好相反。线程同步的真实意思,其实是“排队”:几个线程之间要排队,一个一个对共享资源进行操作,而不是同时进行操作
基本上所有解决线程安全问题的方式都是采用“序列化临界资源访问”的方式,即在同一时刻只有一个线程操作临界资源,操作完了才能让其他线程进行操作,也称作同步互斥访问。
在Java中一般采用synchronized和Lock来实现同步互斥访问。

10. 解释Java中wait和sleep方法的不同?

通常会在电话面试中经常被问到的Java线程面试问题。最大的不同是在等待时wait会释放锁,而sleep一直持有锁。Wait通常被用于线程间交互,sleep通常被用于暂停执行。

11. 如何使用thread dump?你将如何分析Thread dump?

在UNIX中你可以使用kill -3,然后thread dump将会打印日志,在windows中你可以使用”CTRL+Break”。非常简单和专业的线程面试问题,但是如果他问你怎样分析它,就会很棘手。

12. Java中你怎样唤醒一个阻塞的线程?

在Java中,线程可以通过等待/通知机制来实现线程之间的协作和同步。当一个线程需要等待另一个线程的某个条件满足时,可以调用wait()方法进入阻塞状态,并释放所持有的锁。而当条件满足后,可以通过notify()或notifyAll()方法来唤醒正在等待的线程,使其重新进入运行状态。
下面将详细介绍Java中唤醒一个阻塞的线程的方法和注意事项。
一、唤醒线程的方法
1、notify()方法
notify()方法用于唤醒在该对象监视器上等待的单个线程。如果多个线程在该对象上等待,则只有其中的一个线程能被唤醒,具体哪个线程被唤醒是不确定的,取决于虚拟机的实现,因此该方法一般不建议使用。
2、notifyAll()方法
notifyAll()方法用于唤醒在该对象监视器上等待的所有线程,这些线程竞争该对象监视器的访问权,但只有一个线程能够获得该对象的控制权,使其从wait()方法退出并从线程阻塞状态返回到可执行状态。其他线程仍然处于等待状态,直到它们重新获取该对象的控制权为止。
3、interrupt()方法
当线程正在等待阻塞时,可以通过调用该线程的interrupt()方法来中断其等待状态,并抛出InterruptedException异常,从而唤醒该线程。
二、注意事项
1、wait()和notify()/notifyAll()方法必须在同步代码块中使用。
2、在获取对象锁之前使用wait()或notify()/notifyAll()方法可能会导致
IllegalMonitorStateException异常发生。
3、当某个条件得到满足时,应该广播通知所有的等待线程,以确保全部线程都能够及时唤醒并恢复执行。
4、不要假定工作线程正常运行,因为它们有可能被中断或被等待超时。
5、一旦线程进入了wait()状态,将无法自行唤醒。因此,在调用wait()方法之前,请确保已经设置好了相应的条件。
6、在Java 1.7之前,线程阻塞和唤醒的机制存在一些问题,可能会引起多线程的死锁和饥饿问题。从Java 1.7开始,JDK对这些问题进行了改进,因此建议使用最新版本的Java。
总之,Java中唤醒一个阻塞的线程通常需要使用wait()和notify()/notifyAll()方法来实现,其中更加推荐使用notifyAll()方法。在使用这些方法时,需要注意线程同步问题、对象锁的获取和释放、异常处理及协作机制等方面的问题,以确保线程能够正常启动和运行

13. 简述Java中CycliBarriar和CountdownLatch有什么区别?

CyclicBarrier可以重复使用,而CountdownLatch不能重复使用。

Java的concurrent包里面的CountDownLatch其实可以把它看作一个计数器,只不过这个计数器的操作是原子操作,同时只能有一个线程去操作这个计数器,也就是同时只能有一个线程去减这个计数器里面的值。

你可以向CountDownLatch对象设置一个初始的数字作为计数值,任何调用这个对象上的await()方法都会阻塞,直到这个计数器的计数值被其他的线程减为0为止。

所以在当前计数到达零之前,await 方法会一直受阻塞。之后,会释放所有等待的线程,await的所有后续调用都将立即返回。这种现象只出现一次——计数无法被重置。如果需要重置计数,请考虑使用 CyclicBarrier。

CountDownLatch的一个非常典型的应用场景是:有一个任务想要往下执行,但必须要等到其他的任务执行完毕后才可以继续往下执行。假如我们这个想要继续往下执行的任务调用一个CountDownLatch对象的await()方法,其他的任务执行完自己的任务后调用同一个CountDownLatch对象上的countDown()方法,这个调用await()方法的任务将一直阻塞等待,直到这个CountDownLatch对象的计数值减到0为止

CyclicBarrier一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有用。因为该 barrier 在释放等待线程后可以重用,所以称它为循环 的 barrier。

两个看上去有点像的类,都在java.util.concurrent下,都可以用来表示代码运行到某个点上,二者
的区别在于:
(1)CyclicBarrier的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;CountDownLatch则不是,某线程运行到某个点上之后,只是给某个数值-1而已,该线程继续运行
(2)CyclicBarrier只能唤起一个任务,CountDownLatch可以唤起多个任务
(3)CyclicBarrier可重用,CountDownLatch不可重用,计数值为0该CountDownLatch就不可再用了

14. 简述volatile 类型变量提供什么保证?

Volatile 变量提供顺序和可见性保证,例如,JVM 或者 JIT为了获得更好的性能会对语句重排序,但是 volatile 类型变量即使在没有同步块的情况下赋值也不会与其他语句重排序。 volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。某些情况下,volatile 还能提供原子性,如读 64 位数据类型,像 long 和 double 都不是原子的,但 volatile 类型的 double 和 long 就是原子的

15. 简述如何调用 wait()方法的?使用 if 块还是循环?为什么?

wait() 方法应该在循环调用,因为当线程获取到 CPU 开始执行的时候,其他条件可能还没有满足,所以在处理前,循环检测条件是否满足会更好。下面是一段标准的使用 wait 和 notify 方法的代码:

// The standard idiom for using the wait method
synchronized (obj) {
while (condition does not hold)
obj.wait(); // (Releases lock, and reacquires on wakeup)
… // Perform action appropriate to condition
}

16. 解释什么是多线程环境下的伪共享(false sharing)?

伪共享是多线程系统(每个处理器有自己的局部缓存)中一个众所周知的性能问题。伪共享发生在不同处理器的上的线程对变量的修改依赖于相同的缓存行

17. 简述什么是线程局部变量?

线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java 提供 ThreadLocal 类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。

18. Java 中 ++ 操作符是线程安全的吗?

不是线程安全的操作。它涉及到多个指令,如读取变量值,增加,然后存储回内存,这个过程可能会出现多个线程交差

19. Java编写多线程程序的时候你会遵循哪些最佳实践?

a)给线程命名,这样可以帮助调试。
b)最小化同步的范围,而不是将整个方法同步,只对关键部分做同步。
c)如果可以,更偏向于使用 volatile 而不是 synchronized。
d)使用更高层次的并发工具,而不是使用 wait() 和 notify() 来实现线程间通信,如 BlockingQueue,CountDownLatch 及 Semeaphore。
e)优先使用并发集合,而不是对集合进行同步。并发集合提供更好的可扩展性

20. 解释在多线程环境下,SimpleDateFormat 是线程安全的吗?

不是,非常不幸,DateFormat 的所有实现,包括 SimpleDateFormat 都不是线程安全的,因此你不应该在多线程序中使用,除非是在对外线程安全的环境中使用,如 将 SimpleDateFormat 限制在 ThreadLocal 中。如果你不这么做,在解析或者格式化日期的时候,可能会获取到一个不正确的结果。因此,从日期、时间处理的所有实践来说,我强力推荐 joda-time 库。

21. 说明哪些Java集合类是线程安全的?

Vector、HashTable、Properties和Stack是同步类,所以它们是线程安全的,可以在多线程环境下使用。Java1.5并发API包括一些集合类,允许迭代时修改,因为它们都工作在集合的克隆上,所以它们在多线程环境中是安全的。

22. 请简述Java堆和栈的区别 ?

栈区(stack)由编译器自动分配释放 ,存放方法(函数)的参数值, 局部变量的值等,栈是向低地址扩展的数据结构,是一块连续的内存的区域。即栈顶的地址和栈的最大容量是系统预先规定好的。

堆区(heap)一般由程序员分配释放, 若程序员不释放,程序结束时由OS回收,向高地址扩展的数据结构,是不连续的内存区域,从而堆获得的空间比较灵活。

碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出.
分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。

分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的。

全局区(静态区)(static),全局变量和静态变量的存储是放在一块 的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后有系统释放。
文字常量区—常量字符串就是放在这里的。程序结束后由系统释放。

程序代码区—存放函数体的二进制代码

23. 请简述ReadWriteLock 和 StampedLock ?

ReadWriteLock包括两种子锁
(1)ReadWriteLock
ReadWriteLock 可以实现多个读锁同时进行,但是读与写和写于写互斥,只能有一个写锁线程在进行。
(2)StampedLock
StampedLock是Jdk在1.8提供的一种读写锁,相比较ReentrantReadWriteLock性能更好,因为ReentrantReadWriteLock在读写之间是互斥的,使用的是一种悲观策略,在读线程特别多的情况下,会造成写线程处于饥饿状态,虽然可以在初始化的时候设置为true指定为公平,但是吞吐量又下去了,而StampedLock是提供了一种乐观策略,更好的实现读写分离,并且吞吐量不会下降。
StampedLock包括三种锁:
(1)写锁writeLock:
writeLock是一个独占锁写锁,当一个线程获得该锁后,其他请求读锁或者写锁的线程阻塞, 获取成功后,会返回一个stamp(凭据)变量来表示该锁的版本,在释放锁时调用unlockWrite方法传递stamp参数。提供了非阻塞式获取锁tryWriteLock。
(2)悲观读锁readLock:
readLock是一个共享读锁,在没有线程获取写锁情况下,多个线程可以获取该锁。如果有写锁获取,那么其他线程请求读锁会被阻塞。悲观读锁会认为其他线程可能要对自己操作的数据进行修改,所以需要先对数据进行加锁,这是在读少写多的情况下考虑的。请求该锁成功后会返回一个stamp值,在释放锁时调用unlockRead方法传递stamp参数。提供了非阻塞式获取锁方法tryWriteLock。
(3)乐观读锁tryOptimisticRead:
tryOptimisticRead相对比悲观读锁,在操作数据前并没有通过CAS设置锁的状态,如果没有线程获取写锁,则返回一个非0的stamp变量,获取该stamp后在操作数据前还需要调用validate方法来判断期间是否有线程获取了写锁,如果是返回值为0则有线程获取写锁,如果不是0则可以使用stamp变量的锁来操作数据。由于tryOptimisticRead并没有修改锁状态,所以不需要释放锁。这是读多写少的情况下考虑的,不涉及CAS操作,所以效率较高,在保证数据一致性上需要复制一份要操作的变量到方法栈中,并且在操作数据时可能其他写线程已经修改了数据,而我们操作的是方法栈里面的数据,也就是一个快照,所以最多返回的不是最新的数据,但是一致性得到了保证。

24. Java线程的run()和start()有什么区别?

每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,run()方法称为线程体。通过调用Thread类的start()方法来启动一个线程。

start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。

start()方法来启动一个线程,真正实现了多线程运行。调用start()方法无需等待run方法体代码执行完毕,可以直接继续执行其他的代码; 此时线程是处于就绪状态,并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, run()方法运行结束, 此线程终止。然后CPU再调度其它线程。

run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法

25. 简述为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

这是另一个非常经典的 java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来!
new 一个 Thread,线程进入了新建状态。调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。
而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行

26. 简述 Synchronized 的原理 ?

(1)可重入性
synchronized的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁;
可重入的好处:
可以避免死锁;
可以让我们更好的封装代码;
synchronized是可重入锁,每部锁对象会有一个计数器记录线程获取几次锁,在执行完同步代码块时,计数器的数量会-1,直到计数器的数量为0,就释放这个锁。
(2)不可中断性
一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第二个线程会一直阻塞或等待,不可被中断;
synchronized 属于不可被中断;
Lock lock方法是不可中断的;
Lock tryLock方法是可中断的;

27. 解释为什么 wait(), notify()和 notifyAll()必须在同步方法或者同步块中被调用?

Java中,任何对象都可以作为锁,并且 wait(),notify()等方法用于等待对象的锁或者唤醒线程,在 Java 的线程中并没有可供任何对象使用的锁,所以任意对象调用方法一定定义在Object类中。
wait(), notify()和 notifyAll()这些方法在同步代码块中调用
有的人会说,既然是线程放弃对象锁,那也可以把wait()定义在Thread类里面啊,新定义的线程继承于Thread类,也不需要重新定义wait()方法的实现。然而,这样做有一个非常大的问题,一个线程完全可以持有很多锁,你一个线程放弃锁的时候,到底要放弃哪个锁?当然了,这种设计并不是不能实现,只是管理起来更加复杂。
综上所述,wait()、notify()和notifyAll()方法要定义在Object类中

28. Java 如何实现多线程之间的通讯和协作?

可以通过中断 和 共享变量的方式实现线程间的通讯和协作
比如说最经典的生产者-消费者模型:当队列满时,生产者需要等待队列有空间才能继续往里面放入商品,而在等待的期间内,生产者必须释放对临界资源(即队列)的占用权。因为生产者如果不释放对临界资源的占用权,那么消费者就无法消费队列中的商品,就不会让队列有空间,那么生产者就会一直无限等待下去。因此,一般情况下,当队列满时,会让生产者交出对临界资源的占用权,并进入挂起状态。然后等待消费者消费了商品,然后消费者通知生产者队列有空间了。同样地,当队列空时,消费者也必须等待,等待生产者通知它队列中有商品了。这种互相通信的过程就是线程间的协作。
Java中线程通信协作的最常见的两种方式:
1、syncrhoized加锁的线程的Object类的wait()/notify()/notifyAll()
2、ReentrantLock类加锁的线程的Condition类的await()/signal()/signalAll()
线程间直接的数据交换:
通过管道进行线程间通信:1)字节流;2)字符流

29. Thread 类中的 yield 方法有什么作用?

yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。

结论:yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果

30. 为什么说 Synchronized 是非公平锁?

当锁被释放后,任何一个线程都有机会竞争得到锁,这样做的目的是提高效率,但缺点是可能产生线程饥饿现象

31. 详细阐述volatile ?为什么它能保证变量对所有线程的可见性?

volatile只能作用于变量,保证了操作可见性和有序性,不保证原子性。

在Java的内存模型中分为主内存和工作内存,Java内存模型规定所有的变量存储在主内存中,每条线程都有自己的工作内存。

主内存和工作内存之间的交互分为8个原子操作:

1.lock
2.unlock
3.read
4.load
5.assign
6.use
7.store
8.write

volatile修饰的变量,只有对volatile进行assign操作,才可以load,只有load才可以use,这样就保证了在工作内存操作volatile变量,都会同步到主内存中。

32. 乐观锁一定就是好的吗?

乐观锁认为对一个对象的操作不会引发冲突,所以每次操作都不进行加锁,只是在最后提交更改时验证是否发生冲突,如果冲突则再试一遍,直至成功为止,这个尝试的过程称为自旋。

乐观锁没有加锁,但乐观锁引入了ABA问题,此时一般采用版本号进行控制;
也可能产生自旋次数过多问题,此时并不能提高效率,反而不如直接加锁的效率高;
只能保证一个对象的原子性,可以封装成对象,再进行CAS操作

33. 请对比下 Synchronized 和 ReentrantLock 的异同 ?

(1)相似点
它们都是阻塞式的同步,也就是说一个线程获得了对象锁,进入代码块,其它访问该同步块的线程都必须阻塞在同步代码块外面等待,而进行线程阻塞和唤醒的代码是比较高的。

(2)功能区别
Synchronized是java语言的关键字,是原生语法层面的互斥,需要JVM实现;ReentrantLock 是JDK1.5之后提供的API层面的互斥锁,需要lock和unlock()方法配合try/finally代码块来完成。
Synchronized使用较ReentrantLock 便利一些;
锁的细粒度和灵活性:ReentrantLock强于Synchronized;

(3)性能区别
Synchronized引入偏向锁,自旋锁之后,两者的性能差不多,在这种情况下,官方建议使用Synchronized。

① Synchronized
Synchronized会在同步块的前后分别形成monitorenter和monitorexit两个字节码指令。

在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计数器+1,相应的执行monitorexit时,计数器-1,当计数器为0时,锁就会被释放。如果获取锁失败,当前线程就要阻塞,知道对象锁被另一个线程释放为止。

② ReentrantLock
ReentrantLock是java.util.concurrent包下提供的一套互斥锁,相比Synchronized,ReentrantLock类提供了一些高级功能,主要有如下三项:
等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized避免出现死锁的情况。通过lock.lockInterruptibly()来实现这一机制;
公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁是非公平锁;ReentrantLock默认也是非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好;
锁绑定多个条件,一个ReentrantLock对象可以同时绑定多个对象。ReentrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像Synchronized要么随机唤醒一个线程,要么唤醒全部线程

34. 请解释什么是ReentrantLock ?

一、ReentrantLock是什么?
  jdk1.5新增了并发包,里面包含Lock接口,与synchronized关键字一样能实现同步功能,但相比synchronized,Lock更加灵活,可以手动获取、释放锁,而ReentrantLock就是Lock的一个实现类。

二、基本使用
1、可重入锁
  ReentrantLock从字面意思翻译就是可重入锁,那什么是可重入锁?简单来说就是某个线程获取改锁后,可以重复的进入改锁锁住的代码。ReentrantLock也是可重入锁,但是要注意的是,因为ReentrantLock需要自己手动加锁、解锁,所以加锁几次就必须解锁几次,不然其他线程就没法获取资源。

public class Main6 {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
f1(lock);
}
public static void f1(ReentrantLock lock){
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + “第1次”);
f2(lock);
}finally {
lock.unlock();
}
}

public static void f2(ReentrantLock lock){
try {
lock.lock(); // 这里再次加锁
System.out.println(Thread.currentThread().getName() + “第2次”);
}finally {
lock.unlock();
}
}
}
输出:

main第1次
main第2次

2、非公平锁与公平锁
  非公平锁就是多个线程抢夺资源时,不分先来后到,只拼手速,算抢到算谁的,ReentrantLock默认情况下就是非公平锁。

public class Main1 {

public static void main(String[] args) {
Ticket ticket = new Ticket();

new Thread(() -> {
for (int i = 0; i < 20; i++) {
ticket.sale();
}
},“A”).start();

new Thread(() -> {
for (int i = 0; i < 20; i++) {
ticket.sale();
}
},“B”).start();

new Thread(() -> {
for (int i = 0; i < 20; i++) {
ticket.sale();
}
},“C”).start();
}

}

class Ticket{
private int count = 20;
// 默认非公平锁
private ReentrantLock lock = new ReentrantLock();

public void sale(){
try {
lock.lock();
if(count > 0){
count–;
System.out.println(Thread.currentThread().getName() + “:卖出1张,剩余” + count);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}

}
}
可以看出这一轮中B压根都没抢到,输出:

A:卖出1张,剩余19
A:卖出1张,剩余18
A:卖出1张,剩余17
A:卖出1张,剩余16
C:卖出1张,剩余15
C:卖出1张,剩余14
C:卖出1张,剩余13
C:卖出1张,剩余12
C:卖出1张,剩余11
C:卖出1张,剩余10
C:卖出1张,剩余9
C:卖出1张,剩余8
C:卖出1张,剩余7
C:卖出1张,剩余6
C:卖出1张,剩余5
C:卖出1张,剩余4
C:卖出1张,剩余3
C:卖出1张,剩余2
C:卖出1张,剩余1
C:卖出1张,剩余0

那公平锁就是多个线程抢夺资源时,会按照你请求获取锁的时间先来后到,能够尽可能的雨露均沾。ReentrantLock设置成公平锁只需要在构造方法传入true,表示当前是公平锁。所以更改上述代码:

class Ticket{
private int count = 20;
// 公平锁
private ReentrantLock lock = new ReentrantLock(true);

public void sale(){

}
现在可以看到三个线程均获取到过资源。

A:卖出1张,剩余19
A:卖出1张,剩余18
A:卖出1张,剩余17
A:卖出1张,剩余16
A:卖出1张,剩余15
B:卖出1张,剩余14
C:卖出1张,剩余13
A:卖出1张,剩余12
B:卖出1张,剩余11
C:卖出1张,剩余10
A:卖出1张,剩余9
B:卖出1张,剩余8
C:卖出1张,剩余7
A:卖出1张,剩余6
B:卖出1张,剩余5
C:卖出1张,剩余4
A:卖出1张,剩余3
B:卖出1张,剩余2
C:卖出1张,剩余1
A:卖出1张,剩余0

三、小结
  ReentrantLock除了上面提到多的是可重入锁、公平锁、非公平锁外,它也是独占锁,独占锁就是任何时候都只有一个线程能得到锁,那ReentrantLock与synchronized的区别:

性能
目前优化后,synchronized与ReentrantLock性能其实已经没什么太大区别。
灵活性
这个一目了然,ReentrantLock肯定比synchronized更加灵活,自行加锁、解锁。并且synchronized只能是非公平锁,而ReentrantLock可以设定成非公平锁或公平锁。

35. 简述ReentrantLock 是如何实现可重入性的?

(1)什么是可重入性
一个线程持有锁时,当其他线程尝试获取该锁时,会被阻塞;而这个线程尝试获取自己持有锁时,如果成功说明该锁是可重入的,反之则不可重入。
(2)synchronized是如何实现可重入性
synchronized关键字经过编译后,会在同步块的前后分别形成monitorenter和monitorexit两个字节码指令。每个锁对象内部维护一个计数器,该计数器初始值为0,表示任何线程都可以获取该锁并执行相应的方法。根据虚拟机规范要求,在执行monitorenter指令时,首先要尝试获取对象的锁,如果这个对象没有被锁定,或者当前线程已经拥有了对象的锁,把锁的计数器+1,相应的在执行monitorexit指令后锁计数器-1,当计数器为0时,锁就被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到对象锁被另一个线程释放为止。
(3)ReentrantLock如何实现可重入性
ReentrantLock使用内部类Sync来管理锁,所以真正的获取锁是由Sync的实现类控制的。Sync有两个实现,分别为NonfairSync(非公公平锁)和FairSync(公平锁)。Sync通过继承AQS实现,在AQS中维护了一个private volatile int state来计算重入次数,避免频繁的持有释放操作带来的线程问题。
(4)ReentrantLock代码实例
// Sync继承于AQS
abstract static class Sync extends AbstractQueuedSynchronizer {

}
// ReentrantLock默认是非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
// 可以通过向构造方法中传true来实现公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
protected final boolean tryAcquire(int acquires) {
// 当前想要获取锁的线程
final Thread current = Thread.currentThread();
// 当前锁的状态
int c = getState();
// state == 0 此时此刻没有线程持有锁
if (c == 0) {
// 虽然此时此刻锁是可以用的,但是这是公平锁,既然是公平,就得讲究先来后到,
// 看看有没有别人在队列中等了半天了
if (!hasQueuedPredecessors() &&
// 如果没有线程在等待,那就用CAS尝试一下,成功了就获取到锁了,
// 不成功的话,只能说明一个问题,就在刚刚几乎同一时刻有个线程抢先了 =_=
// 因为刚刚还没人的,我判断过了
compareAndSetState(0, acquires)) {
// 到这里就是获取到锁了,标记一下,告诉大家,现在是我占用了锁
setExclusiveOwnerThread(current);
return true;
}
}
// 会进入这个else if分支,说明是重入了,需要操作:state=state+1
// 这里不存在并发问题 else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error(“Maximum lock count exceeded”);
setState(nextc);
return true;
}
// 如果到这里,说明前面的if和else if都没有返回true,说明没有获取到锁
return false;
}
(5)代码分析
当一个线程在获取锁过程中,先判断state的值是否为0,如果是表示没有线程持有锁,就可以尝试获取锁。
当state的值不为0时,表示锁已经被一个线程占用了,这时会做一个判断current==getExclusiveOwnerThread(),这个方法返回的是当前持有锁的线程,这个判断是看当前持有锁的线程是不是自己,如果是自己,那么将state的值+1,表示重入返回即可。

36. 请问什么是锁消除和锁粗化?

(1)锁消除
所消除就是虚拟机根据一个对象是否真正存在同步情况,若不存在同步情况,则对该对象的访问无需经过加锁解锁的操作。
比如StringBuffer的append方法,因为append方法需要判断对象是否被占用,而如果代码不存在锁竞争,那么这部分的性能消耗是无意义的。于是虚拟机在即时编译的时候就会将上面的代码进行优化,也就是锁消除。
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
从源码可以看出,append方法用了 synchronized关键字,它是线程安全的。但我们可能仅在线程内部把StringBuffer当做局部变量使用;StringBuffer仅在方法内作用域有效,不存在线程安全的问题,这时我们可以通过编译器将其优化,将锁消除,前提是Java必须运行在server模式,同时必须开启逃逸分析;
-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks
其中+DoEscapeAnalysis表示开启逃逸分析,+EliminateLocks表示锁消除。
public static String createStringBuffer(String str1, String str2) {
StringBuffer sBuf = new StringBuffer();
sBuf.append(str1);
// append方法是同步操作
sBuf.append(str2);
return sBuf.toString();
}
逃逸分析:比如上面的代码,它要看sBuf是否可能逃出它的作用域?如果将sBuf作为方法的返回值进行返回,那么它在方法外部可能被当作一个全局对象使用,就有可能发生线程安全问题,这时就可以说sBuf这个对象发生逃逸了,因而不应将append操作的锁消除,但我们上面的代码没有发生锁逃逸,锁消除就可以带来一定的性能提升。
(2)锁粗化
锁的请求、同步、释放都会消耗一定的系统资源,如果高频的锁请求反而不利于系统性能的优化,锁粗化就是把多次的锁请求合并成一个请求,扩大锁的范围,降低锁请求、同步、释放带来的性能损耗。

37. Synchronized 相比,可重入锁 ReentrantLock 其实现原理有什么不同?

相似点:
这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的.
区别:
这两种方式最大区别就是对于Synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成。
Synchronized进过编译,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码指令。在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1,相应的,在执行monitorexit指令时会将锁计算器就减1,当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止。
由于ReentrantLock是java.util.concurrent包下提供的一套互斥锁,相比Synchronized,ReentrantLock类提供了一些高级功能,主要有以下3项:
1.等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。
2.公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。
3.锁绑定多个条件,一个ReentrantLock对象可以同时绑定对个对象。

38. 简述AQS 框架 ?

AQS,即AbstractQueuedSynchronizer,在 ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch、ThreadPoolExcutor 的 Worker 中都有运用(JDK 1.8),AQS 是这些类的底层原理。
为什么要有AQS呢?是因为对于ReentrantLock、Semaphore等线程协作工具类而言,它们有很多工作是类似的,所以如果能把实现类似工作的代码给提取出来,变成一个新的底层工具类(或称为框架)的话,就可以直接使用这个工具类来构建上层代码了,而这个工具类其实就是 AQS。
AQS至少为线程协作工具类实现了以下内容:
状态的原子性管理
线程的阻塞与解除阻塞
队列的管理
总而言之,AQS 是一个用于构建锁、同步器等线程协作工具类的框架,有了 AQS 以后,很多用于线程协作的工具类就都可以很方便的被写出来,有了 AQS 之后,可以让更上层的开发极大的减少工作量,避免重复造轮子,同时也避免了上层因处理不当而导致的线程安全问题,因为 AQS 把这些事情都做好了

39. 简述AQS 对资源的共享方式?

AQS定义两种资源共享方式

(1)Exclusive(独占)

只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:

公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
(2)Share(共享)

多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。

ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读。

不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了

40. 简述如何让 Java 的线程彼此同步?

1、同步方法,使用 synchronized关键字,可以修饰普通方法、静态方法,以及语句块。
2、同步代码块,用synchronized关键字修饰语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步
3、使用特殊域变量(volatile)实现线程同步。
4、使用重入锁实现线程同步,在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。
5、使用局部变量实现线程同步,如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响

41. Java中都有哪些同步器?

1.synchronized关键字
  在Java中,使用synchronized关键字可以对代码块或方法进行同步,使得在同一时刻只有一个线程可以执行该代码块或方法。
  下面是一个使用synchronized关键字同步的示例代码:
public class SynchronizedExample {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
2.ReentrantLock类
  ReentrantLock是一个可重入的互斥锁,它可以和synchronized关键字一样实现对临界区的同步。使用ReentrantLock时需要手动获取和释放锁。
  下面是一个使用ReentrantLock同步的示例代码:
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private int count = 0;
private ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
}
finally {
lock.unlock();
}
}
public int getCount() {
lock.lock();
try {
return count;
}
finally {
lock.unlock();
}
}
}
3.Semaphore类
  Semaphore是一个信号量,它可以限制同时访问某一资源的线程数量。Semaphore可以用来实现生产者-消费者模型等。
  下面是一个使用Semaphore实现生产者-消费者模型的示例代码:
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
private final Semaphore producerSemaphore = new Semaphore(1);
private final Semaphore consumerSemaphore = new Semaphore(0);
private int data;
public void produce(int newData) throws InterruptedException {
producerSemaphore.acquire();
data = newData;
consumerSemaphore.release();
}
public int consume() throws InterruptedException {
consumerSemaphore.acquire();
int consumedData = data;
producerSemaphore.release();
return consumedData;
}
}
4.Condition接口
  Condition是一个条件变量,它可以和Lock一起使用,可以实现更加灵活的线程同步。
  下面是一个使用Condition实现等待-通知模型的示例代码:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionExample {
private int data;
private Lock lock = new ReentrantLock();
private Condition notEmpty = lock.newCondition();
private Condition notFull = lock.newCondition();
private boolean full = false;
public void put(int newData) throws InterruptedException {
lock.lock();
try {
while (full) {
notFull.await();
}
data = newData;
full = true;
notEmpty.signal();
}
finally {
lock.unlock();
}
}
public int take() throws InterruptedException {
lock.lock();
try {
while (!full) {
notEmpty.await();
}
int takenData = data;
full = false;
notFull.signal();
return takenData;
}
finally {
lock.unlock();
}
}
}
5.CountDownLatch类
  CountDownLatch是一个同步工具类,它可以使一个或多个线程等待其他线程完成操作后再执行。CountDownLatch的使用需要指定计数器的初始值,并通过await()方法等待计数器归零,同时通过countDown()方法将计数器减一。
  下面是一个使用CountDownLatch实现等待其他线程完成操作的示例代码:
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
int count = 5;
CountDownLatch latch = new CountDownLatch(count);
for (int i = 0; i < count; i++) {
Thread thread = new Thread(() -> {
// 模拟线程操作
try {
Thread.sleep(1000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(“线程” + Thread.currentThread().getName() + “执行完成”);
// 计数器减一
latch.countDown();
}
);
thread.start();
}
// 等待计数器归零
latch.await();
System.out.println(“所有线程执行完成”);
}
}
6.CyclicBarrier类
  CyclicBarrier也是一个同步工具类,它可以使一组线程相互等待,直到所有线程都到达某个屏障点后再同时执行。CyclicBarrier的使用需要指定参与线程的数量,并通过await()方法等待所有线程到达屏障点,同时通过reset()方法将屏障重置,可以用于多次使用。
  下面是一个使用CyclicBarrier实现线程同步的示例代码:
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
public static void main(String[] args) {
int count = 3;
CyclicBarrier barrier = new CyclicBarrier(count, () -> {
System.out.println(“所有线程执行完成”);
}
);
for (int i = 0; i < count; i++) {
Thread thread = new Thread(() -> {
// 模拟线程操作
try {
Thread.sleep(1000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(“线程” + Thread.currentThread().getName() + “执行完成”);
try {
// 等待其他线程
barrier.await();
}
catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
);
thread.start();
}
}
}
以上是Java中常用的同步器,不同的同步器有着不同的适用场景和使用方式,需要根据实际情况选择合适的同步器进行使用。

42. Java 中的线程池是如何实现 ?

创建一个阻塞队列来容纳任务,在第一次执行任务时创建足够多的线程,并处理任务,之后每个工作线程自动从任务队列中获取线程,直到任务队列中任务为0为止,此时线程处于等待状态,一旦有工作任务加入任务队列中,即刻唤醒工作线程进行处理,实现线程的可复用性。
线程池一般包括四个基本组成部分:
(1)线程池管理器
用于创建线程池,销毁线程池,添加新任务。
(2)工作线程
线程池中线程,可循环执行任务,在没有任务时处于等待状态。
(3)任务队列
用于存放没有处理的任务,一种缓存机制。
(4)任务接口
每个任务必须实现的接口,供工作线程调度任务的执行,主要规定了任务的开始和收尾工作,和任务的状态

43. Java创建线程池的几个核心构造参数?

// Java线程池的完整构造函数
public ThreadPoolExecutor(
int corePoolSize, // 线程池长期维持的最小线程数,即使线程处于Idle状态,也不会回收。
int maximumPoolSize, // 线程数的上限
long keepAliveTime, // 线程最大生命周期。
TimeUnit unit, //时间单位
BlockingQueue workQueue, //任务队列。当线程池中的线程都处于运行状态,而此时任务数量继续增加,则需要一个容器来容纳这些任务,这就是任务队列。
ThreadFactory threadFactory, // 线程工厂。定义如何启动一个线程,可以设置线程名称,并且可以确认是否是后台线程等。
RejectedExecutionHandler handler // 拒绝任务处理器。由于超出线程数量和队列容量而对继续增加的任务进行处理的程序。
)

44. 请简述Java线程池中的线程是怎么创建的?

线程池中的线程是在第一次提交任务submit时创建的
创建线程的方式有继承Thread和实现Runnable,重写run方法,start开始执行,wait等待,sleep休眠,shutdown停止。
(1)newSingleThreadExecutor:单线程池。
顾名思义就是一个池中只有一个线程在运行,该线程永不超时,而且由于是一个线程,当有多个任务需要处理时,会将它们放置到一个无界阻塞队列中逐个处理,它的实现代码如下:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue }
它的使用方法也很简单,下面是简单的示例:
public static void main(String[] args) throws ExecutionException,InterruptedException {
// 创建单线程执行器
ExecutorService es = Executors.newSingleThreadExecutor();
// 执行一个任务
Future future = es.submit(new Callable() {
@Override
public String call() throws Exception {
return “”;
}
}
);
// 获得任务执行后的返回值
System.out.println(“返回值:” + future.get());
// 关闭执行器
es.shutdown();
}
(2)newCachedThreadPool:缓冲功能的线程。
建立了一个线程池,而且线程数量是没有限制的(当然,不能超过Integer的最大值),新增一个任务即有一个线程处理,或者复用之前空闲的线程,或者重亲启动一个线程,但是一旦一个线程在60秒内一直处于等待状态时(也就是一分钟无事可做),则会被终止,其源码如下:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
这里需要说明的是,任务队列使用了同步阻塞队列,这意味着向队列中加入一个元素,即可唤醒一个线程(新创建的线程或复用空闲线程来处理),这种队列已经没有队列深度的概念了。
(3)newFixedThreadPool:固定线程数量的线程池。
在初始化时已经决定了线程的最大数量,若任务添加的能力超出了线程的处理能力,则建立阻塞队列容纳多余的任务,其源码如下: 
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}
上面返回的是一个ThreadPoolExecutor,它的corePoolSize和maximumPoolSize是相等的,也就是说,最大线程数量为nThreads。如果任务增长的速度非常快,超过了LinkedBlockingQuene的最大容量(Integer的最大值),那此时会如何处理呢?会按照ThreadPoolExecutor默认的拒绝策略(默认是DiscardPolicy,直接丢弃)来处理。
以上三种线程池执行器都是ThreadPoolExecutor的简化版,目的是帮助开发人员屏蔽过得线程细节,简化多线程开发。当需要运行异步任务时,可以直接通过Executors获得一个线程池,然后运行任务,不需要关注ThreadPoolExecutor的一系列参数时什么含义。当然,有时候这三个线程不能满足要求,此时则可以直接操作ThreadPoolExecutor来实现复杂的多线程计算。
newSingleThreadExecutor、newCachedThreadPool、newFixedThreadPool是线程池的简化版,而ThreadPoolExecutor则是旗舰版___简化版容易操作,需要了解的知识相对少些,方便使用,而旗舰版功能齐全,适用面广,难以驾驭。

45. 简述Java Volatile 关键字的作用?

Volatile关键字的作用主要有如下两个:
1.线程的可见性:当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
2. 顺序一致性:禁止指令重排序。

46. Volatile 能够保证线程间的变量可见性,是不是就意味着基于 volatile 变量的运算就是并发安全的?

volatile修饰的变量在各个线程的工作内存中不存在一致性的问题(在各个线程工作的内存中,volatile修饰的变量也会存在不一致的情况,但是由于每次使用之前都会先刷新主存中的数据到工作内存,执行引擎看不到不一致的情况,因此可以认为不存在不一致的问题),但是java的运算并非原子性的操作,导致volatile在并发下并非是线程安全的。

47. 简述Java ThreadLocal 是什么?有哪些使用场景?

ThreadLocal 是一个本地线程副本变量工具类,在每个线程中都创建了一个 ThreadLocalMap 对象,简单说 ThreadLocal 就是一种以空间换时间的做法,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。通过这种方式,避免资源在多线程间共享。

原理:线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。

经典的使用场景是为每个线程分配一个 JDBC 连接 Connection。这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A 线程关了 B线程正在使用的 Connection; 还有 Session 管理等问题

48. 简述ThreadLocal 是怎么解决并发安全的?

Java程序中,常用的有两种机制来解决多线程并发问题,一种是sychronized方式,通过锁机制,一个线程执行时,让另一个线程等待,是以时间换空间的方式来让多线程串行执行。而另外一种方式就是ThreadLocal方式,通过创建线程局部变量,以空间换时间的方式来让多线程并行执行。两种方式各有优劣,适用于不同的场景,要根据不同的业务场景来进行选择。

在spring的源码中,就使用了ThreadLocal来管理连接,在很多开源项目中,都经常使用ThreadLocal来控制多线程并发问题,因为它足够的简单,我们不需要关心是否有线程安全问题,因为变量是每个线程所特有的。

49. 为什么说要慎用 ThreadLocal,谈谈你的理解,使用 ThreadLocal 需要注意些什么?

ThreadLocal 变量解决了多线程环境下单个线程中变量的共享问题,使用名为ThreadLocalMap的哈希表进行维护(key为ThreadLocal变量名,value为ThreadLocal变量的值);

使用时需要注意以下几点:
线程之间的threadLocal变量是互不影响的,
使用private final static进行修饰,防止多实例时内存的泄露问题
线程池环境下使用后将threadLocal变量remove掉或设置成一个初始值

50. 简述Java中的自旋是什么意思?

在Java中,自旋是指在多线程编程中,当线程尝试获得某个锁时,如果该锁已经被其他线程占用,线程会一直循环检查该锁是否被释放,直到获取到该锁为止。这个循环等待的过程被称为自旋。

自旋的主要目的是避免线程切换带来的开销。当线程需要获得某个锁时,如果该锁已经被其他线程占用,线程会进入等待状态,操作系统需要进行线程切换,从而导致一定的开销。如果等待时间很短,那么这种开销是不必要的。在这种情况下,自旋可以避免线程切换,提高程序的性能。
下面是一个简单的代码演示,其中两个线程同时对一个对象加锁,其中一个线程会通过自旋等待另一个线程释放锁。

public class SpinDemo {
private static Object lock = new Object();

public static void main(String[] args) {
new Thread(() -> {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " acquired lock.");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, “Thread-1”).start();

new Thread(() -> {
while (true) {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " acquired lock.");
break;
}
}
}, “Thread-2”).start();
}
}
  在这个例子中,线程1先获得锁并保持5秒钟,线程2在尝试获得锁时会通过自旋等待线程1释放锁。当线程1释放锁后,线程2获得锁并输出信息。

51. 简述多线程中 synchronized 锁升级的原理?

synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。

锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗

52. Java Concurrency API 中的 Lock 接口(Lock interface)是什么?对比同步它有什么优势?

Lock 接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。
它的优势有:
(1)可以使锁更公平
(2)可以使线程在等待锁的时候响应中断
(3)可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间
(4)可以在不同的范围,以不同的顺序获取和释放锁
整体上来说 Lock 是 synchronized 的扩展版,Lock 提供了无条件的、可轮询的(tryLock 方法)、定时的(tryLock 带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition 方法)锁操作。另外 Lock 的实现类基本都支持非公平锁(默认)和公平锁,synchronized 只支持非公平锁,当然,在大部分情况下,非公平锁是高效的选择

53. 多线程编程中什么是上下文切换?

多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。

概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。

上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。

Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少

54. ReadWriteLock读写之间互斥吗?

是的,ReadWriteLock(读写锁)提供了读写操作之间的互斥。它允许多个线程同时进行读操作,但在写操作期间会阻塞其他线程的读和写操作。

55. 请阐述synchronized和volatile的区别 ?

在多线程编程中,synchronized和volatile是两个关键字,用于确保多个线程之间的可见性和顺序性。它们的作用和使用场景有所不同。
  1.synchronized关键字:
  ·synchronized关键字用于实现线程之间的互斥同步,保证同一时刻只有一个线程可以执行被synchronized修饰的代码块或方法。
  ·synchronized关键字可以用于修饰代码块或方法,也可以用于修饰静态方法或类。
  当一个线程获得了对synchronized代码块或方法的锁定,其他试图访问该代码块或方法的线程将被阻塞,直到该线程释放锁定。
synchronized和volatile的区别是什么
  ·synchronized关键字保证了原子性、可见性和有序性。
  下面是一个使用synchronized关键字的简单示例:
public class SynchronizedExample {
private int count = 0;
public synchronized void increment() {
count++;
}
}
2.volatile关键字:
  ·volatile关键字用于标记变量,确保多个线程之间对变量的修改可见。
  ·当一个线程修改了volatile变量的值,该值会立即被写入主内存,而不是被缓存到线程的本地内存。
  ·当其他线程需要读取该变量时,它们会从主内存中读取最新的值,而不是使用缓存的旧值。
  ·volatile关键字只提供了可见性的保证,并不保证原子性。
  下面是一个使用volatile关键字的简单示例:
public class VolatileExample {
private volatile int count = 0;
public void increment() {
count++;
}
}
需要注意的是,volatile关键字适用于某个变量在多个线程之间进行简单的读取和写入操作,并不能代替synchronized关键字来实现复杂的互斥同步逻辑。
  总结:
  ·synchronized关键字用于实现互斥同步,保证同一时刻只有一个线程可以执行同步代码块或方法,同时提供了原子性、可见性和有序性的保证。
  ·volatile关键字用于确保多个线程之间对变量的修改可见,但并不提供互斥同步的功能,也不保证原子性

56. 简述Java中用到的线程调度算法

Java中用到的线程调度算法是抢占式调度算法。抢占式调度算法指的是操作系统可以随时中断当前执行的线程,并将CPU分配给其他可运行的线程,以达到最大化CPU利用率和系统响应速度的目的。
  在Java中,可以使用Thread类来创建并启动线程。下面是一个简单的示例代码,其中创建了两个线程并启动它们,它们会交替执行:
public class ThreadDemo {
public static void main(String[] args) {
Thread t1 = new MyThread(“Thread 1”);
Thread t2 = new MyThread(“Thread 2”);
t1.start();
t2.start();
}
static class MyThread extends Thread {
private String name;

public MyThread(String name) {
this.name = name;
}
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(name + " is running");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
  在这个示例中,我们定义了一个MyThread类,它继承自Thread类,并实现了run方法。在run方法中,我们打印线程的名字,并让线程睡眠1秒钟,然后再次执行,以便让两个线程交替运行。

java中用到了哪些线程调度算法?

在main方法中,我们创建了两个MyThread对象,并将它们分别命名为"Thread 1"和"Thread 2"。然后我们调用了start方法来启动它们。这将导致 JVM 创建两个操作系统线程,并让它们开始运行。由于Java使用的是抢占式调度算法,因此这两个线程将交替运行,直到它们完成为止。

需要注意的是,虽然我们可以在run方法中使用sleep方法来模拟线程执行的时间,但在实际的生产环境中,线程的运行时间是无法确定的,并且线程之间的竞争条件可能会导致不同的执行结果。因此,在编写多线程应用程序时,需要格外小心,以确保线程安全和正确性。

57. 当线程进入一个对象的一个synchronized方法后,其它线程是否可进入此对象的其它方法?

不能,一个对象的一个synchronized方法只能由一个线程访问。

58. 解释Static 属性为什么不会被序列化?

因为序列化是针对对象而言的,而 static 属性优先于对象存在,随着类的加载而加载,所以不会被序列化.
看到这个结论,是不是有人会问,serialVersionUID 也被 static 修饰,为什么 serialVersionUID 会被序列化? 其实 serialVersionUID 属性并没有被序列化,JVM 在序列化对象时会自动生成一个 serialVersionUID,然后将我们显示指定的 serialVersionUID 属性值赋给自动生成的 serialVersionUID.

59. 简述什么是阻塞队列 ?

阻塞队列也是一种先进先出的队列,但是它还具有以下特性:
(1) 是线程安全的队列;
(2) 带有阻塞功能:

(a) 如果队列满,继续入队列时,入队列操作就会阻塞,直到队列不满才能完成入队列;
(b) 如果队列空,继续出队列时,出队列操作就会阻塞,直到队列不空才能完成出队列。

2、应用场景
阻塞队列的典型应用场景:生产者消费者模型
生产者消费者模型是一种常见的多线程协调工作的模式:生产者和消费者之间通过阻塞队列进行通讯,生产者生产出数据不用等待消费者来处理,而是会直接放入到阻塞队列中;消费者也不找生产者索要数据,而是直接从阻塞队列中取。

类似于我们去超市买东西:老板作为“生产者”,他把商品放在货物架上让我们自己拿,我们作为“消费者”,也不需要直接去找老板要商品,而是直接去相应的货物架上拿它就行,这个货物架就相当于“交易场所”。

如果每个逛超市的人都去找老板说:“老板,我要一个xxx”,那么老板就会非常忙,放在代码中,就是会产生严重的锁冲突。

60. 如何在 Java 中实现一个阻塞队列?

标准库的阻塞队列
标准库中的阻塞队列是一个接口:BlockingQueue

实现该接口的类:

类名 说明
LinkedBlockingQueue<> 基于链表的阻塞队列
LinkedBlockingDeque<> 基于链表的双端阻塞队列
LinkedTransferQueue<> 基于链表的无界阻塞队列
ArrayBlockingQueue<> 基于顺序表的阻塞队列
PriorityBlockingQueue<> 带有优先级功能的阻塞队列
入队列方法:put(E e)

出队列方法:take()

注意:

(1) offer(E e)方法也可以入队列,但是这个方法不具有阻塞功能。

(2) 阻塞队列无法阻塞式的查看队首元素,只能先取出队首元素,查看之后再放入队列中。

阻塞队列的简单使用:

public class Test {
public static void main(String[] args) throws InterruptedException {
BlockingQueue queue = new LinkedBlockingQueue<>();
queue.put(1);
queue.put(2);
queue.put(3);
System.out.println(queue.take());
System.out.println(queue.take());
System.out.println(queue.take());

//如果队列为空,继续取元素会阻塞等待
queue.take();
}
}

需要实现两个阻塞功能:

(1) 队列满,继续入队列会阻塞,直到有元素出队;
(2) 队列空,继续出队列会阻塞,直到有元素入队。

可以使用wait和notify来进行线程阻塞和唤醒线程的操作:
/**

  • Created with IntelliJ IDEA.
  • Description:
  • Users:YanJunJun
  • Date:2022-09-21
  • Time:11:43
    */
    public class MyBlockingQueue {
    private int[] array = new int[100];
    private volatile int head;//记录队首元素下标
    private volatile int tail;//记录队尾元素下标
    private volatile int size;//记录有效元素个数

public void put(int elem) throws InterruptedException {
synchronized (this){
//判断队列是否为满
if(size == array.length){
this.wait();
}
//插入元素
array[tail] = elem;
tail++;
//循环队列
//队尾元素下标如果超出数组长度,则从头开始存储
if(tail >= array.length){
tail = 0;
}
size++;
this.notify();
}
}
public int take() throws InterruptedException {
synchronized (this) {
//判断队列是非为空
if(size == 0){
this.wait();
}
//取出元素
int ret = array[head];
head++;
//队首元素下标如果超出数组长度,则从头开始
if(head >= array.length){
head = 0;
}
size–;
this.notify();
return ret;
}
}
}
两个方法中的wait和notify会相互唤醒

61. 简述什么是原子操作?Java 中有哪些原子操作?

java中的原子操作和线程安全是具有一定的联系性的,这其中的内容也是比较复杂的。它们所涉及的范围也是非常的广阔的。不知道你掌握了吗?一起来看看吧。
首先说一下,什么叫原子的(原子操作)?
Java原子操作是指:不会被打断地的操作。(就是做到互斥和可见性)
那难道原子操作就可以真的达到线程安全同步效果了吗?实际上有一些原子操作不一定是线程安全的。那么,原子操作在什么情况下不是线程安全的呢?
也许是这个原因导致的:java线程允许线程在自己的内存区保存变量的副本。
允许线程使用本地的私有拷贝进行工作而非每次都使用主存的值是为了提高性能后,就是各自做自己的副本了,更新操作(写操作)因未写入主存中,导致其它线程不可见)。
那该如何解决呢?
因此需要通过java同步机制。
在java中,32位或者更少位数的赋值是原子的。在一个32位的硬件平台上,除了double和long型的其它原始类型通常都是使用32位进行表示,而double和long通常使用64位表示。另外,对象引用使用本机指针实现,通常也是32位的。对这些32位的类型的操作是原子的。
这些原始类型通常使用32位或者64位表示,这又引入了另一个小小的神话:原始类型的大小是由语言保证的。这是不对的。java语言保证的是原始类型的表数范围而非JVM中的存储大小。因此,int型总是有相同的表数范围。在一个JVM上可能使用32位实现,而在另一个JVM上可能是64位的。
再次强调:在所有平台上被保证的是表数范围,32位以及更小的值的操作是原子的。
然后举例为大家区分同步与异步。
举例说明:普通B/S模式(同步)AJAX技术(异步)
同步:提交请求->等待服务器处理->处理完返回这个期间客户端浏览器不能干任何事
异步:请求通过事件触发->服务器处理(这是浏览器仍然可以作其他事情)->处理完毕
可见,彼“同步”非此“同步”——我们说的java中的那个共享数据同步(synchronized)
一个同步的对象是指行为(动作),一个是同步的对象是指物质(共享数据)。

62. 简述什么是Java竞态条件?你如何发现并解决竞态条件?

当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。
在临界区中使用适当的同步就可以避免竞态条件。
界区实现方法有两种,一种是用 synchronized ,一种是用 Lock 显式锁实现。

63. 简述Java 中你如何转储线程(thread dump)?

  1. 线程转储简介
    线程转储(Thread Dump)就是JVM中所有线程状态信息的一次快照。
    线程转储一般使用文本格式, 可以将其保存到文本文件中, 然后人工查看和分析, 或者使用工具/API自动分析。
    Java中的线程模型, 直接使用了操作系统的线程调度模型, 只进行简单的封装。
    线程调用栈, 也称为方法调用栈。 比如在程序执行过程中, 有一连串的方法调用链:obj1.method2调用了obj2.methodB,obj2.methodB又调用了obj3.methodC。 每个线程的状态都可以通过这种调用栈来表示。
    线程转储展示了各个线程的行为, 对于诊断和排查问题非常有用。
    下面我们通过具体示例, 来演示各种获取Java线程转储的工具, 以及使用方法。
  2. 使用JDK自带的工具
    我们一般使用JDK自带的命令行工具来获取Java应用程序的线程转储。 这些工具都在JDK主目录的bin文件夹下。
    所以, 只要配置好 PATH 路径即可。 如果不会配置, 可以参考:JDK环境准备
    2.1 jstack 工具
    jstack 是JDK内置的一款命令行工具, 专门用来查看线程状态, 也可以用来执行线程转储。
    一般先通过jps或者ps命令找到Java进程对应的pid, 然后在控制台中通过pid来输出线程转储。 当然, 我们也可以将输出内容重定向到某个文件中。
    2.2 Java Mission Control
    Java Mission Control(JMC)是一款客户端图形界面工具, 用于收集和分析Java应用程序的各种数据。
    启动JMC后, 首先会显示本地计算机上运行的Java进程列表。 当然也可以通过JMC连接到远程Java进程。
    可以鼠标右键单击对应的进程, 选择 “Start Flight Recording(开始飞行记录)” 。 结束之后, “Threads(线程)” 选项卡会显示“线程转储”
    2.3 jvisualvm
    jvisualvm 是一款客户端图形界面工具, 既简单又实用, 可用来监控 Java应用程序, 对JVM进行故障排查和性能分析。
    也可以用来获取线程转储。 鼠标右键单击Java进程, 选择“ Thread Dump”选项, 则可以创建线程转储, 完成后会在新选项卡中自动打开
    2.4 jcmd
    jcmd工具本质上是向目标JVM发送一串命令。 尽管支持很多功能, 但不支持连接远程JVM - 只能在Java进程的本地机器上使用。
    其中一个命令是Thread.print, 用来获取线程转储, 示例用法如下:
    jcmd 17264 Thread.print
    2.5 jconsole
    jconsole 工具也可以查看线程栈跟踪。
    打开jconsole并连接到正在运行的Java进程, 导航到“线程”选项卡, 可以查看每个线程的堆栈跟踪

64. 如果你的Serializable类包含一个不可序列化的成员,会发生什么?你是如何解决的?

任何序列化该类的尝试都会因NotSerializableException而失败,但这可以通过在 Java中 为 static 设置瞬态(trancient)变量来轻松解决。
Java 序列化是一个重要概念, 但它很少用作持久性解决方案, 开发人员大多忽略了 Java 序列化 API。根据我的经验, Java 序列化在任何 Java核心内容面试中都是一个相当重要的话题, 在几乎所有的网面试中, 我都遇到过一两个 Java 序列化问题, 我看过一次面试, 在问几个关于序列化的问题之后候选人开始感到不自在, 因为缺乏这方面的经验。 他们不知道如何在 Java 中序列化对象, 或者他们不熟悉任何 Java 示例来解释序列化, 忘记了诸如序列化在 Java 中如何工作, 什么是标记接口, 标记接口的目的是什么, 瞬态变量和可变变量之间的差异, 可序列化接口具有多少种方法, 在 Java 中,Serializable 和 Externalizable 有什么区别, 或者在引入注解之后, 为什么不用 @Serializable 注解或替换 Serializalbe 接口

65. 解释为什么Java中 wait 方法需要在 synchronized 的方法中调用?

wait 和 notify。它们是在有 synchronized 标记的方法或 synchronized 块中调用的,因为 wait 和 modify 需要监视对其上调用 wait 或 notify-get 的 Object。

大多数Java开发人员都知道对象类的 wait(),notify() 和 notifyAll()方法必须在Java中的 synchronized 方法或 synchronized 块中调用, 但是我们想过多少次, 为什么在 Java 中 wait, notify 和 notifyAll 来自 synchronized 块或方法?

最近这个问题在Java面试中被问到我的一位朋友,他思索了一下,并回答说: 如果我们不从同步上下文中调用 wait() 或 notify() 方法,我们将在 Java 中收到 IllegalMonitorStateException。

他的回答从实际效果上年是正确的,但面试官对这样的答案不会完全满意,并希望向他解释这个问题。面试结束后 他和我讨论了同样的问题,我认为他应该告诉面试官关于 Java 中 wait()和 notify()之间的竞态条件,如果我们不在同步方法或块中调用它们就可能存在。

让我们看看竞态条件如何在Java程序中发生。它也是流行的线程面试问题之一,并经常在电话和面对面的Java开发人员面试中出现。因此,如果你正在准备Java面试,那么你应该准备这样的问题,并且可以真正帮助你的一本书是《Java程序员面试公式书》的。这是一本罕见的书,涵盖了Java访谈的几乎所有重要主题,例如核心Java,多线程,IO 和 NIO 以及 Spring 和 Hibernate 等框架。你可以在这里查看。

为什么要等待来自 Java中的 synchronized 方法的 wait方法为什么必须从 Java 中的 synchronized 块或方法调用 ? 我们主要使用 wait(),notify() 或 notifyAll() 方法用于 Java 中的线程间通信。一个线程在检查条件后正在等待,例如,在经典的生产者 - 消费者问题中,如果缓冲区已满,则生产者线程等待,并且消费者线程通过使用元素在缓冲区中创建空间后通知生产者线程。调用notify()或notifyAll()方法向单个或多个线程发出一个条件已更改的通知,并且一旦通知线程离开 synchronized 块,正在等待的所有线程开始获取正在等待的对象锁定,幸运的线程在重新获取锁之后从 wait() 方法返回并继续进行。

让我们将整个操作分成几步,以查看Java中wait()和notify()方法之间的竞争条件的可能性,我们将使用Produce Consumer 线程示例更好地理解方案:

Producer 线程测试条件(缓冲区是是否完整)并确认必须等待(找到缓冲区已满)。
Consumer 线程在使用缓冲区中的元素后设置条件。
Consumer 线程调用 notify() 方法; 这是不会被听到的,因为 Producer 线程还没有等待。
Producer 线程调用 wait() 方法并进入等待状态。
因此,由于竞态条件,我们可能会丢失通知,如果我们使用缓冲区或只使用一个元素,生产线程将永远等待,你的程序将挂起。“在java同步中等待 notify 和 notifyall 现在让我们考虑如何解决这个潜在的竞态条件?这个竞态条件通过使用 Java 提供的 synchronized 关键字和锁定来解决。为了调用 wait(),notify() 或 notifyAll(), 在Java中,我们必须获得对我们调用方法的对象的锁定。由于 Java 中的 wait() 方法在等待之前释放锁定并在从 wait() 返回之前重新获取锁定方法,我们必须使用这个锁来确保检查条件(缓冲区是否已满)和设置条件(从缓冲区获取元素)是原子的,这可以通过在 Java 中使用 synchronized 方法或块来实现。

我不确定这是否是面试官实际期待的,但这个我认为至少有意义,请纠正我如果我错了,请告诉我们是否还有其他令人信服的理由调用 wait(),notify() 或 Java 中的 notifyAll() 方法。

总结一下,我们用 Java 中的 synchronized 方法或 synchronized 块调用 Java 中的 wait(),notify() 或 notifyAll() 方法来避免:

  1. Java 会抛出 IllegalMonitorStateException,如果我们不调用来自同步上下文的wait(),notify()或者notifyAll()方法。
  2. Javac 中 wait 和 notify 方法之间的任何潜在竞争条件。

66. 如何避免 Java 线程死锁?

要想避免死锁,只需要破坏掉至少一个构造死锁的必要条件即可,而在操作系统中,互斥条件和不可剥夺条件是系统规定的,这也没办法人为更改,而且这两个条件很明显是一个标准的程序应该所具备的特性。所以目前只有请求并持有和环路等待条件是可以被破坏的。
(1)保持加锁顺序:当多个线程都需要加相同的几个锁的时候(例如上述情况一的死锁),按照不同的顺序枷锁那么就可能导致死锁产生,所以我们如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。
(2)获取锁添加时限:上述死锁代码情况二就是因为出现了获取锁失败无限等待的情况,如果我们在获取锁的时候进行限时等待,例如wait(1000)或者使用ReentrantLock的tryLock(1,TimeUntil.SECONDS)这样在指定时间内获取锁失败就不等待;
(3)进行死锁检测:我们可以通过一些手段检查代码并预防其出现死锁。

67. 简述Java死锁的检测方式 ?

Java中死锁检测手段最多的就是使用JDK带有的jstack和JConsole工具了。下面我们以jstack为例来进行死锁的检测;
(1)先运行我们的代码程序
(2)使用JDK的工具JPS查看运行的进程信息
(3)使用jps查看到的进程ID对其进行jstack 进程分析

68. 编写Java代码实现一个死锁的案例 ?

当线程 A 持有独占锁a,并尝试去获取独占锁 b 的同时,线程 B 持有独占锁 b,并尝试获取独占锁 a 的情况下,就会发生 AB 两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。
import java.util.Date;

public class LockTest {
public static String obj1 = “obj1”;
public static String obj2 = “obj2”;
public static void main(String[] args) {
LockA la = new LockA();
new Thread(la).start();
LockB lb = new LockB();
new Thread(lb).start();
}
}
class LockA implements Runnable{
public void run() {
try {
System.out.println(new Date().toString() + " LockA 开始执行");
while(true){
synchronized (LockTest.obj1) {
System.out.println(new Date().toString() + " LockA 锁住 obj1");
Thread.sleep(3000); // 此处等待是给B能锁住机会
synchronized (LockTest.obj2) {
System.out.println(new Date().toString() + " LockA 锁住 obj2");
Thread.sleep(60 * 1000); // 为测试,占用了就不放
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
class LockB implements Runnable{
public void run() {
try {
System.out.println(new Date().toString() + " LockB 开始执行");
while(true){
synchronized (LockTest.obj2) {
System.out.println(new Date().toString() + " LockB 锁住 obj2");
Thread.sleep(3000); // 此处等待是给A能锁住机会
synchronized (LockTest.obj1) {
System.out.println(new Date().toString() + " LockB 锁住 obj1");
Thread.sleep(60 * 1000); // 为测试,占用了就不放
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

69. 如何使用双重检查锁定在 Java 中创建线程安全的单例?

艰难的核心 Java 面试问题.这个 Java 问题也常被问: 什么是线程安全的单例,你怎么创建它。好吧,在Java 5之前的版本, 使用双重检查锁定创建单例 Singleton 时,如果多个线程试图同时创建 Singleton 实例,则可能有多个 Singleton 实例被创建。从 Java 5 开始,使用 Enum 创建线程安全的Singleton很容易。但如果面试官坚持双重检查锁定,那么你必须为他们编写代码。记得使用volatile变量。

70. 解释为什么等待和通知是在 Object 类而不是 Thread 中声明的?

一个棘手的 Java 问题,如果 Java编程语言不是你设计的,你怎么能回答这个问题呢。Java编程的常识和深入了解有助于回答这种棘手的 Java 核心方面的面试问题。
为什么 wait,notify 和 notifyAll 是在 Object 类中定义的而不是在 Thread 类中定义
这个问题的好在它能反映了面试者对等待通知机制的了解, 以及他对此主题的理解是否明确。就像为什么 Java 中不支持多继承或者为什么 String 在 Java 中是 final 的问题一样,这个问题也可能有多个答案。
为什么在 Object 类中定义 wait 和 notify 方法,每个人都能说出一些理由。 从我的面试经验来看, wait 和 nofity 仍然是大多数Java 程序员最困惑的,特别是2到3年的开发人员,如果他们要求使用 wait 和 notify, 他们会很困惑。因此,如果你去参加 Java 面试,请确保对 wait 和 notify 机制有充分的了解,并且可以轻松地使用 wait 来编写代码,并通过生产者-消费者问题或实现阻塞队列等了解通知的机制。
为什么等待和通知需要从同步块或方法中调用, 以及 Java 中的 wait,sleep 和 yield 方法之间的差异,如果你还没有读过,你会觉得有趣。为何 wait,notify 和 notifyAll 属于 Object 类? 为什么它们不应该在 Thread 类中? 以下是我认为有意义的一些想法:

  1. wait 和 notify 不仅仅是普通方法或同步工具,更重要的是它们是 Java 中两个线程之间的通信机制。对语言设计者而言, 如果不能通过 Java 关键字(例如 synchronized)实现通信此机制,同时又要确保这个机制对每个对象可用, 那么 Object 类则是的正确声明位置。记住同步和等待通知是两个不同的领域,不要把它们看成是相同的或相关的。同步是提供互斥并确保 Java 类的线程安全,而 wait 和 notify 是两个线程之间的通信机制。
  2. 每个对象都可上锁,这是在 Object 类而不是 Thread 类中声明 wait 和 notify 的另一个原因。
  3. 在 Java 中为了进入代码的临界区,线程需要锁定并等待锁定,他们不知道哪些线程持有锁,而只是知道锁被某个线程持有, 并且他们应该等待取得锁, 而不是去了解哪个线程在同步块内,并请求它们释放锁定。
  4. Java 是基于 Hoare 的监视器的思想,在Java中,所有对象都有一个监视器。
    线程在监视器上等待,为执行等待,我们需要2个参数:
    一个线程
    一个监视器(任何对象)
    在 Java 设计中,线程不能被指定,它总是运行当前代码的线程。但是,我们可以指定监视器(这是我们称之为等待的对象)。这是一个很好的设计,因为如果我们可以让任何其他线程在所需的监视器上等待,这将导致“入侵”,导致在设计并发程序时会遇到困难。请记住,在 Java 中,所有在另一个线程的执行中侵入的操作都被弃用了(例如 stop 方法)。

71. 简述线程池都有哪些状态?

RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。
SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。
STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。
TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。
TERMINATED:terminated()方法结束后,线程池的状态就会变成这个。

72. 简述线程池中 submit() 和 execute() 方法有什么区别?

execute():只能执行 Runnable 类型的任务。
submit():可以执行 Runnable 和 Callable 类型的任务。
Callable 类型的任务可以获取执行的返回值,而 Runnable 执行无返回值

73. 怎么实现一个线程安全的计数器 ?

public class MySafeThread implements Runnable {
//设置计数初值为0
private static AtomicInteger count = new AtomicInteger(0);
@Override
public void run() {
while (true) {
try {
Thread.sleep(1);
}
catch (InterruptedException e) {
e.printStackTrace();
}
MySafeThread.calc();
}
}
private synchronized static void calc() {
if (count.get() < 1000) {
// 自增1,返回更新后的值
int c = count.incrementAndGet();
System.out.println(“线程:” + Thread.currentThread().getName()+" :" + c);
}
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
MySafeThread myThread = new MySafeThread();
Thread t = new Thread(myThread);
t.start();
}
}
}

74. 编写代码实现LRU算法 ?

import java.util.HashMap;
public class LRU {
private int currentSize;
//当前的大小
private int capcity;
//总容量
private HashMap caches;
//所有的node节点
private Node first;
//头节点
private Node last;
//尾节点
public LRU(int size) {
currentSize = 0;
this.capcity = size;
caches = new HashMap(size);
}
/**

  • 放入元素
  • @param key
  • @param value
    /
    public void put(K key, V value) {
    Node node = caches.get(key);
    //如果新元素
    if (node == null) {
    //如果超过元素容纳量
    if (caches.size() >= capcity) {
    //移除最后一个节点
    caches.remove(last.key);
    removeLast();
    }
    //创建新节点
    node = new Node(key,value);
    }
    //已经存在的元素覆盖旧值
    node.value = value;
    //把元素移动到首部
    moveToHead(node);
    caches.put(key, node);
    }
    /
    *
  • 通过key获取元素
  • @param key
  • @return
    /
    public Object get(K key) {
    Node node = caches.get(key);
    if (node == null) {
    return null;
    }
    //把访问的节点移动到首部
    moveToHead(node);
    return node.value;
    }
    /
    *
  • 根据key移除节点
  • @param key
  • @return
    /
    public Object remove(K key) {
    Node node = caches.get(key);
    if (node != null) {
    if (node.pre != null) {
    node.pre.next = node.next;
    }
    if (node.next != null) {
    node.next.pre = node.pre;
    }
    if (node == first) {
    first = node.next;
    }
    if (node == last) {
    last = node.pre;
    }
    }
    return caches.remove(key);
    }
    /
    *
  • 清除所有节点
    /
    public void clear() {
    first = null;
    last = null;
    caches.clear();
    }
    /
    *
  • 把当前节点移动到首部
  • @param node
    /
    private void moveToHead(Node node) {
    if (first == node) {
    return;
    }
    if (node.next != null) {
    node.next.pre = node.pre;
    }
    if (node.pre != null) {
    node.pre.next = node.next;
    }
    if (node == last) {
    last = last.pre;
    }
    if (first == null || last == null) {
    first = last = node;
    return;
    }
    node.next = first;
    first.pre = node;
    first = node;
    first.pre = null;
    }
    /
    *
  • 移除最后一个节点
    */
    private void removeLast() {
    if (last != null) {
    last = last.pre;
    if (last == null) {
    first = null;
    } else {
    last.next = null;
    }
    }
    }
    @Override
    public String toString() {
    StringBuilder sb = new StringBuilder();
    Node node = first;
    while (node != null) {
    sb.append(String.format("%s:%s ", node.key, node.value));
    node = node.next;
    }
    return sb.toString();
    }
    public static void main(String[] args) {
    LRU lru = new LRU(5);
    lru.put(1, “a”);
    lru.put(2, “b”);
    lru.put(3, “c”);
    lru.put(4,“d”);
    lru.put(5,“e”);
    System.out.println(“原始链表为:”+lru.toString());
    lru.get(4);
    System.out.println(“获取key为4的元素之后的链表:”+lru.toString());
    lru.put(6,“f”);
    System.out.println(“新添加一个key为6之后的链表:”+lru.toString());
    lru.remove(3);
    System.out.println(“移除key=3的之后的链表:”+lru.toString());
    }
    }

75. 如何停⽌线程运⾏?

可以设置⼀个标志位,任务定期检查这个标记,如果标志设置为取消则任务停⽌执⾏,但已执⾏部分⽆法停
⽌,标志变量最好设置为volatile

76. 简述普通线程与守护线程的区别 ?

java 中的线程分为两种:守护线程(Daemon)和用户线程(User)
任何线程都可以设置为守护线程和用户线程,通过方法 Thread.setDaemon(boolon);true 则把该线程设置为守护线程,反之则为用户线程。Thread.setDaemon()必须在 Thread.start()之前调用,否则运行时会抛出异常。
两者的区别:
唯一的区别是判断虚拟机 (JVM)何时离开,Daemon 是为其他线程提供服务,如果全部的 User Thread 已经撤离,Daemon 没有可服务的线程, JVM 撤离。也可以理解为守护线程是 JVM 自动创建的线程(但不一定),用户线程是程序创建的线程;比如 JVM 的垃圾回收线程是一个守护线程,当所有线程已经撤离,不再产生垃圾,守护线程自然就没事可干了,当垃圾回收线程是 Java 虚拟机上仅剩的线程时, Java 虚拟机会自动离开。
扩展:Thread Dump 打印出来的线程信息,含有 daemon 字样的线程即为守护进程,可能会有:服务守护进程、编译守护进程、windows 下的监听 Ctrl+break的守护进程、 Finalizer 守护进程、引用处理守护进程、 GC 守护进程。

77. 简述什么是锁顺序死锁?

两个线程试图以不同的顺序获得相同的锁,那么可能发发⽣死锁。⽐如转账问题,由from账户向to账户转账,
假设每次我们先同步from对象,再同步to账户,然后执⾏转账操作,貌似没什么问题。如果这时候to账户同时向from账户转账,那
么两个线程可能要永久等待了。

78. 死锁与活锁的区别,死锁与饥饿的区别?

死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
产生死锁的必要条件:
1、互斥条件:所谓互斥就是进程在某一时间内独占资源。
2、请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
3、不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
4、循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
活锁:任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。
活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。
饥饿:一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。
Java 中导致饥饿的原因:
1、高优先级线程吞噬所有的低优先级线程的 CPU 时间。
2、线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。
3、线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方法),因为其他线程总是被持续地获得唤醒。

79. 如何降低锁的竞争?

减少锁的持有时间,减少锁的请求频率,使⽤带有协调机制的独占锁。具体实现可以缩⼩锁的范围,快进快出。
⽐如只锁同步操作代码块,不要把相关⾮同步业务逻辑也包含到同步代码块中。可以减⼩锁的粒度,能对⽬标对象进⾏上锁,就不要
对操作⽬标对象的⽅法上锁,也可以使⽤⼀些锁分段技术的组件,⽐如ConcurrentHashMap。也可以使⽤⼀些⾮独占锁,⽐如
ReadWriteLock。

80. 请列举Java中常见的同步机制?

java主要同步机制是synchronized关键字, 还有显式的Lock,volatile,atomic,还有⼀些同步集合、阻塞队列等。

81. 共享变量在多线程下如何保证线程安全?

因为多线程是交替执⾏,每个线程操作共享变量时可能会导致数据不⼀致,要确保线程
安全,需要在访问共享变量时添加同步机制。当然,如果这个变量本⾝是线程安全的,⽐如AtomicLong,那么多线程访问也是安全

82. Java中 是否共享变量都使⽤类似AtomicLong原⼦安全类,多线程访问就是安全的?

这个不确定,因为⽆法保证多个变量同时操作,⼀个原⼦变量可以保证⾃⼰的安全性,但是同时操作多个有逻辑依赖原⼦的变量,仍可能带来线程安全问题。单个安全不代表组合也安
全。

83. 解释Final修饰的不可变对象?

由关键字final修饰的对象是不可变的,不能被重新赋值,但是final仍可以修饰可变对象的引⽤,例如集合:final修饰的集合本⾝引⽤地址不能改变,但是集合内的数据还是可以修改的。不可变对象会减少加锁或保护性副本的需求,可以带来⼀些性能上的优势。

84. 列举Java常见的并发容器?

Java并发容器是一类可以在多线程环境下安全使用的数据结构。Java并发容器提供了多线程环境下的安全操作,而不需要开发者进行额外的同步控制。以下是几种常用的Java并发容器:

1、ConcurrentHashMap
ConcurrentHashMap是一种线程安全的Map实现,支持高并发的读写操作。ConcurrentHashMap内部采用分段锁技术,将数据划分为若干个Segment,每个Segment内部都是一个HashMap实例,任何时候只有一个Segment被修改,其它Segment可以被并发访问,从而提高了Map的并发性能。

2、CopyOnWriteArrayList
CopyOnWriteArrayList是一种线程安全的List实现,内部采用读写分离的策略,即读取数据时不需要进行同步控制,而写入数据时则进行复制,保证读写之间互不干扰。这种策略可以极大地提高List的读取性能,适用于数据读取远远大于写入的场景。

3、ConcurrentLinkedQueue
ConcurrentLinkedQueue是一种线程安全的队列实现,内部采用非阻塞算法,通过CAS(Compare and Swap)操作实现线程安全,适用于高并发的生产者-消费者模型。ConcurrentLinkedQueue的性能优于BlockingQueue,尤其是在生产者比消费者多的场景。

4、BlockingQueue
BlockingQueue是一种阻塞队列,支持阻塞式的入队和出队操作。当队列为空时,消费者线程会被阻塞,直到有数据被加入到队列中;当队列已满时,生产者线程会被阻塞,直到有空闲位置可以存储数据。BlockingQueue是实现生产者-消费者模型的一个重要工具。常用的BlockingQueue有ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue等。

5、Phaser
Phaser是一种并发辅助器,提供了在多个线程之间同步计算的机制。Phaser可以将多个线程分为多个阶段,并在每个阶段进行等待,直到所有线程都完成当前阶段的计算任务后才进入下一阶段。Phaser可以替代CountDownLatch和CyclicBarrier等机制。

综上所述,Java并发容器是Java中实现多线程编程的重要工具,在并发编程中发挥着重要的作用。 Java并发容器通过提供高效、安全的线程安全容器,帮助开发者处理多线程并发问题,保证程序的正确和稳定。开发者可以选择不同的Java并发容器来满足不同的业务需求。

85. 简述多线程常见的同步⼯具类?

CountDownLatch:递减计数器闭锁,直到达到某个条件时才放⾏,多线程可以调⽤await⽅法⼀直阻塞,
直到计数器递减为零。⽐如我们连接zookeeper,由于连接操作是异步的,所以可以使⽤countDownLatch创建⼀个计数器为1的
锁,连接挂起,当异步连接成功时,调⽤countDown通知挂起线程;再⽐如5V5游戏竞技,只有房间⼈满了才可以开始游戏。
FutureTask:带有计算结果的任务,在计算完成时才能获取结果,如果计算尚未完成,则阻塞 get
⽅法。FutureTask将计算结果从执⾏线程传递到获取这个结果的线程。
Semaphore:信号量,⽤来控制同时访问某个特定资源的数量,只有获取到许可acquire,才能够正常执⾏,并在完成后释放许
可,acquire会⼀致阻塞到有许可或中断超时。使⽤信号量可以轻松实现⼀个阻塞队列。
CyclicBarrier:类似于闭锁,它可以阻塞⼀组线程,只有所有线程全部到达以后,才能够继续执⾏,so线程必须相互等待。这在并⾏
计算中是很有⽤的,将⼀个问题拆分为多个独⽴的⼦问题,当线程到达栅栏时,调⽤await等待,⼀直阻塞到所有参与线程全部到达,
再执⾏下⼀步任务。

86. 请列举ThreadPoolexecutor参数配置?

corePoolSize- 池中所保存的线程数,包括空闲线程。 maximumPoolSize - 池中允许的最⼤线程数。 keepAliveTime当线程数⼤于核⼼时,此为终⽌前多余的空闲线程等待新任务的最长时间。 unit - keepAliveTime 参数的时间单位。
workQueue - 执⾏前⽤于保持任务的队列。此队列仅保持由 execute ⽅法提交的 Runnable 任务。
threadFactory- 执⾏程序创建新线程时使⽤的⼯⼚。 handler -由于超出线程范围和队列容量⽽使执⾏被阻塞时所使⽤的处理程序。
如果池中运⾏的线程⼩于coreSize,那么新的请求将创建新的线程,如果coreSize线程全部忙碌,新请求将被添加到队列,如果队列
已满,那么将继续创建新线程,但线程总数<=maximumPoolSize,多余coreSize的线程会在超过keepAliveTime终⽌。

87. 简述线程池任务饱和时处理策略?

当线程池线程数已经达到最⼤值,注意这时队列⼀定已经满了,线程池已经处于饱和状态,那么新来的请求将会按照相对应的策略被处理。
AbortPolicy:拒绝任务,抛出RejectedExecutionException CallerRunsPolicy:在
execute ⽅法的调⽤线程中运⾏被拒绝的任务;如果执⾏程序已关闭,则会丢弃该任务。 DiscardPolicy:直接丢弃

88. 简述什么是Executor ?

Executor执⾏已提交的Runnable任务,把任务和执⾏机制分离开来,可以看作是⼀个任务执⾏框架,任务与执⾏的解耦,可以更灵活的指定执⾏⽅式。所以应该使⽤Executor代替new Thread(Runnable).start()。

89. 列举Executors可以创建哪些类型的线程池?

newFixedThreadPool:创建⼀个固定⼤⼩的线程池,每当提交⼀个任务就创建⼀个线程,直到达到最⼤数量,达到最⼤线程数后线程规模不再变化。
newCachedThreadPool:创建⼀个可以根据需要创建新线程的线程池,当线程规模⼤于处理请求时,将回收空闲线程,当需求增加
时,可以添加新的线程。
newSingleThreadExecutor:创建⼀个单线程Executor。
newScheduledThreadPool:创建⼀个固定长度,以延迟或定时的执⾏⽅式执⾏任务。

90. 简述列举Executor的⽣命周期?

1.线程池有运行、关闭、停止、结束四种状态,结束后就会释放所有资源
2.平缓关闭线程池使用shutdown()
3.立即关闭线程池使用shutdownNow(),同时得到未执行的任务列表
4.检测线程池是否正处于关闭中,使用isShutdown()
5.检测线程池是否已经关闭使用isTerminated()
6.定时或者永久等待线程池关闭结束使用awaitTermination()操作

91. 简述java中有几种方法可以实现一个线程?用什么关键字修饰同步方法? stop()和suspend()方法为何不推荐使用?

有两种实现方法,分别是继承Thread类与实现Runnable接口
用synchronized关键字修饰同步方法
反对使用stop(),是因为它不安全。它会解除由线程获取的所有锁定,而且如果对象处于一种不连贯状态,那么其他线程能在那种状态下检查和修改它们。结果很难检查出真正的问题所在。suspend()方法容易发生死锁。调用suspend()的时候,目标线程会停下来,但却仍然持有在这之前获得的锁定。此时,其他任何线程都不能访问锁定的资源,除非被"挂起"的线程恢复运行。对任何线程来说,如果它们想恢复目标线程,同时又试图使用任何一个锁定的资源,就会造成死锁。所以不应该使用suspend(),而应在自己的Thread类中置入一个标志,指出线程应该活动还是挂起。若标志指出线程应该挂起,便用wait()命其进入等待状态。若标志指出线程应当恢复,则用一个notify()重新启动线程。

92. 当一个线程进入一个对象的一个synchronized方法后,其它线程是否可进入此对象的其它方法?

不能,一个对象的一个synchronized方法只能由一个线程访问。

93. 简述请说出你所知道的线程同步的方法 ?

wait():使一个线程处于等待状态,并且释放所持有的对象的lock。
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException异常。
notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。
Allnotity():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。

94. 简述synchronized和java.util.concurrent.locks.Lock的异同 ?

主要相同点:Lock能完成synchronized所实现的所有功能
主要不同点:Lock有比synchronized更精确的线程语义和更好的性能。synchronized会自动释放锁,而Lock一定要求程序员手工释放,并且必须在finally从句中释放

95. 简述如何停止一个正在运行的线程 ?

1、使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
2、使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作
废的方法。
3、使用interrupt方法中断线程。
class MyThread extends Thread {
volatile boolean stop = false;
public void run() {
while (!stop) {
System.out.println(getName() + " is running");
try {
sleep(1000);
} catch (InterruptedException e) {
System.out.println(“week up from blcok…”);
stop = true; // 在异常处理代码中修改共享变量的状态
}
}
System.out.println(getName() + " is exiting…");
}
}
class InterruptThreadDemo3 {
public static void main(String[] args) throws InterruptedException {
MyThread m1 = new MyThread();
System.out.println(“Starting thread…”);
m1.start();
Thread.sleep(3000);
System.out.println("Interrupt thread…: " + m1.getName());
m1.stop = true; // 设置共享变量为true
m1.interrupt(); // 阻塞时退出阻塞状态
Thread.sleep(3000); // 主线程休眠3秒以便观察线程m1的中断情况
System.out.println(“Stopping application…”);
}
}

96. 简述notify()和notifyAll()有什么区别 ?

notify可能会导致死锁,而notifyAll则不会任何时候只有一个线程可以获得锁,也就是说只有一个线程可以运行synchronized 中的代码
使用notifyall,可以唤醒 所有处于wait状态的线程,使其重新进入锁的争夺队列中,而notify只能唤醒一个。
wait() 应配合while循环使用,不应使用if,务必在wait()调用前后都检查条件,如果不满足,必须调用notify()唤醒另外的线程来处理,自己继续wait()直至条件满足再往下执行。
notify() 是对notifyAll()的一个优化,但它有很精确的应用场景,并且要求正确使用。不然可能导致死锁。正确的场景应该是 WaitSet中等待的是相同的条件,唤醒任一个都能正确处理接下来的事项,如果唤醒的线程无法正确处理,务必确保继续notify()下一个线程,并且自身需要重新回到WaitSet中.

97. 简述Java中interrupted 和 isInterruptedd方法的区别? ?

interrupted() 和 isInterrupted()的主要区别是前者会将中断状态清除而后者不会。Java多线程的中断机制是用内部标识来实现的,调用Thread.interrupt()来中断一个线程就会设置中断标识为true。
当中断线程调用静态方法Thread.interrupted()来检查中断状态时,中断状态会被清零。而非静态方法isInterrupted()用来查询其它线程的中断状态且不会改变中断状态标识。简单的说就是任何抛出InterruptedException异常的方法都会将中断状态清零。无论如何,一个线程的中断状态有有可能
被其它线程调用中断来改变

98. 简述有三个线程T1,T2,T3,如何保证顺序执行 ?

在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的join()方法在一个线程中启动另一
个线程,另外一个线程完成该线程继续执行。为了确保三个线程的顺序你应该先启动最后一个(T3调
用T2,T2调用T1),这样T1就会先完成而T3最后完成。
实际上先启动三个线程中哪一个都行, 因为在每个线程的run方法中用join方法限定了三个线程的
执行顺序。
public class JoinTest2 {

// 1.现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行
public static void main(String[] args) {
final Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(“t1”);
}
});
final Thread t2 = new Thread(new Runnable() {

@Override
public void run() {
try {
// 引用t1线程,等待t1线程执行完
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(“t2”);
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
try {
// 引用t2线程,等待t2线程执行完
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(“t3”);
}
});
t3.start();//这里三个线程的启动顺序可以任意,大家可以试下!
t2.start();
t1.start();
}
}

99. 简述SynchronizedMap和ConcurrentHashMap有什么区别 ?

SynchronizedMap()和Hashtable一样,实现上在调用map所有方法时,都对整个map进行同步。
而ConcurrentHashMap的实现却更加精细,它对map中的所有桶加了锁。所以,只要有一个线程访问map,其他线程就无法进入map,而如果一个线程在访问ConcurrentHashMap某个桶时,其他线程,仍然可以对map执行某些操作。
所以,ConcurrentHashMap在性能以及安全性方面,明显比Collections.synchronizedMap()更加有优势。同时,同步操作精确控制到桶,这样,即使在遍历map时,如果其他线程试图对map进行数据修改,也不会抛出ConcurrentModificationException。

100. 简述什么是线程安全 ?

线程安全就是说多线程访问同一段代码,不会产生不确定的结果。
又是一个理论的问题,各式各样的答案有很多,我给出一个个人认为解释地最好的:如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。
这个问题有值得一提的地方,就是线程安全也是有几个级别的:
(1)不可变
像String、Integer、Long这些,都是final类型的类,任何一个线程都改变不了它们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用
(2)绝对线程安全
不管运行时环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代价,Java中标注自己是线程安全的类,实际上绝大多数都不是线程安全的,不过绝对线程安全的类,Java中也有,比方说CopyOnWriteArrayList、CopyOnWriteArraySet
(3)相对线程安全
相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种,add、remove方法都是原子操作,不会被打断,但也仅限于此,如果有个线程在遍历某个Vector、有个线程同时在add这个Vector,99%的情况下都会出现ConcurrentModificationException,也就是fail-fast机制。
(4)线程非安全
这个就没什么好说的了,ArrayList、LinkedList、HashMap等都是线程非安全的类

101. 简述Java体系中锁的优化机制 ?

从JDK1.6版本之后,synchronized本身也在不断优化锁的机制,有些情况下他并不会是一个很重量级的锁了。优化机制包括自适应锁、自旋锁、锁消除、锁粗化、轻量级锁和偏向锁。
锁的状态从低到高依次为无锁->偏向锁->轻量级锁->重量级锁,升级的过程就是从低到高,降级在一定条件也是有可能发生的。自旋锁:由于大部分时候,锁被占用的时间很短,共享变量的锁定时间也很短,所有没有必要挂起线程,用户态和内核态的来回上下文切换严重影响性能。自旋的概念就是让线程执行一个忙循环,
可以理解为就是啥也不干,防止从用户态转入内核态,自旋锁可以通过设置-XX:+UseSpining来开启,自旋的默认次数是10次,可以使用-XX:PreBlockSpin设置。
自适应锁:自适应锁就是自适应的自旋锁,自旋的时间不是固定时间,而是由前一次在同一个锁上的自旋时间和锁的持有者状态来决定。
锁消除:锁消除指的是JVM检测到一些同步的代码块,完全不存在数据竞争的场景,也就是不需要加锁,就会进行锁消除。
锁粗化:锁粗化指的是有很多操作都是对同一个对象进行加锁,就会把锁的同步范围扩展到整个操作序列之外。

偏向锁:当线程访问同步块获取锁时,会在对象头和栈帧中的锁记录里存储偏向锁的线程ID,之后这个线程再次进入同步块时都不需要CAS来加锁和解锁了,偏向锁会永远偏向第一个获得锁的线程,如果后续没有其他线程获得过这个锁,持有锁的线程就永远不需要进行同步,反之,当有其他
线程竞争偏向锁时,持有偏向锁的线程就会释放偏向锁。可以用过设置-XX:+UseBiasedLocking开启偏向锁。
轻量级锁:JVM的对象的对象头中包含有一些锁的标志位,代码进入同步块的时候,JVM将会使用CAS方式来尝试获取锁,如果更新成功则会把对象头中的状态位标记为轻量级锁,如果更新失败,当前线程就尝试自旋来获得锁。
整个锁升级的过程非常复杂,我尽力去除一些无用的环节,简单来描述整个升级的机制。
简单点说,偏向锁就是通过对象头的偏向线程ID来对比,甚至都不需要CAS了,而轻量级锁主要就是通过CAS修改对象头锁记录和自旋来实现,重量级锁则是除了拥有锁的线程其他全部阻塞。

102. 简述Java线程池核心线程数怎么设置呢 ?

分为CPU密集型和IO密集型
CPU这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
IO密集型
这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 : 核心线程数=CPU核心数量*2。

103. 简述Java线程池中队列常用类型有哪些 ?

ArrayBlockingQueue 是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
LinkedBlockingQueue 一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于 ArrayBlockingQueue 。
SynchronousQueue 一个不存储元素的阻塞队列。
PriorityBlockingQueue 一个具有优先级的无限阻塞队列。 PriorityBlockingQueue 也是基于最小二叉堆实现
DelayQueue只有当其指定的延迟时间到了,才能够从队列中获取到该元素。
DelayQueue 是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费
者)才会被阻塞。
这里能说出前三种也就差不多了,如果能说全那是最好

104. 简述线程安全需要保证几个基本特征? ?

原子性,简单说就是相关操作不会中途被其他线程干扰,一般通过同步机制实现。
可见性,是一个线程修改了某个共享变量,其状态能够立即被其他线程知晓,通常被解释为将线程本地状态反映到主内存上,volatile 就是负责保证可见性的。
有序性,是保证线程内串行语义,避免指令重排等。

105. 简述线程池原理以及核心参数 ?

首先线程池有几个核心的参数概念:

  1. 最大线程数maximumPoolSize
  2. 核心线程数corePoolSize
  3. 活跃时间keepAliveTime
  4. 阻塞队列workQueue
  5. 拒绝策略RejectedExecutionHandler
    当提交一个新任务到线程池时,具体的执行流程如下:
  6. 当我们提交任务,线程池会根据corePoolSize大小创建若干任务数量线程执行任务
  7. 当任务的数量超过corePoolSize数量,后续的任务将会进入阻塞队列阻塞排队
  8. 当阻塞队列也满了之后,那么将会继续创建(maximumPoolSize-corePoolSize)个数量的线程来执行任务,如果任务处理完成,maximumPoolSize-corePoolSize额外创建的线程等待keepAliveTime之后被自动销毁
  9. 如果达到maximumPoolSize,阻塞队列还是满的状态,那么将根据不同的拒绝策略对应处理

106. 简述什么是AQS ?

简单说一下AQS,AQS全称为AbstractQueuedSychronizer,翻译过来应该是抽象队列同步器。
如果说java.util.concurrent的基础是CAS的话,那么AQS就是整个Java并发包的核心了,ReentrantLock、CountDownLatch、Semaphore等等都用到了它。AQS实际上以双向队列的形式连接所有的Entry,比方说ReentrantLock,所有等待的线程都被放在一个Entry中并连成双向队列,前面一个线程使用ReentrantLock好了,则双向队列实际上的第一个Entry开始运行。AQS定义了对双向队列所有的操作,而只开放了tryLock和tryRelease方法给开发者使用,开发者可以根据自己的实现重写tryLock和tryRelease方法,以实现自己的并发功能。

107. 简述什么是Semaphore ?

Semaphore就是一个信号量,它的作用是限制某段代码块的并发数。Semaphore有一个构造函数,可以传入一个int型整数n,表示某段代码最多只有n个线程可以访问,如果超出了n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果Semaphore构造函数中传入的int型整数n=1,相当于变成了一个synchronized了。

108. 简述什么是Callable和Future ?

Callable接口类似于Runnable,从名字就可以看出来了,但是Runnable不会返回结果,并且无法抛出返回结果的异常,而Callable功能更强大一些,被线程执行后,可以返回值,这个返回值可以被Future拿到,也就是说,Future可以拿到异步执行任务的返回值。可以认为是带有回调的
Runnable。
Future接口表示异步任务,是还没有完成的任务给出的未来结果。所以说Callable用于产生结果,Future用于获取结果。

109. 简述Java并行和并发有什么区别 ?

1、** 并发:多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行。
2、** 并行:单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的“同时进行”。
3、** 串行:有n个任务,由一个线程按顺序执行。由于任务、方法都在一个线程执行所以不存在线程不安全情况,也就不存在临界区的问题。
做一个形象的比喻:**
1、** 并发 = 俩个人用一台电脑。
2、** 并行 = 俩个人分配了俩台电脑。
3、** 串行 = 俩个人排队使用一台电脑。

110. 简述什么是线程组,为什么在 Java 中不推荐使用 ?

ThreadGroup 类,可以把线程归属到某一个线程组中,线程组中可以有线程对象,也可以有线程组,组中还可以有线程,这样的组织结构有点类似于树的形式。
为什么不推荐使用?因为使用有很多的安全隐患吧,没有具体追究,如果需要使用,推荐使用线程池。

111. 简述在 Java 中 Executor 和 Executors 的区别 ?

Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。
Executor 接口对象能执行我们的线程任务。
ExecutorService 接口继承了 Executor 接口并进行了扩展,提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值。
使用 ThreadPoolExecutor 可以创建自定义线程池Future 表示异步计算的结果,他提供了检查计算是否完成的方法,以等待计算的
完成,并可以使用 get()方法获取计算的结果。

112. 简述什么是原子操作?在 Java Concurrency API 中有哪些原 子类(atomic classes) ?

原子操作(atomic operation)意为”不可被中断的一个或一系列操作” 。
处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。
在 Java 中可以通过锁和循环 CAS 的方式来实现原子操作。 CAS 操作——Compare & Set,或是 Compare & Swap,现在几乎所有的 CPU 指令都支持 CAS的原子操作。
原子操作是指一个不受其他操作影响的操作任务单元。原子操作是在多线程环境下避免数据不一致必须的手段。
int++并不是一个原子操作,所以当一个线程读取它的值并加 1 时,另外一个线程有可能会读到之前的值,这就会引发错误。
为了解决这个问题,必须保证增加操作是原子的,在 JDK1.5 之前我们可以使用同步技术来做到这一点。到 JDK1.5,java.util.concurrent.atomic 包提供了 int 和long 类型的原子包装类,它们可以自动的保证对于他们的操作是原子的并且不需要使用同步
java.util.concurrent 这个包里面提供了一组原子类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由 JVM 从等待队列中选择一个另一个线程进入,这只是一种逻辑上的理解。
原子类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference原子数组:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray原子属性更新器:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
解决 ABA 问题的原子类:AtomicMarkableReference(通过引入一个 boolean来反映中间有没有变过),AtomicStampedReference(通过引入一个 int 来累加来反映中间有没有变过)

113. 简述什么是 Executors 框架 ?

Executor 框架是一个根据一组执行策略调用,调度,执行和控制的异步任务的框架。
无限制的创建线程会引起应用程序内存溢出。所以创建一个线程池是个更好的的解决方案,因为可以限制线程的数量并且可以回收再利用这些线程。利用Executors 框架可以非常方便的创建一个线程池

114. 简述什么是 FutureTask?使用 ExecutorService 启动任务 ?

在 Java 并发程序中 FutureTask 表示一个可以取消的异步运算。它有启动和取消运算、查询运算是否完成和取回运算结果等方法。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调用了 Callable 和 Runnable 的对象进行包装,由于 FutureTask 也是调用了 Runnable接口所以它可以提交给 Executor 来执行

115. 简述什么是并发容器的实现 ?

何为同步容器:可以简单地理解为通过 synchronized 来实现同步的容器,如果有多个线程调用同步容器的方法,它们将会串行执行。比如 Vector,Hashtable,以及 Collections.synchronizedSet,synchronizedList 等方法返回的容器。
可以通过查看 Vector,Hashtable 等这些同步容器的实现代码,可以看到这些容器实现线程安全的方式就是将它们的状态封装起来,并在需要同步的方法上加上关键字 synchronized。
并发容器使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性,例如在 ConcurrentHashMap 中采用了一种粒度更细的加锁机制,可以称为分段锁,在这种锁机制下,允许任意数量的读线程并发地访问 map,并且执行读操作的线程和写操作的线程也可以并发的访问 map,同时允许一定数量的写操作线程并发地修改 map,所以它可以在并发环境下实现更高的吞吐量。

116. 简述多线程同步和互斥有几种实现方法,都是什么 ?

线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。
线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。
线程间的同步方法大体可分为两类:用户模式和内核模式。顾名思义,内核模式就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态,而用户模式就是不需要切换到内核态,只在用户态完成操作。
用户模式下的方法有:原子操作(例如一个单一的全局变量),临界区。内核模式下的方法有:事件,信号量,互斥量

117. 简述什么是竞争条件?你怎样发现和解决竞争 ?

当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,则我们认为这发生了竞争条件(race condition)。

118. 简述为什么使用 Executor 框架比使用应用创建和管理线程好 ?

为什么要使用 Executor 线程池框架
1、每次执行任务创建线程 new Thread()比较消耗性能,创建一个线程是比较耗时、耗资源的。
2、调用 new Thread()创建的线程缺乏管理,被称为野线程,而且可以无限制的创建,线程之间的相互竞争会导致过多占用系统资源而导致系统瘫痪,还有线程之间的频繁交替也会消耗很多系统资源。
3、直接使用 new Thread() 启动的线程不利于扩展,比如定时执行、定期执行、定时定期执行、线程中断等都不便实现。

119. 简述使用 Executor 线程池框架的优点 ?

1、能复用已存在并空闲的线程从而减少线程对象的创建从而减少了消亡线程的开销。
2、可有效控制最大并发线程数,提高系统资源使用率,同时避免过多资源竞争。
3、框架中已经有定时、定期、单线程、并发数控制等功能。
综上所述使用线程池框架 Executor 能更好的管理线程、提供系统资源使用率。

120. 简述什么是可重入锁(ReentrantLock) ?

举例来说明锁的可重入性
public class UnReentrant{
Lock lock = new Lock();
public void outer(){
第 179 页 共 485 页
lock.lock();
inner();
lock.unlock();
}
public void inner(){
lock.lock();
//do something
lock.unlock();
}
}
outer 中调用了 inner,outer 先锁住了 lock,这样 inner 就不能再获取 lock。其实调用 outer 的线程已经获取了 lock 锁,但是不能在 inner 中重复利用已经获取的锁资源,这种锁即称之为 不可重入可重入就意味着:线程可以进入任何一个它已经拥有的锁所同步着的代码块。
synchronized、ReentrantLock 都是可重入的锁,可重入锁相对来说简化了并发编程的开发。

121. 简述当一个线程进入某个对象的一个 synchronized 的实例方 法后,其它线程是否可进入此对象的其它方法 ?

如果其他方法没有 synchronized 的话,其他线程是可以进入的。
所以要开放一个线程安全的对象时,得保证每个方法都是线程安全的

122. 简述乐观锁和悲观锁的理解及如何实现,有哪些实现方式?

悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如 Java 里面的同步原语 synchronized 关键字的实现也是悲观锁。
乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制,其实都是提供的乐观锁。在 Java中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。

乐观锁的实现方式:
1、使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。
2、java 中的 Compare and Swap 即 CAS ,当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。 CAS 操作中包含三个操作数 —— 需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置 V 的值与预期原值 A 相匹配,那么处理器会自动将该位置值更新为新值 B。否则处理器不做任何操作。
CAS 缺点:
1、ABA 问题:
比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但可能存在潜藏的问题。从 Java1.5 开始 JDK 的 atomic包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。
2、循环时间长开销大:
对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized。
3、只能保证一个共享变量的原子操作:
当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。

123. 简述CopyOnWriteArrayList 可以用于什么应用场景 ?

CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出 ConcurrentModificationException。在
CopyOnWriteArrayList 中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。
1、由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致 young gc 或者 full gc;
2、不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个 set操作后,读取到数据可能还是旧的,虽然 CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求;
CopyOnWriteArrayList 透露的思想
1、读写分离,读和写分开
2、最终一致性
3、使用另外开辟空间的思路,来解决并发冲突

124. 简述如何在两个线程间共享数据?

在两个线程间共享变量即可实现共享。
一般来说,共享变量要求变量本身是线程安全的,然后在线程内使用的时候,如果有对共享变量的复合操作,那么也得保证复合操作的线程安全性

125. 简述为什么 wait 和 notify 方法要在同步块中调用?

Java API 强制要求这样做,如果你不这么做,你的代码会抛出
IllegalMonitorStateException 异常。还有一个原因是为了避免 wait 和 notify之间产生竞态条件

126. 简述为什么你应该在循环中检查等待条件?

处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。

127. 简述Java 中的同步集合与并发集合有什么区别 ?

同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更高。在 Java1.5 之前程序员们只有同步集合来用且在多线程并发的时候会导致争用,阻碍了系统的扩展性。Java5 介绍了并发集合像ConcurrentHashMap,不仅提供线程安全还用锁分离和内部分区等现代技术提高了可扩展性。

128. 简述怎么检测一个线程是否拥有锁? ?

在 java.lang.Thread 中有一个方法叫 holdsLock(),它返回 true 如果当且仅当当前线程拥有某个具体对象的锁

129. 简述你如何在 Java 中获取线程堆栈 ?

kill -3 [java pid]
不会在当前终端输出,它会输出到代码执行的或指定的地方去。比如,kill -3
tomcat pid, 输出堆栈到 log 目录下。
Jstack [java pid]
这个比较简单,在当前终端显示,也可以重定向到指定文件中。
-JvisualVM:Thread Dump
不做说明,打开 JvisualVM 后,都是界面操作,过程还是很简单的。
55、JVM 中哪个参数是用来控制线程的栈堆栈小的?
-Xss 每个线程的栈大小

130. 简述Java 中 ConcurrentHashMap 的并发度是什么 ?

ConcurrentHashMap 把实际 map 划分成若干部分来实现它的可扩展性和线程安全。这种划分是使用并发度获得的,它是 ConcurrentHashMap 类构造函数的一个可选参数,默认值为 16,这样在多线程情况下就能避免争用。
在 JDK8 后,它摒弃了 Segment(锁段)的概念,而是启用了一种全新的方式实现,利用 CAS 算法。同时加入了更多的辅助变量来提高并发度,具体内容还是查看源码吧

131. 简述什么是阻塞式方法 ?

阻塞式方法是指程序会一直等待该方法完成期间不做其他事情,ServerSocket 的accept()方法就是一直等待客户端连接。这里的阻塞是指调用结果返回之前,当前线程会被挂起,直到得到结果之后才会返回。此外,还有异步和非阻塞式方法在任务完成前就返回

132. 简述volatile 变量和 atomic 变量有什么不同?

Volatile 变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。例如用 volatile 修饰 count 变量那么 count++ 操作就不是原子性的。
而 AtomicInteger 类提供的 atomic 方法可以让这种操作具有原子性如getAndIncrement()方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作

133. 简述可以直接调用 Thread 类的 run ()方法么 ?

当然可以。但是如果我们调用了 Thread 的 run()方法,它的行为就会和普通的方法一样,会在当前线程中执行。为了在新的线程中执行我们的代码,必须使用Thread.start()方法

134. 简述如何让正在运行的线程暂停一段时间 ?

我们可以使用 Thread 类的 Sleep()方法让线程暂停一段时间。需要注意的是,这并不会让线程终止,一旦从休眠中唤醒线程,线程的状态将会被改变为 Runnable,并且根据线程调度,它将得到执行

135. 简述你对线程优先级的理解是什么 ?

每一个线程都是有优先级的,一般来说,高优先级的线程在运行时会具有优先权,但这依赖于线程调度的实现,这个实现是和操作系统相关的(OS dependent)。我们可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。线程优先级是一个 int 变量(从 1-10),1 代表最低优先级,10 代表最高优先级。
java 的线程优先级调度会委托给操作系统去处理,所以与具体的操作系统优先级有关,如非特别需要,一般无需设置线程优先级

136. 简述什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing ) ?

线程调度器是一个操作系统服务,它负责为 Runnable 状态的线程分配 CPU 时间。
一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。
同上一个问题,线程调度并不受到 Java 虚拟机控制,所以由应用程序来控制它是更好的选择(也就是说不要让你的程序依赖于线程的优先级)。
时间分片是指将可用的 CPU 时间分配给可用的 Runnable 线程的过程。分配 CPU时间可以基于线程优先级或者线程等待的时间

137. 简述你如何确保 main()方法所在的线程是 Java 程序最后结束 的线程 ?

我们可以使用 Thread 类的 join()方法来确保所有程序创建的线程在 main()方法退出前结束

138. 简述为什么线程通信的方法 wait(), notify()和 notifyAll()被定 义在 Object 类里 ?

Java 的每个对象中都有一个锁(monitor,也可以成为监视器) 并且 wait(),notify()等方法用于等待对象的锁或者通知其他线程对象的监视器可用。在 Java 的线程中并没有可供任何对象使用的锁和同步器。这就是为什么这些方法是 Object 类的一部分,这样 Java 的每一个类都有用于线程间通信的基本方法

139. 简述为什么 wait(), notify()和 notifyAll ()必须在同步方法或 者同步块中被调用?

当一个线程需要调用对象的 wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的 notify()方法。同样的,当一个线程需要调用对象的 notify()方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在同步方法或者同步块中被调用。

140. 简述为什么 Thread 类的 sleep()和 yield ()方法是静态的 ?

Thread 类的 sleep()和 yield()方法将在当前正在执行的线程上运行。所以在其他处于等待状态的线程上调用这些方法是没有意义的。这就是为什么这些方法是静态的。它们可以在当前正在执行的线程中工作,并避免程序员错误的认为可以在其他非运行线程调用这些方法。

141. 简述同步方法和同步块,哪个是更好的选择 ?

同步块是更好的选择,因为它不会锁住整个对象(当然你也可以让它锁住整个对象)。同步方法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这通常会导致他们停止执行并需要等待获得这个对象上的锁。
同步块更要符合开放调用的原则,只在需要锁住的代码块锁住相应的对象,这样从侧面来说也可以避免死锁

142. 简述如何创建守护线程 ?

使用 Thread 类的 setDaemon(true)方法可以将线程设置为守护线程,需要注意的是,需要在调用 start()方法前调用这个方法,否则会抛出
IllegalThreadStateException 异常

143. 简述什么是 Java Timer 类?如何创建一个有特定时间间隔的任务 ?

java.util.Timer 是一个工具类,可以用于安排一个线程在未来的某个特定时间执行。Timer 类可以用安排一次性任务或者周期任务。
java.util.TimerTask 是一个实现了 Runnable 接口的抽象类,我们需要去继承这个类来创建我们自己的定时任务并使用 Timer 去安排它的执行

144. 简述实现可见性的方法有哪些 ?

synchronized 或者 Lock:保证同一个时刻只有一个线程获取锁执行代码,锁释放之前把最新的值刷新到主内存,实现可见性

145. 简述创建线程的三种方式的对比 ?

1、采用实现 Runnable、Callable 接口的方式创建多线程。
优势是:
线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类。
在这种方式下,多个线程可以共享同一个 target 对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将 CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
劣势是:
编程稍微复杂,如果要访问当前线程,则必须使用 Thread.currentThread()方法。
2、使用继承 Thread 类的方式创建多线程
优势是:
编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread()方法,
直接使用 this 即可获得当前线程。
劣势是:
线程类已经继承了 Thread 类,所以不能再继承其他父类。
3、Runnable 和 Callable 的区别
1、Callable 规定(重写)的方法是 call(),Runnable 规定(重写)的方法是 run()。
2、Callable 的任务执行后可返回值,而 Runnable 的任务是不能返回值的。
3、Call 方法可以抛出异常,run 方法不可以。
4、运行 Callable 任务可以拿到一个 Future 对象,表示异步计算的结果。它提供
了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过 Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

146. 简述AQS 支持两种同步方式 ?

1、独占式
2、共享式
这样方便使用者实现不同类型的同步组件,独占式如 ReentrantLock,共享式如Semaphore,CountDownLatch,组合式的如 ReentrantReadWriteLock。总之,AQS 为使用提供了底层支撑,如何组装实现,使用者可以自由发挥。

147. 简述ReadWriteLock 是什么 ?

首先明确一下,不是说 ReentrantLock 不好,只是 ReentrantLock 某些时候有局限。如果使用 ReentrantLock,可能本身是为了防止线程 A 在写数据、线程 B 在读数据造成的数据不一致,但这样,如果线程 C 在读数据、线程 D 也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。
因为这个,才诞生了读写锁 ReadWriteLock。ReadWriteLock 是一个读写锁接口,ReentrantReadWriteLock 是 ReadWriteLock 接口的一个具体实现,实现了读写
的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能

148. 简述Swing 是线程安全的 ?

不是,Swing 不是线程安全的。你不能通过任何线程来更新 Swing 组件,如JTable、JList 或 JPanel,事实上,它们只能通过 GUI 或 AWT 线程来更新。这就是为什么 Swing 提供 invokeAndWait() 和 invokeLater() 方法来获取其他线程的 GUI 更新请求。这些方法将更新请求放入 AWT 的线程队列中,可以一直等待,也可以通过异步更新直接返回结果。你也可以在参考答案中查看和学习到更详细的内容

149. 简述什么是BIO ?

BIO:同步并阻塞,服务器实现一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,没处理完之前此线程不能做其他操作(如果是单线程的情况下,我传输的文件很大呢?),当然可以通过线程池机制改善。BIO方式适用于连接数目比较小且固定的架构,这种方
式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解

150. 简述什么是NIO ?

NIO:同步非阻塞,服务器实现一个连接一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4之后开始支持

151. 简述什么是AIO ?

AIO:异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由操作系统先完成了再通知服务器应用去启动线程进行处理,AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用操作系统参与并发操作,编程比较复杂,JDK1.7之后开
始支持。.
AIO属于NIO包中的类实现,其实IO主要分为BIO和NIO,AIO只是附加品,解决IO不能异步的实现在以前很少有Linux系统支持AIO,Windows的IOCP就是该AIO模型。但是现在的服务器一般都是支持AIO操作

152. 简述五种IO模型 ?

1.阻塞BIO(blocking I/O)
A拿着一支鱼竿在河边钓鱼,并且一直在鱼竿前等,在等的时候不做其他的事情,十分专心。只有鱼上钩的时,才结束掉等的动作,把鱼钓上来。
在内核将数据准备好之前,系统调用会一直等待所有的套接字,默认的是阻塞方式。
2.非阻塞NIO(noblocking I/O)
B也在河边钓鱼,但是B不想将自己的所有时间都花费在钓鱼上,在等鱼上钩这个时间段中,B也在做其他的事情(一会看看书,一会读读报纸,一会又去看其他人的钓鱼等),但B在做这些事情的时候,每隔一个固定的时间检查鱼是否上钩。一旦检查到有鱼上钩,就停下手中的事情,把鱼钓上
来。 B在检查鱼竿是否有鱼,是一个轮询的过程。
3.异步AIO(asynchronous I/O)
C也想钓鱼,但C有事情,于是他雇来了D、E、F,让他们帮他等待鱼上钩,一旦有鱼上钩,就打电话给C,C就会将鱼钓上去。
4.信号驱动IO(signal blocking I/O)
G也在河边钓鱼,但与A、B、C不同的是,G比较聪明,他给鱼竿上挂一个铃铛,当有鱼上钩的时候,这个铃铛就会被碰响,G就会将鱼钓上来。
当应用程序请求数据时,内核一方面去取数据报内容返回,另一方面将程序控制权还给应用进程,应用进程继续处理其他事情,是一种非阻塞的状态。
5.IO多路转接(I/O multiplexing)
H同样也在河边钓鱼,但是H生活水平比较好,H拿了很多的鱼竿,一次性有很多鱼竿在等,H不断的查看每个鱼竿是否有鱼上钩。增加了效率,减少了等待的时间。IO多路转接是属于阻塞IO,但可以对多个文件描述符进行阻塞监听,所以效率较阻塞IO的高

;