每日一狗(田园犬西瓜瓜)
Spring 框架中还有一次补充
线程是稀缺资源,如果被无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,合理的使用线程池对线程进行统一分配、调优和监控,线程池可以很好的解决这一问题。
引入
Java1.5中引入的Executor框架把任务的提交和执行进行解耦,只需要定义好任务,然后提交给线程池,而不用关心该任务是如何执行、被哪个线程执行,以及什么时候执行。
顶层接口
Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具,而真正的线程池接口是ExecutorService。
ExecutorService
ExecutorService是Java中对线程池定义的一个接口,它java.util.concurrent包中。Java API对ExecutorService接口的实现有两个(ThreadPoolExecutor和ScheduledThreadPoolExecutor),所以这两个即是Java线程池具体实现类。除此之外,ExecutorService还继承了Executor接口(注意区分Executor接口Executors工厂类),这个接口只有一个execute()方法。
名称 | 类型 | 含义 |
---|---|---|
corePoolSize | int | 核心线程池大小 |
maximumPoolSize | int | 最大线程池大小 |
keepAliveTime | long | 线程最大空闲时间 |
unit | TimeUnit | 时间单位 |
workQueue | BlockingQueue | 线程等待队列 |
threadFactory | ThreadFactory | 线程创建工厂 |
handler | RejectedExecutionHandler | 拒绝策略 |
使用有界队列时队列大小需和线程池大小互相配合,线程池较小有界队列较大时可减少内存消耗,降低cpu使用率和上下文切换,但是可能会限制系统吞吐量。
同步移交队列适合那些任务不能等的情况
任务队列、核心线程数、最大线程数的逻辑关系
阿里开发规范为什么不允许Executors快速创建线程池
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor方法,这样的处理方式让编程人员更加明确线程池的运行规则,规避资源耗尽的风险说明: Executors返回的线程池对象的弊端:
为什么使用线程池?
如何合理配置线程池的大小
任务性质可分为:CPU密集型任务,IO密集型任务,混合型任务。
可以先将线程池大小设置为参考值,再观察任务运行情况和系统负载、资源利用率来进行适当调整。
Runtime.getRuntime().availableProcessors():int获取CPU的核数
java.util.concurrent.ExecutorService是java线程池框架的主要接口,用Future保存任务的运行状态及计算结果,主要方法有:
void execute(Runnable)提交任务到线程池
Future submit(Runnable)提交任务到线程池并返回Future
Future submit(Runnable, T) 提交任务到线程池并返回Future,第二个参数会作为计算结果封装到Future
Future submit(Callable)提交任务到线程池并返回Future,call方法的计算结果会封装到Future
List invokeAll(Collection extends Callable>) 批量提交任务并返回计算结果
T invokeAny(Collectionextends Callable> tasks) 只执行其中一个任务并返回结果
void shutdown() 线程池不再接受新任务,继续运行正在执行中的任务及等待中的任务
List shutdownNow() 线程池不再接受新任务,继续运行正在执行中的任务,返回待执行的任务列表
shutdownNow:对正在执行的任务全部发出interrupt(),停止执行,对还未开始执行的任务全部取消,并且返回还没开始的任务列表,不建议使用
shutdown:当调用shutdown后,线程池将不再接受新的任务,但也不会去强制终止已经提交或者正在执行中的任务
**可缓存线程池:**newCachedThreadPool
实现:没有核心线程,工作线程数量为Integer.MAX_VALUE。阻塞队列使用同步队列SynchronousQueue没有提交了我不给分配线程执行那一说。
默认60秒就是线程的最大空闲时间,超过即被回收。
长期闲置不会消耗任何线程资源,因为最后活的时候,执行完毕的线程在闲置60S后就会被回收,到最后池子就剩一个空壳,没有一个线程。
优点:任务来了就一定会被分配线程进行执行,会根据线程的提交速度,来动态匹配线程数量。
缺点:工作线程数无上限,如果任务过多,可能会因为线程数量太多导致效率降低,毕竟线程之间的切换也是需要成本的;而且电脑很有可能因为同时存在的线程数量过多而直接瘫痪,这里就需要任务提交者自行把握,因为池子没在这里做限制。
用来创建一个可以无限扩大的线程池,适用于服务器负载较轻,执行很多短期异步任务。
**定长池子:**newFixedThreadPool
实现:核心线程数和最大线程数为固定传入参数值,阻塞队列使用LinkedBlockingQueue双链表阻塞队列(没有长度限制)。
当线程池中的某个线程失败而终止时,新的线程会代替它执行剩下的任务,这么说的话就是这个线程可能会进行更替,并不是老是那几个线程在哪里执行任务,可以使用异常将线程给终止掉,线程池会重新创建新的线程执行剩余的任务。
由于线程池的最大线程数量就是核心线程数的原因,这些线程都是不会被超时回收的,所以线程池中的线程只有池子调用shutdown函数式才会进行回收,程序才能结束。这可以算是一个常驻程序了。
**周期定长池子:**newScheduledThreadPool
**单线程池子:**newSingleThreadExecutor :保证执行顺序
**工作窃取算法的线程池:**newWorkStealingPool (适合将线程数量控制到于可用CPU核心树相同)
为每个工作线程分配一个双端队列(本地队列)用于存放需要执行的任务,当自己的队列没有数据的时候从其它工作者队列中获得一个任务继续执行。
这个线程池厉害了,他每一个线程都有一个阻塞队列,自己线程对应的阻塞队列中的任务完了,他会去偷其他线程对应的阻塞队列中的任务来执行(吾辈楷模)
创建持有足够数量线程的线程池来支持给定的并行级别,该方法还会使用多个队列来减少竞争,无参则是根据CPU个数定义并行级别。
核心思想是
work-stealing工作窃取,ForkJoinPool提供了一个更有效的利用线程的机制,当ThreadPoolExecutor还在用单个队列存放任务时,ForkJoinPool已经分配了与线程数相等的队列,当有任务加入线程池时,会被平均分配到对应的队列上,各线程进行正常工作,当有线程提前完成时,会从队列的末端窃取其他线程未执行完的任务,当任务量特别大时,CPU多的计算机会表现出更好的性能。
优点:快呀,而且拥有多个任务队列,可以减少连接数创建
缺点:这种情况就是让CPU专心去做一个线程中的事,适用与大耗时并行任务的执行。老换线程就没意思了。
创建一个拥有多个任务队列的线程池,可以减少连接数,创建当前可用cpu数量的线程来并行执行,适用于大耗时的操作,可以并行来执行
当线程在系统内运行时,线程的调度具有一定的透明性,程序通常无法精确控制线程的轮换执行,但Java也有一些机制来保证线程协调执行。
线程间通信的模型有两种:共享内存和消息传递
关键字来实现线程间相互通信是使用共享内存的思想,大致意思就是多个线程同时监听一个变量,当这个变量发生变化的时候 ,线程能够感知并执行相应的业务,而起对volatile修饰的共享资源的修改会通知其他拿了这个资源的线程赶紧过来再来重新拿一下。这也是最简单的一种实现方式。
Object类提供了线程间通信的方法:wait()、notify()、notifyaAll(),它们是多线程通信的基础,而这种实现方式的思想自然是线程间通信。
注意: wait和notify必须配合synchronized使用,wait方法释放锁,sleep方法不释放锁
public class TestSync {
public static void main(String[] args) {
// 定义一个锁对象,不是Lock接口
Object lock = new Object();
List<String> list = new ArrayList<>();
// 实现线程A
Thread threadA = new Thread(() -> {
synchronized (lock) {
for (int i = 1; i <= 10; i++) {
list.add("abc");
System.out.println("线程A向list中添加一个元素,此时list中的元素个数为:" + list.size());
try {
Thread.sleep(500); //wait
} catch (InterruptedException e) {
e.printStackTrace();
}
if (list.size() == 5)
lock.notify();// 唤醒B线程
}
}
});
// 实现线程B
Thread threadB = new Thread(() -> {
while (true) {
synchronized (lock) {
if (list.size() != 5) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程B收到通知,开始执行自己的业务...");
}
}
});
// 需要先启动线程B
threadB.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 再启动线程A
threadA.start();
}
}
线程B在被A唤醒之后由于没有获取锁还是不能立即执行,也就是说,A在唤醒操作之后,并不释放锁。
public class TestSync {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
List<String> list = new ArrayList<>();
// 实现线程A
Thread threadA = new Thread(() -> {
lock.lock();
for (int i = 1; i <= 10; i++) {
list.add("abc");
System.out.println("线程A向list中添加一个元素,此时list中元素个数为:" + list.size());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (list.size() == 5)
condition.signal();
}
lock.unlock();
});
// 实现线程B
Thread threadB = new Thread(() -> {
lock.lock();
if (list.size() != 5) {
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程B收到通知,开始执行自己的业务...");
lock.unlock();
});
threadB.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
threadA.start();
}
}
public class TestSync {
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(1);
List<String> list = new ArrayList<>();
// 实现线程A
Thread threadA = new Thread(() -> {
for (int i = 1; i <= 10; i++) {
list.add("abc");
System.out.println("线程A向list中添加一个元素,此时list中的元素个数为:" + list.size());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (list.size() == 5)
countDownLatch.countDown();
}
});
// 实现线程B
Thread threadB = new Thread(() -> {
while (true) {
if (list.size() != 5) {
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程B收到通知,开始执行自己的业务...");
break;
}
});
// 需要先启动线程B
threadB.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 再启动线程A
threadA.start();
}
}
LockSupport是一种非常灵活的实现线程间阻塞和唤醒的工具,使用它不用关注是等待线程先进行还是
唤醒线程先运行,但是得知道线程的名字。
public class TestSync {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
// 实现线程B
final Thread threadB = new Thread(() -> {
if (list.size() != 5) {
LockSupport.park();
}
System.out.println("线程B收到通知,开始执行自己的业务...");
});
// 实现线程A
Thread threadA = new Thread(() -> {
for (int i = 1; i <= 10; i++) {
list.add("abc");
System.out.println("线程A向list中添加一个元素,此时list中的元素个数为:" + list.size());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (list.size() == 5)
LockSupport.unpark(threadB);
}
});
threadA.start();
threadB.start();
}
}
1)发挥多核CPU的优势。随着工业的进步,现在的笔记本、台式机乃至商用的应用服务器至少也都是双核的,4核、8核甚至16核的也都不少见,如果是单线程的程序,那么在双核CPU上就浪费了50%,在4核CPU上就浪费了75%。单核CPU上所谓的多线程那是假的多线程,同一时间处理器只会处理一段逻辑,只不过线程之间切换得比较快,看着像多个线程"同时"运行罢了。多核CPU上的多线程才是真正的多线程,它能让你的多段逻辑同时工作,多线程,可以真正发挥出多核CPU的优势来,达到充分利用CPU的目的。
2)防止阻塞。从程序运行效率的角度来看,单核CPU不但不会发挥出多线程的优势,反而会因为在单核CPU上运行多线程导致线程上下文的切换,而降低程序整体的效率。但是单核CPU还是要应用多线程,就是为了防止阻塞。试想,如果单核CPU使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。
3)便于建模。假设有一个大的任务A,单线程编程,那么就要考虑很多,建立整个程序模型比较麻烦。但是如果把这个大的任务A分解成几个小任务,任务B、任务C、任务D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。
如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。
1)不可变:像String、Integer、Long这些,都是final类型的类,任何一个线程都改变不了它们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用。final能够做出如下保证:当你创建一个对象时,使用final关键字能够使得另一个线程不会访问到处于“部分创建”的对象。final类型的成员变量的值,包括那些用final引用指向的collections的对象,是读线程安全而无需使用synchronization的
2)绝对线程安全:不管运行时环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代价,Java中标注自己是线程安全的类,实际上绝大多数都不是线程安全的,不过绝对线程安全的类,Java中也有,比方说CopyOnWriteArrayList、CopyOnWriteArraySet
3)相对线程安全:相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种,add、remove方法都是原子操作,不会被打断,但也仅限于此,如果有个线程在遍历某个Vector、有个线程同时在add这个 Vector,99%的情况下都会出现ConcurrentModificationException,也就是fail-fast机制。
4)线程非安全:ArrayList、LinkedList、HashMap等都是线程非安全的类
ThreadLocal叫做线程变量,ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。通过get和set方法就可以得到当前线程对应的值。
ThreadLocal是除了加锁这种同步方式之外的另一种保证多线程访问变量时的线程安全的方法;如果每个线程对变量的访问都是基于线程自己的变量这样就不会存在线程不安全问题。
ThreadLocal
其实是与线程绑定的一个变量。ThreadLocal和Synchonized都用于解决多线程并发访问。
但是ThreadLocal与synchronized有本质的区别:
synchronized用于线程间的数据共享,ThreadLocal则用于线程间的数据隔离
synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享
对于多线程资源共享的问题,同步机制采用了以时间换空间的方式,而ThreadLocal采用了以空间换时间的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
每个线程需要有自己单独的实例
实例需要在多个方法中共享,但不希望被多线程共享
早期版本的ThreadLocal可以理解为一个Map。
当工作线程Thread实例向本地变量保持某个值时,会以key-value形式保存在ThreadLocal内部的Map中,其中Key为线程Thread实例,Value为待保存的值。
当工作线程Thread实例从ThreadLocal本地变量取值时,会以Thread实例为Key,获取其绑定的Value。
JDK1.8每个线程拥有一个ThreadLocalMap,ThreadLocalMap中元素Entry由key-value对组成,key为ThreadLocal对象,value为Object类型的值。
Entry继承自WeakReference,并且在Entry构造函数中,key也就是ThreadLocal被设置为弱引用,弱引用在垃圾回收时会被回收
当线程往ThreadLocal中设置值时,实际上是向自己的ThreadLocalMap中以设置值,调用ThreadLocal.get取数据时,也是以ThreadLocal为key去ThreadLocalMap中查找,查到后变返回了。
ThreadLocal用于连接ThreadLocalMap和Thread,来处理Thread的TheadLocalMap属性,包括init初始化属性赋值、get对应的变量,set设置变量等。通过当前线程,获取线程上的ThreadLocalMap属性,对数据进行get、set等操作
ThreadLocalMap用来存储数据,采用类似hashmap机制,存储了以threadLocal为key,需要隔离的数据为value的Entry键值对数组结构。HashMap使用链表+红黑树解决hash冲突,ThreadLocalMap初始化容积16,负载因子2/3,使用线性探测算法(开放寻址)解决hash冲突
ThreadLocal有个ThreadLocalMap类型的属性,存储的数据就放在这儿。
1)每个Thread维护着一个ThreadLocalMap的引用
2)ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储
3)ThreadLocal创建的副本是存储在自己的threadLocals中的,也就是自己的ThreadLocalMap。4)ThreadLocalMap的键值为ThreadLocal对象,而且可以有多个threadLocal变量,因此保存在 map中数据隔离的秘诀其实是这样的,Thread有个TheadLocalMap类型的属性,叫做threadLocals,该属性用来保存该线程本地变量。这样每个线程都有自己的数据,就做到了不同线程间数据的隔离,保证了数据安全。
5)在进行get之前,必须先set,否则会报空指针异常,当然也可以初始化一个,但是必须重写initialValue()方法
6)ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。
阻塞队列与平常接触的普通队列LinkedList或ArrayList等的最大不同点,在于阻塞队列支出阻塞添加和阻塞删除方法。
队列 | 是否阻塞 | 是否有界 | 如何保证线程安全 | 适用场景 | 注意事项 |
---|---|---|---|---|---|
ArrayBlockingQueue | 阻塞 | 有界 | 一把全局锁 | 生产者消费者模型,平和二者处理速度 | 先进先出,有界,不支持元素为空 |
LinkedBockingQueue | 阻塞 | 无界可配置 | 存储采用两把锁 | 生产者消费者模型,平和二者处理速度 | 无界时注意OOM问题 |
ConcurrentLinkedQueue | 非阻塞 | 无界 | CAS | 对全局的几个进行操作 | size()需要遍历集合,慎用 |
集合和数组之间互相转换
List list=new ArrayList();
Object[] arr=list.toArray(); // 将集合转换为数组
Arrays.asList(arr):List // 可以将一个数组装换为集合,务必注意这里返回的ArrayList不是java.util.ArrayList
static void sort(简单数据类型数组)
排序static int binarySearch
折半查找,要求有序static void fill
使用特定数据填充整个数组(每个数组的元素的值都是这个值)static T[] copyOf
数组复制,返回新数组1、 Synchronized 用过吗,其原理是什么?
Synchronized 经常在解决线程安全问题中使用。
Java中的Synchronized底层实现使用的是系统的排它锁进行实现的。
在对象头的work区域最后两个标志位标识了锁的类型,1.6后synchronized引入了偏向锁和轻量级锁的机制,最后四位可以标识区分偏向锁和无锁,最后两位可以区分轻量级锁、重量级锁、GC标志和非这仨玩意。
2、 你刚才提到获取对象的锁,这个“锁”到底是什么?如何确定对象的锁?
这个锁是啥?资源呀,进入同步程序必须要获取对应的锁资源才能继续执行,否则会进行阻塞。
确定对象锁首相要看锁对象是那个,这就得说道synchronized的三种使用方法了,同步代码块锁对象是指定的对象,同步方法的所对象是实例对象,同步静态方法是类对象,不管是那个同步程序,只要保证所对象是唯一的就可保证线程安全。
3、 什么是可重入性,为什么说 Synchronized 是可重入锁?
可重入性就是我自己有这一把锁,我在此进入其对应的同步程序的时候申请就直接拿到,就可以直接进入执行其对应的同步程序。
Synchronized从底层上看,他申请的时候就是在其对象头monitor中的计数器count给加上1,当然第一次申请的时候这个计数器必须为0时才能尝试加一,之后的重入就是再此基础上申请一次加一次1,退出同步程序时会同步给这个计数器减1,直到计数器减到0这个锁才算是被完全释放,这样其他线程才可以申请尝试获取锁。
而已排他锁的可重入性对其非常重要,如果不可重入,好家伙,自己申请到了锁,然后自己再此申请,自己把自己卡出,单线程写出死锁属于是有点东西。
4、 JVM 对 Java 的原生锁做了哪些优化?
1.6版本之前的Synchronized是直接使用的是重量级锁,但是这样基本就没人用了,因为重量级锁的线程切换需要在用户态和核心太来回切换,这个切换也是相当的耗费资源的,1.6后为了提高锁的效率引入了偏向锁和轻量级锁,就是1.6后虚拟机看到你这个synchronized同步程序,不着急给你上重量级锁,你一个线程来获取锁资源,我在锁对象的对象头中的PID位存储上你的线程ID值,象征性的标识一下这把锁是属于你的,同时这把锁也只有这个线程能够申请到,这就是偏向锁;当第二个线程也要来申请锁资源的时候,第二个线程发现来晚了呀,跑去忙等一会,此时这个锁会从偏向锁升级成轻量级锁,第二个线程忙等结束后发现第一个线程已经将锁释放了,赶紧申请;当自旋失败的次数达到一定阈值的时候时,这把锁会升级成重量级锁,这时来申请锁的线程申请失败后就去锁对象的阻塞队里中睡觉去了,等待有线程释放锁后来将其唤醒。
这里为啥轻量级锁一定要升级成重量级锁的原因就是轻量级锁的核心操作自旋忙等,自旋忙等是不会释放CPU资源的,执行的也是无意义的操作,也就是说CPU这会还是挺忙的,那着不行呀。这种自旋就是牺牲一部分CPU资源以维持上下文状态的,一旦自旋次数过多就得不偿失了。
5、 为什么说 Synchronized 是非公平锁?
因为他本身就是非公平锁呀!在底层是来一个线程看所对象的计数器为0就直接尝试加锁,但是这个时候阻塞线程中如果还有等待的线程,这显然就不公平呀,要是这后来的给加上了,阻塞队列里的线程相当于是被插队了。
这不用说就是不公平的了
错:6、 什么是锁消除和锁粗化?
锁消除:就是虚拟机在执行过程中发现你这个锁根本没啥用,他就给你
锁粗化:无锁->偏向锁->轻量级锁->重量级锁
7、 为什么说 Synchronized 是一个悲观锁?乐观锁的实现原理又是什么?什么是 CAS,它有什么特性?
悲观锁:我认为你这个临界资源一定会发生线程安全问题。
synchronized就是典型的悲观锁,只要被修饰了,第一个线程来了就直接是偏向锁,这还不悲观
乐观锁:与悲观锁刚好相反,我认为这个临界资源不会发生线程安全问题,就算发生了我自己有者能解决。
CAS:对比和交换,这个翻译就已经说的很明白了,拿出来的时候在自己工作内存中备份一份预期值,到时候算完了要写回的时候进行一次判定,如果预期值和当前值相同,则将要写入的数据写入主内存中的共享数据;如果不是那就说明发生了线程安全问题,就不能写入,需要另行处理。
8、 乐观锁一定就是好的吗?
不是
乐观锁的典型实现CAS
乐观锁适合那种经常读的临界资源的线程安全问题。悲观锁的读写也是互相之间隔离的才能保证线程安全,乐观锁是只有在写入操作过多时会降低效率,读取操作相互之间并不会出现问题。
9、 跟 Synchronized 相比,可重入锁 ReentrantLock 其实现原理有什么不同?
Synchronized使用的是系统提供的排他锁来实现的,可重入锁ReentrantLock底层使用Sync(抽象队列同步器的子类来实现的)
10、 那么请谈谈 AQS 框架是怎么回事儿?
AQS:抽象队列同步器
11、 请尽可能详尽地对比下 Synchronized 和 ReentrantLock 的异同。
12、 ReentrantLock 是如何实现可重入性的?
13、 除了 ReetrantLock,你还接触过 JUC 中的哪些并发工具?