目录
CountDownLatch和Semaphore的区别和底层原理
ReentrantLock中tryLock()和lock()⽅法的区别
ReentrantLock中的公平锁和⾮公平锁的底层实现
sleep()、wait()、join()、yield()的区别
Sychronized的偏向锁、轻量级锁、重量级锁
Sychronized和ReentrantLock的区别
JAVA如何开启线程?怎么保证线程安全?
Volatile和Synchronized有什么区别?Volatile能不能保证线程安全?DCL(Double Check Lock)单例为什么要加Volatile?
谈谈你对AQS的理解。AQS如何实现可重入锁
谈谈你对线程安全的理解
谈谈死锁如何避免?
如果再提交任务时,线程池队列已满,这时候会发生什么?
谈谈对守护线程的理解
线程池的底层⼯作原理
线程池中线程复用原理
线程池中为什么用阻塞队列而不用普通队列?为什么是先添加到阻塞队列中而不是先创建最大线程?
CountDownLatch和Semaphore的区别和底层原理
CountDownLatch表示计数器,可以给CountDownLatch设置⼀个数字,⼀个线程调⽤
CountDownLatch的
await()将会阻塞,其他线程可以调⽤
CountDownLatch的countDown()⽅法来对
CountDownLatch中的数字减⼀,当数字被减成0后,所有await的线程都将被唤醒。
对应的底层原理就是,调⽤
await()⽅法的线程会利⽤AQS排队,⼀旦数字被减为0,则会将AQS中
排队的线程依次唤醒。
Semaphore表示信号量,可以设置许可的个数,表示同时允许最多多少个线程使⽤该信号量,通
过acquire()来获取许可,如果没有许可可⽤则线程阻塞,并通过AQS来排队,可以通过release()
⽅法来释放许可,当某个线程释放了某个许可后,会从AQS中正在排队的第⼀个线程开始依次唤
醒,直到没有空闲许可
ReentrantLock中tryLock()和lock()⽅法的区别
1. tryLock()表示尝试加锁,可能加到,也可能加不到,该⽅法不会阻塞线程,如果加到锁则返回
true,没有加到则返回false ,可以使用tryLock进行自旋,提高性能。代码如下:
ReentrantLock reentrantLock = new ReentrantLock();
while ( !reentrantLock.tryLock()){
//编写一些业务代码
TimeUnit.SECONDS.sleep(5);
}
2. lock()表示阻塞加锁,线程会阻塞直到加到锁,⽅法也没有返回值
ReentrantLock中的公平锁和⾮公平锁的底层实现
⾸先不管是公平锁和⾮公平锁,它们的底层实现都会使⽤AQS来进⾏排队,它们的区别在于:线程在使 ⽤lock()⽅法加锁时,如果是公平锁,会先检查AQS队列中是否存在线程在排队,如果有线程在排队, 则当前线程也进⾏排队,如果是⾮公平锁,则不会去检查是否有线程在排队,⽽是直接竞争锁。
不管是公平锁还是⾮公平锁,⼀旦没竞争到锁,都会进⾏排队,当锁释放时,都是唤醒排在最前⾯的线 程,所以⾮公平锁只是体现在了线程加锁阶段,⽽没有体现在线程被唤醒阶段。
另外,ReentrantLock是可重⼊锁,不管是公平锁还是⾮公平锁都是可重⼊的。
sleep()、wait()、join()、yield()的区别
1.
锁池
所有需要竞争同步锁的线程都会放在锁池当中,比如当前对象的锁已经被其中一个线程得到,则其他线 程需要在这个锁池进行等待,当前面的线程释放同步锁后锁池中的线程去竞争同步锁,当某个线程得到 后会进入就绪队列进行等待cpu
资源分配。
2.
等待池
当我们调用
wait
()方法后,线程会放到等待池当中,等待池的线程是不会去竞争同步锁。只有调用了 notify()或
notifyAll()
后等待池的线程才会开始去竞争锁,
notify
()是随机从等待池选出一个线程放 到锁池,而notifyAll()
是将等待池的所有线程放到锁池当中
1
、
sleep
是
Thread
类的静态本地方法,
wait
则是
Object
类的本地方法。
2
、
sleep
方法不会释放
lock
,但是
wait
会释放,而且会加入到等待队列中。
3
、
sleep
方法不依赖于同步器
synchronized
,但是
wait
需要依赖
synchronized
关键字。
4
、
sleep
不需要被唤醒(休眠之后推出阻塞),但是
wait
需要(不指定时间需要被别人中断)。
5
、
sleep
一般用于当前线程休眠,或者轮循暂停操作,
wait
则多用于多线程之间的通信。
6
、
sleep
会让出
CPU
执行时间且强制上下文切换,而
wait
则不一定,
wait
后可能还是有机会重新竞 争到锁继续执行的。
yield
()执行后线程直接进入就绪状态,马上释放了
cpu
的执行权,但是依然保留了
cpu
的执行资格, 所以有可能cpu
下次进行线程调度还会让这个线程获取到执行权继续执行
join
()执行后线程进入阻塞状态,例如在线程
B
中调用线程
A
的
join
(),那线程
B
会进入到阻塞队
列,直到线程
A
结束或中断线程
Sychronized的偏向锁、轻量级锁、重量级锁
1. 偏向锁:在锁对象的对象头中记录⼀下当前获取到该锁的线程ID,该线程下次如果⼜来获取该锁就 可以直接获取到了
2. 轻量级锁:由偏向锁升级⽽来,当⼀个线程获取到锁后,此时这把锁是偏向锁,此时如果有第⼆个 线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻 量级锁底层是通过⾃旋来实现的,并不会阻塞线程
3. 如果⾃旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞
4. ⾃旋锁:⾃旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就⽆所谓唤醒线程,阻塞和唤醒 这两个步骤都是需要操作系统去进⾏的,⽐较消耗时间,⾃旋锁是线程通过CAS获取预期的⼀个标 记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程⼀直在运 ⾏中,相对⽽⾔没有使⽤太多的操作系统资源,⽐较轻量
Sychronized和ReentrantLock的区别
1. sychronized是⼀个关键字,ReentrantLock是⼀个类
2. sychronized会⾃动的加锁与释放锁,ReentrantLock需要程序员⼿动加锁与释放锁
3. sychronized的底层是JVM层⾯的锁,ReentrantLock是API层⾯的锁
4. sychronized是⾮公平锁,ReentrantLock可以选择公平锁或⾮公平锁
5. sychronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过代码中int类型的state标识 来标识锁的状态
6. sychronized底层有⼀个锁升级的过程
JAVA如何开启线程?怎么保证线程安全?
线程和进程的区别:进程是操作系统进行资源分配的最小单元。线程是操作系统进行任务分配的最小单元,线程隶属于进程。
如何开启线程? 1、继承Thread类,重写run方法。 2、实现Runnable接口,实现run方法。3、实现Callable接口,实现call方法。通过FutureTask创建一个线程,获取到线程执行的返回值。4、通过线程池来开启线程。
怎么保证线程安全? 加锁: 1、 JVM提供的锁, 也就是Synchronized关键字。 2、 JDK提供的各种锁 Lock。
Volatile和Synchronized有什么区别?Volatile能不能保证线程安全?DCL(Double Check Lock)单例为什么要加Volatile?
1、Synchronized关键字,用来加锁。 Volatile只是保持变量的线程可见性。通常适用于一个线程写,多个线程读的场景。
2、不能。Volatile关键字只能保证线程可见性, 不能保证原子性。
3、Volatile防止指令重排。在DCL中,防止高并发情况下,指令重排造成的线程安全问题。
dcl主要是指在加锁的前后都要对对象进行非空判断,之所以加volatile是为了防止指令重排,一个对象在创建的时候不是一蹴而就的,而是分几步走的,例如分配空间、初始化、将堆中空间地址分配给变量,而这些步骤并不是按顺序来执行的,所以加上volatile是为了防止指令重排带来的干扰。
谈谈你对AQS的理解。AQS如何实现可重入锁
1、AQS是一个JAVA线程同步的框架。是JDK中很多锁工具的核心实现框架。
2、 在AQS中,维护了一个信号量state和一个线程组成的双向链表队列。其中,这个线程队列,就是用来给线程排队的,而state就像是一个红绿灯,用来控制线程排队或者放行的。 在不同的场景下,有不用的意义。
3、在可重入锁这个场景下,state就用来表示加锁的次数。0标识无锁,每加一次锁,state就加1。释放锁state就减1。
AQS核心思想:AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态
。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
核心主件:SyncQueue 同步队列,是一个双向链表。包括head节点和tail节点。head节点主要用作后续的调度。Condition queue:非必须,单向链表。当程序中存在cindition的时候才会存在此列表。state(int类型),充当锁的状态,state=0时表示无锁,1时表示有锁,>1时表示重入锁。
通过compareAndSetState进行尝试修改state。会存在一个字段用于保存当前线程,标明哪个线程持有锁。
而大多数的锁例如ReentrantLock就是基于AQS实现的,主要负责维护好想信号量state,至于入队出队这些操作AQS底层都帮我们实现了。
谈谈你对线程安全的理解
应该是内存安全,堆是共享内存,可以被所有线程访问
堆
是进程和线程共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是用完了要还给操作系统,要不然就是内存泄漏。
在
Java
中,堆是
Java
虚拟机所管理的内存中最大的一块,是所有线程共享的一块内存区域,在虚
拟机启动时创建。堆所存在的内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及
数组都在这里分配内存。
栈
是每个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈互相独立,因此,栈是线程安全的。操作系统在切换线程的时候会自动切换栈。栈空间不需要在高级语言里面显式的分配和释放。
目前主流操作系统都是多任务的,即多个进程同时运行。为了保证安全,每个进程只能访问分配给自己的内存空间,而不能访问别的进程的,这是由操作系统保障的。
在每个进程的内存空间中都会有一块特殊的公共区域,通常称为堆(内存)。进程内的所有线程都可以访问到该区域,这就是造成问题的潜在原因。
谈谈死锁如何避免?
那我们首先得知道死锁的原因:
互斥条件:线程(进程)对于所分配到的资源具有排它性,即一个资源只能被一个线程(进程)占用,直到被该线程(进程)释放
请求与保持条件:一个线程(进程)因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
不剥夺条件:线程(进程)已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
循环等待条件:当发生死锁时,所等待的线程(进程)必定会形成一个环路(类似于死循环),造成永阻塞
而我们想要避免死锁,就是要打破上面的原则:
1、要注意加锁的顺序,保证每个线程按照同样的顺序加锁
2、对获取锁的时间进行限制,设置一个超时时间,可以通过trylock(time)的方法
3、注意死锁检查、第一时间发现死锁并解决死锁
如果再提交任务时,线程池队列已满,这时候会发生什么?
提交任务的时候,线程池队列满了,那么这时候就会启动救急线程(非核心线程),如果救急线程有空余的(不设为0或者未达到上限),就使用救急线程开始任务,如果救急线程也无空余,那么就使用线程的拒绝策略
线程的拒绝策略有四种:
ThreadPoolExecutor.AbortPolicy: 丢弃任务并抛出RejectedExecutionException异常。是默认的策略。
ThreadPoolExecutor.DiscardPolicy: 丢弃任务,但是不抛出异常 这是不推荐的做法。
ThreadPoolExecutor.DiscardOldestPolicy: 抛弃队列中等待最久的任务 然后把当前任务加入队列中。
ThreadPoolExecutor.CallerRunsPolicy: 调用任务的run()方法绕过线程池直接执行。
谈谈对守护线程的理解
守护线程:为所有非守护线程提供服务的线程;任何一个守护线程都是整个
JVM
中所有非守护线程的保姆;守护线程类似于整个进程的一个默默无闻的小喽喽;它的生死无关重要,它却依赖整个进程而运行;哪天其他线程结束了,没有要执行的了,程序就结束了,理都没理守护线程,就把它中断了;
注意: 由于守护线程的终止是自身无法控制的,因此千万不要把
IO
、
File
等重要操作逻辑分配给它;因为它不靠谱;
守护线程的作用是什么?
举例,
GC
垃圾回收线程:就是一个经典的守护线程,当我们的程序中不再有任何运行的
Thread,
程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM
上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。
应用场景:(
1
)来为其它线程提供服务支持的情况;(
2
) 或者在任何情况下,程序结束时,这个线程必须正常且立刻关闭,就可以作为守护线程来使用;反之,如果一个正在执行某个操作的线程必须要正确地关闭掉否则就会出现不好的后果的话,那么这个线程就不能是守护线程,而是用户线程。通常都是些关键的事务,比方说,数据库录入或者更新,这些操作都是不能中断的。
thread.setDaemon(true)
必须在
thread.start()
之前设置,否则会抛出IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。
在
Daemon
线程中产生的新线程也是
Daemon
的。
守护线程不能用于去访问固有资源,比如读写操作或者计算逻辑。因为它会在任何时候甚至在一个操作的中间发生中断。
Java自带的多线程框架,比如ExecutorService,会将守护线程转换为用户线程,所以如果要使用后台线 程就不能用Java的线程池。
线程池的底层⼯作原理
线程池内部是通过队列+线程实现的,当我们利⽤线程池执⾏任务时:
1. 如果此时线程池中的线程数量⼩于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建 新的线程来处理被添加的任务。
2. 如果此时线程池中的线程数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放⼊ 缓冲队列。
3. 如果此时线程池中的线程数量⼤于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数
量⼩于maximumPoolSize,建新的线程来处理被添加的任务。
4. 如果此时线程池中的线程数量⼤于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等 于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。
5. 当线程池中的线程数量⼤于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终⽌。这样,线程池可以动态的调整池中的线程数
线程池中线程复用原理
线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前通过
Thread
创建线程时的
一个线程必须对应一个任务的限制。
在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对
Thread
进行了封装,并不是每次执行任务都会调用
Thread.start()
来创建新线程,而是让每个线程去执行一个“
循环任务
”
,在这个
“
循环任务
”
中不停检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的 run
方法,将
run
方法当成一个普通的方法执行,通过这种方式只使用固定的线程就将所有任务的 run
方法串联起来。
线程池中为什么用阻塞队列而不用普通队列?为什么是先添加到阻塞队列中而不是先创建最大线程?
问题一:
问题二:
线程池的初衷就是为了减少线程的创建和销毁的操作,因为这两个操作相对来说是比较消耗性能的,如果当核心队列满了之后就立马创建救急线程,那么就会开始线程创建的操作,而救急线程是有存活时间的,时间到了之后切线程未使用,那么就会立即销毁,这又加大的系统的开销。在一般情况下,任务是可以积压的,在可控的范围内适当挤压任务,减少救急线程的创建和销毁,才是线程池存在的意义,