文章目录
QA:
线程:
cpu 的调度算法?
-
分时调度
给每个线程同样的执行时间,时间耗尽以后,将cpu的使用权交给下一个线程
-
抢占式调度
每个线程都有一个运行优先级(1-10),默认为5,cpu使用权优先给优先级高的线程,如果几个线程的优先级一样,则随机给一个。
抢占式调度有可能会造成某个线程迟迟不能运行
如何结束一个线程?
优雅的结束:通过循环检测它的中断状态,一旦发现它中断了,就直接不在进行循环。
线程执行遇到异常会怎样?
如果该异常被捕获,则会进行下面的处理。
如果没有被捕获,则会停止线程的运行,同时释放这个线程拥有的锁
线程的创建
- 实现Runnable或者Callable接口
- 有点就是简单,而且是实现的方式,callable还可以有阻塞的方式来获取返回值
- 继承Thread
- 缺点是,java的类只能有一个父类
- 使用线程池
- 线程池来调度 线程执行任务。
Thread的sleep和对象的wait方法都可以暂停线程的执行,他们的区别是什么?
- Thread的sleep方法是线程的方法,这个方法会让线程等待,让出cpu的执行权,但是不会释放锁,执行完成以后进入就绪状态。
- 对象的wait方法是Object类的方法,所有的类都是继承Object方法,wait的执行会使得线程让出cpu执行权的同时释放锁。可以用Object 的notify或着notifyAll方法让阻塞于锁的线程苏醒(notify-对应一个线程,而notifyAll对应所有线程),苏醒的线程需要继续竞争锁才能进入就绪状态
notify和notifyAll的区别
- notify是根据优先级唤醒阻塞于这个锁一个线程,具体是哪个,由jvm决定
- notifyAll是唤醒所有阻塞于这个锁的线程。
为什么wait、notify和notifyAll不在Thread类中
- 因为jvm中,锁的标示是在对象的对象头中。由对象记录了锁的信息
sleep、join、yield的区别
- sleep,这个方法会让线程等待,让出cpu的执行权,但是不会释放锁,执行完成以后进入就绪状态。
- yield,线程让出cpu的执行权,但是不会释放锁,并且不会等待,直接进入就绪状态
- join,ta.join(),当前线程进入等待状态,等到ta执行完成,并且当前线程需要获得锁以后进入就绪状态
#####什么叫线程安全?
如果多线程访问同一个对象时,不用人为的管理线程的调度或者同步,多个线程也能得到正确的结果,那么就认为这个对象是线程安全的
线程的状态?
初始、运行、阻塞、等待、超时等待、终止
-
初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
-
运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
-
阻塞(BLOCKED):表示线程阻塞于锁。
-
等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
-
超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。相较于等待状态,增加了一个超时限制,超时以后直接返回到运行状态
-
终止(TERMINATED):表示该线程已经执行完毕。
等待和通知机制:
-
Thread.sleep(long millis),一定是当前线程调用此方法,当前线程进入TIMED_WAITING状态,但不释放对象锁,millis后线程自动苏醒进入就绪状态。作用:给其它线程执行机会的最佳方式。
-
Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的CPU时间片,但不释放锁资源,由运行状态变为就绪状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中
-
Thread.yield()不会导致阻塞。该方法与sleep()类似,只是不能由用户指定暂停多长时间。
-
thread.join()/thread.join(long millis),当前线程里调用其它线程t的join方法,当前线程进入WAITING/TIMED_WAITING状态,当前线程不会释放已经持有的对象锁。线程t执行完毕或者millis时间到,当前线程一般情况下进入RUNNABLE状态,也有可能进入BLOCKED状态(因为join是基于wait实现的)。
-
obj.wait(),当前线程调用对象的wait()方法,当前线程释放对象锁,进入等待队列。依靠notify()/notifyAll()唤醒或者wait(long timeout) timeout时间到自动唤醒。
-
obj.notify()唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的所有线程。但是唤醒的线程依然需要重新获取锁才能执行
-
LockSupport.park()/LockSupport.parkNanos(long nanos),LockSupport.parkUntil(long deadlines), 当前线程进入WAITING/TIMED_WAITING状态。对比wait方法,不需要获得锁就可以让线程进入WAITING/TIMED_WAITING状态,需要通过LockSupport.unpark(Thread thread)唤醒。
-
线程阻塞的方式:
被动的阻塞于锁(synchronized)
-
线程等待的方式:
wait(需要被notify或notifyAll唤醒,被唤醒的线程要重新获取锁才能执行),join(),LockSupport.park()
ThreadLocal的原理和注意事项?
每个Thread
维护一个 ThreadLocalMap
映射表,这个映射表的 key
是 ThreadLocal
实例本身,value
是真正需要存储的 Object
。
这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询绑定在这个线程上的一个值
可以通过set(T)方法来设置一个值,在当前线程下再通过get()方法获取到原先设置的值
其内部有个ThreadLocalMap静态类,set的时候以当前ThreadLocal实例为key,想要保存的内容为value进行保存
ThreadLocal中的key是弱引用,很容易被gc掉,但是value却是当前线程的强引用,不会被gc。
在key被GC掉以后,Map中就保存着(null,value)的内容,而map是根据ThreadLocal实例来获取value的,这样子就再也获取不到value了,除非线程结束。
需要手动的执行remove才能完成
如何排查死锁
-
jps 打印对应java进程
-
jstack -l xxx(对应进程):
volatile
-
volatile可以有什么效果?
- 保证对应变量的可见性(所有线程均可以看到改变值),通过缓存一致性,当某一线程在自己的空间进行修改时,会将修改结果设置到主内存,同时设置其余线程持有的该共享变量拷贝无效,使得其他线程使用该共享变量只能从主内存中读取。
- 禁止重排序,通过内存屏障来实现,重排序是指,编译器会在保证结果不变的前提下,修改代码的执行顺序来达到提升效率的结果
- 自增无法保证原子性
-
volatile可以修饰引用变量吗?
- 可以修饰对象,但是不保证引用变量内的变量有volatile的性质。修饰引用变量或者数组时只保证其内存地址的可见性
synchronized
该关键字可以用于代码块和方法,
- 在同步代码块时,锁住的是括号类的对象
- 其余线程来会阻塞在代码块外,知道括号类的对象的锁释放
- 当同步方法时,锁住的是当前类的实例
- 其余非同步方法和静态方法(同步)不会被阻塞住
- 但是同一个实例下的,该类中的其他同步方法,如果有线程想运行,会被阻塞住
- 不同实例的不会阻塞
- 当同步静态方法时,锁住的是当前类的class类
- 静态方法和非静态方法之间不阻塞
AQS
-
synchronized与ReentrantLock的区别?
相似点:两者都是阻塞式的加锁同步,如果一个线程获得锁,那么另外的线程只能在阻塞住,直到锁释放
区别:
- 实现方式不同:
- synchronized是原生语言层面的互斥,通过监视器来实现,所以需要在jvm才能生效。
- 而ReentrantLock是api层次的互斥锁,通过CAS的方式来实现,阻塞方式是通过Lock.supportLock来实现
- ReentrantLock提供超时阻塞的方式,设定超时时间,尝试在超时时间内获取锁,在超时时间内可以被中断,获取失败会返回false。
- ReentrantLock可以被中断(内部使用LockSupport.park,该方式可以响应中断并返回,却不抛错)
- ReentranLock提供了公平锁和非公平锁,内部依靠队列实现
- ReentranLock可以绑定多个Condition对象,只需多次调用newCondition方法即可。阻塞队列可以用这个实现
- 实现方式不同:
-
乐观锁和悲观锁的区别?
- 乐观锁是认为读的次数多于写的次数,所以不必要每次都给代码加锁,CAS的实现就是乐观锁。
- 悲观锁是认为写的次数多于读的次数,所以每次读都要加锁,synchronized关键字和Lock的实现类都是悲观锁
-
如何实现一个乐观锁?
- 利用CAS的方式,会有ABA的问题,此时可以使用版本号配上CAS的方式
-
AQS是如何唤醒下一个线程的?
- 在公平锁的实现方式下,唤醒下一个节点,如果节点状态>=0,则从队列尾部开始回溯一个状态正常的节点
-
AQS框架的主要方法:
- acquire(int ),独占式的获取锁,如果没有获取到锁,会被阻塞住,直到获取到
- tryAcquire(int) 需要子类实现,独占式的获取同步状态,ReentrantLock借此实现了公平锁和非公平锁
- tryAcquireShared(int) 共享式的获取同步状态,返回值>=0表示成功,否则失败,借此实现了ReentrantReadWriteLock
- 其他的都是他们对应的释放方法,中断以及超时获取的方法
-
AQS中,节点的状态:
- int CANCELLED = 1//节点从同步队列中取消(中断、超时等原因)
- int SIGNAL = -1//后继节点的线程处于等待状态,如果当前节点释放同步状态会通知后继节点,使得后继节点的线程能够运行,所以当一个节点前面的节点为
- int CONDITION = -2//当前节点进入等待队列中
- int PROPAGATE = -3//表示下一次共享式同步状态获取将会无条件传播下去
- int INITIAL = 0;//初始状态
-
ReentrantLock中,节点入队后,为什么只有前驱节点是头节点的节点才能尝试获取同步状态?
- 头节点是成功获取同步状态的节点,在头节点释放以后,会唤醒后续节点
- 维护队列的FIFO特性
表现形式:
如果线程A从同步队列获取到锁,则此时线程A对应的节点Node是首节点,当线程A执行完成释放锁,会唤醒线程A对应节点的后继节点 - 线程B对应节点,线程B节点在自旋时获取到锁(acquireQueued),此时头结点head会指向线程B节点,切断线程A节点的next引用,则线程A对应节点就从同步队列移除了
-
Exchanger的作用?
可以允许两个线程在Exchanger.exchange(Object )点交换数据。先到达同步点的线程会阻塞住
-
Semeaphore的作用?
可以控制并发线程数,实例化的时候确定数目,然后semaphore.acquire();申请信号量,如果没有多余信号量了,就会阻塞住,直到有。
semaphore.release()表示线程释放许可证
-
CountDownLatch的作用?
CountDownLatch允许一个或多个线程等待其他线程完成操作,实例化的时候确定数目,当调用CountDownLatch 的countDown方法是,count就会减1,CountDownLatch 的await方法会阻塞当前线程直到count成0。
-
CyclicBarrier
CyclicBarrier 可以让一组线程到达一个屏障时被阻塞,知道最后一个线程到达屏障时,屏障才开门,所有被屏障拦截的线程都会继续进行。
构造方法CyclicBarrier(int parties) 其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier其已经到达了屏障,然后当前线程被阻塞,直到所有线程到达屏障。
-
CyclicBarrier和CountDownLatch的区别?
CyclicBarrier加计数方式,且能重复使用,计数达到指定值时,计数置为 0 重新开始
而CountDownLatch减计数方式,无法重复使用,计数打到0时,无法重置
线程池
- 线程池的工作原理?
- 提交任务到线程池,如果核心线程数未满,则新建核心线程执行任务。
- 如果核心线程已满,则放入队列中,等待执行完任务的线程从中获取并执行。
- 如果队列已满,查看最大线程数是否达到,如果未达到,新建线程执行任务。
- 按照设定执行拒绝策略
- 线程池的重要参数?
- 核心线程数、最大线程数(超出并包括核心线程数)、多余线程存活时间、任务队列、线程工厂(规定线程的名字)、拒绝策略
- 非核心的线程什么时候释放?
- 超出存活时间以后
- 关闭方式?
- 立即关闭,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务。
- 正常关闭,不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务。
- 线程池的参数设置?
- CPU 密集型应用,则线程池大小设置为 N+1,因为CPU密集型,cpu本身使用频率很高,即使开很多的线程,只能增加线程的上下文切换消耗
- IO密集型应用,线程池大小设置为2N+1,IO密 集型任务 CPU 使用率并不高,因此可以让 CPU 在等待 IO 的时候去处理别的任务,充分利用 CPU 时间。
- 如果是混合密集型,那么把任务分成两类,分别建立线程池来执行
- 线程池的线程销毁?
- 在getTask中,根据当前线程数是否超出核心线程数
- 如果超出,则通过超时阻塞的方式来拿任务,如果超过时间没有拿到,则返回空,同时退出getTask的死循环。执行processWorkerExit(w, completedAbruptly)来终止线程
- processWorkerExit将w(worker)从workers的一个hashSet中移除。
- 如果没超出,则阻塞的方式来拿任务
- 如果超出,则通过超时阻塞的方式来拿任务,如果超过时间没有拿到,则返回空,同时退出getTask的死循环。执行processWorkerExit(w, completedAbruptly)来终止线程
- 在getTask中,根据当前线程数是否超出核心线程数
ReentrantLock
###非公平锁
加锁 lock()
一定要获取到锁,获取不到就阻塞,除非中断,不然不返回
-
cas设置状态从0到1,成功拿到锁,设置锁占有线程
-
失败执行AQS中的acquire(1),其中包括先执行tryAcquire(1)
- 根据锁的持有线程和state检验是否重入,重入的state+1并返回true
- 非重入,cas拿锁,拿锁成功返回true
- 拿锁失败返回false
-
根据返回结果执行addWaiter(Node.EXCLUSIVE),是一个AQS的入队操作
- 将当前线程组装成一个node
- cas将该node插入队列的尾部(这里面会调用可能会执行enq方法)
-
执行acquireQueued方法(自旋直到获得同步状态成功)
无限循环,以一次循环为例:
- 当前线程节点的前驱节点是头节点(说明线程就是下一个)且获取同步状态成功,设置当前节点为头节点并返回
- 失败,线程进入shouldParkAfterFailedAcquire,判断该线程是否需要进入阻塞状态进行等待
- 前驱节点状态为Node.SIGNAL,节点需要阻塞,返回true。当前驱节点释放锁以后,会唤醒当前节点
- 前驱节点状态>0,说明前置节点已经取消了。已经是一个无效节点。这时会将前置节点删除掉,继续往前回溯,知道前驱节点状态<=0,并返回false
- 前置节点状态=0或Node.PROPAGATE。将前驱节点状态设置为Node.SIGNAL。返回false
- 根据shouldParkAfterFailedAcquire的返回结果,调用LockSupport.park来执行阻塞。
- 如果阻塞过程中,被中断了,会返回true,进而进入下一次循环(不是被中断就是正常唤醒,那此时会返回false,进而执行下一次循环)
-
根据返回结果判断是否执行selfInterrupt,当前线程自己中断方法。
尝试获取锁 tryAcquire()
尝试获取锁,获取不到就算了
- 尝试获取锁,判断当前锁的状态是不是0,
- 不是0,判断是否锁的持有线程是自己,是的话就是重入锁,直接加1返回成功,否则返回失败
- 是0,cas将状态加1,成功返回成功,否则返回失败
尝试获取相应中断锁acquireInterruptibly
该方法在等待获取同步状态时,如果当前线程被中断了,会立刻响应中断,并抛出 InterruptedException 异常。
先尝试获取tryAcquire(),如果失败,执行doAcquireInterruptibly方法,该方法与acquireQueued类似,只是入如果中断阻塞,会抛错,进而推出循环,进而返回
公平锁
加锁 lock()
- 执行的是AQS的acquire(1),方式和非公平锁的一样
尝试获取锁 tryAcquire(1)
- 先判断状态,如果不为0,看持有锁的线程是不是自己,是的话,重入次数+1并返回成功。不是的话返回失败
- 如果状态为0,则判断队列是否有节点。
- 如果没有节点,则将cas设置状态,如果成功,视为拿到锁,返回成功
- 如果队列有节点,返回false
阻塞队列
支持两个附加操作的队列:
- 取操作,如果队列中没有数据,线程会被阻塞住,直到队列有任务
- 塞操作,如果队列已满,线程会被阻塞住,直到队列中有空间
主要有7种队列:
-
ArrayBlockingQueue,一种FIFO,数组结构的有界队列
-
LinkedBlockingQueue,一种FIFO,链表结构的可有界可无界队列
-
PriorityBlockingQueue,一种支持优先级的无界阻塞队列
-
DelayQueue,支持延时获取元素的无界阻塞队列,即可以指定多久才能从队列中获取当前元素。
-
SynchronousQueue:一个不存储元素的阻塞队列。每一个塞操作都等待一个取操作,塞操作才会执行
-
LinkedTransferQueue,链表结构组成的无界阻塞队列。
-
LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。多线程时,能两边同时入队
以ArrayBlockingQueue为例,主要的阻塞功能由两个Condition来实现:notFull,notEmpty
notFull和notEmpty分别会在dequeue和enqueue的时候执行signal方法。
- 插入方法:
- add(e),如果满了,则抛出异常
- offer(e),返回Boolean值
- put(e),如果队列满了,notFull.await()
- offer( e, timeout, unit),如果队列满了,notFull.awaitNanos,如果超时以后队列都还没有多余空间,则返回false
- 移除方法:
- remove(e),返回Boolean值
- poll(),返回对应值或者null,如果返回的是对应值,那么队列会删除对应值
- take(),队列元素数目为0,notEmpty.await()
- poll(time, unit),队列元素数目为0,notEmpty.awaitNanos(nanos),超时以后返回null
- 检查方法:
- peek(),返回第一个元素,但是不删除
ArrayBlockingQueue 与 LinkedBlockingQueue 的区别?
-
LinkedBlockingQueue可以无界(Integer.MAX_VALUE,当添加速度大于移除速度时,在无界的情况下,可能会造成内存溢出等问题。),ArrayBlockingQueue是有界的初始化必须指定大小
-
LinkedBlockingQueue使用takeLock和putLock两个锁,其添加采用的是putLock,移除采用的则是takeLock,这样能大大提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
而ArrayBlockingQueue只使用一把锁,在并发操作时,吞吐量低,该锁锁住的是所有针对该队列进行的操作
-
LinkedBlockingQueue的实现原理图与ArrayBlockingQueue是类似的,除了对添加和移除方法使用单独的锁控制外,两者都使用了不同的Condition条件对象作为等待队列,用于挂起take线程和put线程。
-
LinkedBlockingQueue使用链表来存储数据,ArrayBlockingQueue使用数组
延时队列 DelayQueue
元素需要实现Delayed接口。队列前部是最早过期的元素,只有在延迟期到时才能够从队列中取元素。
主要用于处理超时任务和清掉缓存中超时的缓存数据
实现的关键是:
- 可重入锁ReentrantLock
- 用于阻塞和通知的Condition对象
- 根据Delay时间排序的优先级队列:PriorityQueue
- 用于优化阻塞通知的线程元素leader
- Delayed接口
Delayed接口
Delayed接口是用来标记那些应该在给定延迟时间之后执行的对象,它定义了一个long getDelay(TimeUnit unit)方法,该方法返回与此对象相关的的剩余时间。同时实现该接口的对象必须定义一个compareTo 方法,该方法提供与此接口的 getDelay 方法一致的排序。
所以DelayQueue的对象都实现了getDelay方法和compareTo方法
DelayQueue以支持优先级无界队列的PriorityQueue作为一个容器,容器里面的元素都应该实现Delayed接口,在每次往优先级队列中添加元素时以元素的过期时间作为排序条件,最先过期的元素放在优先级最高。
优先级堆排序队列 PriorityQueue
根据元素的优先级进行堆排序的队列,所以需要元素实现compareTo方法,拥有可比较的属性。