目录
十八:为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
二十五:你是如何调用wait()方法的,在if块还是循环,为什么?
二十六:为什么线程通信的方法 wait(), notify()和 notifyAll()被定义在Object 类里?而sleep定义在Thread类里面?
二十七:为什么 wait(),notify(),notifyAll()必须在同步方法或者同步块中被调用?
三十一:Java 中 interrupted 和 isInterrupted 方法的区别?
三十七:说说自己是怎么使用 synchronized 关键字,在项目中用到了吗
三十九:Synchronized优化---锁粗化,锁消除,锁升级
四十二:线程池的拒绝策略********************
四十六:Java实现CAS的原理 - Unsafe类********
五十三:谈谈对AQS(AbstractQueuedSynchronizer)理解
(3)获取资源acquire,addWriter,acquireQueued
五十六:线程池的原理---ThreadPoolExecutor
五十九:*****ThreadPoolExcuter是如何做到线程复用的
六十一:JAVA定时任务--ScheduledThreadPoolExecutor
一:并发编程的优点
-
充分利用多核CPU的计算能力:通过并发编程的形式可以将多核CPU 的计算能力发挥到极致,性能得到提升
- 方便进行业务拆分,提升应用性能现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分
二:并发编程的缺点
-
并发编程的目的就是为了能提高程序的执行效率,提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如 **:内存泄漏、上下文切换、线程安全、死锁**等问题
三:并发编程三要素是什么?
- 原子性:原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。
- 可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。 (synchronized,volatile)
- 有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)
四:在 Java 程序中怎么保证多线程的运行安全?
四(1)voliate为什么不能保证原子性
可见性与Java的内存模型有关,模型采用缓存与主存的方式对变量进行操作,也就是说,每个线程都有自己的缓存空间,对变量的操作都是在缓存中进行的,之后再将修改后的值返回到主存中,这就带来了问题,有可能一个线程在将共享变量修改后,还没有来的及将缓存中的变量返回给主存中,另外一个线程就对共享变量进行修改,那么这个线程拿到的值是主存中未被修改的值,这就是可见性的问题。
Volatile 是轻量级的 synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
volatile很好的保证了变量的可见性,变量经过volatile修饰后,对此变量进行写操作时,汇编指令中会有一个LOCK前缀指令,这个不需要过多了解,但是加了这个指令后,会引发两件事情:
(1)将当前处理器缓存行的数据写回到系统内存
(2)这个写回内存的操作会使得在其他处理器缓存了该内存地址无效
什么意思呢?意思就是说当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值,这就保证了可见性。
问题来了,既然它可以保证修改的值立即能更新到主存,其他线程也会捕捉到被修改后的值,那么为什么不能保证原子性呢?
首先需要了解的是,Java中只有对基本类型变量的赋值和读取是原子操作,如i = 1的赋值操作,但是像j = i或者i++这样的操作都不是原子操作,因为他们都进行了多次原子操作,比如先读取i的值,再将i的值赋值给j,两个原子操作加起来就不是原子操作了。
所以,如果一个变量被volatile修饰了,那么肯定可以保证每次读取这个变量值的时候得到的值是最新的,但是一旦需要对变量进行自增这样的非原子操作,就不会保证这个变量的原子性了。
i++操作,线程A 和线程 B 都执行完了加操作,都还没刷新到主存,此时其中一个线程执行了写入操作,强制刷新主存,对于另一个线程来说,自身的缓存变得无效,需要和主存一致,但自身的加法操作已经执行过了,而volatile 无法像 MVCC 一样保证加操作再执行一遍。这样就出现了漏算结果的操作。
四(2)volatile禁止指令重排序的原理
首先我们就需要了解4个内存屏障,他们分别是
(1)LoadLoad:禁止下面的所有的普通读操作喝上面的voliate读重排序
(2)LoadStore:禁止下面的所有普通写操作喝上面的voliate读重排序
(3)StroeStore:禁止上面的所有普通写操作和下面的voliate写重排序
(4)StroeLoad:禁止下面的voliate写操作和下面可能有的voliate读写操作重排序
五:并行和并发有什么区别?
-
并发:多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行
-
并行:单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的“同时进行
-
串行:有n个任务,由一个线程按顺序执行。由于任务、方法都在一个线程执行所以不存在线程不安全情况,也就不存在临界区的问题。
六:什么是JMM模型
Java内存模型(Java Memory Model简称JMM)是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段 和构成数组对象的元素)的访问方式。
JVM运行程序的实体是线程,而每个线程创建时 JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据;
而Java 内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问, 但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自 己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝。
前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必 须通过主内存来完成。
七:什么是多线程,多线程的优劣?
多线程是指一个程序含有多个执行流,即一个程序可以同时运行多个线程来执行多个不同的任务
多线程的好处
可以提高CPU的利用率;在多线程环境中,一个线程必须等待时,CPU可以运行其他的线程而不是等待,这样就大大提高了程序的运行效率,
八:什么是进程和线程
一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以拥有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进程。
线程:
进程中的一个执行任务(控制单元) ,负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可以共享数据。
九:进程和线程的区别
十:什么是上下文切换
十一:用户线程和非守护线程
在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程)
用个比较通俗的比如,任何一个守护线程都是整个JVM中所有非守护线程的保姆:
Thread daemonTread = new Thread();
// 设定 daemonThread 为 守护线程,default false(非守护线程)
daemonThread.setDaemon(true);
// 验证当前线程是否为守护线程,返回 true 则为守护线程
daemonThread.isDaemon();
只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个
非守护线程结束时,守护线程随着JVM一同结束工作。
如果 JVM 中没有一个正在运行的非守护线程,这个时候,JVM 会退出。换句话说,守护线程拥有
自动结束自己生命周期的特性,而非守护线程不具备这个特点
Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器),它就是一个很称职的守护者。如果说不具备该特性,会发生什么呢?
当 JVM 要退出时,由于垃圾回收线程还在运行着,导致程序无法退出,这就很尴尬了!!!由此可见,守护线程的重要性了。
通常来说,守护线程经常被用来执行一些后台任务,但是呢,你又希望在程序退出时,或者说 JVM 退出时,线程能够自动关闭,此时,守护线程是你的首选。
User和Daemon两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果 User Thread已经
全部退出运行了,只剩下Daemon Thread存在了,虚拟机也就退出了。 因为没有了被守护者,
Daemon也就没有工作可做了,也就没有继续运行程序的必要了。
十二:形成死锁的四个必要条件
(1)互斥条件:进程对于所分配的资源具有排他性,即一个资源只能被一个线程占用,直到该资源被释放
(2)请求与保持条件:一个资源因请求被占用资源而发送阻塞时,对已获得的资源保持不放
(3)不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只能自己使用完之后释放资源
(4)循环等待条件:当发生死锁时,所等待的线程必定会形成一个环路,造成永久堵塞。
十三:如何避免线程死锁
我们只要破坏产生死锁的四个必要条件中的其中一个就可以了
十四:创建线程的四种方式
-
继承 Thread 类;
package java.lang;
public class Thread implements Runnable {
// 构造方法
public Thread(Runnable target);
public Thread(Runnable target, String name);
public synchronized void start();
}
public class Main {
public static void main(String[] args) {
new MyThread().start();
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "\t" +
Thread.currentThread().getId());
}
}
- 实现 Runnable 接口;
package java.lang;
@FunctionalInterface
public interface Runnable {
pubic abstract void run();
}
public class Main {
public static void main(String[] args) {
// 将Runnable实现类作为Thread的构造参数传递到Thread类中,然后启动Thread类
MyRunnable runnable = new MyRunnable();
new Thread(runnable).start();
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "\t" +
Thread.currentThread().getId());
}
}
- 实现 Callable 接口;重写call()方法,然后包装成java.util.concurrent.FutureTask, 再然后包装成Thread
public class Main {
public static void main(String[] args) throws Exception {
// 将Callable包装成FutureTask,FutureTask也是一种Runnable
MyCallable callable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(callable);
new Thread(futureTask).start();
// get方法会阻塞调用的线程
Integer sum = futureTask.get();
System.out.println(Thread.currentThread().getName() + Thread.currentThread().getId() + "=" + sum);
}
}
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId() + "\t" + new Date() + " \tstarting...");
int sum = 0;
for (int i = 0; i <= 100000; i++) {
sum += i;
}
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId() + "\t" + new Date() + " \tover...");
return sum;
}
}
-
使用 Executors 工具类创建线程池
public class Main {
public static void main(String[] args) throws Exception {
Executor executor = Executors.newFixedThreadPool(5);
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread()+"创建线程的第四种方法");
}
});
((ExecutorService) executor).shutdown();
}
}
十五:继承Thread和实现Runnable区别
可以看到第一种方法和第二种方法这两种方式都是围绕着Thread和Runnable,继承Thread类把
run()写到类中,实现Runnable接口,是把run()方法写到接口中然后再用Thread类来包装, 两种方
式最终都是调用Thread类的start()方法来启动线程的。
两种方式在本质上没有明显的区别,在外观上有很大的区别,第一种方式是继承Thread类,因
Java是单继承,如果一个类继承了Thread类,那么就没办法继承其它的类了,在继承上有一点受
制,有一点不灵活,第二种方式就是为了解决第一种方式的单继承不灵活的问题,所以平常使用就
使用第二种方式
十六:实现Runnable和Callable的区别
run方法没有返回值,call方法必须有返回值。
run方法无法抛出异常,call方法可以抛出checked exception。
Callable和Runnable都可以应用于executors。而Thread类只支持Runnable.
十七:线程的Run()和Start()有什么区别
十八:为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
new 一个 Thread,线程进入了新建状态。调用 start() 方法,会启动一个线程并使线程进入了就绪
状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行
run() 方法的内容,这是真正的多线程工作。
十九:什么是 Callable 和 Future?
二十:sleep() 和 wait() 有什么区别?
- 类的不同:sleep() 是 Thread线程类的静态方法,wait() 是 Object类的方法。
- 是否释放锁:sleep() 不释放锁;wait() 释放锁。
- 用途不同:Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
- 用法不同:wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。
二十一:说说线程的生命周期和五种基本状态
- 新建(new):新创建了一个线程对象
- 可运行(runnable):线程对象创建后,调用start()方法,该线程处于就绪状态,等待被线程调度使用,获取CPU的使用权
- 运行(running):可运行状态的线程(runnable)的线程获取了cpu时间片,执行程序代码;
- 阻塞(block):处于运行状态的线程因为某种原因,暂时放弃对cpu的使用权,进入阻塞状态,直到其进入到就绪状态,才有机会被cpu调用以进入到运行状态。
- 死亡(dead):线程run()或者main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。线程的死亡不可复生。
二十二: Java 中用到的线程调度算法是什么?
二十三:什么是线程调度器和时间片
线程调度器是一个操作系统服务,它负责为 Runnable 状态的线程分配 CPU 时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。 时间分片是指将可用的 CPU 时间分配给可用的 Runnable 线程的过程。分配CPU 时间可以基于线程优先级或者线程等待的时间。 线程调度并不受到 Java 虚拟机控制,所以由应用程序来控制它是更好的选择
(也就是说不要让你的程序依赖于线程的优先级)
二十四:请说出线程同步与线程调度的相关方法
(1)wait():使线程处于一个等待(阻塞)状态,并且释放所持有的对象的锁
(2)sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常
(3)notify():唤醒一个处于等待的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待的线程,而是由JVM确定唤醒哪一个线程,并且与优先级无关
(4)notifyAll():唤醒所有处于等待的线程,该方法并不是将对象的锁给所有线程,而是让他们竞争,只有获得锁的线程才可以进入就绪状态;
二十五:你是如何调用wait()方法的,在if块还是循环,为什么?
因为正在等待的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足情况的条件下退出;
二十六:为什么线程通信的方法 wait(), notify()和 notifyAll()被定义在Object 类里?而sleep定义在Thread类里面?
JAVA提供的锁是对象级的而不是线程级的,每个对象都有个锁,而线程是可以获得这个对象的。因此线程需要等待某些锁,那么只要调用对象中的wait()方法便可以了。而wait()方法如果定义在Thread类中的话,那么线程正在等待的是哪个锁就不明确了。这也就是说wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中是因为锁是属于对象的原因。
sleep的作用是:让线程在预期的时间内执行,其他时候不要来占用CPU资源。从上面的话术中,便可以理解为sleep是属于线程级别的,它是为了让线程在限定的时间后去执行。而且sleep方法是不会去释放锁的
二十七:为什么 wait(),notify(),notifyAll()必须在同步方法或者同步块中被调用?
二十八:notify()和notifyAll()有什么区别
如果线程调用了对象的wait()方法,那么线程便会处于该对象的等待池中,等待线程池中的对象不会去竞争对象的锁。
notify()只会唤醒一个线程,notifyAll()会唤醒所有的线程
notiayAll()调用后,会将所有线程由等待池移动到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功就会继续锁被释放,继续参与竞争;
notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。
二十九:sleep()和yield()的区别
sleep与yield都属于暂停线程。都是静态方法,直接写在线程体中。
sleep()可以理解为“抱着资源睡觉”,由原来的运行状态进入阻塞状态,当时间到达,再由阻塞状态回到就绪状态,等待CPU的调度。
yield()直接由运行状态跳回就绪状态,表示退让线程,让出CPU,让CPU调度器重新调度。礼让可能成功,也可能不成功,也就是说,回到调度器和其他线程进行公平竞争。
三十:如何停止一个正在运行的线程
三十一:Java 中 interrupted 和 isInterrupted 方法的区别?
(
)
三十二:同步方法和同步块哪个更好
三十三:Java 线程数过多会造成什么异常?
三十四:线程类的构造方法、静态块是被哪个线程调用的
三十五:为什么代码会重排序?
三十六:synchronized 的作用?
在 Java 中,synchronized 关键字是用来控制线程同步的,就是在多线程的环境下,控制 synchronized 代码段不被多个线程同时执行。synchronized 可以修饰类、方法、变量。
三十七:说说自己是怎么使用 synchronized 关键字,在项目中用到了吗
三十八:说一下 synchronized 底层实现原理?
(1)monitorenter monitorexit
synchronized是Java中的一个关键字,在使用的过程中并没有看到显示的加锁和解锁过程。因此有必要通过javap命令,查看相应的字节码文件。
通过JDK 反汇编指令 javap -c -v SynchronizedDemo
(2)为什么会有两个monitorexit呢?
(3)synchronized可重入的原理
三十九:Synchronized优化---锁粗化,锁消除,锁升级
锁粗化
互斥的临界区范围应该尽可能小,这样做的目的是为了使同步的操作数量尽可能缩小,缩短阻塞时间,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。
但是加锁解锁也需要消耗资源,如果存在一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,锁粗化就是将「多个连续的加锁、解锁操作连接在一起」,扩展成一个范围更大的锁,避免频繁的加锁解锁操作
锁消除
Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,经过逃逸分析(对象在函数中被使用,也可能被外部函数所引用,称为函数逃逸),去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的时间消耗。
代码中使用Object作为锁,但是Object对象的生命周期只在incrFour()函数中,并不会被其他线程所访问到,所以在J I T编译阶段就会被优化掉(此处的Object属于没有逃逸的对象)。
四十:什么是自旋
四十一:如果你提交任务时,线程池队列已满,这时会发生什么
(1)如果使用的是无界队列LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为 LinkedBlockingQueue 可以近乎认为是一个无穷大的队列,可以无限存放任务
(2)如果使用的是有界队列例如ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue 中,ArrayBlockingQueue 满了,会根据 maximumPoolSize 的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue 继续满,那么则会使用拒绝策略RejectedExecutionHandler 处理满了的任务,默认是 AbortPolicy
四十二:线程池的拒绝策略********************
(1)CallerRunsPolicy
当有新任务提交后,如果线程池没被关闭且没有能力执行,则把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务。
(2)AbortPolicy
丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略。必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行。
(3)DiscardPolicy
直接丢弃,啥也不说
(4)DiscardOldestPolicy
要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入
四十三:了解单例模式吗,来手写一个
懒汉式(线程不安全)
public class Singleton{
private Singleton(){}
private static Singleton single=null;
public static Singleton getInstance(){
if(single==null){
single=new Singleton();
}
return single;
}
}
双重检查锁定
public class Singleton{
private SingleTon(){}
private static Singleton single=null;
private static Singleton getInstance(){
if(singleton==null){
synchronized(Singleton.class){
if(singleton==null){
singleton=new SingleTon();
}
}
}
return single;
}
}
第一次校验:也就是第一个if(singleton==null),这个是为了代码提高代码执行效率,由于单例模式只要一次创建实例即可,所以当创建了一个实例之后,再次调用getInstance方法就不必要进入同步代码块,不用竞争锁。直接返回前面创建的实例即可。
第二次校验:也就是第二个if(singleton==null),这个校验是防止二次创建实例,假如有一种情况,当singleton还未被创建时,线程t1调用getInstance方法,由于第一次判断singleton==null,此时线程t1准备继续执行,但是由于资源被线程t2抢占了,此时t2页调用getInstance方法,同样的,由于singleton并没有实例化,t2同样可以通过第一个if,然后继续往下执行,同步代码块,第二个if也通过,然后t2线程创建了一个实例singleton。此时t2线程完成任务,资源又回到t1线程,t1此时也进入同步代码块,如果没有这个第二个if,那么,t1就也会创建一个singleton实例,那么,就会出现创建多个实例的情况,但是加上第二个if,就可以完全避免这个多线程导致多次创建实例的问题。
饿汉式
饿汉式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以天生是线程安全的。
public clsss Singleton(){
private Singleton();
private static final Singleton single=new Singleton();
public static Singleton getInstance(){
return single;
}
}
四十四:各种锁
(无状态锁,偏向锁、轻量级锁、重量级锁的升级以及区别)
首先通过一个小例子来解释一下三种锁的区别:
假如家里只有一个碗,当我自己在家时,没有人会和我争碗,这时即为偏向锁状态
当我和女朋友都在家吃饭时,如果女朋友不是很饿,则她会等我吃完再用我的碗去吃饭,这就是轻量级锁状态
当我和女朋友都很饿的时候,这时候就会去争抢这唯一的一个碗(贫穷的我)吃饭,这就是重量级锁状态
(1)对象头
(2)偏向锁
⼤⽩话就是对锁置个变量,如果发现为 true ,代表资源⽆竞争,则⽆需再⾛各种加锁/ 解锁流程。如果为 false ,代表存在其他线程竞争资源,那么就会⾛后⾯的流程。
实现原理
- 成功,表示之前的线程不存在了, Mark Word⾥⾯的线程ID为新线程的ID,锁不会升级,仍然为偏向锁;
- 失败,表示之前的线程仍然存在,那么暂停之前的线程,设置偏向锁标识为0,并设置锁标志位为00,升级为轻量级锁,会按照轻量级锁的⽅式进⾏竞争锁。
(3)轻量级锁
⾃旋:不断尝试去获取锁,⼀般⽤循环来实现。
(4)重量级锁
Contention List :所有请求锁的线程将被⾸先放置到该竞争队列Entry List : Contention List 中那些有资格成为候选⼈的线程被移到 Entry ListWait Set :那些调⽤ wait ⽅法被阻塞的线程被放置到 Wait SetOnDeck :任何时刻最多只能有⼀个线程正在竞争锁,该线程称为 OnDeckOwner :获得锁的线程称为 Owner!Owner :释放锁的线程
四十五:锁的升级过程
- 检测Mark Word里面是不是当前线程的ID,如果是则表示当前线程处于偏向锁;
- 如果不是,则使用CAS将当前线程的ID替换Mard Word,如果成功则表示当前线程获得偏向锁,置偏向标志位1;
- 如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁;
- 当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁;
- 如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁;
- 如果自旋成功则依然处于轻量级状态;
- 如果自旋失败,则升级为重量级锁;
四十六:哪些是乐观锁,哪些是悲观锁
四十七:什么是CAS机制?
V :要更新的变量 (var)E :预期值 (expected)N :新值 (new)
我们先看一段代码:
启动两个线程,每个线程中让静态变量count循环累加100次。
最终输出的count结果一定是200吗?因为这段代码是非线程安全的,所以最终的自增结果很可能会小于200。我们再加上synchronized同步锁,再来看一下。
加了同步锁之后,count自增的操作变成了原子性操作,所以最终输出一定是count=200,代码实现了线程安全。虽然synchronized确保了线程安全,但是在某些情况下,这并不是一个最有的选择。
关键在于性能问题。
synchronized关键字会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。
尽管JAVA 1.6为synchronized做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过过度,但是在最终转变为重量级锁之后,性能仍然比较低。所以面对这种情况,我们就可以使用java中的“原子操作类”。
所谓原子操作类,指的是java.util.concurrent.atomic包下,一系列以Atomic开头的包装类。如AtomicBoolean,AtomicUInteger,AtomicLong。它们分别用于Boolean,Integer,Long类型的原子性操作。
现在我们尝试使用AtomicInteger类:
使用AtomicInteger之后,最终的输出结果同样可以保证是200。并且在某些情况下,代码的性能会比synchronized更好。
而Atomic操作类的底层正是用到了“CAS机制”。
CAS是英文单词Compare and Swap的缩写,翻译过来就是比较并替换。
我们看一个例子:
1. 在内存地址V当中,存储着值为10的变量。
2. 此时线程1想把变量的值增加1.对线程1来说,旧的预期值A=10,要修改的新值B=11.
3. 在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。
4. 线程1开始提交更新,首先进行A和地址V的实际值比较,发现A不等于V的实际值,提交失败。
5. 线程1 重新获取内存地址V的当前值,并重新计算想要修改的值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。
6. 这一次比较幸运,没有其他线程改变地址V的值。线程1进行比较,发现A和地址V的实际值是相等的。
7. 线程1进行交换,把地址V的值替换为B,也就是12.
从思想上来说,synchronized属于悲观锁,悲观的认为程序中的并发情况严重,所以严防死守,CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去重试更新。
在java中除了上面提到的Atomic系列类,以及Lock系列类夺得底层实现,甚至在JAVA1.6以上版本,synchronized转变为重量级锁之前,也会采用CAS机制。
四十五:CAS的缺点
1) CPU开销过大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。
2) 不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。
3) ABA问题
这是CAS机制最大的问题所在。(后面有介绍)
我们下面来介绍一下两个问题:
1. JAVA中CAS的底层实现
2. CAS的ABA问题和解决办法。
我们看一下AtomicInteger当中常用的自增方法incrementAndGet:
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
private volatile int value;
public final int get() {
return value;
}
这段代码是一个无限循环,也就是CAS的自旋,循环体中做了三件事:
1. 获取当前值
2. 当前值+1,计算出目标值
3. 进行CAS操作,如果成功则跳出循环,如果失败则重复上述步骤
这里需要注意的重点是get方法,这个方法的作用是获取变量的当前值。
如何保证获取的当前值是内存中的最新值?很简单,用volatile关键字来保证(保证线程间的可见性)。我们接下来看一下compareAndSet方法的实现:
compareAndSet方法的实现很简单,只有一行代码。这里涉及到两个重要的对象,一个是unsafe,一个是valueOffset。
什么是unsafe呢?Java语言不像C,C++那样可以直接访问底层操作系统,但是JVM为我们提供了一个后门,这个后门就是unsafe。unsafe为我们提供了硬件级别的原子操作。
至于valueOffset对象,是通过unsafe.objectFiledOffset方法得到,所代表的是AtomicInteger对象value成员变量在内存中的偏移量。我们可以简单的把valueOffset理解为value变量的内存地址。
我们上面说过,CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
而unsafe的compareAndSwapInt方法的参数包括了这三个基本元素:valueOffset参数代表了V,expect参数代表了A,update参数代表了B。
正是unsafe的compareAndSwapInt方法保证了Compare和Swap操作之间的原子性操作。
我们现在来说什么是ABA问题。假设内存中有一个值为A的变量,存储在地址V中。
此时有三个线程想使用CAS的方式更新这个变量的值,每个线程的执行时间有略微偏差。线程1和线程2已经获取当前值,线程3还未获取当前值。
接下来,线程1先一步执行成功,把当前值成功从A更新为B;同时线程2因为某种原因被阻塞住,没有做更新操作;线程3在线程1更新之后,获取了当前值B。
在之后,线程2仍然处于阻塞状态,线程3继续执行,成功把当前值从B更新成了A。
最后,线程2终于恢复了运行状态,由于阻塞之前已经获得了“当前值A”,并且经过compare检测,内存地址V中的实际值也是A,所以成功把变量值A更新成了B。
看起来这个例子没啥问题,但如果结合实际,就可以发现它的问题所在。
我们假设一个提款机的例子。假设有一个遵循CAS原理的提款机,小灰有100元存款,要用这个提款机来提款50元。
由于提款机硬件出了点问题,小灰的提款操作被同时提交了两次,开启了两个线程,两个线程都是获取当前值100元,要更新成50元。
理想情况下,应该一个线程更新成功,一个线程更新失败,小灰的存款值被扣一次。
线程1首先执行成功,把余额从100改成50.线程2因为某种原因阻塞。这时,小灰的妈妈刚好给小灰汇款50元。
线程2仍然是阻塞状态,线程3执行成功,把余额从50改成了100。
线程2恢复运行,由于阻塞之前获得了“当前值”100,并且经过compare检测,此时存款实际值也是100,所以会成功把变量值100更新成50。
原本线程2应当提交失败,小灰的正确余额应该保持100元,结果由于ABA问题提交成功了。
怎么解决呢?加个版本号就可以了。
真正要做到严谨的CAS机制,我们在compare阶段不仅要比较期望值A和地址V中的实际值,还要比较变量的版本号是否一致。
我们仍然以刚才的例子来说明,假设地址V中存储着变量值A,当前版本号是01。线程1获取了当前值A和版本号01,想要更新为B,但是被阻塞了。
这时候,内存地址V中变量发生了多次改变,版本号提升为03,但是变量值仍然是A。
随后线程1恢复运行,进行compare操作。经过比较,线程1所获得的值和地址的实际值都是A,但是版本号不相等,所以这一次更新失败。
在Java中,AtomicStampedReference类就实现了用版本号作比较额CAS机制。
1. java语言CAS底层如何实现?
利用unsafe提供的原子性操作方法。
2.什么事ABA问题?怎么解决?
当一个值从A变成B,又更新回A,普通CAS机制会误判通过检测。
利用版本号比较可以有效解决ABA问题。
四十六:Java实现CAS的原理 - Unsafe类********
boolean compareAndSwapObject (Object o, long offset,Object expected, Object x) ;boolean compareAndSwapInt (Object o, long offset, int expected, int x) ;boolean compareAndSwapLong (Object o, long offset, long expected, long x) ;
四十七:Semaphore介绍
// 默认情况下使⽤⾮公平
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
四十八:Lock与synchronized有以下区别:
- Lock是一个接口,而synchronized是关键字。
- Lock必须手动释放锁,而synchronized会自动释放锁。
- Lock可以让等待锁的线程响应中断,而synchronized不会,线程会一直等待下去。
- 通过Lock可以知道线程有没有拿到锁,而synchronized不能。
- Lock能提高多个线程读操作的效率。
- Lock是块范围内的锁,而synchronized能锁住类、方法和代码块。
四十九:synchronized的不⾜之处
五十:锁的几种分类
(1)可重入锁和非可重入锁
(2)公平锁与非公平锁
(3)读写锁和排它锁
注意,即使⽤读写锁,在写线程访问时,所有的读线程和其它写线程均被阻塞。
五十二 原子操作-AtomicInteger类源码简析
从名字就可以看得出来这些类⼤概的⽤途:
原⼦更新基本类型原⼦更新数组原⼦更新引⽤原⼦更新字段(属性)
public final int getAndAdd ( int delta) {return U.getAndAddInt( this , VALUE, delta);}
private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getU
@HotSpotIntrinsicCandidatepublic final int getAndAddInt (Object o, long offset, int delta) {int v;do {v = getIntVolatile(o, offset);} while (!weakCompareAndSetInt(o, offset, v, v + delta));return v;}
⽤于获取某个字段相对 Java 对象的 “ 起始地址 ” 的偏移量。⼀个 java 对象可以看成是⼀段内存,各个字段都得按照⼀定的顺序放在这段内存⾥,同时考虑到对⻬要求,可能这些字段不是连续放置的,⽤这个⽅法能准确地告诉你某个字段相对于对象的起始内存地址的字节偏移量,因为是相对偏移量,所以它其实跟某个具体对象⼜没什么太⼤关系,跟class的定义和虚拟机的内存模型的实现细节更相关。
public final boolean weakCompareAndSetInt (Object o, long offset, int expected, int x) {return compareAndSetInt(o, offset, expected, x);}public final native boolean compareAndSetInt (Object o, long offset, int expected, int x) ;
五十三:谈谈对AQS(AbstractQueuedSynchronizer)理解
private volatile int state;//共享变量,使用volatile修饰保证线程可见性
(3)获取资源acquire,addWriter,acquireQueued
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
private Node addWaiter(Node mode) {
// ⽣成该线程对应的Node节点
Node node = new Node(Thread.currentThread(), mode);
// 将Node插⼊队列中
Node pred = tail;
if (pred != null) {
node.prev = pred;
// 使⽤CAS尝试,如果成功就返回
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 如果等待队列为空或者上述CAS失败,再⾃旋CAS插⼊
enq(node);
return node;
}
// ⾃旋CAS插⼊等待队列
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
上⾯的两个函数⽐较好理解,就是在队列的尾部插⼊新的 Node 节点,但是需要注意的是由于AQS 中会存在多个线程同时争夺资源的情况,因此肯定会出现多个线程同时插⼊节点的操作,在这⾥是通过CAS ⾃旋的⽅式保证了操作的线程安全性
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// ⾃旋
for (;;) {
final Node p = node.predecessor();
// 如果node的前驱结点p是head,表示node是第⼆个结点,就可以尝试去获取资源了
if (p == head && tryAcquire(arg)) {
// 拿到资源后,将head指向该结点。
// 所以head所指的结点,就是当前获取到资源的那个结点或null。
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 如果⾃⼰可以休息了,就进⼊waiting状态,直到被unpark()
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
(4)释放资源release
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
// 如果状态是负数,尝试把它设置为0
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 得到头结点的后继结点head.next
Node s = node.next;
// 如果这个后继结点为空或者状态⼤于0
// 通过前⾯的定义我们知道,⼤于0只有⼀种可能,就是这个结点已被取消
if (s == null || s.waitStatus > 0) {
s = null;
// 等待队列中所有还有⽤的结点,都向前移动
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 如果后继结点不为空,
if (s != null)
LockSupport.unpark(s.thread);
}
五十四:reetrankLoc的实现
ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似。
ReentrantLock的基本实现可以概括为:先通过CAS尝试获取锁。如果此时已经有线程占据了锁,那就加入AQS队列并且被挂起。当锁被释放之后,排在CLH队列队首的线程会被唤醒,然后CAS再次尝试获取锁。在这个时候,如果:
(1)非公平锁:如果同时还有另一个线程进来尝试获取,那么有可能会让这个线程抢先获取;
(2)公平锁:如果同时还有另一个线程进来尝试获取,当它发现自己不是在队首的话,就会排到队尾,由队首的线程获取到锁。
五十五:线程池的基本知识
(1)什么是线程池
(2)为什么要使用线程池
五十六:线程池的原理---ThreadPoolExecutor
(1)ThreadPoolExecutor提供的构造方法
// 五个参数的构造函数public ThreadPoolExecutor ( int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue)// 六个参数的构造函数 -1public ThreadPoolExecutor ( int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory)// 六个参数的构造函数 -2public ThreadPoolExecutor ( int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler)// 七个参数的构造函数public ThreadPoolExecutor ( int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)
int corePoolSize :该线程池中核⼼线程数最⼤值核⼼线程:线程池中有两类线程,核⼼线程和⾮核⼼线程。核⼼线程默认情况下会⼀直存在于线程池中,即使这个核⼼线程什么都不⼲(铁饭碗),⽽⾮核⼼线程如果⻓时间的闲置,就会被销毁(临时⼯)。int maximumPoolSize :该线程池中线程总数最⼤值 。该值等于核⼼线程数量 + ⾮核⼼线程数量。long keepAliveTime :⾮核⼼线程闲置超时时⻓。⾮核⼼线程如果处于闲置状态超过该值,就会被销毁。如果设置 allowCoreThreadTimeOut(true),则会也作⽤于核⼼线程。TimeUnit unit : keepAliveTime 的单位。TimeUnit 是⼀个枚举类型 ,包括以下属性:NANOSECONDS : 1 微毫秒 = 1 微秒 / 1000 MICROSECONDS : 1 微秒 = 1毫秒 / 1000 MILLISECONDS : 1 毫秒 = 1 秒 /1000 SECONDS : 秒 MINUTES : 分 HOURS : ⼩时DAYS : 天BlockingQueue workQueue :阻塞队列,维护着等待执⾏的 Runnable 任务对象。常⽤的⼏个阻塞队列:1. LinkedBlockingQueue链式阻塞队列,底层数据结构是链表,默认⼤⼩是 Integer.MAX_VALUE , 也可以指定⼤⼩。2. ArrayBlockingQueue数组阻塞队列,底层数据结构是数组,需要指定队列的⼤⼩。3. SynchronousQueue同步队列,内部容量为 0 ,每个 put 操作必须等待⼀个 take 操作,反之亦然。4. DelayQueue延迟队列,该队列中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素 。
ThreadFactory threadFactory创建线程的工厂 ,⽤于批量创建线程,统⼀在创建线程时设置⼀些参数,如是否守护线程、线程的优先级等。如果不指定,会新建⼀个默认的线程工厂。
static class DefaultThreadFactory implements ThreadFactory {
// 省略属性
// 构造函数
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
// 省略
}
RejectedExecutionHandler handler拒绝处理策略,线程数量⼤于最⼤线程数就会采⽤拒绝处理策略,四种拒绝处理的策略为 :1. ThreadPoolExecutor.AbortPolicy :默认拒绝处理策略,丢弃任务并抛出RejectedExecutionException 异常。2. ThreadPoolExecutor.DiscardPolicy :丢弃新来的任务,但是不抛出异常。3. ThreadPoolExecutor.DiscardOldestPolicy :丢弃队列头部(最旧的)的任务,然后重新尝试执⾏程序(如果再次失败,重复此过程)。4. ThreadPoolExecutor.CallerRunsPolicy :由调⽤线程处理该任务。
五十七:**ThreadPoolExecutor的策略
-
线程池创建后处于 RUNNING 状态。
-
调⽤ shutdown()方 法后处于 SHUTDOWN 状态,线程池不能接受新的任务,清除⼀些空闲worker, 会等待阻塞队列的任务完成。
-
调⽤shutdownNow()⽅法后处于 STOP 状态,线程池不能接受新的任务,中断所有线程,阻塞队列中没有被执⾏的任务全部丢弃。此时,poolsize=0, 阻塞队列的size 也为 0。
-
当所有的任务已终止, ctl 记录的 ” 任务数量 ” 为 0 ,线程池会变为 TIDYING 状态。接着会执⾏terminated() 函数。ThreadPoolExecutor 中有⼀个控制状态的属性叫 ctl ,它是⼀个AtomicInteger类型的变量。
-
线程池处在 TIDYING 状态时,执⾏完 terminated() ⽅法之后,就会由 TIDYING -> TERMINATED , 线程池被设置为 TERMINATED 状态。(线程池彻底终止,就变成TERMINATED状态。)
五十八:线程池主要的任务处理流程(源码)
// JDK 1.8 public void execute(Runnable command) { if (command == null) throw new NullPointerException(); int c = ctl.get(); // 1.当前线程数⼩于corePoolSize,则调⽤addWorker创建核⼼线程执⾏任务 if (workerCountOf(c) < corePoolSize) { if (addWorker(command, true)) return; c = ctl.get(); } // 2.如果不⼩于corePoolSize,则将任务添加到workQueue队列。 if (isRunning(c) && workQueue.offer(command)) { int recheck = ctl.get(); // 2.1 如果isRunning返回false(状态检查),则remove这个任务,然后执⾏拒绝策略。 if (! isRunning(recheck) && remove(command)) reject(command); // 2.2 线程池处于running状态,但是没有线程,则创建线程 else if (workerCountOf(recheck) == 0) addWorker(null, false); } // 3.如果放⼊workQueue失败,则创建⾮核⼼线程执⾏任务, // 如果这时创建⾮核⼼线程失败(当前线程总数不⼩于maximumPoolSize时),就会执⾏拒绝策略。 else if (!addWorker(command, false)) reject(command); }
ctl.get() 是获取线程池状态,⽤ int 类型表示。第⼆步中,⼊队前进⾏了⼀次 isRunning 判断,⼊队之后,又进⾏了⼀次 isRunning 判断。为什么要二次检查线程池的状态?
在多线程的环境下,线程池的状态是时刻发⽣变化的。很有可能刚获取线程池状态后线程池状态就改变了。判断是否将 command 加⼊ workqueue 是线程池之前的状态。倘若没有⼆次检查,万⼀线程池处于⾮RUNNING 状态(在多线程环境下很有可能发⽣),那么 command 永远不会执⾏。总结⼀下处理流程1. 线程总数量 < corePoolSize ,⽆论线程是否空闲,都会新建⼀个核⼼线程执行任务(让核心线程数量快速达到corePoolSize ,在核心线程数量 < corePoolSize时)。注意,这⼀步需要获得全局锁。2. 线程总数量 >= corePoolSize 时,新来的线程任务会进⼊任务队列中等待,然后空闲的核心线程会依次去缓存队列中取任务来执⾏(体现了线程复⽤)。3. 当缓存队列满了,说明这个时候任务已经多到爆棚,需要⼀些 “ 临时⼯ ” 来执⾏这些任务了。于是会创建⾮核⼼线程去执⾏这个任务。注意,这⼀步需要获得全局锁。4. 缓存队列满了, 且总线程数达到了 maximumPoolSize ,则会采取上⾯提到的拒绝策略进⾏处理。
五十九:*****ThreadPoolExcuter是如何做到线程复用的
(1)ThreadPoolExcuter在创建线程的时候,会把线程封装成工作线程work
(2)首先会判断核心线程是否创建满了,如果没有创建满,就会继续创建核心线程,任务添加到workQueue队列中
(3)如果核心线程数创建满了,队列也放不下了,就会去创建非核心线程
(4)如果非核心线程都创建满了,就开始拒绝策略.
---------------------------------------------------------------------------------------------------------
此时关注Work类(工人)----addWork方法
(1)首先进行循环,判断线程池的生命状态是不是running状态
(2)如果线程池生命状态Ok,那就new Worker(firstTask),把任务交给worker
但是注意,是把自己丢给线程,而不是把工人丢给线程
(3)然后执行runWork方法--此处的while循环就是线程可以重用的根本原因
while(task!=null || (task=getTask()!=null){}
首先看看自己拿的任务是否为空,如果是,那就从getTask里面去拿
(4)getTask中会判断 timed= allowCoreThreadTimeout 核心线程超过一定时间没工作,要
不要销毁一般设置的大都是false,因为销毁可能带来更大的损失
Runnable r = timed ? workQueue.poll(xxxx) :workQueue.take();
意思就是当前的线程会不会因为时间而销毁,如果会,走poll(),否则走take()
所以我们的核心线程会一直卡在take(),被阻塞,被挂起,而非核心线程就会走poll()
addWork
Worker类源码
注意,是把worker自己交给了当前线程
this.thread=getThreadFactory.newThread(this);
我们再看看 runWorker 的逻辑:
getTask源码
timed= allowCoreThreadTimeout 核心线程超过一定时间没工作,要不要销毁
一般设置的大都是false,因为销毁可能带来更大的损失
Runnable r = timed ? workQueue.poll(xxxx) :workQueue.take();
意思就是当前的线程会不会因为时间而销毁,如果会,走poll(),否则走take()
所以我们的核心线程会一直卡在take(),被阻塞,被挂起,而非核心线程就会走poll()
六十:四种常见的线程池
(1)newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
1. 提交任务进线程池。2. 因为 corePoolSize 为 0 的关系,不创建核⼼线程,线程池最⼤为Integer.MAX_VALUE。3. 尝试将任务添加到 SynchronousQueue 队列。4. 如果 SynchronousQueue ⼊列成功,等待被当前运⾏的线程空闲后拉取执⾏。如果当前没有空闲线程,那么就创建⼀个⾮核⼼线程,然后从SynchronousQueue拉取任务并在当前线程执⾏。5. 如果 SynchronousQueue 已有任务在等待,⼊列操作将会阻塞。当需要执⾏很多短时间的任务时,CacheThreadPool 的线程复⽤率⽐较⾼, 会显著的提⾼性能。⽽且线程60s 后会回收,意味着即使没有任务进来,CacheThreadPool并不会占⽤很多资源。
(2)newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
核⼼线程数量和总线程数量相等,都是传⼊的参数 nThreads ,所以只能创建核⼼线程,不能创建⾮核⼼线程。因为LinkedBlockingQueue 的默认⼤⼩是Integer.MAX_VALUE,故如果核⼼线程空闲,则交给核⼼线程处理;如果核⼼线程不空闲,则⼊列等待,直到核⼼线程空闲。与 CachedThreadPool 的区别:因为 corePoolSize == maximumPoolSize ,所以 FixedThreadPool 只会创建核⼼线程。 ⽽CachedThreadPool 因为 corePoolSize=0 ,所以只会创建⾮核⼼线程。在 getTask() ⽅法,如果队列⾥没有任务可取,线程会⼀直阻塞在 LinkedBlockingQueue.take() ,线程不会被回收。 CachedThreadPool 会在60s后收回。由于线程不会被回收,会⼀直卡在阻塞,所以没有任务的情况下,FixedThreadPool 占⽤资源更多。 都⼏乎不会触发拒绝策略,但是原理不同FixedThreadPool 是因为阻塞队列可以很⼤(最⼤为Integer 最⼤值),故⼏乎不会触发拒绝策略; CachedThreadPool是因为线程池很⼤(最⼤为 Integer 最⼤值),⼏乎不会导致线程数量⼤于最⼤线程数,故⼏乎不会触发拒绝策略。
(3)newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
有且仅有⼀个核⼼线程( corePoolSize == maximumPoolSize=1 ),使⽤了LinkedBlockingQueue (容量很⼤),所以,不会创建⾮核⼼线程。所有任务按照先来先执⾏的顺序执⾏。如果这个唯⼀的线程不空闲,那么新来的任务会存储在任务队列⾥等待执⾏。
(4)newScheduledThreadPool
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize
return new ScheduledThreadPoolExecutor(corePoolSize);
}
//ScheduledThreadPoolExecutor():
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
创建⼀个定⻓线程池,⽀持定时及周期性任务执⾏。四种常⻅的线程池基本够我们使⽤了,但是《阿⾥把把开发⼿册》不建议我们直接使⽤Executors类中的线程池,⽽是通过 ThreadPoolExecutor 的⽅式,这样的处理⽅式让写的同学需要更加明确线程池的运⾏规则,规避资源耗尽的⻛险。
六十一:JAVA定时任务--ScheduledThreadPoolExecutor
(1)优缺点
- Timer是单线程模式,Timer类不会捕捉TimeTask所抛出的异常,所以一旦出现异常,线程就会终止,其他任务也得不到执行
- 如果在执行任务期间某个TimerTask耗时比较久,那么就会影响其他任务的调度
- Timer的任务调度是基于绝对时间的,对系统时间敏感
(2)使用案例
public class ThreadPool {
private static final ScheduledExecutorService executor = new
ScheduledThreadPoolExecutor(1, Executors.defaultThreadFactory());
private static SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm
public static void main(String[] args){
// 新建⼀个固定延迟时间的计划任务
executor.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
if (haveMsgAtCurrentTime()) {
System.out.println(df.format(new Date()));
System.out.println("⼤家注意了,我要发消息了");
}
}
}, 1, 1, TimeUnit.SECONDS);
}
public static boolean haveMsgAtCurrentTime(){
//查询数据库,有没有当前时间需要发送的消息
//这⾥省略实现,直接返回true
return true;
}
}
2019 - 01 - 23 16 : 16 : 48⼤家注意了,我要发消息了2019 - 01 - 23 16 : 16 : 49⼤家注意了,我要发消息了2019 - 01 - 23 16 : 16 : 50⼤家注意了,我要发消息了2019 - 01 - 23 16 : 16 : 51⼤家注意了,我要发消息了2019 - 01 - 23 16 : 16 : 52⼤家注意了,我要发消息了2019 - 01 - 23 16 : 16 : 53⼤家注意了,我要发消息了2019 - 01 - 23 16 : 16 : 54⼤家注意了,我要发消息了2019 - 01 - 23 16 : 16 : 55⼤家注意了,我要发消息了
public class ScheduledThreadPoolExecutor extends ThreadPoolExecutorimplements ScheduledExecutorService {public ScheduledThreadPoolExecutor ( int corePoolSize,ThreadFactory threadFasuper (corePoolSize, Integer.MAX_VALUE, 0 , NANOSECONDS,new DelayedWorkQueue(), threadFactory);}//……}
public interface ScheduledExecutorService extends ExecutorService {public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit upublic <V> ScheduledFuture<V> schedule (Callable<V> callable, long delay, Timpublic ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit);public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit);}
scheduleAtFixedRate该⽅法在 initialDelay 时⻓后第⼀次执⾏任务,以后每隔 period 时⻓,再次执⾏任务。 注意,period是从任务开始执⾏算起的。开始执⾏任务后,定时器每隔period时⻓检查该任务是否完成,如果完成则再次启动任务,否则等该任务结束后才再次启动任务。scheduleWithFixDelay该⽅法在 initialDelay 时⻓后第⼀次执⾏任务, 以后每当任务执⾏完成后,等待 delay 时⻓,再次执⾏任务。