目录
CyclicBarrier和CountDownLatch的区别
如何控制多线程的执行顺序
- 使用线程的join方法
- 使用主线程的join方法
- 使用对象的wait和notify方法
- 使用单线程的线程池
- 使用AQS的Condition(条件变量)
- 使用CountDownLatch
- 使用CyclicBarrier(回环栅栏)
- 使用Semaphore(信号量)
- 使用CompletableFuture
线程池的优点
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
线程池的创建
ThreadPoolExecutor mExecute = new ThreadPoolExecutor(
corePoolSize, //线程池的核心线程数
maximumPoolSize,//线程池所能容纳的最大线程数
keepAliveTime,//线程的空闲时间
TimeUnit.SECONDS,//keepAliveTime对应的单位
workQueue,//线程池中的任务队列
threadFactory, //线程工厂
rejectHandler//当任务无法被执行时的拒绝策略
);
线程池的工作原理
当有请求到来时:
1.若当前实际线程数量 少于 corePoolSize,即使有空闲线程,也会创建一个新的工作线程;
2 若当前实际线程数量处于corePoolSize和maximumPoolSize之间,并且阻塞队列没满,则任务将被放入阻塞队列中等待执行;
3.若当前实际线程数量 小于 maximumPoolSize,但阻塞队列已满,则直接创建新线程处理任务;
4.若当前实际线程数量已经达到maximumPoolSize,并且阻塞队列已满,则使用饱和策略

4种线程池
1.newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
2.newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
3.newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
4.newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
线程池4种拒绝策略
AbortPolicy
这种拒绝策略在拒绝任务时,会直接抛出一个类型为 RejectedExecutionException 的 RuntimeException,让你感知到任务被拒绝了,于是你便可以根据业务逻辑选择重试或者放弃提交等策略。
DiscardPolicy
当有新任务被提交后直接被丢弃掉,也不会给你任何的通知,相对而言存在一定的风险,因为我们提交的时候根本不知道这个任务会被丢弃,可能造成数据丢失。
DiscardOldestPolicy
丢弃任务队列中的头结点,通常是存活时间最长的任务,它也存在一定的数据丢失风险。
CallerRunsPolicy(常用)
当有新任务提交后,如果线程池没被关闭且没有能力执行,则把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务。这样做主要有两点好处:
第一点新提交的任务不会被丢弃,这样也就不会造成业务损失。 第二点好处是,由于谁提交任务谁就要负责执行任务,这样提交任务的线程就得负责执行任务,而执行任务又是比较耗时的,在这段期间,提交任务的线程被占用,也就不会再提交新的任务,减缓了任务提交的速度,相当于是一个负反馈。在此期间,线程池中的线程也可以充分利用这段时间来执行掉一部分任务,腾出一定的空间,相当于是给了线程池一定的缓冲期。
线程的状态
如何查看线程死锁
1.可以通过jstack命令来查看发生死锁的线程
2 .或者两个线程去操作数据库时,数据库发生了死锁,这是可以查询数据库的死锁情况
1.查询是否锁表
show OPEN TABLES where In_use > 0;
2.查询进程
show processlist;
3.查看正在锁的事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;
4.查看等待锁的事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
写一个死锁
public class DeadLock implements Runnable{
private int flag = 1;
private static final Object o1 = new Object();
private static final Object o2 = new Object();
public void setFlag(int flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag == 1) {
synchronized (o1) {
System.out.println(Thread.currentThread().getName() + " o1");
try {
Thread.sleep(800);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println(Thread.currentThread().getName() + " o2");
}
}
}
if (flag == 2) {
synchronized (o2) {
System.out.println(Thread.currentThread().getName() + " o2");
try {
Thread.sleep(800);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println(Thread.currentThread().getName() + " o1");
}
}
}
}
public static void main(String[] args) {
DeadLock deadLock1 = new DeadLock();
DeadLock deadLock2 = new DeadLock();
deadLock1.setFlag(1);
Thread thread1= new Thread(deadLock1, "Thread1");
thread1.start();
deadLock2.setFlag(2);
Thread thread2= new Thread(deadLock2, "Thread2");
thread2.start();
}
}
说明
o1、o2是static类型属于整个累,所以当定义deadLock1、deadLock2时是公用o1、o2的
deadLock1设置flag=1,先锁住o1,然后睡800ms,此时
deadLock2设置flag=2,先锁住o2,然后睡800ms
然后deadLock1开始锁o2,不过o2还被deadLock2锁着,只好等待。。。
此时deadLock1开始锁01,不过o1还被deadLock1锁着,只好等待。。。
相互等待造成死锁
产生死锁的条件?如何避免死锁?
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
互斥条件:一个资源每次只能被一个进程使用。
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
避免死锁:
1)尽量避免使用多个锁,并且只有需要时才持有锁定位死锁,嵌套的 synchronized 或者 lock 非常容易出问题。
2)如果必须使用多个锁,尽量设计好锁的获取顺序,保证每个线程按同样的顺序加锁
3)使用带超时的方法,为程序带来更多可控性。Object.wait(…) 或者 CountDownLatch.await(…),都支持所谓的 timed_wait。或者中间件redis设置定时
4)尽量不要几个功能用同一把锁。
synchronized和ReentrantLock的区别
1)ReentrantLock 使用起来比较灵活,可以对获取锁的等待时间进行设置,可以获取各种锁的信息,但是必须有释放锁的配合动作;
2)ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
3)ReentrantLock 只适用于代码块锁,而 synchronized 可用于修饰方法、代码块等。
4)synchronized是关键字,ReentrantLock是类
5)Synchronized是依赖于JVM实现的,而ReenTrantLock是JDK实现的
6)ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。
7)ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
8)ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。
场景:
在确实需要一些 synchronized 所没有的特性的时候,比如时间锁等候、可中断锁等候、无块结构锁、多个条件变量或者锁投票。 ReentrantLock 还具有可伸缩性的好处,应当在高度争用的情况下使用它,但是请记住,大多数 synchronized 块几乎从来没有出现过争用,所以可以把高度争用放在一边。我建议用 synchronized 开发
synchronized和 Lock 的区别
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁(tryLock()方法:如果获取锁成功,则返回true),而synchronized却无法办到。
5)Lock可以提高多个线程进行读操作的效率。
6)在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized
wait() 和 sleep()方法有什么不同?

