Java 多线程八股文!
1.你将如何使用thread dump?你将如何分析Thread dump?
在UNIX中你可以使用kill -3,然后thread dump将会打印日志,在windows中你可以使用” CTRL+Break”。非常简单和专业的线程面试问题,但是如果他问你怎样分析它,就会很棘 手。
2.在Java中Lock接口比synchronized块的优势是什么?你需要实现一个高效的缓存, 它允许多个用户读,但只允许一个用户写,以此来保持它的完整性,你会怎样去实现 它?
lock接口在多线程和并发编程中最大的优势是它们为读和写分别提供了锁,它能满足你写像 ConcurrentHashMap这样的高性能数据结构和有条件的阻塞。Java线程面试的问题越来越会 根据面试者的回答来提问。我强烈建议在你去参加多线程的面试之前认真读一下Locks,因为 当前其大量用于构建电子交易终统的客户端缓存和交易连接空间。
3.高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务 怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?
1)高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文 的切换
2)并发不高、任务执行时间长的业务要区分开看:
a)假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU, 所以不要让所有的CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务
b)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1) 一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换
c)并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设 计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的 设置,设置参考其他有关线程池的文章。最后,业务执行时间长的问题,也可能需要分析一 下,看看能不能使用中间件对任务进行拆分和解耦。
4.同步方法和同步块,哪个是更好的选择?
同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。请 知道一条原则:同步的范围越小越好。
虽说同步的范围越少越好,但是在Java虚拟机中还是存在着一种叫做锁粗化的优化方法,这种 方法就是把同步范围变大。这是有用的,比方说StringBuffer,它是一个线程安全的类,自然 最常用的append()方法是一个同步方法,我们写代码的时候会反复append字符串,这意味着 要进行反复的加锁->解锁,这对性能不利,因为这意味着Java虚拟机在这条线程上要反复地在 内核态和用户态之间进行切换,因此Java虚拟机会将多次append方法调用的代码进行一个锁 粗化的操作,将多次的append的操作扩展到append方法的头尾,变成一个大的同步块,这样 就减少了加锁–>解锁的次数,有效地提升了代码执行的效率。
5.Hashtable的size()方法中明明只有一条语句”return count”,为什么还要做同步?
1)同一时间只能有一条线程执行固定类的同步方法,但是对于类的非同步方法,可以多条线程 同时访问。所以,这样就有问题了,可能线程A在执行Hashtable的put方法添加数据,线程B 则可以正常调用size()方法读取Hashtable中当前元素的个数,那读取到的值可能不是最新 的,可能线程A添加了完了数据,但是没有对size++,线程B就已经读取size了,那么对于线 程B来说读取到的size一定是不准确的。而给size()方法加了同步之后,意味着线程B调用 size()方法只有在线程A调用put方法完毕之后才可以调用,这样就保证了线程安全性
2)CPU执行代码,执行的不是Java代码,这点很关键,一定得记住。Java代码最终是被翻译 成机器码执行的,机器码才是真正可以和硬件电路交互的代码。即使你看到Java代码只有一 行,甚至你看到Java代码编译之后生成的字节码也只有一行,也不意味着对于底层来说这句语 句的操作只有一个。一句"return count"假设被翻译成了三句汇编语句执行,一句汇编语句和 其机器码做对应,完全可能执行完第一句,线程就切换了。
6.Semaphore有什么作用?
Semaphore就是一个信号量,它的作用是限制某段代码块的并发数。
Semaphore有一个构造 函数,可以传入一个int型整数n,表示某段代码最多只有n个线程可以访问,如果超出了n,那 么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。
由此可以看出如果 Semaphore构造函数中传入的int型整数n=1,相当于变成了一个synchronized了。
7.单例模式的线程安全性
1)饿汉式单例模式的写法:线程安全
2)懒汉式单例模式的写法:非线程安全
3)双检锁单例模式的写法:线程安全
8.Java中用到的线程调度算法是什么?
抢占式。一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个 总的优先级并分配下一个时间片给某个线程执行。
9.Java中如何获取到线程dump文件
死循环、死锁、阻塞、页面打开慢等问题,打线程dump是最好的解决问题的途径。所谓线程 dump也就是线程堆栈,获取到线程堆栈有两步:
1)获取到线程的pid,可以通过使用jps命令,在Linux环境下还可以使用ps -ef | grep java
2)打印线程堆栈,可以通过使用jstack pid命令,在Linux环境下还可以使用kill -3 pid
另外提一点,Thread类提供了一个getStackTrace()方法也可以用于获取线程堆栈。这是一个 实例方法,因此此方法是和具体线程实例绑定的,每次获取获取到的是具体某个线程当前运行 的堆栈。
10.什么是线程安全?
如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是 线程安全的。
这个问题有值得一提的地方,就是线程安全也是有几个级别的:
1)不可变
像String、Integer、Long这些,都是final类型的类,任何一个线程都改变不了它们的值,要 改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使 用
2)绝对线程安全
不管运行时环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外 的代价,Java中标注自己是线程安全的类,实际上绝大多数都不是线程安全的,不过绝对线程 安全的类,Java中也有,比方说CopyOnWriteArrayList、CopyOnWriteArraySet
3)相对线程安全
相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种,add、remove方法都 是原子操作,不会被打断,但也仅限于此,如果有个线程在遍历某个Vector、有个线程同时在 add这个Vector,99%的情况下都会出现ConcurrentModificationException,也就是failfast机制。
4)线程非安全
这个就没什么好说的了,ArrayList、LinkedList、HashMap等都是线程非安全的类,点击这 里了解为什么不安全。
11.线程池都有哪几种工作队列?
ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原 则对元素进行排序。
LinkedBlockingQueue:是一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量 通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了 这个队列。
SynchronousQueue:是一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用 移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于Linked-BlockingQueue,静 态工厂方法Executors.newCachedThreadPool使用了这个队列。
PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
12.说一说几种常见的线程池及适用场景?
FixedThreadPool:可重用固定线程数的线程池。(适用于负载比较重的服务器) FixedThreadPool使用无界队列LinkedBlockingQueue作为线程池的工作队列 该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即 执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列 中的任务。
SingleThreadExecutor:只会创建一个线程执行任务。(适用于需要保证顺序执行各个任 务;并且在任意时间点,没有多线程活动的场景。) SingleThreadExecutorl也使用无界队列LinkedBlockingQueue作为工作队列 若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先 出的顺序执行队列中的任务。
CachedThreadPool:是一个会根据需要调整线程数量的线程池。(大小无界,适用于执行很 多的短期异步任务的小程序,或负载较轻的服务器) CachedThreadPool使用没有容量的SynchronousQueue作为线程池的工作队列,但 CachedThreadPool的maximumPool是无界的。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线 程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕 后,将返回线程池进行复用。
ScheduledThreadPool:继承自ThreadPoolExecutor。它主要用来在给定的延迟之后运行 任务,或者定期执行任务。使用DelayQueue作为任务队列。
13.synchronized 关键字和 volatile 关键字的区别
volatile关键字是线程同步的轻量级实现,所以volatile性能比synchronized关键字要好。但 是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。
多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞。volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的 是多个线程之间访问资源的同步性。
volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都 能保证。
14.什么是线程的阻塞问题?如何解决?
阻塞是用来形容多线程的问题,几个线程之间共享临界区资源,那么当一个线程占用了临界区 资源后,所有需要使用该资源的线程都需要进入该临界区等待,等待会导致线程挂起,一直不 能工作,这种情况就是阻塞,如果某一线程一直都不释放资源,将会导致其他所有等待在这个 临界区的线程都不能工作。当我们使用synchronized或重入锁时,我们得到的就是阻塞线 程,如论是synchronized或者重入锁,都会在试图执行代码前,得到临界区的锁,如果得不 到锁,线程将会被挂起等待,知道其他线程执行完成并释放锁且拿到锁为止。
解决方法:可以通过减少锁持有时间,读写锁分离,减小锁的粒度,锁分离,锁粗化等方式来优化锁的性 能。
15.什么是线程的饥饿问题?如何解决?
饥饿指的是某一线程或多个线程因为某些原因一直获取不到资源,导致程序一直无法执行。如 某一线程优先级太低导致一直分配不到资源,或者是某一线程一直占着某种资源不放,导致该 线程无法执行等。
解决方法:与死锁相比,饥饿现象还是有可能在一段时间之后恢复执行的。可以设置合适的线程优先级来 尽量避免饥饿的产生。
16.什么是活锁?
活锁体现了一种谦让的美德,每个线程都想把资源让给对方,但是由于机器“智商”不够,可能 会产生一直将资源让来让去,导致资源在两个线程间跳动而无法使某一线程真正的到资源并执 行,这就是活锁的问题。
17.什么是线程安全问题?如何解决?
线程安全问题指的是在某一线程从开始访问到结束访问某一数据期间,该数据被其他的线程所 修改,那么对于当前线程而言,该线程就发生了线程安全问题,表现形式为数据的缺失,数据 不一致等。
线程安全问题发生的条件:
1)多线程环境下,即存在包括自己在内存在有多个线程。
2)多线程环境下存在共享资源,且多线程操作该共享资源。
3)多个线程必须对该共享资源有非原子性操作。
线程安全问题的解决思路:
1)尽量不使用共享变量,将不必要的共享变量变成局部变量来使用。
2)使用synchronized关键字同步代码块,或者使用jdk包中提供的Lock为操作进行加锁。
3)使用ThreadLocal为每一个线程建立一个变量的副本,各个线程间独立操作,互不影响。
18.为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
new 一个 Thread,线程进入初始状态;调用 start()方法,会启动一个线程并使线程进入了就 绪状态,当分配到时间片后就可以开始运行了。start() 会执行线程的相应准备工作,然后自 动执行 run() 方法的内容,这是真正的多线程工作。而直接执行 run() 方法,会把 run 方法 当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程 工作。
总结:调用 start 方法可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普 通方法调用,还是在主线程里执行。
19.什么是线程死锁?如何避免死锁?
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地 阻塞,因此程序不可能正常终止。
假如线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程 就会互相等待而进入死锁状态。
避免死锁的几个常见方法:避免一个线程同时获取多个锁 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
尝试使用定时锁,使用 lock.tryLock(timeout) 来代替使用内部锁机制。
对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
20.并发与并行的区别?
并发指的是多个任务交替进行,并行则是指真正意义上的“同时进行”。
实际上,如果系统内只有一个CPU,使用多线程时,在真实系统环境下不能并行,只能通过切 换时间片的方式交替进行,从而并发执行任务。真正的并行只能出现在拥有多个CPU的系统 中。
21.虚拟机栈和本地方法栈为什么是私有的?
虚拟机栈:每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
本地方法栈:和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。
在 HotSpot 虚 拟机中和 Java 虚拟机栈合二为一。所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有 的。
22.程序计数器为什么是私有的?
程序计数器主要有下面两个作用:
字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执 行、选择、循环、异常处理。在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候 能够知道该线程上次运行到哪儿了。
所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。
23.什么是线程和进程?
进程:在操作系统中能够独立运行,并且作为资源分配的基本单位。它表示运行中的程序。系 统运行一个程序就是一个进程从创建、运行到消亡的过程。
线程:是一个比进程更小的执行单位,能够完成进程中的一个功能,也被称为轻量级进程。一 个进程在其执行的过程中可以产生多个线程。
线程与进程不同的是:同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序 计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作 时,负担要比进程小得多。
24.什么是多线程的上下文切换?
即使是单核CPU也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机 制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程 执行,让我们感觉多个线程时同时执行的,时间片一般是几十毫秒(ms)
上下文切换过程中,CPU会停止处理当前运行的程序,并保存当前程序运行的具体位置以便之后继续运行 CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。
但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再次加载这个任务的状态从任务保存到再加载的过程就是一次上下文切换
25.什么是自旋锁?
自旋锁是SMP架构中的一种low-level的同步机制。
当线程A想要获取一把自旋锁而该锁又被其它线程锁持有时,线程A会在一个循环中自旋以检测 锁是不是已经可用了。
自旋锁需要注意:
由于自旋时不释放CPU,因而持有自旋锁的线程应该尽快释放自旋锁,否则等待该自旋锁的线 程会一直在那里自旋,这就会浪费CPU时间。
持有自旋锁的线程在sleep之前应该释放自旋锁以便其它线程可以获得自旋锁。
26.AQS支持几种同步方式?
1)独占式
2)共享式
这样方便使用者实现不同类型的同步组件,独占式如ReentrantLock,共享式如 Semaphore,CountDownLatch,组合式的如ReentrantReadWriteLock。
总之,AQS为使 用提供了底层支撑,如何组装实现,使用者可以自由发挥。
27.什么是AQS?
AQS是AbustactQueuedSynchronizer的简称,它是一个Java提高的底层同步工具类,用一 个int类型的变量表示同步状态,并提供了一系列的CAS操作来管理这个同步状态。
AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同 步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。
28.CAS的问题
1)CAS容易造成ABA问题
一个线程a将数值改成了b,接着又改成了a,此时CAS认为是没有变化,其实是已经变化过 了,而这个问题的解决方案可以使用版本号标识,每操作一次version加1。在java5中,已经 提供了AtomicStampedReference来解决问题。
2) 不能保证代码块的原子性
CAS机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保 证3个变量共同进行原子性的更新,就不得不使用synchronized了。
3)CAS造成CPU利用率增加
之前说过了CAS里面是一个循环判断的过程,如果线程一直没有获取到状态,cpu资源会一直 被占用。
29.什么是CAS?
CAS是compare and swap的缩写,即我们所说的比较交换。
cas是一种基于锁的操作,而且是乐观锁。在java中锁分为乐观锁和悲观锁。悲观锁是将资源 锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽 泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version来获取数据,性能较 悲观锁有很大的提高。
java.util.concurrent.atomic 包下的类大多是使用CAS操作来实现的(AtomicInteger,AtomicBoolean,AtomicLong)。
30.CyclicBarrier和CountDownLatch的区别
1)CountDownLatch简单的说就是一个线程等待,直到他所等待的其他线程都执行完成并且 调用countDown()方法发出通知后,当前线程才可以继续执行。
2)cyclicBarrier是所有线程都进行等待,直到所有线程都准备好进入await()方法之后,所有 线程同时开始执行!
3)CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使用reset() 方 法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计 数器,并让线程们重新执行一次。
4)CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得 CyclicBarrier阻塞的线程数量。isBroken方法用来知道阻塞的线程是否被中断。如果被中断 返回true,否则返回false。
31.线程池的优点?
1)重用存在的线程,减少对象创建销毁的开销。
2)可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵 塞。
3)提供定时执行、定期执行、单线程、并发数控制等功能。
32.创建线程有哪些方式?
1)继承Thread类创建线程类
2)通过Runnable接口创建线程类
3)通过Callable和Future创建线程
4)通过线程池创建
33.并发编程三要素?
1)原子性 原子性指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么 就全部都不执行。
2)可见性 可见性指多个线程操作一个共享变量时,其中一个线程对变量进行修改后,其他线程可以立即 看到修改的结果。
3)有序性 有序性,即程序的执行顺序按照代码的先后顺序来执行。
34.什么是悲观锁?什么是乐观锁?
当我们要对一个数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办 法就是直接对该数据进行加锁以防止并发。
这种借助数据库锁机制在修改数据之前先锁定,再修改的方式被称之为悲观并发控制(又名 “悲观锁”,Pessimistic Concurrency Control,缩写“PCC”)。
之所以叫做悲观锁,是因为这是一种对数据的修改抱有悲观态度的并发控制方式。我们一般认 为数据被并发修改的概率比较大,所以需要在修改之前先加锁。
乐观锁( Optimistic Locking ) 是相对悲观锁而言的,乐观锁假设数据一般情况下不会造成 冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突 了,则让返回用户错误的信息,让用户决定如何去做。
相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。一般的 实现乐观锁的方式就是记录数据版本。
35.Java里的线程有哪些状态?
-
初始(NEW):新创建了一个线程对象,但还没有调用start()方法。 -
运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统 的称为“运行”。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方 法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此 时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态 (running)。 -
阻塞(BLOCKED):表示线程阻塞于锁。 -
等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中 断)。 -
超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行 返回。 -
终止(TERMINATED):表示该线程已经执行完毕。
36.如何避免“伪共享”?
-
字节填充(创建变量时,使用字段对其进行填充,避免多个变量被分派到同一个缓存行 里)。 -
JDK8提供了一个Contended注解来解决伪共享。
37.“伪共享”出现的原因是什么?
因为CPU缓存和内存交换数据的单位是缓存行,而同一个缓存行里的多个变量不能同时被多个 线程修改。
38.了解过什么是“伪共享”吗?
CPU缓存从内存读数据时,是按缓存行读取的,即使只用到一个变量,也要将整行数据进行读 取,这行数据量可能包含其他变量。当多个线程同时修改同一个缓存行里的不同变量时,由于 同时只能有一个线程在操作,所以相比将每个变量放到不同缓存行里,性能会有所下降。多个 线程同时修改了同一个缓存行上的不同变量,由于不能并发修改,所以称为“伪共享”。
39.说一下synchronized锁升级过程
-
偏向锁 在 JDK1.8 中,其实默认是轻量级锁,但如果设定了 -XX:BiasedLockingStartupDelay = 0 ,那在对一个 Object 做 syncronized 的时候,会立即上一把偏向锁。当处于偏向锁 状态时, markwork 会记录当前线程 ID 。 -
升级到轻量级锁 当下一个线程参与到偏向锁竞争时,会先判断 markword 中保存的线程 ID 是否与这个 线程 ID 相等,如果不相等,会立即撤销偏向锁,升级为轻量级锁。每个线程在自己的线 程栈中生成一个 LockRecord ( LR ),然后每个线程通过 CAS (自旋)的操作将锁对象头 中的 markwork 设置为指向自己的 LR 的指针,哪个线程设置成功,就意味着获得锁。关于 synchronized 中此时执行的 CAS 操作是通过 native 的调用 HotSpot 中 bytecodeInterpreter.cpp 文件 C++ 代码实现的,有兴趣的可以继续深挖。 -
升级到重量级锁 如果锁竞争加剧(如线程自旋次数或者自旋的线程数超过某阈值, JDK1.6 之后,由 JVM 自己控制该规则),就会升级为重量级锁。此时就会向操作系统申请资源,线程挂起,进 入到操作系统内核态的等待队列中,等待操作系统调度,然后映射回用户态。在重量级 锁中,由于需要做内核态到用户态的转换,而这个过程中需要消耗较多时间,也就 是"重"的原因之一。
40.ReentrantLock与synchronized的区别
ReentrantLock 有如下特点:
-
可重入 ReentrantLock 和 syncronized 关键字一样,都是可重入锁,不过两者实现原理稍有差 别, RetrantLock 利用 AQS 的的 state 状态来判断资源是否已锁,同一线程重入加锁, state 的状态 +1 ; 同一线程重入解锁, state 状态 -1 (解锁必须为当前独占线程,否则异 常); 当 state 为 0 时解锁成功。 -
需要手动加锁、解锁 synchronized 关键字是自动进行加锁、解锁的,而 ReentrantLock 需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成,来手动加锁、解锁。 -
支持设置锁的超时时间 synchronized 关键字无法设置锁的超时时间,如果一个获得锁的线程内部发生死锁,那 么其他线程就会一直进入阻塞状态,而 ReentrantLock 提供 tryLock 方法,允许设置线 程获取锁的超时时间,如果超时,则跳过,不进行任何操作,避免死锁的发生。 -
支持公平/非公平锁 synchronized 关键字是一种非公平锁,先抢到锁的线程先执行。而 ReentrantLock 的 构造方法中允许设置 true/false 来实现公平、非公平锁,如果设置为 true ,则线程获取 锁要遵循"先来后到"的规则,每次都会构造一个线程 Node ,然后到双向链表的"尾 巴"后面排队,等待前面的 Node 释放锁资源。 -
可中断锁 ReentrantLock 中的 lockInterruptibly() 方法使得线程可以在被阻塞时响应中断,比 如一个线程 t1 通过 lockInterruptibly() 方法获取到一个可重入锁,并执行一个长时间 的任务,另一个线程通过 interrupt() 方法就可以立刻打断 t1 线程的执行,来获取t1持 有的那个可重入锁。而通过 ReentrantLock 的 lock() 方法或者 Synchronized 持有锁 的线程是不会响应其他线程的 interrupt() 方法的,直到该方法主动释放锁之后才会响应 interrupt() 方法。
41.说说synchronized的实现原理
在 Java 中,每个对象都隐式包含一个 monitor(监视器)对象,加锁的过程其实就是竞争 monitor 的过程,当线程进入字节码 monitorenter 指令之后,线程将持有 monitor 对象, 执行 monitorexit 时释放 monitor 对象,当其他线程没有拿到 monitor 对象时,则需要阻塞 等待获取该对象。
42.sleep() 方法和 wait() 方法的区别和共同点?
相同点:两者都可以暂停线程的执行,都会让线程进入等待状态。
不同点:sleep()方法没有释放锁,而 wait()方法释放了锁。
sleep()方法属于Thread类的静态方法,作用于当前线程;而wait()方法是Object类的实例方 法,作用于对象本身。
执行sleep()方法后,可以通过超时或者调用interrupt()方法唤醒休眠中的线程;执行wait() 方法后,通过调用notify()或notifyAll()方法唤醒等待线程。
43.Thread.sleep(0)的作用是什么?
由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情 况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动 触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。