1、进程是一个应用程序。线程是一个进程中的执行场景/执行单元。一个进程可以启动多个线程。
2、进程和线程有什么区别呢?
-
每个进程是一个应用程序,都有独立的内存空间。进程和进程之间的内存独立
-
同一个进程中的线程共享其进程中的内存和资源 (共享的内存是堆内存和方法区内存,栈内存不共享,每个线程有自己的栈内存)
3、描述 java 程序的执行原理
java 命令执行会启动 JVM,JVM 的启动表示启动一个应用程序,表示启动了一个进程。 该进程会自动启动一个“主线程”,然后主线程负责调用某个类的 main 方法。所以 main 方法的执行是在主线程中执行的。然后通过 main 方法代码的执行可以启动其他的“分支线程”。 所以,main 方法结束程序不一定结束,因为其他的分支线程有可能还在执行。
2. 线程的创建和启动线程的创建和启动有三种方式
-
继承 Thread 类
-
实现 Runnable 接口(推荐使用 Runnable 接口)
-
实现Callable接口(方式实现的线程可以获取线程的返回值)
1、继承 Thread 类
采用 Thread 类创建线程,用户只需要继承 Thread,覆盖 Thread 中的 run 方法,最后采用 start 启动线程即可。
public class ThreadTest {
public static void main(String[] args) {
// 这里是main方法,这里的代码属于主线程,在主栈中运行。
// 新建一个分支线程对象
MyThread t = new MyThread();
// 启动线程
//t.run(); // 不会启动线程,不会分配新的分支栈。(这种方式就是单线程。)
t.start();
for(int i = 0; i < 1000; i++){
System.out.println("主线程--->" + i);
}
}
}
class MyThread extends Thread {
///run()方法是由 java 线程掉机制调用的
//run()方法就是子线程要执行的代码
@Override
public void run() {
for(int i = 0; i < 1000; i++){
System.out.println("分支线程--->" + i);
}
}
}
2、实现 Runnable 接口
Thread 对象本身就实现了 Runnable 接口
public class RunnableTest {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable()); // 合并代码
// 启动线程
t.start();
for(int i = 0; i < 100; i++){
System.out.println("主线程--->" + i);
}
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
for(int i = 0; i < 100; i++){
System.out.println("分支线程--->" + i);
}
}
}
public class RunnableTest1{
public static void main(String[] args) {
// 创建线程对象,采用匿名内部类方式。
// 这是通过一个没有名字的类,new出来的对象。
Thread t = new Thread(new Runnable(){
@Override
public void run() {
for(int i = 0; i < 100; i++){
System.out.println("t线程---> " + i);
}
}
});
// 启动线程
t.start();
for(int i = 0; i < 100; i++){
System.out.println("main线程---> " + i);
}
}
}
3、实现Callable接口
这种方式的优点:可以获取到线程的执行结果。 这种方式的缺点:效率比较低,在获取t线程执行结果的时候,当前线程受阻塞,效率较低。
public class CallableTest{
public static void main(String[] args) throws Exception {
// 第一步:创建一个“未来任务类”对象。
// 参数非常重要,需要给一个Callable接口实现类对象。
FutureTask task = new FutureTask(new Callable() {
@Override
public Object call() throws Exception { // call()方法就相当于run方法。只不过这个有返回值
// 线程执行一个任务,执行之后可能会有一个执行结果
// 模拟执行
System.out.println("call method begin");
Thread.sleep(1000 * 10);
System.out.println("call method end!");
int a = 100;
int b = 200;
return a + b; //自动装箱(300结果变成Integer)
}
});
// 创建线程对象
Thread t = new Thread(task);
// 启动线程
t.start();
// 这里是main方法,这是在主线程中。
// 在主线程中,怎么获取t线程的返回结果?
// get()方法的执行会导致“当前线程阻塞”
Object obj = task.get();
System.out.println("线程执行结果:" + obj);
// main方法这里的程序要想执行必须等待get()方法的结束
// 而get()方法可能需要很久。因为get()方法是为了拿另一个线程的执行结果
// 另一个线程执行是需要时间的。
System.out.println("hello world!");
}
}
3. 常见的线程调度模型有哪些?
抢占式调度模型: 那个线程的优先级比较高,抢到的CPU时间片的概率就高一些/多一些。java采用的就是抢占式调度模型。
均分式调度模型: 平均分配CPU时间片。每个线程占有的CPU时间片时间长度一样。平均分配,一切平等。
4. 线程的常用方法1、currentThread()
静态方法:Thread.currentThread()可以获得当前线程
同一段代码可能被不同的线程执行, 因此当前线程是相对的,Thread.currentThread()方法的返回值是在代码实际运行时候的线程对象。
2、setName()/getName()
设置线程名称:线程对象.setName(线程名称)
返回线程名称:线程对象.getName()
通过设置线程名称,有助于程序调试,提高程序的可读性, 建议为每个线程都设置一个能够体现线程功能的名称。
3、isAlive()
线程对象.isAlive()判断当前线程是否处于活动状态
4、sleep()
1)静态方法:Thread.sleep(1000); 2)参数是毫秒 3)作用:让当前线程进入休眠,进入“阻塞状态”,放弃占有CPU时间片,让给其它线程使用。 这行代码出现在A线程中,A线程就会进入休眠。 这行代码出现在B线程中,B线程就会进入休眠。 4)Thread.sleep()方法,可以做到这种效果: 间隔特定的时间,去执行一段特定的代码,每隔多久执行一次。
public class ThreadTest07 {
public static void main(String[] args) {
// 创建线程对象
Thread t = new MyThread3();
t.start();
// 调用sleep方法
try {
// 问题:这行代码会让线程t进入休眠状态吗?
t.sleep(1000 * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("hello World!");
}
}
class MyThread3 extends Thread {
public void run(){
for(int i = 0; i < 10000; i++){
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
}
}
5、getId()
线程对象.getId()可以获得线程的唯一标识
注意: 某个编号的线程运行结束后,该编号可能被后续创建的线程使用
重启的 JVM 后,同一个线程的编号可能不一样
6、yield()
1)static void yield() 让位方法,Thread.yield()暂停当前正在执行的线程对象,并执行其他线程 2)yield()方法不是阻塞方法。让当前线程让位,让给其它线程使用。 3)yield()方法的执行会让当前线程从“运行状态”回到“就绪状态”。 注意:在回到就绪之后,有可能还会再次抢到。
7、setPriority()
线程对象.setPriority(num)设置线程的优先级。
java 线程的优先级取值范围是 1 ~ 10,优先级比较高的获取CPU时间片可能会多一些, 如果超出这个范围会抛出异常 IllegalArgumentException。
线程优先级本质上是只是给线程调度器一个提示信息,以便于调度器决定先调度哪些线程. 注意不能保证优先级高的线程先运行。
Java 优先级设置不当或者滥用可能会导致某些线程永远无法得到运行,即产生了线程饥饿。
线程的优先级并不是设置的越高越好,一般情况下使用普通的优先级即可,即在开发时不必设置线程的优先级。
线程的优先级具有继承性, 在 A 线程中创建了 B 线程,则 B 线程的 优先级与 A 线程是一样的。
8、interrupt()
中断线程。注意调用 interrupt()方法仅仅是在当前线程打一个停止标志,并不是真正的停止线程。
public class Test {
public static void main(String[] args) {
SubThread t1 = new SubThread();
t1.start(); ///开启子线程
//当前线程是 main 线程
for(int i = 1; i<=100; i++){
System.out.println("main ==> " + i);
}
//中断子线程
t1.interrupt(); //仅仅是给子线程标记中断
}
}
public class SubThread extends Thread {
@Override
public void run() {
super.run();
for(int i = 1; i <= 10000; i++){
//判断线程的中断标志,线程有 isInterrupted()方法,该方法返回线程的中断标志
if ( this.isInterrupted() ){
System.out.println("当前线程的中断标志为 true, 我要退出了");
// break; //中断循环, run()方法体执行完毕, 子线程运行完毕
return; //直接结束当前 run()方法的执行
}
System.out.println("sub thread --> " + i);
}
}
}
合理的终止一个线程的执行
public class ThreadTest {
public static void main(String[] args) {
MyRunable r = new MyRunable();
Thread t = new Thread(r);
t.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 终止线程
// 你想要什么时候终止t的执行,那么你把标记修改为false,就结束了。
r.run = false;
}
}
class MyRunable implements Runnable {
// 打一个布尔标记
boolean run = true;
@Override
public void run() {
for (int i = 0; i < 10; i++){
if(run){
System.out.println(Thread.currentThread().getName() + "--->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
//终止当前线程
return;
}
}
}
}
9、setDaemon()
Java 中的线程分为用户线程与守护线程。
守护线程是为其他线程提供服务的线程,如垃圾回收器(GC)就是一个典型的守护线程。
守护线程不能单独运行, 当 JVM 中没有其他用户线程,只有守护线程时,守护线程会自动销毁, JVM 会退出。
public class ThreadTest{
public static void main(String[] args) {
Thread t = new BakDataThread();
t.setName("备份数据的线程");
// 启动线程之前,将线程设置为守护线程
t.setDaemon(true);
t.start();
// 主线程:主线程是用户线程
for(int i = 0; i < 10; i++){
System.out.println(Thread.currentThread().getName() + "--->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class BakDataThread extends Thread {
public void run(){
int i = 0;
// 即使是死循环,但由于该线程是守护者,当用户线程结束,守护线程自动终止。
while(true){
System.out.println(Thread.currentThread().getName() + "--->" + (++i));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
10、join()
public class ThreadTest {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start();
//合并线程
try {
t.join(); // t合并到当前线程中,当前线程受阻塞,t线程执行结束,再执行当前线程。
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main over");
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
for(int i = 0; i < 10000; i++){
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
}
}
5. 线程的生命周期
1、线程的生命周期是线程对象的生老病死,即线程的状态。
2、线程生命周期可以通过 getState()方法获得, 线程的状态是Thread.State 枚举类型定义的。
3、线程的状态如下:
NEW新建状态
创建了线程对象,在调用 start()启动之前的状态。
RUNNABLE可运行状态
它是一个复合状态, 包含:READY 和 RUNNING 两个状态。 READY 状态该线程可以被线程调度器进行调度使它处于RUNNING状态 , RUNNING 状态表示该线程正在执行 。 Thread.yield()方法可以把线程由 RUNNING 状态转换为 READY 状态。
BLOCKED阻塞状态
线程发起阻塞的 I/O 操作,或者申请由其他线程占用的独占资源,线程会转换为 BLOCKED 阻塞状态。处于阻塞状态的线程不会占用CPU 资源。 当阻塞I/O 操作执行完,或者线程获得了其申请的资源,线程可以转换为 RUNNABLE 状态 。
WAITING 等待状态
线程执行了 object.wait(), thread.join()方法会把线程转换为 WAITING 等待状态, 执行 object.notify()方法,或者加入的线程执行完毕,当前线程会转换为 RUNNABLE 状态 。
TIMED_WAITING 状态
与 WAITING 状态类似,都是等待状态。区别在于处在该状态的线程不会无限的等待,如果线程没有在指定的时间范围内完成期望的操作,该线程自动转换为 RUNNABLE 状态 。
TERMINATED终止状态
线程结束处于终止状态 。
6. 多线程编程的优势与存在的风险多线程编程具有以下优势:
-
提高系统的吞吐率(Throughout)。多线程编程可以使一个进程有多个并发(concurrent,即同时进行的)的操作
-
提高响应性(Responsiveness)。Web 服务器会采用一些专门的线程负责用户的请求处理,缩短了用户的等待时间
-
充分利用多核(Multicore)处理器资源。通过多线程可以充分的利用 CPU 资源
多线程编程存在的问题与风险:
-
线程安全(Thread safe)问题。多线程共享数据时,如果没有采取正确的并发访问控制措施,就可能会产生数据一致性问题,如读取脏数据(过期的数据), 如丢失数据更新。
-
线程活性(thread liveness)问题。由于程序自身的缺陷或者由资源稀缺性导致线程一直处于非 RUNNABLE 状态,这就是线程活性问题。
常见的活性故障有以下几种:
(1) 死锁(Deadlock),类似鹬蚌相争
public class DeadLock { public static void main(String[] args) { Object o1 = new Object(); Object o2 = new Object(); // t1和t2两个线程共享o1,o2 Thread t1 = new MyThread1(o1,o2); Thread t2 = new MyThread2(o1,o2); t1.start(); t2.start(); } } class MyThread1 extends Thread{ Object o1; Object o2; public MyThread1(Object o1,Object o2){ this.o1 = o1; this.o2 = o2; } public void run(){ synchronized (o1){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (o2){ } } } } class MyThread2 extends Thread { Object o1; Object o2; public MyThread2(Object o1,Object o2){ this.o1 = o1; this.o2 = o2; } public void run(){ synchronized (o2){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (o1){ } } } } (2) 锁死(Lockout),类似于睡美人故事中王子挂了
(3) 活锁(Livelock),线程A和B都需要过桥(都需要使用进程),而都礼让不走(那到的系统优先级相同,都认为不是自己优先级高),就这么僵持下去。(很绅士,互相谦让)
(4) 饥饿(Starvation),即线程一直在等待却无法执行
-
上下文切换(Context Switch)。处理器从执行一个线程切换到执行另外一个线程
-
可靠性。可能会由一个线程导致 JVM 意外终止,其他的线程也无法执行
1. 什么是线程同步?
线程同步机制是一套用于协调线程之间的数据访问的机制。该机制可以保障线程安全。
2. 锁概述
线程安全问题的产生前提是多个线程并发访问共享数据。
将多个线程对共享数据的并发访问转换为串行访问,即一个共享数据一次只能被一个线程访问。锁就是复用这种思路来保障线程安全的。
锁(Lock)可以理解为对共享数据进行保护的一个许可证。 对于同一个许可证保护的共享数据来说,任何线程想要访问这些共享数据必须先持有该许可证。一个线程只有在持有许可证的情况下才能对这些共享数据进行访问; 并且一个许可证一次只能被一个线程持有; 许可证线程在结束对共享数据的访问后必须释放其持有的许可证。
锁具有排他性(Exclusive), 即一个锁一次只能被一个线程持有。这种锁称为排它锁或互斥锁(Mutex)。
JVM把锁分为内部锁和显示锁两种:
-
内部锁通过 synchronized关键字实现
-
显示锁通过 java.concurrent.locks.Lock 接口的实现类实现的
1、锁的作用
锁可以实现对共享数据的安全访问。保障线程的原子性,可见性与有序性。
-
锁是通过互斥保障原子性。 一个锁只能被一个线程持有, 这就保证临界区的代码一次只能被一个线程执行。使得临界区代码所执行的操作自然而然的具有不可分割的特性,即具备了原子性。
-
可见性的保障是通过写线程冲刷处理器的缓存和读线程刷新处理器缓存这两个动作实现的。在 java 平台中,锁的获得隐含着刷新处理器缓存的动作, 锁的释放隐含着冲刷处理器缓存的动作。
-
锁能够保障有序性。写线程在临界区所执行的在读线程所执行的临界区看来像是完全按照源码顺序执行的。
注意: 使用锁保障线程的安全性,必须满足以下条件:
这些线程在访问共享数据时必须使用同一个锁,即使是读取共享数据的线程也需要使用同步锁。
2、锁相关的概念
1)可重入性
可重入性(Reentrancy)描述这样一个问题: 一个线程持有该锁的时候能再次(多次)申请该锁
如果一个线程持有一个锁的时候还能够继续成功申请该锁,称该锁是可重入的, 否则就称该锁为不可重入的
2)锁的争用与调度
Java 平台中内部锁synchronized 属于非公平锁, 显示 Lock 锁既支持公平锁又支持非公平锁。
3)锁的粒度
一个锁可以保护的共享数据的数量大小称为锁的粒度。
锁保护共享数据量大,称该锁的粒度粗, 否则就称该锁的粒度细。
锁的粒度过粗会导致线程在申请锁时会进行不必要的等待。锁的粒度过细会增加锁调度的开销。
3. 内部锁:synchronized关键字
1)synchronized三种写法
第一种:同步代码块
synchronized(线程共享对象){
同步代码块;
}
第二种:在实例方法上使用synchronized
表示共享对象一定是this
并且同步代码块是整个方法体
public synchronized void method(){
同步代码块;
}
第三种:在静态方法上使用synchronized
表示找类锁
类锁永远只有1把
就算创建了100个对象,那类锁也只有一把
public synchronized static void method(){
同步代码块;
}
对象锁:1个对象1把锁,100个对象100把锁。 类锁:100个对象,也可能只是1把类锁。
2)脏读
出现读取属性值出现了一些意外, 读取的是中间值,而不是修改之后的值
出现脏读的原因是对共享数据的修改与对共享数据的读取不同步
解决方法: 不仅对修改数据的代码块进行同步,还要对读取数据的代码块同步
3)线程出现异常会自动释放锁
4. 显示锁Lock
1、锁的可重入性
public class Test01 {
public synchronized void sm1(){
System.out.println("同步方法 1");
sm2();
}
private synchronized void sm2() {
System.out.println("同步方法 2");
}
public static void main(String[] args) {
Test01 obj = new Test01();
new Thread(new Runnable() {
@Override
public void run() {
obj.sm1();
}
}).start();
}
}
2、ReentrantLock 的基本使用
public class Test {
static Lock lock = new ReentrantLock();//定义锁对象
public static void sm1(){
//经常在 try 代码块中获得 Lock 锁, 在 finally 子句
try {
lock.lock();//获得锁
Thread.sleep(new Random().nextInt(1000));
}catch (InterruptedException e){
e.printStackTrace();
}finally {
lock.unlock();//释放锁
}
}
public static void main(String[] args) {
Runnable r1 = new Runnable() {
@Override
public void run() {
sm1();
}
};
new Thread(r1).start();
}
}
3、ReentrantLock 锁的可重入性
public class Test04 {
static class Subthread extends Thread{
//定义锁对象,要用静态变量保证同一把锁
private static Lock lock = new ReentrantLock();
public static int num = 0; //定义变量
@Override
public void run() {
for (int i = 0; i <10000 ; i++) {
try {
//可重入锁指可以反复获得该锁
lock.lock();
lock.lock();
num++;
}finally {
lock.unlock();
lock.unlock();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Subthread t1 = new Subthread();
Subthread t2 = new Subthread();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println( Subthread.num );
}
}
4、lockInterruptibly()方法
lockInterruptibly() 方法的作用:如果当前线程未被中断则获得锁, 如果当前线程被中断则出现异常
通过 ReentrantLock 锁的 lockInterruptibly()方法避免死锁的产生。如果发生死锁,可以中断任何一个线程来解决死锁。
5、tryLock()方法
tryLock(long time, TimeUnit unit) 的作用在给定等待时长内锁没有被另外的线程持有,并且当前线程也没有被中断,则获得该锁。通过该方法可以实现锁对象的限时等待。
public class Test07 {
static class TimeLock implements Runnable{
private static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
try{
if(lock.tryLock(3, TimeUnit.SECONDS)){//获得锁返回 true
Thread.sleep(4000);//假设 Thread-0 线程先持有锁,完成任务需要 4 秒钟,Thread-1 线程尝试获得锁,Thread-1 线程在 3 秒内还没有获得锁的话,Thread-1线程会放弃
//Thread.sleep(2000);
// 假设 Thread-0 线程先持有锁,完成任务需要 2 秒钟,Thread-1 线程尝试获得锁,Thread-1 线程会一直尝试,在它约定尝试的 3 秒内可以获得锁对象
}else {
System.out.println(Thread.currentThread().getName() + "没有获得锁");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if(lock.isHeldByCurrentThread()){
lock.unlock();
}
}
}
}
public static void main(String[] args) {
TimeLock timeLock1 = new TimeLock();
TimeLock timeLock2 = new TimeLock();
Thread t1 = new Thread(timeLock1);
Thread t2 = new Thread(timeLock2);
t1.start();
t2.start();
}
}
6、tryLock()**
当锁对象没有被其他线程持有的情况下才会获得该锁定。如果锁对象被其他线程持有,则当前线程不会获得锁,线程结束。
7、newCondition()方法**
关键字 synchronized 与 wait()/notify()这两个方法一起使用可以实现等待/通知模式。Lock 锁的 newContition()方法返回 Condition 对象,Condition 类也可以实现等待/通知模式。
使用 notify()通知时, JVM 会随机唤醒某个等待的线程。使用 Condition 类可以进行选择性通知。
Condition 比较常用的两个方法: await()会使当前线程等待,同时会释放锁。signal()用于唤醒一个等待的线程,不会释放锁。
注意:在调用 Condition 的 await()/signal()方法前,也需要线程持有相关的 Lock 锁。调用 await()后线程会释放这个锁,在 singal()调用后会从当前 Condition 对象的等待队列中,唤醒 一个线程,唤醒的线程尝试获得锁, 一旦获得锁成功就继续执行。
public class Test{
static Lock lock = new ReentrantLock();
static Condition condition = lock.newCondition();//获得 Condtion 对象
static class SubThread extends Thread{
@Override
public void run() {
try{
lock.lock();//在调用 await()前必须先获得锁
System.out.println("method lock");
condition.await();//当前线程进入等待状态,释放锁
System.out.println("method await");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println("method unlock");
}
}
}
public static void main(String[] args) throws InterruptedException {
SubThread t = new SubThread();
t.start();
Thread.sleep(2000);
try{
lock.lock();
condition.signal();
}finally {
lock.unlock();
}
}
}
多个 Condition 实现通知部分线程, 使用更灵活。例如创建两个Condition,这两个Condition分别使用它们自己的await()和signal()方法。
8、公平锁与非公平锁
大多数情况下,锁的申请都是非公平的。如果线程1与线程2都在请求锁 A, 当锁 A 可用时, 系统只是会从阻塞队列中随机的选择一个线程, 不能保证其公平性。
公平的锁会按照时间先后顺序,保证先到先得, 公平锁的这一特点不会出现线程饥饿现象。
synchronized 内部锁就是非公平的,ReentrantLock 重入锁提供了一个构造方法:ReentrantLock(boolean fair) ,当在创建锁对象时实参传递 true 可以把该锁设置为公平锁。
公平锁看起来很公平,但是要实现公平锁必须要求系统维护一个有序队列,公平锁的实现成本较高,性能也 低。因此默认情况下锁是非公平的。不是特别的需求,一般不使用公平锁。
public class {
//static ReentrantLock lock = new ReentrantLock(); //默认是非公平锁
static ReentrantLock lock = new ReentrantLock(true); //定义公平锁
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
while (true){
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + " 获 得 了 锁对象");
}finally {
lock.unlock();
}
}
}
};
for (int i = 0; i < 5; i++) {
new Thread(runnable).start();
}
}
}
9、ReentrantLock常用的方法**
int getHoldCount() 返回当前线程调用 lock()方法的次数
int getQueueLength() 返回正等待获得锁的线程预估数
int getWaitQueueLength(Condition condition) 返回与 Condition 条件相关的等待的线程预估数
boolean hasQueuedThread(Thread thread) 查询参数指定的线程是否在等待获得锁
boolean hasQueuedThreads() 查询是否还有线程在等待获得该锁
boolean hasWaiters(Condition condition) 查询是否有线程正在等待指定的 Condition 条件
boolean isFair() 判断是否为公平锁
boolean isHeldByCurrentThread() 判断当前线程是否持有该锁
boolean isLocked() 查询当前锁是否被线程持有
10、ReentrantReadWriteLock 读写锁
synchronized 内部锁与 ReentrantLock 锁都是独占锁(排它锁), 同一时间只允许一个线程执行同步代码块,可以保证线程的安全性,但是执行效率低。ReentrantReadWriteLock 读写锁是一种改进的排他锁,也可以称作共享/排他锁,允许多个线程同时读取共享数据,但是一次只允许一个线程对共享数据进行更新。
读写锁通过读锁与写锁来完成读写操作。线程在读取共享数据前必须先持有读锁,该读锁可以同时被多个线程持有,即它是共享的。线程在修改共享数据前必须先持有写锁,写锁是排他的, 一个线程持有写锁时其他线程无法获得相应的锁。
读锁只是在读线程之间共享,任何一个线程持有读锁时,其他线程都无法获得写锁, 保证线程在读取数据期间没有其他线程对数据进行更新,使得读线程能够读到数据的最新值。
| 获得条件 | 排他性 | 作用 | |
|---|---|---|---|
| 读锁 | 写锁未被任意线程持有 | 读线程是共享的, 对写线程是排他的 | 允许多个读线程可以同时读取共享数据,保证在读共享数 据时,没有其他线程对共享数据进行修改 |
| 写锁 | 该写锁未被其他线程持有,并且相应的读锁也未被其他线程持有 | 对读线程或者写线程都是排他的 | 保证写线程以独占的方式修改共享数据 |
读写锁允许读读共享, 读写互斥,写写互斥
在java.util.concurrent.locks包中定义了ReadWriteLock接口,该接口中定义了 readLock()返回读锁,定义 writeLock()方法返回写锁。该接口的实现类是 ReentrantReadWriteLock。
注意 readLock()与 writeLock()方法返回的锁对象是同一个锁的两个不同的角色, 不是分别获得两个不同的锁。
//定义读写锁
ReadWriteLock rwLock = new ReentrantReadWriteLock();
//获得读锁
Lock readLock = rwLock.readLock();
//获得写锁
Lock writeLock = rwLock.writeLock();
//读数据
readLock.lock(); //申请读锁
try{
读取共享数据
}finally{
readLock.unlock(); //总是在 finally 子句中释放锁
}
//写数据
writeLock.lock(); //申请写锁
try{
更新修改共享数据
}finally{
writeLock.unlock(); //总是在 finally 子句中释放锁
}
轻量级同步机制:volative 关键字
1、volatile 的作用
volatile 关键的作用使变量在多个线程之间可见,保证可见性。
volatile 的作用可以强制线程从公共内存中读取变量的值,而不是从工作内存中读取
2、volatile 与 synchronized 比较
-
volatile 关键字是线程同步的轻量级实现,所以volatile性能肯定比 synchronized 要好; volatile 只能修饰变量,而 synchronized 可以修饰方法,代码块。
-
多线程访问 volatile 变量不会发生阻塞,而 synchronized 可能会阻塞。
-
volatile 能保证数据的可见性,但是不能保证原子性; 而 synchronized 可以保证原子性,也可以保证可见性。
-
关键字 volatile 解决的是变量在多个线程之间的可见性; synchronized 关键字解决多个线程之间访问公共资源的同步性。
3、volatile 非原子特性
volatile 关键字增加了实例变量在多个线程之间的可见性,但是不具备原子性。
6. CAS
1、CAS(Compare And Swap)是由硬件实现的。
2、CAS 可以将 read- modify - write 这类的操作转换为原子操作。
3、CAS 原理: 在把数据更新到主内存时,再次读取主内存变量的值,如果现在变量的值与期望的值(操作起始时读取的值)一样就更新。
4、CAS 实现原子操作背后有一个假设: 共享变量的当前值与当前线程提供的期望值相同, 就认为这个变量没有被其他线程修改过。
实际上这种假设不一定总是成立,如有共享变量 count = 0,A线程想把count值修改为10,B线程已经成功把count再改为0。A线程看到 count 变量的值现在是 0,现在是否认为 count 变量的值没有被其他线程更新呢?
这就是 CAS 中的 ABA 问题,即共享变量经历了 A->B->A 的更新。
如果想要规避 ABA 问题,可以为共享变量引入一个修订号(时间戳), 每次修改共享变量时,相应的修订号就会增加 1。ABA 变量更新过程变量: [A,0] ->[B,1]->[A,2], 每次对共享变量的修改都会导致修订号的增加,通过修订号依然可以准确判断变量是否被其他线程修改过。AtomicStampedReference 类就是基于这种思想产生的
7. 原子变量类
原子变量类基于CAS实现的, 当对共享变量进行read-modify-write 更新操作时,通过原子变量类可以保障操作的原子性与可见性。
对变量的 read-modify-write 操作是指当前操作不是一个简单的赋值,而是变量的新值依赖变量的旧值。
volatile只能保证可见性,无法保障原子性。原子变量类内部是借助一个 Volatile 变量, 并且保障了该变量的 read-modify-write 操作的原子性, 有时把原子变量类看作增强的 volatile。
原子变量类有 12 个
| 分组 | 原子变量类 |
|---|---|
| 基础数据型 | AtomicInteger、AtomicLong、 AtomicBoolean |
| 数组型 | AtomicIntegerArray,、AtomicLongArray,、AtomicReferenceArray |
| 字段更新器 | AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdate |
| 引用型 | AtomicReference、 AtomicStampedReference、 AtomicMarkableReference |
1. 等待/通知机制
1、什么是等待通知机制
在多线程编程中,可能 A 线程的条件没有满足只是暂时的, 稍后其他的线程 B 可能会更新条件使得 A 线程的条件得到满足。可以将 A 线程暂停,直到它的条件得到满足后再将 A 线程唤醒。
2、等待/通知机制的实现
1)Object 类中的wait()方法可以使执行当前代码的线程等待,暂停执行,直到接到通知或被中断为止。
注意:
-
wait()方法只能在同步代码块中由锁对象调用
-
调用 wait()方法,当前线程会释放锁
2)Object 类的 notify()方法可以唤醒线程,该方法也必须在同步代码块中由锁对象调用。如果有多个等待的线程,notify()方法只能唤醒其中的一个,notifyAll()方法能唤醒全部。在同步代码块中调用 notify()方法后,并不会立即释放锁对象,需要等当前同步代码块执行完后才会释放锁对象,一般将notify()方法放在同步代码块的最后。
public class Test{
public static void main(String[] args) {
String lock = "asd";//定义一个字符串作为锁对象
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock){
try {
lock.wait();//线程等待,会释放锁对象,当前线程转入blocked 阻塞状态
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
//定义第二个线程,在第二个线程中唤醒第一个线程
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
//notify()方法也需要在同步代码块中,由锁对象调用
synchronized (lock){
lock.notify();
}
}
});
t1.start();
try {
Thread.sleep(3000);//main 线程睡眠 3 秒,确保 t1先执行
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
3、interrupt()方法会中断 wait()
当线程处于wait()等待状态时, 调用线程对象的interrupt()方法会中断线程的等待状态, 会产生 InterruptedException 异常。
4、notify()与 notifyAll()
notify()一次只能唤醒一个线程,如果有多个等待的线程,只能随机唤醒其中的某一个; 想要唤醒所有等待线程,需要调用 notifyAll()。
5、wait(long)的使用
wait(long)带有 long 类型参数的 wait()等待,如果在参数指定的时间内没有被唤醒,超时后会自动唤醒。
public class Test {
public static void main(String[] args) {
Object obj = new Object();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (obj){
try {
System.out.println("begin wait...");
obj.wait(3000);//超过3秒没有被唤醒,会自动唤醒
System.out.println("end wait...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
6、通知过早
线程wait()等待后,可以调用 notify()唤醒线程。 如果 notify()唤醒的线程先执行,再执行wait()等待线程,那么就没有唤醒它的线程,等待线程就会一直等待。
解决方案
public class Test09 {
static boolean isFirst = true; //定义静态变量作为是否第一个运行的线程标志
public static void main(String[] args) throws InterruptedException {
final Object Lock = new Object(); //定义对象作为锁对象
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (Lock){
while ( isFirst ) { //当线程是第一个开启的线程就等待
try {
Lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (Lock){
Lock.notify();
isFirst = false; //通知后,就把第一个线程标志修改为 false
}
}
});
t2.start();
Thread.sleep(1000);//main线程睡一秒,保证t2线程先执行
t1.start();
}
}
2. 生产者消费者模式
public class ThreadTest16 {
public static void main(String[] args) {
// 创建1个仓库对象,共享的。
List list = new ArrayList();
// 创建两个线程对象
// 生产者线程
Thread t1 = new Thread(new Producer(list));
// 消费者线程
Thread t2 = new Thread(new Consumer(list));
t1.setName("生产者线程");
t2.setName("消费者线程");
t1.start();
t2.start();
}
}
// 生产线程
class Producer implements Runnable {
// 仓库
private List list;
public Producer(List list) {
this.list = list;
}
@Override
public void run() {
// 一直生产(使用死循环来模拟一直生产)
while(true){
// 给仓库对象list加锁。
synchronized (list){
if(list.size() > 0){ // 大于0,说明仓库中已经有1个元素了。
try {
// 当前线程进入等待状态,并且释放Producer之前占有的list集合的锁。
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 程序能够执行到这里说明仓库是空的,可以生产
Object obj = new Object();
list.add(obj);
System.out.println(Thread.currentThread().getName() + "--->" + obj);
// 唤醒消费者进行消费
list.notify();
}
}
}
}
// 消费线程
class Consumer implements Runnable {
// 仓库
private List list;
public Consumer(List list) {
this.list = list;
}
@Override
public void run() {
// 一直消费
while(true){
synchronized (list) {
if(list.size() == 0){
try {
// 仓库已经空了。
// 消费者线程等待,释放掉list集合的锁
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 程序能够执行到此处说明仓库中有数据,进行消费。
Object obj = list.remove(0);
System.out.println(Thread.currentThread().getName() + "--->" + obj);
// 唤醒生产者生产。
list.notify();
}
}
}
}
9. 注入 Hook 钩子线程
现在很多软件包括 MySQL, Zookeeper, kafka 等都存在 Hook 线程的校验机制, 目的是校验进程是否已启动,防止重复启动程序。Hook 线程也称为钩子线程, 当 JVM 退出的时候会执行 Hook 线程。
经常在程序启动时创建一个.lock 文件, 用.lock 文件校验程序是否启动,在程序退出(JVM 退出)时删除该.lock 文件, 在 Hook 线程中除了防止重新启动进程外,还可以做资源释放, 尽量避免在 Hook 线程中进行复杂的操作。
Runtime.getRuntime().addShutdownHook(new Thread(){
@Override
public void run() {}
});
10. 线程池
线程池是有效使用线程的一种常用方式。线程池内部可以预先创建一定数量的工作线程,客户端代码直接将任务作为一个对象提交给线程池, 线程池将这些任务缓存在工作队列中, 线程池中的工作线程不断地从队列中取出任务并执行。
1、线程池类关系UML图 2、 Java 中的线程池是如何实现的创建一个阻塞队列来容纳任务,在第一次执行任务时创建足够多的线程,并处理任务,之后每个工作线程自动从任务队列中获取线程,直到任务队列中任务为0为止,此时线程处于等待状态,一旦有工作任务加入任务队列中,即刻唤醒工作线程进行处理,实现线程的可复用性。
线程池一般包括四个基本组成部分:
1)线程池管理器
用于创建线程池,销毁线程池,添加新任务。
2)工作线程
线程池中线程,可循环执行任务,在没有任务时处于等待状态。
3)任务队列
用于存放没有处理的任务,一种缓存机制。
4)任务接口
每个任务必须实现的接口,供工作线程调度任务的执行,主要规定了任务的开始和收尾工作,和任务的状态。
3、核心线程池的底层实现三种线程池创建方式
1)newCachedThreadPool:缓冲功能的线程池
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue());
}
//该线程池在极端情况下,每次提交新的任务都会创建新的线程执行. 适合用来执行大量耗时短并且提交频繁的任务
//该线程池,线程数量是没有限制的(当然,不能超过Integer的最大值),新增一个任务即有一个线程处理,或者复用之前空闲的线程,或者重亲启动一个线程,但是一旦一个线程在60秒内一直处于等待状态时(也就是一分钟无事可做),则会被终止。
2)newFixedThreadPool:固定线程数量的线程池
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());
}
3)newSingleThreadExecutor:单线程池
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()));
}
//该线程池中只有一个线程在运行,该线程永不超时,而且由于是一个线程,当有多个任务需要处理时,会将它们放置到一个无界阻塞队列中逐个处理。
创建线程池的几个核心构造参数
Excutors 工 具 类 中 返 回 线 程 池 的 方 法 底 层 都 使 用 了 ThreadPoolExecutor 线程池,这些方法都是 ThreadPoolExecutor 线程池的封装。
public ThreadPoolExecutor( int corePoolSize, // 指定线程池中核心线程的数量 int maximumPoolSize, // 指定线程池中最大线程数量 long keepAliveTime, // 当线程数大于核心数时,那些空闲线程在终止前等待新任务的最长时间 TimeUnit unit, //时间单位 BlockingQueueworkQueue, //任务队列,把任务提交到该任务队列中等待执行 ThreadFactory threadFactory, // 线程工厂,用于创建线程 RejectedExecutionHandler handler // 拒绝任务处理器。由于超出线程数量和队列容量而对继续增加的任务进行处理的程序。 )
1、ThreadPoolExecutor 构造方法中可以使用以下几种阻塞队列:
1) 直接提交队列,由 SynchronousQueue 对象提供,该队列没有容量,提交给线程池的任务不会被真实的保存,总是将新的任务提交给线程执行,如果没有空闲线程,则尝试创建新的线程,如果线程数量已经达到 maxinumPoolSize 规定的最大值则执行拒绝策略。
2) 有 界 任 务 队 列 , 由 ArrayBlockingQueue 实 现 , 在创建 ArrayBlockingQueue 对象时,可以指定一个容量。 当有任务需要执行时,如果线程池中线程数小于 corePoolSize 核心线程数则创建新的线程;如果大于 corePoolSize 核心线程数则加入等待队列。如果队列已满则无法加入,在线程数小于 maxinumPoolSize 指定的最大线程数前提下会创 建 新 的 线 程 来 执 行 , 如 果 线 程 数 大 于 maxinumPoolSize 最大线程数则执行拒绝策略 。
3) 无界任务队列,由 LinkedBlockingQueue 对象实现,与有界队列相比,除非系统资源耗尽,否则无界队列不存在任务入队失败的情况。当有新的任务时,在系统线程数小于 corePoolSize 核心线程数则创建新的线程来执行任务;当线程池中线程数量大于corePoolSize 核心线程数则把任务加入阻塞队列
4) 优先任务队列,是通过 PriorityBlockingQueue 实现的,是带有 任 务 优 先 级 的 队 列 , 是 一 个 特 殊 的 无 界 队 列 . 不 管 是 ArrayBlockingQueue 队列还是 LinkedBlockingQueue 队列都是按照先进先出算法处理任务的。在 PriorityBlockingQueue 队列中可以根据任务优先级顺序先后执行。
2、拒绝策略
-
AbortPolicy 策略,会抛出异常
-
CallerRunsPolicy 策略,只要线程池没关闭,会在调用者线程中运行当前被丢弃的任务
-
DiscardOldestPolicy 将任务队列中最老的任务丢弃,尝试再次提交新任务
-
DiscardPolicy 直接丢弃这个无法处理的任务
Executors 工具类提供的静态方法返回的线程池默认的拒绝策略是 AbortPolicy 抛出异常,如果内置的拒绝策略无法满足实际需求,可以扩展RejectedExecutionHandler 接口
public class Test03 {
public static void main(String[] args) {
//定义任务
Runnable r = new Runnable() {
@Override
public void run() {
int num = new Random().nextInt(5);
System.out.println(Thread.currentThread().getId() + "--" + System.currentTimeMillis() + "开始睡眠" + num + "秒");
try {
TimeUnit.SECONDS.sleep(num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
//创建线程池, 自定义拒绝策略
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 0, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10), Executors.defaultThreadFactory(),
new RejectedExecutionHandler(){
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
//r就是请求的任务, executor就是当前线程池
System.out.println(r + " is discarding..");
}
});
//向线程池提交若干任务
for (int i = 0; i < Integer.MAX_VALUE; i++) {
threadPoolExecutor.submit(r);
}
}
}
4、线程池的计划任务
public class Test {
public static void main(String[] args) {
//创建一个有调度功能的线程池
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
//在延迟 5 秒后执行任务, schedule( Runnable 任务, 延迟时长, 时间单位)
scheduledExecutorService.schedule(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getId() + " -- " + System.currentTimeMillis() );
}
}, 5, TimeUnit.SECONDS);
//以固定的频率执行任务,开启任务的时间是固定的, 最开始在 5 秒后执行任务,之后如果任务执行时间小于period,则period时间后执行下一次任务;如果任务执行时间大于period,则任务结束后立即执行下一次任务
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getId() + "----在固定频率开启任务---" + System.currentTimeMillis());
try {
TimeUnit.SECONDS.sleep(1); //睡眠模拟任务执行时间 ,如果任务执
//行时长超过了时间间隔,则任务完成后立即开启下个任务
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, 5, 2, TimeUnit.SECONDS);
//在上次任务结束后,在固定延迟后再次执行该任务,不管执行任务耗时多长,总是在任务结束后的 2 秒再次开启新的任务。执行下一个任务时间就是上一个任务执行时间+delay时间
scheduledExecutorService.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getId() + "----在固定频率开启任 务---" + System.currentTimeMillis());
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, 3, 2, TimeUnit.SECONDS);
}
}
5、ThreadFactory
ThreadFactory 是一个接口,用来创建线程池中的线程。
ThreadFactory 只有一个用来创建线程的方法: Thread newThread(Runnable r)。当线程池中需要创建线程时就会调用该方法。
public class Test04 {
public static void main(String[] args) throws InterruptedException {
//定义任务
Runnable r = new Runnable() {
@Override
public void run() {
int num = new Random().nextInt(10);
System.out.println(Thread.currentThread().getId() + "--" + System.currentTimeMillis() + "开始睡眠:" + num + "秒");
try {
TimeUnit.SECONDS.sleep(num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
//创建线程池, 使用自定义线程工厂, 采用默认的拒绝策略是抛出异常
ExecutorService executorService = new ThreadPoolExecutor(5, 5, 0,
TimeUnit.SECONDS, new SynchronousQueue<>(), new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
//根据参数r接收的任务,创建一个线程
Thread t = new Thread( r );
t.setDaemon(true);//设置为守护线程, 当主线程运行结束,线程池中的线程会自动退出
System.out.println("创建了线程: " + t);
return t ;
}
});
//提交5个任务, 给当前线程池提交的任务超过5个时,线程池默认抛出异常
for (int i = 0; i < 5; i++) {
executorService.submit(r);
}
//主线程睡眠
Thread.sleep(10000);
//主线程睡眠超时, 主线程结束, 线程池中的线程会自动退出
}
}
6、监控线程池方法
ThreadPoolExecutor 提供了一组方法用于监控线程池
-
int getActiveCount() 获得线程池中当前活动线程的数量
-
long getCompletedTaskCount() 返回线程池完成任务的数量
-
int getCorePoolSize() 线程池中核心线程的数量
-
int getLargestPoolSize() 返回线程池曾经达到的线程的最大数
-
int getMaximumPoolSize() 返回线程池的最大容量
-
int getPoolSize() 当前线程池的大小
-
BlockingQueue getQueue() 返回阻塞队列
-
long getTaskCount() 返回线程池收到的任务总数
ThreadPoolExecutor 线程池提供了两个方法:
-
protected void afterExecute(Runnable r, Throwable t)
-
protected void beforeExecute(Thread t, Runnable r)
在线程池执行某个任务前会调用 beforeExecute()方法,在任务结束后(任务异常退出)会执行 afterExecute()方法
查看 ThreadPoolExecutor 源码,在该类中定义了一个内部类 Worker, ThreadPoolExecutor 线程池中的工作线程就是 Worker 类的实例, Worker 实例在执行时会调用 beforeExecute()与 afterExecute()方法
public class Test06 {
//定义任务类
private static class MyTask implements Runnable{
String name;
public MyTask(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println(name + "任务正在被线程 " + Thread.currentThread().getId() + " 执行");
try {
Thread.sleep(1000); //模拟任务执行时长
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
//定义扩展线程池, 可以定义线程池类继承ThreadPoolExecutor,在子类中重写beforeExecute()/afterExecute()方法
//也可以直接使用readPoolExecutor的内部类
ExecutorService executorService = new ThreadPoolExecutor(5, 5, 0,
TimeUnit.SECONDS, new LinkedBlockingQueue<>() ){
//在内部类中重写任务开始方法
@Override
protected void beforeExecute(Thread t, Runnable r) {
System.out.println(t.getId() + "线程准备执行任务: " + ((MyTask)r).name);
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
System.out.println( ((MyTask)r).name + "任务执行完毕");
}
@Override
protected void terminated() {
System.out.println("线程池退出");
}
};
//向线程池中添加任务
for (int i = 0; i < 5; i++) {
MyTask task = new MyTask("task-" + i);
executorService.execute(task);
}
//关闭线程池
executorService.shutdown(); //关闭线程池仅仅是说线程池不再接收新的任务 , 线程池中已接收的任务正常执行完毕
}
}
8、线程池死锁
如果在线程池中执行的任务 A 在执行过程中又向线程池提交了任务 B, 任务 B 添加到了线程池的等待队列中, 如果任务 A 的结束需要等待任务 B 的执行结果,就有可能会出现这种情况: 线程池中所有的工作线程都处于等待任务处理结果,而这些任务在阻塞队列中等待执行, 线程池中没有可以对阻塞队列中的任务进行处理的线程,这种 等待会一直持续下去,从而造成死锁。
适合给线程池提交相互独立的任务,而不是彼此依赖的任务。对于彼此依赖的任务,可以考虑分别提交给不同的线程池来执行。
9、 线程池中的异常处理在使用 ThreadPoolExecutor 进行 submit 提交任务时,有的任务抛出了异常,但是线程池并没有进行提示,即线程池把任务中的异常给吃掉了,可以把 submit 提交改为 execute 执行,也可以对 ThreadPoolExecutor 线程池进行扩展,对提交的任务进行包装。
public class Test08 {
//自定义线程池类
private static class TraceThreadPollExecutor extends ThreadPoolExecutor{
public TraceThreadPollExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
//定义方法,对执行的任务进行包装,接收两个参数,第一个参数接收要执行的任务,第二个参数是一个Exception异常
public Runnable wrap( Runnable task, Exception exception){
return new Runnable() {
@Override
public void run() {
try {
task.run();
}catch (Exception e ){
exception.printStackTrace();
throw e;
}
}
};
}
//重写submit方法
@Override
public Future> submit(Runnable task) {
return super.submit(wrap(task, new Exception("客户跟踪异常")));
}
@Override
public void execute(Runnable command) {
super.execute(wrap(command, new Exception("客户跟踪异常")));
}
}
//定义类实现Runnable接口,用于计算两个数相除
private static class DivideTask implements Runnable{
private int x;
private int y;
public DivideTask(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "计算:" + x + " / " + y + " = " + (x/y));
}
}
public static void main(String[] args) {
//创建线程池
//ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 0, TimeUnit.SECONDS, new SynchronousQueue<>());
//使用自定义的线程池
ThreadPoolExecutor poolExecutor = new TraceThreadPollExecutor(0, Integer.MAX_VALUE, 0,TimeUnit.SECONDS, new SynchronousQueue<>());
//向线程池中添加计算两个数相除的任务
for (int i = 0; i < 5; i++) {
poolExecutor.submit(new DivideTask(10, i));
// poolExecutor.execute(new DivideTask(10, i));
}
}
}



