Java 线程
一、线程创建
- 继承Thread类,重写run方法
1、定义Thread类的子类,并重写该类的run方法
2、创建Thread子类的实例,即创建了线程对象
3、调用线程对象的start()方法来启动该线程 - 实现runnable 接口
1、定义runnable接口的实现类,并重写该接口的run()方法
2、创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
3、调用线程对象的start()方法来启动该线程 - 实现callable接口
1、创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值
2、创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
3、使用FutureTask对象作为Thread对象的target创建并启动新线程。
4、调用FutureTask对象的get()方法来获得子线程执行结束后的返回值,调用get()方法会阻塞线程。
class callImpl implements Callable<String>{
@Override
public String call() throws Exception {
return null;
}
}
new Thread(new FutureTask<String>(new callImpl())).start();
- 区别:继承只有一次机会,Runnable实现不占继承机会且可以多实现,Callable有返回值
- 通过runnable接口和callable接口实现:
优势是还可以继承其他类 并且多个线程公用一个target对象 非常适合多个相同线程来处理同一份资源 从而将cpu、代码和数据分开
缺点是 编程稍复杂 而且访问当前线程需要使用Thread.currentThread - 通过继承Thread类实现:
优势是编写简单 无需使用Thread.currentThread 直接使用this即可获取当前线程
缺点是无法继承其他类
Callable、Future、FutureTask
- Callable声明了一个名称为call()的方法,同时这个方法可以有返回值V,也可以抛出异常.
- Future是一个接口,用来获取异步计算结果,它可以中断正在执行的任务,也可以查询任务是否完成,还可以获取任务完成后的结果。
- FutureTask是future的实现类,它实现了runnable和future两个接口。
FutureTask是分状态的
(1)当FutureTask处于未启动或已启动状态时,如果此时我们执行FutureTask.get()方法将导致调用线程阻塞;当FutureTask处于已完成状态时,执行FutureTask.get()方法将导致调用线程立即返回结果或者抛出异常。
(2)当FutureTask处于未启动状态时,执行FutureTask.cancel()方法将导致此任务永远不会执行。
当FutureTask处于已启动状态时,执行cancel(true)方法将以中断执行此任务线程的方式来试图停止任务,如果任务取消成功,cancel(...)返回true;但如果执行cancel(false)方法将不会对正在执行的任务线程产生影响(让线程正常执行到完成),此时cancel(...)返回false。
当任务已经完成,执行cancel(...)方法将返回false。
阅读链接:
2.4线程并发工具-Callable、Future和FutureTask原理+源码解析_wangle965235568的博客-CSDN博客_collable futuretask
二、生命周期
主要状态包括:创建、就绪、运行、阻塞、终止。
新建(NEW):新创建了一个线程对象。
可运行(RUNNABLE):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。
运行(RUNNING):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。
-
阻塞(BLOCKED):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。阻塞的情况分三种:
(一). 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
(二). 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
(三). 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。 死亡(DEAD):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。
三、线程方法
- sleep()与wait():
sleep:当前线程睡眠;
wait: 访问当前对象的线程wait,前提是当前方法必须加锁,如果锁不住方法,那么调用的对象wait无从谈起。 wait的时候,锁被释放了,sleep的时候,锁一直被持有。
notify:叫醒当前wait在这个对象上的线程。 - join() 在A线程中调用B线程的join,意思是两个线程合并,A线程要等待B线程执行完才恢复执行。
- yield()让出cpu,当前线程进入就绪队列等待调度
- getPriority()/setPriority():获取与设置线程优先级。 interrupt()中断, 配合是否被中断的方法一起使用,可以停止wait()方法和sleep()方法。
- stop()非常粗暴,会强行把执行到一半的线程终止,不推荐使用,已经废弃。
阅读链接:
四、死锁
死锁产生条件
1、互斥即资源是互斥的
2、请求和保持 即请求资源时遇到阻塞不会放弃已获得的资源
3、不剥夺 即线程获得资源后在未使用完之前不能被剥夺
4、循环等待
死锁类型
静态的锁顺序死锁
如 线程1和2都要获取A、B两种资源 线程1已获取A尝试获取B 线程2已获取B尝试获取A
解决方案是以相同顺序获取资源
动态的锁顺序死锁
两线程调用同一方法时传入的参数顺序是颠倒的造成死锁
解决方案是使用System.identifyHashCode来定义锁的顺序
协作对象之间发生死锁
解决方案是避免在持有锁的情况下调用外部方法
阅读链接:
五、并发与线程同步
产生原因
多个线程对同一资源进行操作导致数据不同步,它是解决线程安全的方式
- 并行与并发:1个核对1个线程是并行执行,1个核对多个线程是并发执行。
- 线程安全:并发带来竞争,竞争的结果会让多个线程同时写某个共享变量时出现数据错误问题,该问题即线程安全问题。
并发三大原则
原子性
一个操作或多个操作要么全部执行 要不都不执行
在Java中只有读数据和赋值(指将数字赋值给变量的操作)才是原子操作
可以用synchronize和lock来实现原子性 因为加锁后只有一个线程可以进行操作
有序性
即程序的执行顺序按照代码的先后顺序执行
指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。
happen -before原则:
①程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作②锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作
③volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
④传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
⑤线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
⑥线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
⑦线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
⑧对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
可见性
当多个线程可操作变量时 只要一个线程对数据进行修改其他线程可以立即看到
Java中的可见性可以用volatile关键字实现 这种变量当值改变后会立即更新到主存 同时其他线程的备份会失效然后在使用时回去主存读取最新值 此外也可以用synchronize和lock来实现可见性 加锁后只有一个线程可以操作 操作完后会把新值写会主存
线程同步方法
synchronized
实现同步的基础:Java中每个对象都可以作为锁。当线程试图访问同步代码时,必须先获得对象锁,退出或抛出异常时必须释放锁
原子性:synchronize修饰的代码块、方法可以确保线程互斥访问因此可以保证原子性。
有序性:Java的happen-before原则,一个锁的unlock happen-before该锁的lock
可见性:对一个变量unlock之前要把值更新到主内存中,同时对一个变量lock操作会清空工作内存中的值,在使用变量的值时需要去主内存中去取。
三种用法
- 修饰实例方法 那么锁住的是该实例对象
- 修饰静态方法 那么锁住的是该类所有实例
- 修饰代码块 那么锁住的是括号内的实例对象
//锁住的是一个对象 该类的其他对象执行同步方法时不会形成互斥
public synchronized void method1
//锁住的是该类 该类所有对象执行同步方法俊辉形成互斥
public synchronized static void method3
//锁住括号内的实例对象
synchronized(obj){
//do something
}
原理
synchronize在软件层面依赖JVM。
1、monitorenter:每个对象都是一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
1. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者;
2. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;
3. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权
2、monitorexit:执行monitorexit的线程必须是objectref所对应的monitor的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
代码块同步 和 方法同步 ,两者实现细节不同
- 代码块同步
编译后会将指令插入到同步代码块开始的地方并在结束的地方退出 当线程执行进入命令时会尝试获取对象对应的monitor的所有权 - 方法同步
方法同步通过ACC_SYNCHRONIZED标示符。当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
两者虽然实现细节不同,但本质上都是对一个对象的监视器(monitor)的获取。任意一个对象都拥有自己的监视器,当同步代码块或同步方法时,执行方法的线程必须先获得该对象的监视器才能进入同步块或同步方法,没有获取到监视器的线程将会被阻塞,并进入同步队列,状态变为BLOCKED。当成功获取监视器的线程释放了锁后,会唤醒阻塞在同步队列的线程,使其重新尝试对监视器的获取。
推荐阅读:
深入理解Java并发之synchronized实现原理java synchronized
深入分析Synchronized原理(阿里面试题) - aspirant - 博客园
ReentrantLock
Lock接口
Lock,锁对象。在Java中锁是用来控制多个线程访问共享资源的方式JAVA SE5.0之后并发包中新增了Lock接口用来实现锁的功能,它提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁,缺点就是缺少像synchronized那样隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性,可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。
主要方法 :
- void lock(): 执行此方法时,如果锁处于空闲状态,当前线程将获取到锁。相反,如果锁已经被其他线程持有,将禁用当前线程,直到当前线程获取到锁
- boolean tryLock(): 如果锁可用,则获取锁,并立即返回true,否则返回false. 该方法和lock()的区别在于,tryLock()只是”试图”获取锁,如果锁不可用,不会导致当前线程被禁用,当前线程仍然继续往下执行代码。而lock()方法则是一定要获取到锁,如果锁不可用,就一直等待,在未获得锁之前,当前线程并不继续向下执行.
- void unlock(): 执行此方法时,当前线程将释放持有的锁. 锁只能由持有者释放,如果线程并不持有锁,却执行该方法,可能导致异常的发生.
- Condition newCondition(): 条件对象,获取等待通知组件。该组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的await()方法,而调用后,当前线程将释放锁
为什么要可重入
如果递归调用不可重入会导致死锁。重入的实现是因为锁有所有者和计数器两个属性
Lock实现原理
要想了解Lock的实现原理我们需要先了解一下AbstractQueuedSynchronizer简称AQS,Java中的Lock接口的实现都是通过AQS实现的。
- AbstractQueuedSynchronizer
抽象队列同步器,是用来构建锁或者其他同步组件的基础框架。AQS使用一个FIFO的队列表示排队等待锁的线程,队列头部的线程执行完毕之后,它会调用它的后继的线程.AQS中有一个表示状态的字段state,例如ReentrantLocky用它表示线程重入锁的次数,Semaphore用它表示剩余的许可数量。对state变量值的更新都采用CAS操作保证更新操作的原子性。
AbstractQueuedSynchronizer继承了AbstractOwnableSynchronizer,这个类只有一个变量:exclusiveOwnerThread,表示当前占用该锁的线程,并且提供了相应的get,set方法。
AQS阅读链接:
深度解析Java 8:JDK1.8 AbstractQueuedSynchronizer的实现分析(上)-InfoQ
深度解析Java 8:AbstractQueuedSynchronizer的实现分析(下)-InfoQ
总结
ReentrantLock提供了内置锁类似的功能和内存语义。此外,ReetrantLock还提供了其它功能,包括定时的锁等待、可中断的锁等待、公平性、以及实现非块结构的加锁、Condition,对线程的等待和唤醒等操作更加灵活,一个ReentrantLock可以有多个Condition实例,所以更有扩展性,不过ReetrantLock需要显示的获取锁,并在finally中释放锁,否则后果很严重。ReentrantLock在性能上似乎优于Synchronized,其中在jdk1.6中略有胜出,在1.5中是远远胜出。那么为什么不放弃内置锁,并在新代码中都使用ReetrantLock?在java1.5中, 内置锁与ReentrantLock相比有例外一个优点:在线程转储中能给出在哪些调用帧中获得了哪些锁,并能够检测和识别发生死锁的线程。Reentrant的非块状特性意味着,获取锁的操作不能与特定的栈帧关联起来,而内置锁却可以。因为内置锁时JVM的内置属性,所以未来更可能提升synchronized而不是ReentrantLock的性能。例如对线程封闭的锁对象消除优化,通过增加锁粒度来消除内置锁的同步。
阅读链接:
从ReentrantLock的实现看AQS的原理及应用 - 美团技术团队
解决多线程安全问题-无非两个方法synchronized和lock 具体原理以及如何 获取锁AQS算法 (百度-美团) - aspirant - 博客园
synchronized和ReentrantLock的比较
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
2)synchronized因为是在JVM层面上实现的所以在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
5)Lock可以提高多个线程进行读操作的效率。
6)一个ReentrantLock对象可以同时绑定多个Condition对象,而在synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含的条件,如果要和多余一个条件关联的时候,就不得不额外地添加一个锁,而ReentrantLock则无须这么做,只需要多次调用new Condition()方法即可
7)synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。ReentrantLock可以设置成公平锁。
8)在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而 当竞争资源非常激烈时(即有大量线程同时竞争),此时ReentrantLock的性能要远远优于synchronized 。所以说,在具体使用时要根据适当情况选择。
Volatile关键字
特点
- 可以保证可见性:保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
禁止进行指令重排序 - 保证有序性:当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行。在进行指令优化时,不能将在对volatile变量的读操作或者写操作的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行
- 不保证原子性:要保证原子性可加锁或者用AtomicInteger
原理
- 如何保证可见性
处理器为了提高处理速度,不直接和内存进行通讯,而是将系统内存的数据独到内部缓存后再进行操作,但操作完后不知什么时候会写到内存。如果对声明了volatile变量进行写操作时,JVM会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写会到系统内存。 这一步确保了如果有其他线程对声明了volatile变量进行修改,则立即更新主内存中数据。但这时候其他处理器的缓存还是旧的,所以在多处理器环境下,为了保证各个处理器缓存一致,每个处理会通过嗅探在总线上传播的数据来检查 自己的缓存是否过期, 当处理器发现自己缓存行对应的内存地址被修改了,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作时,会强制重新从系统内存把数据读到处理器缓存里。 这一步确保了其他线程获得的声明了volatile变量都是从主内存中获取最新的。 - 如何保证有序性
Lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。
应用
状态标记量、单例模式的double check
推荐阅读:
锁类型
- 重入锁
内置锁(synchronized)和Lock(ReentrantLock)都是可重入的,当一个线程得到一个对象后,再次请求该对象锁时是可以再次得到该对象的锁的。具体概念就是:自己可以再次获取自己的内部锁 - 公平锁
CPU在调度线程的时候是在等待队列里随机挑选一个线程,由于这种随机性所以是无法保证线程先到先得的(synchronized控制的锁就是这种非公平锁)。但这样就会产生饥饿现象,即有些线程(优先级较低的线程)可能永远也无法获取CPU的执行权,优先级高的线程会不断的强制它的资源。那么如何解决饥饿问题呢,这就需要公平锁了。公平锁可以保证线程按照时间的先后顺序执行,避免饥饿现象的产生。但公平锁的效率比较低.ReentrantLock是公平锁 - 悲观锁
悲观锁总是假设我们处于最坏的情况,即每次执行临界区代码都会产生冲突,所以悲观锁在持有数据的时候总会把资源或者数据锁住,这样其他线程想要请求这个资源的时候就会阻塞,直到等到悲观锁把资源释放为止。Java 中的 Synchronized 和 ReentrantLock 等是一种悲观锁思想的实现 - 乐观锁
乐观锁与悲观锁的思想相反,它总认为资源和数据不会被别人所修改,所以读取不会上锁,但是乐观锁在进行写入操作的时候会判断当前数据是否被修改过。CAS是一种常见的乐观锁实现。
CAS
CAS 即 compare and swap(比较与交换),是一种有名的无锁算法。即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步。CAS 中涉及三个要素: 需要读写的内存值 V、进行比较的值 A、 拟写入的新值 B
当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
在java中可以通过锁和循环CAS的方式来实现原子操作。Java中java.util.concurrent.atomic包相关类就是 CAS的实现
CAS的问题
- ABA问题
CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。 - 循环时间长开销大
- 只能保证一个共享变量的原子操作
循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
补充1: Java中Atomic类的使用分析
补充2:
Java内存模型:
Java内存模型规定了所有的变量都存储在主内存中。每条线程中还有自己的工作内存,线程的工作内存中保存了被该线程所使用到的变量(这些变量是从主内存中拷贝而来)。线程对变量的所有操作(读取,赋值)都必须在工作内存中进行。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
存在问题:
可能导致数据“脏读”初始时,两个线程分别读取i的值存入各自所在的工作内存当中,然后线程1进行加1操作,然后把i的最新值11写入到内存。此时线程2的工作内存当中i的值还是10,进行加1操作之后,i的值为11,然后线程2把i的值写入内存。
锁优化
无锁>>>偏向锁>>>轻量锁>>>重量锁,按该顺序一次升级,不可降级
六、多线程
线程池
优势
- 降低系统资源消耗 通过重用已存在线程减少线程创建和销毁造成的资源消耗
- 提高响应速度 当有任务到达时无需创建新的线程便可立即执行
- 可控的线程并发数 若无限制创建线程 会额外消耗大量系统资源 从而可能导致阻塞系统或者 oom
- 线程池提供了丰富功能 如定时、定期执行任务等
Executor框架
ThreadPoolExecutor
参数
- corePoolSize:
线程池中核心线程数 默认情况核心线程是一直存活在线程池中即使他们处于闲置状态 。如果设置allowCoreThreadTimeOut属性为true 那么当闲置的核心线程等待超时后就会被销毁 超时时间由KeepAliveTime制定 - maximumPoolsize
线程池允许的最大线程数 等于核心+非核心线程数 当超过该数值后后续任务会被阻塞 - keepAliveTime
非核心线程的闲置超时时间 - unit
制定keepalivetime参数的时间单位可以是 天、小时、分钟等 - workQueue
线程池中保存等待执行的任务的阻塞队列 通过execute提交的runnable对象都会存在该队列中 - threadFactory
线程工厂 为线程池提供新线程的创建 - handler
是RejectedExecutionHandler对象,而RejectedExecutionHandler是一个接口,里面只有一个rejectedExecution方法。当任务队列已满并且线程池中的活动线程已经达到所限定的最大值或者是无法成功执行任务,这时候ThreadPoolExecutor会调用RejectedExecutionHandler中的rejectedExecution方法。在ThreadPoolExecutor中有四个内部类实现了RejectedExecutionHandler接口。在线程池中它默认是AbortPolicy,在无法处理新任务时抛出RejectedExecutionException异常
使用
- execute
提交任务 没有返回值 - submit
提交任务 会返回一个future 由此可判断任务是否执行成功 同时可以通过get方法获取返回值如果子线程任务没有完成,get方法会阻塞住直到任务完成,而使用get(long timeout, TimeUnit unit)方法则会阻塞一段时间后立即返回,这时候有可能任务并没有执行完
关闭
- shutdown
讲线程池状态设置为shutdown 然后中断所有没执行的任务 - shutdownNow
设置状态为stop 然后终端所有任务包括正在执行的任务
线程池执行流程
如果线程数量没有达到核心线程的上限 就启动一个核心线程来执行任务
如果当前线程池线程数量超过核心线程数 任务会被插入到任务队列中等待执行
任务队列满后 如果线程池中线程数量未达到线程数量上限就启动非核心线程执行任务 如果达到线程池线程数量上限就会拒绝执行任务 并调用RejectedExecutionHandler中的rejectedExecution方法来通知调用者。
线程池种类
- newFixedThreadPool
特点:最大线程数就是核心线程数 这样可以更快响应外界请求 - newCachedThreadPool
核心线程数为0 最大线程数为Integer.MAX_VALUE 当线程池线程都处于活动状态 线程池会创建新的线程来处理新任务 线程的超时时间为60秒 就是说如果线程池闲置60秒后是不存在任何线程的 此时他几乎不占用任何系统资源 - newScheduledThreadPool
核心线程数固定 非核心线程几乎没有数量限制
schedule(Runnable command, long delay, TimeUnit unit):延迟一定时间后执行Runnable任务;
schedule(Callable callable, long delay, TimeUnit unit):延迟一定时间后执行Callable任务;
scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit):延迟一定时间后,以间隔period时间的频率周期性地执行任务;
scheduleWithFixedDelay(Runnable command, long initialDelay, long delay,TimeUnit unit):与scheduleAtFixedRate()方法很类似,但是不同的是scheduleWithFixedDelay()方法的周期时间间隔是以上一个任务执行结束到下一个任务开始执行的间隔,而scheduleAtFixedRate()方法的周期时间间隔是以上一个任务开始执行到下一个任务开始执行的间隔,也就是这一些任务系列的触发时间都是可预知的 - newSingleThreadExecutor
只有一个核心线程 对任务队列大小无限制 也就是说一个任务处于活跃状态时 其他任务都会在任务队列中排队等候依次执行
线程池选择
- cpu密集型任务
线程数应尽量少 如设置N+1 - I/O密集型
由于IO操作速度远低于CPU速度,那么在运行这类任务时,CPU绝大多数时间处于空闲状态,那么线程池可以配置尽量多些的线程,以提高CPU利用率,如2*N。 - 混合型
可以进行拆分 然后根据各部分特点选择合适的线程池
线程数量分配
如果 CPU 和 I/O 设备的利用率都很低,那么可以尝试通过增加线程来提高吞吐量。
在单核时代,多线程主要就是用来平衡 CPU 和 I/O 设备的。如果程序只有 CPU 计算,而没有 I/O 操作的话,多线程不但不会提升性能,还会使性能变得更差,原因是增加了线程切换的成本。但是在多核时代,纯计算型的程序也可以利用多线程来提升性能。这是为什么呢?因为利用多核可以降低响应时间。
比如要计算 1~100亿 的值,如果在四核的 CPU 上利用四个线程执行,线程 A 计算 [1,25亿),线程 B 计算 [25亿,50亿),线程 C 计算[50亿,75亿),线程 D 计算[75亿,100亿],之后在汇总,那么理论上应该比一个线程计算快四倍。一个线程,对于四核的 CPU,CPU 利用率只有 25%,而四个线程,则能够将 CPU 的利用率提高到 100%。
对于 CPU 密集型计算,多线程本质上是提升多核 CPU 的利用率,所以对于一个四核的 CPU,每个核一个线程,理论上创建四个线程就可以了,再多创建线程只是会增加线程切换的成本。所以,对于 CPU 密集型计算场景,理论上 “线程的数量 = CPU 核数” 就是最合适的。不过在工程上,线程的数量一般会设置为 “ CPU 核数 +1 “。这样的话,当线程因为偶尔的内存页失效或其他原因导致阻塞时,这个额外的线程可以顶上,从而保证 CPU 的利用率。
对于 I/O 密集型的计算场景,最佳的线程数是与程序中 CPU 计算和 I/O 操作的耗时比相关的,可以总结为:
线程数 = 1 + ( I/O 耗时 / CPU 耗时 )
不过上面这个公式只针对单核 CPU 的,至于多核 CPU,只需要等比扩大即可:
线程数 = CPU 核数 * [ 1 + ( I/O 耗时 / CPU 耗时 )]
ThreadpoolExecutor源码
深入理解Java线程池:ThreadPoolExecutor | Idea Buffer
Java线程池实现原理及其在美团业务中的实践 - 美团技术团队
七、线程间通信
wait/notify
两者是object类中定义的 且都是final的无法被其他类重写
- wait
让当前线程进入等待并释放锁 wait(long)则是等待一定时间 超时后自动唤醒
注意事项:1、首先调用wait时必须确保当前线程拥有monitor即拥有锁 否则会出现异常 2、释放锁后等待其他线程来通知他 这样他才能重新获得锁的拥有权和恢复执行
和sleep区别:wait释放锁 sleep不释放锁 - notify
通知当前等待的线程 当前线程执行完毕后释放锁 并从等待线程中唤醒一个 notifyAll则是唤醒所有等待线程
注意事项:1、唤醒的线程是随机的2、被唤醒的线程是不能执行的需要等待当前线程执行完并释放锁才可以
使用注意:1、不可过早notify 否则容易打乱程序运行逻辑 使wait方法释放锁后可能永远无法被唤醒2、注意wait等待条件发生变化也容易造成程序逻辑混乱
Condition实现等待/通知
关键字synchronized与wait()和notify()/notifyAll()方法相结合可以实现等待/通知模式,类似ReentrantLock也可以实现同样的功能,但需要借助于Condition对象
特殊之处:synchronized相当于整个ReentrantLock对象只有一个单一的Condition对象情况。而一个ReentrantLock却可以拥有多个Condition对象,来实现通知部分线程。
具体实现方式:假设有两个Condition对象:ConditionA和ConditionB。那么由ConditionA.await()方法进入等待状态的线程,由ConditionA.signalAll()通知唤醒;由ConditionB.await()方法进入等待状态的线程,由ConditionB.signalAll()通知唤醒。
八、线程相关
ThreadLoacl
作用是提供线程相关联的且与其他线程相互隔离、独立的变量存取。
其提供的方法有,initValue、get\put\remove等。
内部存储变量是通过Thread持有的一个threadLoaclmap,key是当前ThreadLoacl实例 value是要存入的值,所以即便是同一变量也可以按线程存储,且各个线程间变量值相互隔离。
ThreadLocalMap是用来存储与线程关联的value的哈希表,它具有HashMap的部分特性,比如容量、扩容阈值等,它内部通过Entry类来存储key和value。Entry继承自WeakReference,通过上述源码super(k);可以知道,ThreadLocalMap是使用ThreadLocal的 作为Key的
最好的方式就是将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,可以防止内存泄露。
Java并发编程之ThreadLocal详解_Stay Hungry, Stay Foolish-CSDN博客_java threadlocal
生产者消费者模型
单一的生产者消费者模型可采用wait/notify实现,多生产者消费者模型可采用BlockingQueue来实现。
//多消费者生产者模型
public class PoolMax{
private BlockingQueue<String> pool;
public PoolMax(BlockingQueue<String> pool) {
this.pool = pool;
}
public BlockingQueue<String> getPool() {
return pool;
}
public void setPool(BlockingQueue<String> pool) {
this.pool = pool;
}
}
public class ProductMax extends Thread{
PoolMax poolMax;
public ProductMax(PoolMax poolMax) {
this.poolMax = poolMax;
}
@Override
public void run() {
super.run();
while (true){
try {
String good= String.valueOf(System.currentTimeMillis());
poolMax.getPool().put(good);
Log.i(TAG, "run: product "+good);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ConsumerMax extends Thread{
PoolMax poolMax;
public ConsumerMax(PoolMax poolMax) {
this.poolMax = poolMax;
}
@Override
public void run() {
super.run();
while (true){
try {
String good= poolMax.getPool().take();
Log.i(TAG, "run: consumer "+good);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public void testMax(){
PoolMax poolMax=new PoolMax(new ArrayBlockingQueue<String>(20));
for (int i = 0; i < 10; i++) {
new ProductMax(poolMax).start();
new ConsumerMax(poolMax).start();
}
}
https://github.com/LRH1993/android_interview/blob/master/java/concurrence.md
阿里面试官的分享Java面试中需要准备哪些多线程并发的技术要点 - 简书
Java 并发专题 :闭锁 CountDownLatch 之一家人一起吃个饭_Hongyang-CSDN博客_java 闭锁