submit与execute区别
可以接受的任务类型
execute只能接受Runnable类型的任务
submit不管是Runnable还是Callable类型的任务都可以接受,但是Runnable返回值均为void,所以使用Future的get()获得的还是null
返回值
execute没有返回值
submit有返回值,所以需要返回值的时候必须使用submit
异常
execute中的是Runnable接口的实现,所以只能使用try、catch来捕获CheckedException,
不管提交的是Runnable还是Callable类型的任务,如果不对返回值Future调用get()方法,都会吃掉异常
stop()和 suspend()方法的区别
反对使用 stop(),是因为它不安全。它会解除由线程获取的所有锁定,而且如果对象处于一种不连贯状态,那么其他线程能在那种状态下检查和修改它们。
suspend()方法容易发生死锁。调用 suspend()的时候,目标线程会停下来,但却仍然持有在这之前获得的锁定。 此时,其他任何线程都不能访问锁定的资源,除非被“挂起”的线程恢复运行。
notify和notifyAll方法的区别
notify只会唤醒等待该锁的其中一个线程。notifyAll:唤醒等待该锁的所有线程。
1)永远在while循环里而不是if语句下使用wait。这样,循环会在线程睡眠前后都检查wait的条件,并在条件实际上并未改变的情况下处理唤醒通知。
2)永远在synchronized的函数或对象里使用wait、notify和notifyAll,不然Java虚拟机会生成 IllegalMonitorStateException。
CyclicBarrier和CountDownLatch的区别
1)CountDownLatch简单的说就是一个线程等待,直到他所等待的其他线程都执行完成并且调用countDown()方法发出通知后,当前线程才可以继续执行。
2)cyclicBarrier是所有线程都进行等待,直到所有线程都准备好进入await()方法之后,所有线程同时开始执行!
3)CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使用reset() 方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次。
4)CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量。isBroken方法用来知道阻塞的线程是否被中断。如果被中断返回true,否则返回false。
AQS原理:
AQS和Condition各自维护了不同的队列,在使用lock和condition的时候,其实就是两个队列的互相移动。如果我们想自定义一个同步器,可以实现AQS。它提供了获取共享锁和互斥锁的方式,都是基于对state操作而言的。
概念+实现:
ReentrantLock实现了Lock接口,是AQS( 一个用来构建锁和同步工具的框架, AQS没有 锁之 类的概念)的一种。加锁和解锁都需要显式写出,注意一定要在适当时候unlock。ReentranLock这个是可重入的。其实要弄明白它为啥可重入的呢,咋实现的呢。其实它内部自定义了同步器Sync,这个又实现了AQS,同时又实现了AOS,而后者就提供了一种互斥锁持有的方式。其实就是每次获取锁的时候,看下当前维护的那个线程和当前请求的线程是否一样,一样就可重入了。
和synhronized相比:
synchronized相比,ReentrantLock用起来会复杂一些。在基本的加锁和解锁上,两者是一样的,所以无特殊情况下,推荐使用synchronized。ReentrantLock的优势在于它更灵活、更强大,增加了轮训、超时、中断等高级功能。
1)可重入锁。可重入锁是指同一个线程可以多次获取同一把锁。ReentrantLock和synchronized都是可重入锁。
2)可中断锁。可中断锁是指线程尝试获取锁的过程中,是否可以响应中断。synchronized是
不可中断锁,而ReentrantLock则z,dz提供了中断功能。
3)公平锁与非公平锁。公平锁是指多个线程同时尝试获取同一把锁时,获取锁的顺序按照线程达到的顺序,而非公平锁则允许线程“插队”。synchronized是非公平锁,而ReentrantLock的默认实现是非公平锁,但是也可以设置为公平锁。
AtomicInteger底层实现原理?
AtomicIntger 是对 int 类型的一个封装,提供原子性的访问和更新操作,其原子性操作的实现是基于 CAS(compare-and-swap)技术。从 AtomicInteger 的内部属性可以看出,它依赖于 Unsafe 提供的一些底层能力,进行底层操作,以 volatile 的 value 字段,记录数值,以保证可见性,Unsafe 会利用 value 字段的内存地址偏移,直接完成操作。
voliate 的实现原理
volatile可以保证线程可见性且禁止指令重排序,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的, 加入volatile关键字时,汇编后会多出一个lock前缀指令。lock前缀指令其实就相当于一个内存屏障。。
happen-before原则保证了程序的“有序性,对volatile变量的写操作 happen-before 后续的读操作.
当读取一个被volatile修饰的变量时,会直接从共享内存中读,而非线程专属的存储空间中读。
当volatile变量写后,线程中本地内存中共享变量就会置为失效的状态,因此线程B再需要读取从主内存中去读取该变量的最新值。
对该变量的写操作之后,编译器会插入一个写屏障。对该变量的读操作之前,编译器会插入一个读屏障。
线程写入,写屏障会通过类似强迫刷出处理器缓存的方式,让其他线程能够拿到最新数值。
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
happens-before原则有哪些
程序顺序规则:单线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作;
锁定规则:一个unlock操作先行发生于对同一个锁的lock操作;
volatile变量规则:对一个Volatile变量的写操作先行发生于对这个变量的读操作;
线程启动规则:Thread对象的start()方法先行发生于此线程的其他动作;
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
线程终止规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
对象终结规则:一个对象的初始化完成先行发生于它的finalize()方法的开始;
传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
ThreadLocal
1.ThreadLocal是Java中提供的本地线程存储机制,可以利用该机制将数据缓存在某个线程内部,该线程可以在任意时刻,任意方法中获取缓存的数据
2.ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal对象)中都存在一个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value是需要缓存的值
3.如果在线程池中使用ThreadLocal会造成内存溢出,因为当ThreadLocal对象使用完后,应该要把设置的key和value进行回收,但是线程池中线程是不会被回收,而线程对象是通过强引用指向ThreadLocalMap,ThreadLocalMap也是通过强引用指向Entry对象,线程不被回收,Entry对象也不会被回收,从而出现内存溢出,解决的方法是在使用了ThreadLocal对象之后,手动调用ThreadLocal的remove方法,手工清除Entry对象
ThreadLocal如何为每个线程创建变量的副本
首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。
初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。
然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。
总结:
1)实际的通过ThreadLocal创建的副本是存储在每个线程自己的threadLocals中的;
2)为何threadLocals的类型ThreadLocalMap的键值为ThreadLocal对象,因为每个线程中可有多个threadLocal变量,就像上面代码中的longLocal和stringLocal;
3)在进行get之前,必须先set,否则会报空指针异常;
如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。 因为在上面的代码分析过程中,我们发现如果没有先set的话,即在map中查找不到对应的存储,则会通过调用setInitialValue方法返回i,而在setInitialValue方法中,有一个语句是T value = initialValue(), 而默认情况下,initialValue方法返回的是null。
什么是CAS
CAS是compare and swap的缩写,即我们所说的比较交换。
cas是一种乐观锁。CAS
操作包含三个操作数 ——内存位置(V)、预期原值(A)和新值(B)。 当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。
缺点:
1)ABA问题:一个线程a将数值改成了b,接着又改成了a,此时CAS认为是没有变化,其实是已经变化过了。
解决办法:可以使用版本号标识,每操作一次version加1。在java5中,已经提供了AtomicStampedReference来解决问题。
2)CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。
3)CAS造成CPU利用率增加。
什么是AQS
AQS是AbustactQueuedSynchronizer的简称,它是一个Java提供的底层同步工具类,用一个int类型的变量表示同步状态,并提供了一系列的CAS操作来管理这个同步状态。使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore
技术是 CAS自旋Volatile变量:它使用了一个Volatile成员变量表示同步状态,通过CAS修改该变量的值,修改成功的线程表示获取到该锁;若没有修改成功,或者发现状态state已经是加锁状态,则通过一个Waiter对象封装线程,添加到等待队列中,并挂起等待被唤醒。
AQS维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)