Java-多线程
1.进程和线程基本概念
1.1 进程
-
运行中的应用程序叫进程,每个进程运行时,进程负责了内存空间的划分。它是系统进行资源分配和调度的一个独立单位。
-
操作系统都是支持多进程的
-
Windows是多任务的操作系统,那么Windows是同时运行多个应用程序吗?
1.2 线程
1.2.1线程概念和特点
-
线程是轻重级的进程,是进程中一个负责程序执行的控制单元
-
线程是由进程创建的(寄生在进程中)
-
一个进程可以拥有多个线程,至少一个线程
-
线程有几种状态(新建new,就绪Runnable,运行Running,阻塞Blocked,死亡Dead)
-
开启多个线程是为了同时运行多部分代码,每个线程都 有自已的运行的内容,这个内容可以称线程要执行的任务。
-
打开360卫士,就是打开一个进程,一个进程里面有很多代码,这些代码就是谁来执行的呢?线程来执行这些代码。
1.2.2 多线程
-
多线程:在一个进程中有多个线程同时在执行不同的任务。
-
多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务
-
如:百度网盘、腾讯会议。
-
同时执行多个任务的优势:减低CPU的闲置时间,从而提高CPU的利用率
1.2.3 计算机操作系统的进化史
-
当前操作系统支持多线程
2.多线程的优缺点
2.1 多线程优点
-
多线程最大的好处在于可以同时并发执行多个任务;
-
多线程可以最大限度地减低CPU的闲置时间,从而提高CPU的利用率
2.2 多线程缺点
-
线程也是程序,所以线程需要占用内存,线程越多占用内存也越多;
-
多线程需要协调和管理,所以需要CPU时间跟踪线程;
-
线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题;
-
线程太多会导致控制太复杂,最终可能造成很多Bug
3.主线程
-
Java支持多线程
-
任何一个Java程序启动时,一个线程立刻运行,它执行main方法,这个线程称为程序的主线程。
-
一个Java应用程序至少有两个线程,一个是主线程负责main方法代码执行;一个是垃圾回收器线程,负责了回收垃圾。
主线程的特殊之处在于:
-
-
4.创建线程
4.1 创建线程的三种方式
-
方式一:继承Thread类
-
方式二:实现Runnable接口
-
方式三:实现Callable接口
4.2 继承Thread类
- (1)继承Java.lang.Thread类,并重写run() 方法(业务逻辑)。
- (2)创建线程类对象
- (3)start方法:启动一个线程执行继承Thread线程类的run方法
启动一个线程,是由操作系统决定的,启动的时机也是由操作系统决定无法用程序控制
-
案例1:打印输出0-100的数字,创建两个线程交替执行
/*打印输出0-100的数字,创建两个线程交替执行 创建线程 方式一:继承Thread类 重写run方法*/ public class MyPrintThread extends Thread{ //有参数的构造方法--参数 线程名字 public MyPrintThread(String name) { //调用Thread中的构造方法 super(name); } //run方法的方法体就是线程的执行体 @Override public void run() { for (int i = 1; i <100; i++) { try { //休眠1秒钟,此线程进入阻塞状态,苏醒之后,进入可运行状态,不是立刻执行 Thread.sleep(1000/*毫秒*/); } catch (InterruptedException e) { e.printStackTrace(); } //this.getName() 获取当前正在执行的线程名称 System.out.println(this.getName()+ ":"+i); } } } public class MyPrintThreadTest { public static void main(String[] args) { //创建线程类对象 MyPrintThread thread1 = new MyPrintThread("线程1"); MyPrintThread thread2 = new MyPrintThread("线程2"); /* * 1.启动线程(调用start方法),线程将等待调度(什么时候被调度不一定,要看当前的操作系统分配资源的情况) * 2.调度后,自动调用其run方法,run方法是等着被自动调用的。 * 3.结果:每一次都是随机的,再运行一次还是不一样,不同的机器结果不同,每次运行的结果也不同 */ thread1.start(); //启动线程1 thread2.start(); //启动线程2 System.out.println("main方法的最后一条语句"); } }
-
案例2:模拟龟兔赛跑
//兔子的线程 class MyThread1 extends Thread{ private int s = 5; //100米短跑 //线程执行的代码 @Override public void run() { while(true) { if(s<0) { System.out.println("兔子跑完了"); break; //结束线程 } System.out.println("兔子领先了,加油,还剩下"+s+"米"); s--; } } } //乌龟的线程 class MyThread2 extends Thread{ private int s = 5; @Override public void run() { while(true) { if(s<0) { System.out.println("乌龟跑完了"); break; } System.out.println("乌龟领先了,加油,还剩下"+s+"米"); s--; } } } /*龟兔赛跑 继承thread类测试*/ public class RaceThreadTest { public static void main(String[] args) { //兔子 MyThread1 t1 = new MyThread1(); t1.start(); //乌龟 MyThread2 t2 = new MyThread2(); t2.start(); } }
-
案例1中的MyPrintThread要求继承ArrayList,如何创建多线程?继承Thread类就不能继承ArrayList,因为,java是单继承,所以就不合适了
-
也有的人认为让ArrayList继承Thread,MyPrintThread再继承ArrayList,或者Thread继承ArrayList,MyPrintThread再继承Thread,
-
这两种方案都不可以,因为Thread和ArrayList是jdk提供的不能随便修改。
4.3 实现Runnable接口
- (1)实现Java.lang.Runnable接口,并重写run() 方法(业务逻辑);
- (2)创建线程类对象
- (3)创建Thread对象,线程类对象作为参数传给Thread对象的构造方法
- (4)调用Thread对象的start方法
注意:Runnable接口的存在主要是为了解决Java中不允许多继承的问题。
-
案例1:打印输出0-100的数字,创建两个线程交替执行
/* 创建线程的第二种方式 实现Runnable接口 重写run方法 */ class MyThread implements Runnable { @Override public void run() { for (int i = 0; i < 100; i++) { try { //休眠1秒 Thread.sleep(1000/*毫秒*/); } catch (InterruptedException e) { e.printStackTrace(); } // Thread.currentThread().getName() 获取当前执行的线程名字 System.out.println(Thread.currentThread().getName() + ":" + i); } } } public class MyRunnableThreadTest { public static void main(String[] args) { MyThread r1 = new MyThread(); //创建Thread对象作为外壳包裹r1 Thread t1 = new Thread(r1); t1.start(); MyThread r2 = new MyThread(); //创建Thread对象作为外壳包裹r2 Thread t2 = new Thread(r2); t2.start(); } }
-
案例2:模拟龟兔赛跑
//龟兔赛跑 实现runnable接口测试 public class RaceRunnableTest { public static void main(String[] args) { MyThread3 r3 = new MyThread3();//兔 Thread t3 = new Thread(r3); t3.start(); MyThread4 r4 = new MyThread4();//龟 Thread t4 = new Thread(r4); t4.start(); } } //兔子的线程 class MyThread3 implements Runnable { private int s = 100; @Override public void run() { while (true) { if (s < 0) { System.out.println("兔子跑完了"); break; } System.out.println("兔子领先了,加油,还剩下" + s + "米"); s--; } } } //乌龟的线程 class MyThread4 implements Runnable { private int s = 100; @Override public void run() { while (true) { if (s < 0) { System.out.println("乌龟跑完了"); break; } System.out.println("乌龟领先了,加油,还剩下" + s + "米"); s--; } } }
-
案例1中MyThread如何实现返回值?
4.4 实现Callable接口
-
使用Callable和Future创建线程
- Callable接口提供了一个call() 方法作为线程执行体
- Runnable接口提供的是run()方法
- call()方法可以有返回值,而且需要用FutureTask类来包装Callable对象。
-
步骤:
1、创建Callable接口的实现类,实现call() 方法
2、创建Callable实现类实例,通过FutureTask类来包装Callable对象,该对象封装了Callable对象的call()方法的返回值。
3、将创建的FutureTask对象作为target参数传入,创建Thread线程实例并启动新线程。
4、调用FutureTask对象的get方法获取返回值(阻塞代码,直到线程类对象的call执行完毕)。
-
案例
import java.util.concurrent.Callable; /* 创建线程的第三种方式 实现Callable泛型接口 重写call方法 可以返回结果可以抛异常 */ class CallableThreadDemo implements Callable<Integer> { @Override public Integer call() { int sum = 0; for (int i = 1; i <= 100; i++) { sum += i; } return sum; } } import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; /*注意:Callable需要依赖FutureTask,用于接收运算结果。 一个产生结果,一个拿到结果。FutureTask是Future接口的实现类。*/ public class CallDemo { public static void main(String[] args) throws ExecutionException, InterruptedException { // (1)创建Callable实现类的实例化对象 CallableThreadDemo ctd = new CallableThreadDemo(); // (2)创建FutureTask对象,并将Callable对象传入FutureTask的构造方法中 FutureTask<Integer> result = new FutureTask<>(ctd); // (3)实例化Thread对象,并在构造方法中传入FurureTask对象 Thread t = new Thread(result); // (4)启动线程 t.start(); Thread.sleep(2000); Integer sum = result.get(); // 检索结果 System.out.println(sum); } } /*5050*/
-
案例2:
//用户实体类 public class User { private String name; private int age; public User() { } public User(String name, int age) { super(); this.name = name; this.age = age; } public String getName() {return name; } public void setName(String name) {this.name = name;} public int getAge() {return age;} public void setAge(int age) {this.age = age;} @Override public String toString() { return "User{" +"name='" + name + '\'' +", age=" + age + '}'; } } import java.util.ArrayList; import java.util.List; //用户服务类--测试创建线程方式三,实现callable接口 public class UserService { public List<User> selectList() { List<User> list = new ArrayList<>(); User u1 = new User("Yi", 21); User u2 = new User("Jun", 26); list.add(u1); list.add(u2); return list; } } import java.util.List; import java.util.concurrent.Callable; public class UserThread implements Callable<List<User>> { private UserService userService; public UserThread(UserService userService) { this.userService = userService; } @Override public List<User> call() throws Exception { //模拟具体业务逻辑 List<User> list = userService.selectList(); return list; } } import java.util.List; import java.util.concurrent.FutureTask; public class CallableThreadTest { public static void main(String[] args) { //创建UserService对象 UserService userService = new UserService(); // 创建UserThread对象 UserThread userThread = new UserThread(userService); // 创建FutureTask对象 FutureTask futureTask = new FutureTask(userThread); // 创建Thread对象 Thread t = new Thread(futureTask); //启动线程 t.start(); try { Thread.sleep(2000); //FutureTask对象的get方法获取返回值 List<User> list = (List<User>) futureTask.get(); System.out.println(list); } catch (Exception e) { e.printStackTrace(); } } }
4.5 创建线程的三种方式-比较
-
继承Thread类:
-
优势:Thread类已实现了Runnable接口,故使用更简单
-
劣势:无法继承其它父类
-
-
实现Runnable接口:
-
优势:可以继承其它类
-
劣势:编程方式稍微复杂,多写一行代码
-
-
实现Callable接口:
-
类似于Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的,方法可以有返回值,并且可以抛出异常。但是Runnable不行。
-
5.线程的状态
- 当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。
- 在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5 种状态。
- 尤其是当线程启动以后,它不可能一直"霸占"着 CPU 独自运行,所以 CPU 需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换
-
Java中线程状态转换:
-
Java中线程存在以下几种状态(后续将详细讲解对应代码):
-
新线程(新建状态New):
-
当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态。
-
此时仅由 JVM 为其分配内存,并初始化其成员变量的值,它仅仅作为一个对象实例存在。
-
JVM没有为其分配CPU时间片和其他线程运行资源。
-
-
就绪状态(Runnable):
-
在处于创建状态的线程中调用start方法将线程的状态转换为就绪状态。
-
Java 虚拟机会为其创建方法调用栈和程序计数器,线程已经得到除CPU时间之外的其它系统资源。
-
这时,只等JVM的线程调度器按照线程的优先级对该线程进行调度,从而使该线程拥有能够获得CPU时间片的机会。
-
-
运行状态:
-
如果处于就绪状态的线程获得了 CPU,开始执行 run()方法的线程执行体,则该线程处于运行状态。
-
-
阻塞:
-
阻塞状态是线程因为某种原因放弃CPU使用权,也即让出了 cpu timeslice,暂时停止运行。
- 直到线程进入就绪/可运行(runnable)状态,才有机会再次获得 cpu timeslice 转到运行(running)状态。
-
-
-
-
- 阻塞的情况分三种:
-
-
-
-
-
等待阻塞(o.wait->等待对列):
运行(running)的线程执行 o.wait()方法,JVM 会把该线程放入等待队列(waitting queue)中。
-
同步阻塞(lock->锁池):
运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池(lock pool)中。
- 其他阻塞(sleep/join)
运行(running)的线程执行 Thread.sleep(long ms)或 t.join()方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。 当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O处理完毕时,线程重新转入可运行(runnable)状态。
-
-
- 死亡状态:
-
线程执行完它的任务时,由JVM收回线程占用的资源。
-
线程会以下面三种方式结束,结束后就是死亡状态。
- 1、正常结束:run()或 call()方法执行完成,线程正常结束。
- 2、异常结束:线程抛出一个未捕获的 Exception 或 Error。
- 3、调用stop:直接调用该线程的 stop()方法来结束该线程-----该方法通常容易导致死锁,不推荐使用。
-
-
6.线程的调动与控制
6.1 Thread类
-
使用线程的步骤:
-
Thread类提供了较多的构造方法重载版本,以下列出了常用的几个版本
-
Thread类用于创建和操作线程,其中包括几个很重要的静态方法,用于控制当前线程
-
Thread类中的常用方法
6.2 启动
-
为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
-
new 一个 Thread,线程进入初始状态;调用 start()方法,会启动一个线程并使线程进入了就 绪状态,当分配到时间片后就可以开始运行了。
-
start() 会执行线程的相应准备工作,然后自 动执行 run() 方法的内容,这是真正的多线程工作。
-
而直接执行 run() 方法,会把 run 方法 当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程 工作。
-
- 线程启动
- start方法,不会立刻启动线程,启动线程取决于操作系统,程序员无法控制
- 执行start()方法,但不能多次执行
//线程启动测试 public class CreateThreadByExtends extends Thread{ public void run() { System.out.println("First thread with extend Thread @chinasofti"); } public static void main(String[] args) { new CreateThreadByExtends().start(); } }
6.3 停止
-
线程的停止远比线程的启动情况复杂
-
在Thread线程类中提供了stop()方法用于停止一个已经启动的线程,但是它已经被废弃,不建议使用,因为它本质上就是不安全的,它会解锁线程所有已经锁定的监视程序,在这个过程中会出现不确定性,导致这些监视程序监视的对象出现不一致的情况,或者引发死锁。
-
-
那么应该如何安全的停止一个正在运行的线程呢?
-
线程对象的run()方法所有代码执行完成后,线程会自然消亡,因此如果需要在运行过程提前停止线程
-
可以通过改变共享变量值 flag 的方法让run()方法执行结束。
- 线程类通过开发方法控制flag变化
/* 线程停止测试---使用共享变量停止线程的示例 */ public class ThreadStopExample extends Thread { //标记位(线程循环条件) private boolean flag = true; //停止线程 public void stopThread() { this.flag = false; } public void run() { int i = 0; while (flag) { i++; try { Thread.sleep(1000); /*耗时特别长时,线程需要长时间才能停止,会造成很多不方使 Thread.sleep(1000*60*60);*/ } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("thread execute loop..." + i); } } public static void main(String[] args) throws InterruptedException { ThreadStopExample ste = new ThreadStopExample(); ste.start(); Thread.sleep(5000); //不调用停止方法,线程会继续执行 ste.stopThread(); } }
-
-
如果一个线程由于等待某些事件的发生而被阻塞,又该怎样停止该线程呢?
-
使用Thread提供的interrupt()方法,不会停止一个正在运行的线程
-
可以使一个被阻塞的线程抛出一个中断异常,从而使线程提前结束阻塞状态,退出堵塞代码。
class SleepThread extends Thread { private boolean flag = true; //设置线程循环的共享变量为false public void setFlagFalse() { this.flag = false; } public void run() { int i = 0; while (flag) { i++; try { /* 业务逻辑执行很长 */ //模拟阻塞代码场景 Thread.sleep(1000 * 60 * 60); System.out.println(i); } catch (InterruptedException e) { e.printStackTrace(); System.out.println("thread execution interrupted!"); //处理InterruptedException异常时改变共享变量的值 flag = false; } } System.out.println("thread execution is over."); } } /* 线程停止测试---中断阻塞线程 */ public class StopSleepThreadTest { public static void main(String[] args) throws InterruptedException { SleepThread st = new SleepThread(); System.out.println("thread start..."); st.start(); Thread.sleep(2000); //不中断线程,一直运行,即便改变共享变量的值也无效 //st.setFlagFalse(); //中断线程,不能停止线程,可以抛出InterruptedException异常 st.interrupt(); } } /* thread start... java.lang.InterruptedException: sleep interrupted at java.lang.Thread.sleep(Native Method) at com.tjetc.SleepThread.run(StopSleepThread.java:20) thread execution interrupted! thread execution is over. */
-
-
线程停止总结
-
Thread.stop()方法已经过时不再使用
-
定义循环结束标记
-
使用interrupt(中断)方法:该方法是结束线程的冻结状态,使线程回到运行状态中来
-
停止线程方法一般用:标记+interrupt( )联合使用
-
6.4 设置及获取线程的名称
-
设置及获取线程的名称
-
当开启多个线程时,如何区分当前正在运行的线程是哪个?
-
类实现Runnable时用的是Thread.currentThread().getName()
-
类继承Thread时用的是getName()
-
-
6.5 设置线程的优先级
-
设置线程的优先级
-
多个线程处于可运行状态时,将对cpu时间片进行竞争,优先级高的,获取的可能性则高;
-
优先级较高的线程有更多获得CPU的机会,反之亦然;
-
线程优先级用1~10 表示,10的优先级最高,默认值是5或继承其父线程优先级;
-
通过setPriority和getPriority方法来设置或返回优先级;
-
-
Thread类有如下3个静态常量来表示优先级
-
MAX_PRIORITY:取值为10,表示最高优先级。
-
MIN_PRIORITY:取值为1,表示最底优先级。
-
NORM_PRIORITY:取值为5,表示默认的优先级
-
-
案例
/* 打印输出0-100的数字,创建两个线程交替执行 创建线程 方式一:继承Thread类 重写run方法*/ public class MyPrintThread extends Thread { public MyPrintThread(String name) { super(name); //调用Thread中的构造方法 } //run方法的方法体就是线程的执行体 @Override public void run() { for (int i = 0; i <= 100; i++) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(this.getName() + i); } } } public class MyPrintThreadTest { public static void main(String[] args) { //创建线程对象 MyPrintThread thread1 = new MyPrintThread("线程1"); MyPrintThread thread2 = new MyPrintThread("线程2"); /*设置优先级,并不是优先级低的线程就没有机会运行,只是每次优先级高线程的时间片会长一些,得到的控制权的几率也会高一些。*/ System.out.println(thread1.getPriority()); //获取当前线程的优先级,默认是5 System.out.println(thread2.getPriority()); thread1.setPriority(Thread.MAX_PRIORITY); //优先级最高,获取的可能性则高。 thread2.setPriority(Thread.MIN_PRIORITY); //优先级最低,获取的可能性则低。 System.out.println(thread1.getPriority()); /*10*/ System.out.println(thread2.getPriority()); /*1*/ thread1.start(); thread2.start(); System.out.println("main方法的最后一条语句"); } }
6.6 守护线程
-
Java中将线程划分为了两类:
-
用户线程 (User Thread)
-
守护线程 (Daemon Thread)
-
-
所谓守护线程,是指在程序运行的时候在后台提供一种通用服务的线程
-
比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。
-
当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。
-
只要任何非守护线程还在运行,程序就不会终止
-
-
注意:
-
默认创建的线程对象为非守护的用户线程;
-
要将某个线程设置为守护线程的话,必须在它(start)启动之前调用setDeamon方法
-
6.7 sleep方法
-
sleep用法
-
使线程停止运行一段时间,此时将处于阻塞状态。
-
阻塞的时间由指定的毫秒数决定
-
Thread.sleep(long millis)和Thread.sleep(long millis, int nanos)静态方法强制当前正在执行的线程休眠(暂停执行),以“减慢线程”。
-
当线程睡眠时,它入睡在某个地方,在苏醒之前不会返回到可运行状态。
-
当睡眠时间到期,则返回到可运行状态,不是运行状态,因此sleep()方法不能保证该线程睡眠到期后就开始执行。
-
-
线程睡眠的原因:线程执行太快,或者需要强制进入下一轮
-
睡眠的实现:调用静态方法。
-
案例
//sleep方法测试 public class TestSleep { public static void main(String[] args) { System.out.println("Wait"); // 让主线程等待5秒再执行 Wait.bySec(5); // 提示恢复执行 System.out.println("start"); } } class Wait { public static void bySec(long s) { // sleep s个1秒 for (int i = 0; i < s; i++) { System.out.println(i + 1 + "秒"); try { // sleep1秒 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
6.8 yield方法
-
yield() :
-
出让时间片,但不会阻塞线程,转入可运行状态。
-
案例
//Yeild测试 public class YieldTest { public static void main(String[] args) { TheThread mt = new TheThread("线程一"); MyNewThread mnt = new MyNewThread("线程二"); mt.start(); mnt.start(); } } class TheThread extends Thread { public TheThread(String name) { super(name); } public void run() { for (int i = 1; i <= 5; i++) { System.out.println(this.getName() + "的第 " + i + "次运行"); /* 出让cpu时间片,当前线程停止运行,但仍处于可运行状态。可以和其他的等待执行的线程竞争cpu时间片 如果调用了yield方法之后,如果没有其他等待执行的线程(CPU没有满负荷,有很多空闲CPU),这个时候当前线程就会马上恢复执行! */ Thread.yield(); } } } class MyNewThread extends Thread { public MyNewThread(String name) { super(name); } public void run() { for (int i = 0; i < 5; i++) { System.out.println(this.getName() + "的第 " + (i + 1) + "次运行"); Thread.yield(); } } }
多次运行结果是不一样的
-
6.9 sleep和yield对比
-
sleep和yield对比
6.10 join方法
-
join():
-
阻塞当前线程,强行介入执行。
- 等待其他线程终止,在当前线程中调用一个线程的 join() 方法,则当前线程转为阻塞状态,回到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu 的宠幸。
-
-
案例
import java.util.Date; public class JoinTester implements Runnable { private String name; public JoinTester(String name) { this.name = name; } public void run() { System.out.printf("%s线程运行开始: %s\n", this.name, new Date()); try { Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.printf("%s线程运行结束: %s\n", this.name, new Date()); } public static void main(String[] args) { Thread t1 = new Thread(new JoinTester("线程一")); Thread t2 = new Thread(new JoinTester("线程二")); t1.start(); t2.start(); //加入如下代码和注释掉如下代码,运行结果不一样的 try { //强制其它线程等待t1结束后再执行。 t1.join(); //强制其它线程等待t2结束后再执行。 t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Main线程运行结束"); } } /* 线程二线程运行开始: Wed Apr 20 15:16:11 CST 2022 线程一线程运行开始: Wed Apr 20 15:16:11 CST 2022 线程二线程运行结束: Wed Apr 20 15:16:15 CST 2022 线程一线程运行结束: Wed Apr 20 15:16:15 CST 2022 Main线程运行结束 */
7.线程同步
7.1 线程安全
-
需求:模拟三个窗口同时在售50张票
//模拟三个窗口同时在售50张票 public class SaleTicket extends Thread { int num = 50; //票的数量 public SaleTicket(String name) { super(name); } @Override public void run() { // 死循环 while (true) { try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } if (num > 0) { System.out.println(this.getName() + "卖了一张票,剩余" + (num - 1) + "张票"); num--; } else { System.out.println(this.getName() + "票卖完了"); break;//退出循环 } } } public static void main(String[] args) { //创建3个线程 模拟3个窗口 SaleTicket t1 = new SaleTicket("窗口一"); SaleTicket t2 = new SaleTicket("窗口二"); SaleTicket t3 = new SaleTicket("窗口三"); t1.start(); t2.start(); t3.start(); } }
-
问题1:为什么50张票被卖出了150次?
-
出现原因:因为num是非静态的,非静态的成员变量数据是在每个对象中都会维护一份数据的,三个线程对象就会有三份。
-
解决方案:把num票数共享出来给三个对象使用。使用static修饰。
static int num = 50; //票的数量设置静态变量
-
-
问题2:刚刚的问题是解决了,但是为什么会出现两个窗口卖同一张票呢?
-
出现了线程安全问题
-
解决方案:一个时间只能由一个线程操作内容---线程同步机制
-
7.2 线程同步概念
-
线程是一份独立运行的程序,有自己专用的运行栈。线程有可能和其他线程共享一些资源,比如,内存,文件,数据库等;
-
多个线程同时读写同一份共享资源时,可能会引起冲突。所以引入线程“同步”机制,即各线程间要有先来后到;
-
同步就是排队:几个线程之间要排队,一个个对共享资源进行操作,而不是同时进行操作;
-
确保一个时间点只有一个线程访问共享资源。可以给共享资源加一把锁,这把锁只有一把钥匙。哪个线程获取了这把钥匙,才有权利访问该共享资源。
7.3 同步代码块
-
线程同步机制方式一:同步代码块
-
语法
-
同步代码块要注意的事项:
1、任意一个对象都可以作为锁对象,它锁定的是该对象;
2、线程中实现同步块一般是在run方法中添加;
2、在同步代码块中调用sleep方法并不会释放锁对象的;
3、只有真正存在线程安全问题的时候才使用同步代码块,否则会降低效率;
4、多线程操作的锁对象必须是唯一共享的,否则无效;
-
案例
//模拟三个窗口同时在售50张票 public class SaleTicketOk extends Thread { static int num = 50; // 一共50张票 静态变量被所有对象所共享 static Object obj = new Object(); //加static就是唯一的 /*String str = "aa";*/ public SaleTicketOk(String name) { super(name); } @Override public void run() { // 永真循环 while (true) { //字符串不可变性,因为字面量字符串存储在常量池中,只要内容相同,地址都是一样的 /*synchronized (str) {}*/ // 同步代码块 共享对象可以是任意对象,可以是字符串也可以是自定义对象,但是必须是唯一的,否则无效 /*多线程操作的锁对象必须是唯一共享的,否则无效。*/ synchronized (obj) { if (num > 0) { System.out.println(this.getName() + "卖了一张票,还剩" + (num - 1) + "张票"); num--; } else { System.out.println(this.getName() + "票卖完了"); break; } try { // 休眠5秒钟 ,在同步代码块中调用sleep方法并不会释放锁对象的 Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } } } } public static void main(String[] args) { SaleTicketOk t1 = new SaleTicketOk("窗口一"); SaleTicketOk t2 = new SaleTicketOk("窗口二"); SaleTicketOk t3 = new SaleTicketOk("窗口三"); //static obj -> true System.out.println(t1.obj == t2.obj); System.out.println(t1.obj == t3.obj); System.out.println(t3.obj == t2.obj); /*System.out.println(t1.str == t2.str);//true System.out.println(t1.str == t3.str);//true System.out.println(t3.str == t2.str);//true*/ t1.start(); t2.start(); t3.start(); } }
-
练习案例
-
思考:一个银行账户5000块钱,两夫妻一个拿着折,一个拿着卡,开始取钱比赛,每次只能取一千块钱,要求不准出现线程安全问题
public class BankThread extends Thread { static double count = 5000; // 存款 static final Double d = (double) 0; //全局对象 public BankThread(String name) { super(name); } @Override public void run() { while (true) { //1.唯一,否则不生效 //2.不能写在循环外,否则第一个进来的人就会执行完再走 synchronized (d) { if (count > 0) { System.out.println(super.getName() + "取走了1000元,还剩" + (count - 1000) + "元"); count -= 1000; } else { System.out.println("钱取完了"); break; } try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } } } public static void main(String[] args) { BankThread b1 = new BankThread("她"); BankThread b2 = new BankThread("他"); b1.start(); b2.start(); } } /* 她取走了1000元,还剩4000.0元 他取走了1000元,还剩3000.0元 她取走了1000元,还剩2000.0元 她取走了1000元,还剩1000.0元 他取走了1000元,还剩0.0元 钱取完了 钱取完了 */
-
7.4 同步方法
-
同步方法
-
防止多个线程同时访问这个类中的synchronized 方法。它可以对类的所有对象实例起作用。
-
-
同步方法注意:
-
如果是一个非静态的同步方法的锁,对象是this对象;
-
如果是静态的同步方法的锁,对象是当前方法所属的类的字节码文件(Class对象)。
-
同步方法的锁对象是固定的,不能由你来指定。
-
-
案例
public class BankThreadMethod extends Thread { static double count = 5000; public BankThreadMethod(String name) { super(name); } @Override public void run() { while (true) { double remainCount = test(this.getName()); if (remainCount <= 0) { System.out.println("钱莫得了"); break; } } } /*如果是非静态的同步方法的锁,锁对象是this。 多个线程有多个对象,每一个new出来的都是一个this,this引用不同 不生效 //public synchronized double test(String name) {}*/ //jvm每个类的字节码是全局唯一 //同步方法 --- 如果是静态的同步方法的锁,对象是当前方法所属的类的字节码文件(Class对象)---反射。 public static synchronized double test(String name) { if (count > 0) { System.out.println(name + "取走了1000元,还剩" + (count - 1000) + "元"); count -= 1000; } return count; } public static void main(String[] args) { BankThreadMethod b1 = new BankThreadMethod("你"); BankThreadMethod b2 = new BankThreadMethod("我"); b1.start(); b2.start(); } } /* 你取走了1000元,还剩4000.0元 你取走了1000元,还剩3000.0元 你取走了1000元,还剩2000.0元 你取走了1000元,还剩1000.0元 你取走了1000元,还剩0.0元 钱莫得了 钱莫得了 */
-
练习案例
-
需求:作一个讨债软件:
1、有一个人欠雪姐50万,一直不给。
2、雪姐公司有10个小弟,今天早晨开会,给大家安排的任务就是讨债。
3、要求10个小弟全体出动去要钱,要求单独行动。每次一个小弟只能讨5万
4、收到的回来报告,有提成。
public class RecoverMoneyThread extends Thread { //全局变量,50万债务 static int money = 500000; //构造函数,参数name public RecoverMoneyThread(String name) { super(name); } @Override public void run() { while (true) { //剩余的债务 int remainMoney = recoverMoney(super.getName()); if (remainMoney <= 0) { System.out.println("追债完成!"); break; } } } //同步方法,静态方法,锁的对象是Class对象,所有的实例对象共享一个锁 private static synchronized int recoverMoney(String threadName) { try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } if (money <= 0) { return money; } System.out.println(threadName + "追债50000元,还剩" + (money - 50000) + "元"); money -= 50000; return money;//返回值 剩余的债务 } public static void main(String[] args) { //创建数组,存储10个线程(追债的10个小弟) RecoverMoneyThread[] threads = new RecoverMoneyThread[10]; //创建并启动 for (int i = 0; i < 10; i++) { threads[i] = new RecoverMoneyThread("小弟" + i + "号"); threads[i].start(); } } } /* 每次追债的小弟不一定相同 每个小弟都会汇报 追债完成! */
-
7.5 优缺点
-
同步的前提
-
同步需要两个或者两个以上的线程。
-
多个线程使用的是同一个锁
未满足这两个条件,不能称其为同步。
-
-
同步的弊端
-
性能下降
-
会带来死锁
-
-
同步的优势
解决了线程安全问题、对公共资源的使用有序化。
-
同步注意
-
不要盲目对不需要同步的代码使用Synchronized,以避免影响性能和效率。
-
8.线程死锁
-
线程死锁
-
线程死锁指的两个线程互相持有对方依赖的共享资源,造成都无限阻塞。
-
导致死锁的根源在于不适当地运用“synchronized”关键词来管理线程对特定对象的访问。
-
-
解决死锁的方法
-
让线程持有独立的资源。
-
尽量不采用嵌套的synchronized语句。
-
死锁要通过优良的设计来尽量降低死锁的发生。
- 老师复习还未整理:每个线程独立运行,线程并行和顺序,用到锁synchronized(多重锁)
-
-
案例(死锁的情况)
//电池和遥控器(张三拿遥控器,李四拿电池,谁也不肯放手) public class DeadLock extends Thread { public DeadLock(String name) { super(name); } //电池和遥控器都是共享资源,为了线程安全加锁 @Override public void run() { //张三先拿遥控器---等电池 if ("张三".equals(this.getName())) { //锁住遥控器 synchronized ("遥控器") { System.out.println("张三拿到了遥控器,准备拿电池...."); synchronized ("电池") { System.out.println("张三拿到了遥控器也拿到了电池......"); } } //李四先拿着电池---等遥控器 } else if ("李四".equals(this.getName())) { //锁住电池 synchronized ("电池") { System.out.println("李四拿到了电池,准备拿遥控..."); synchronized ("遥控器") { System.out.println("李四拿到了遥控器也拿到了电池......"); } } } } public static void main(String[] args) { DeadLock dl1 = new DeadLock("张三"); DeadLock dl2 = new DeadLock("李四"); dl1.start(); dl2.start(); } } /* 出现的情况: (1)不死锁,张三、李四都拿到了电池和遥控器。 因为start()启动由系统决定的,启动的时机也是由操作系统决定无法用程序控制,可能张三和李四错开了 (2)死锁,张三和李四只能拿到一个电池或遥控器 */
张三等着李四的电池,李四等着张三的遥控器,无限等待中
思考:这种情况一定会发生吗?不一定,当张三快速的执行完毕时
解决死锁办法:
public static void main(String[] args) { DeadLock dl1 = new DeadLock("张三"); DeadLock dl2 = new DeadLock("李四"); dl1.start(); try { //阻塞主函数,d1线程join,先执行完毕,再启动d2线程 dl1.join(); } catch (InterruptedException e) { e.printStackTrace(); } dl2.start(); }
9.线程通讯
9.1 线程通讯的概念
-
线程通讯指的是多个线程通过消息传递实现相互牵制,相互调度,即线程间的相互作用。
-
前面学的是多个线程执行同一动作(一个run方法)即同一个任务,现在线程还是多个,但运行的任务却是不同的,不过它们处理的资源是相同的。
-
Java代码中基于对共享数据进行“wait()、notify()、notifyAll()"来实现多个线程的通讯。
-
经典例子:生产者和消费者的问题
9.2 生产者与消费者
-
假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中的产品取走消费。如果仓库中没有产品,则生产者可以将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止。如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止。
-
对于生产者,在生产者没有生产之前,要通知消费者等待;在生产者生产之后,马上又通知消费者消费;对于消费者,在消费者消费之后,要通知生产者已经消费结束,需要继续生产新的产品以供消费。
9.3 wait()/notify()/notifyAll()用法
-
wait()/notify()/notifyAll()用法
-
wait()和notify()方法(包括上述的所有方法,下同) 都是Object类的最终方法,所以每个类默认都拥有该方法。
-
-
wait():
- 线程等待(释放锁,释放CPU时间片) 放到等待队列
-
使当前执行代码的线程进行等待,该方法用来将当前线程置入“预执行队列”中,并且在wait()所在的代码行处停止执行,直到接到通知或被中断为止。在执行wait()方法后,当前线程释放锁。在从wait()返回前,线程与其他线程竞争重新获得锁。如果调用wait()方法时没有持有适当的锁,则抛出IllegalMonitorStateException异常,它是RuntimeException的一个子类,因此,不需要try-catch语句进行捕捉异常。
-
notify() / notifyAll():
- 唤醒锁住在对象中的wait的线程 放到锁池队列
- 在调用前,线程也必须获得该对象的对象级别锁。如调用notify()时没有持有适当的锁,也会抛出IllegalMonitorStateException。该方法用来通知那些可能等待该对象的对象锁的其他线程,如果有多个线程等待,则由线程规划器随机挑选出其中一个呈wait状态的线程,对其发出通知notify,并使它等待获取该对象的对象锁。需要说明的是,在执行notify()方法后,当前线程不会马上释放该对象锁,呈wait状态的线程也并不能马上获取该对象锁,到等到执行notify()方法的线程将程序执行完,也就是退出synchronized代码块后,当前线程才会释放锁,而呈wait状态所在的线程才可以获取该对象锁。当第一个获得了该对象锁的wait线程运行完毕以后,它会释放掉该对象锁,此时如果该对象没有再次使用notify语句,则即便该对象已经空闲,其他wait状态等待的线程由于没有得到该对象的通知,还会继续阻塞在wait状态,直到这个对象发出一个notify或notifyAll。
-
案例
1、产品:
//商品类 public class Product { private String name; private double price; //true 有商品 false 无商品 private boolean flag = false; public Product(String name, double price) { this.name = name; this.price = price; } public String getName() {return name;} public void setName(String name) {this.name = name;} public double getPrice() {return price;} public void setPrice(double price) {this.price = price;} //注意方法名的写法 isFlag() public boolean isFlag() {return flag;} public void setFlag(boolean flag) {this.flag = flag;} }
2、生产者
//生产者线程 public class Producer extends Thread { private Product p; private int i = 0; public Producer(Product p) { this.p = p; } @Override public void run() { while (true) { /* 同步代码块 不锁住对象,wait会报java.lang.IllegalMonitorStateException异常 */ synchronized (p) { //商品存在(flag为true),停止 if (p.isFlag()) { //商品为空,解除停止 try { p.wait();/*public final void wait() throws InterruptedException {*/ } catch (InterruptedException e) { e.printStackTrace(); } } //商品不存在(flag为false),生产商品 else { i++; if (i % 2 == 0) { p.setName("克拉棒"); p.setPrice(200); } else { p.setName("团戒指"); p.setPrice(99); } System.out.println("Producer生产了商品:" + p.getName() + ",商品价格为:" + p.getPrice()); p.setFlag(true); /* 唤醒所有等待的线程 同一对象 p 等待的线程 p.wait 不阻塞代码 */ p.notifyAll(); } } } } }
3、消费者
//消费者线程 public class Customer extends Thread { private Product p; public Customer(Product p) { this.p = p; } @Override public void run() { while (true) { synchronized (p) { //flag 为 true if (p.isFlag()) { try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Customer消费了商品:" + p.getName() + ",商品价格为:" + p.getPrice()); //商品被消费,无了 p.setFlag(false); //通知生产者要生产商品 p.notifyAll();/*public final native void notifyAll();*/ } //flag 为 false else { try { p.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } } }
4、测试类
//测试类 public class ProducerCustomerTest { public static void main(String[] args) { Product p = new Product("克拉棒", 200);//产品类对象 Producer pp = new Producer(p);//生产者线程对象 Customer cc = new Customer(p);//消费者线程对象 pp.start(); cc.start(); } }
-
为什么 wait 和 notify 方法要在同步块中调用?
- 主要因为 Java API 强制要这样做 。
- 如果不这么做 ,代码会抛出
IllegalMonitorStateException
异常,表明此线程不是此对象监视器的所有者。。 - 还有一个原因是为了避免 wait 和 notify之间产生竞态条件。
- 抛出 IllegalMonitorStateException
- 当我们在没有拥有指定对象的监视器时,就去等待这个对象的监视器或者通知其他线程去等待这个对象的监视器
- 换句话说,我们要想调用一个对象的
wait()
、notify()
等方法来实现线程通信,就要先获取这个对象的监视器。 - 也就是要用synchronized修饰这个代码块。
-
-
synchronized
修饰的代码块编译后会被monitorenter
和monitorexit
包围,这两个虚拟机命令就是获取和释放对象的监视器。所以Object自带的
wait()
、notify()
等方法,是让我们在使用synchronized
时进行线程通信用的。
-
9.4 sleep()和wait()的区别
9.5 管道流通讯
-
Java中的Pipe管道输入流与Pipe管道输出流实现了类似管道的功能,用于不同线程之间的相互通信:
-
案例
1、Sender类
import java.io.IOException; import java.io.PipedWriter; /** * 发送端 */ public class Sender extends Thread { //管道字符输出流 private PipedWriter pipedWriter = new PipedWriter(); public PipedWriter getPipedWriter() {return pipedWriter; } @Override public void run() { while (true) { for (char c = 'A'; c <= 'z'; c++) { try { Thread.sleep(1000); pipedWriter.write(c); } catch (IOException | InterruptedException e) { e.printStackTrace(); } } } } }
2、Receiver类
import java.io.IOException; import java.io.PipedReader; import java.io.PipedWriter; /** * 接收端 */ public class Receiver extends Thread { //管道字符输入流 private PipedReader pipedReader; public Receiver(PipedWriter pipedWriter) { try { this.pipedReader = new PipedReader(pipedWriter); } catch (IOException e) { e.printStackTrace(); } } @Override public void run() { while (true) { for (char c = 'A'; c <= 'z'; c++) { try { //阻塞代码,直到能读取到数据 System.out.println((char) pipedReader.read()); } catch (IOException e) { e.printStackTrace(); } } } } }
3、SenderReceiverTest测试类
public class SenderReceiverTest { public static void main(String[] args) { Sender s = new Sender(); Receiver r = new Receiver(s.getPipedWriter()); s.start(); r.start(); } } /* A到Z [到` a到z 循环输出 */
-
Java在它的jdk文档中提到不要在一个线程中同时使用管道输入流和管道输出流,这可能会造成死锁
10.线程的生命周期
-
Java线程在它的生命周期中会处于不同的状态:
-
新建状态(New):使用new关键字创建线程对象,仅被分配了内存
-
可运行状态(Runnable):线程具备获得CPU时间片的能力。线程进入可运行状态的情况如下:
-
线程start()方法被调用
-
当前线程sleep()、其它线程join()结束、等待用户输入完毕;
-
某个线程取得对象锁;
-
当前线程时间片用完了,调用当前线程的yield()方法
-
-
运行状态(Running):执行run方法,此时线程获得CPU的时间片;
-
阻塞状态(Blocked):线程由于某些事件放弃CPU使用权,暂停运行。直到线程重新进入可运行状态,才有机会转到运行状态。阻塞状态分为如下几种::
-
同步阻塞 – 线程获得同步锁,若被别的线程占用,线程放入锁池队列中
-
等待阻塞 – 线程执行wait()方法,线程放入等待队列中。某个线程取得对象锁;
-
其它阻塞 – 线程执行sleep()或join()或发出I/O请求。
-
-
死亡状态(Dead):run、main() 方法执行结束,或者因异常退出了run()方法,线程进入死亡状态,不可再次复生。
11.线程池调度器
11.1 线程池介绍
-
概念
-
把并发执行的任务传递给一个线程池,来替代为每个并发执行的任务都启动一个新的线程。
-
只要池里有空闲的线程,任务就会分配给一个线程执行。
-
在线程池的内部,任务被插入一个阻塞队列(Blocking Queue ),线程池里的线程会去取这个队列里的任务。
-
当一个新任务插入队列时,一个空闲线程就会成功的从队列中取出任务并且执行它。
-
-
为什么要使用线程池?
-
需要处理的任务较少时,我们可以自己创建线程去处理,但在高并发场景下,我们需要处理的任务数量很多.
-
由于创建销毁线程开销很大,这样频繁创建线程就会大大降低系统的效率。
-
此时,我们就可以使用线程池,线程池中的线程执行完一个任务后可以复用,并不被销毁。
-
合理使用线程池有以下几点好处:
-
1、减少资源的开销。通过复用线程,降低创建销毁线程造成的消耗。
-
2、多个线程并发执行任务,提高系统的响应速度。
-
3、可以统一的分配,调优和监控线程,提高线程的可管理性。
-
-
11.2 线程池创建
-
通过ThreadPoolExecutor来创建一个线程池。
-
java.uitl.concurrent.ThreadPoolExecutor类是线程池中最核心的一个类,它有四个构造方法。
-
构造方法参数说明:
1、corePoolSize:核心线程数,指定了线程池中的线程数量,默认情况下核心线程会一直存活,即使处于闲置状态也不会受存keepAliveTime限制。除非将allowCoreThreadTimeOut设置为true。
2、maximumPoolSize:线程池所能容纳的最大线程数。超过这个数的线程将被阻塞。当任务队列为没有设置大小的LinkedBlockingDeque时,这个值无效。
3、keepAliveTime:非核心线程的闲置超时时间(活跃时间),超过这个时间就会被回收。当前线程池数量超过 corePoolSize 时,多余的空闲线程的存活时间,即多次时间内会被销毁。
4、unit:指定keepAliveTime的单位,如TimeUnit.SECONDS。当将allowCoreThreadTimeOut设置为true时对corePoolSize生效。
5、workQueue:线程池中的任务队列(阻塞队列)被提交但尚未被执行的任务。常用的有三种队列:SynchronousQueue,LinkedBlockingDeque,ArrayBlockingQueue。
6、threadFactory:线程工厂,用于创建线程,一般用默认的即可。
7、handler:拒绝策略,当任务太多来不及处理,如何拒绝任务。
-
线程池执行逻辑
- 1、当前线程数 < corePoolSize : 创建线程执行任务
- 2、当前线程数 >= corePoolSize , 阻塞队列没有满:执行任务放到队列(workQueue)中
- 3、当前线程数 >= corePoolSize并且当workQueue满时,新任务新建线程运行(创建临时线程),当前线程数 < 最大线程数
- 4、当前线程数 = maximumPoolSize并且workQueue满时:执行拒绝策略
-
常用方法:
1.int getCorePoolSize():返回核心线程数。
2.int getPoolSize():返回池中当前的线程数。
3.BlockingQueue<Runnable> getQueue():返回此执行程序使用的任务队列。
4.void shutdown():关闭线程池
5.void execute(Runnable command):执行任务
-
shutdown 和 shutdownNow 二者有啥区别?
- shutdown 方法只是会将
线程池
的状态设置为SHUTWDOWN
,正在执行的任务会继续执行下去,线程池会等待任务的执行完毕,而没有执行的线程则会中断。 - shutdownNow 方法会将线程池的状态设置为
STOP
,正在执行和等待的任务则被停止,返回等待执行的任务列表.
- shutdown 方法只是会将
-
案例
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class ThreadPoolExecutorTest { public static void main(String[] args) { //无边界队列,无长度限制 //ConcurrentLinkedQueue<String> clq = new ConcurrentLinkedQueue<>(); /* corePoolSize:核心线程数,默认情况下核心线程会一直存活 maximumPoolSize:最大线程数 keepAliveTime:当前线程池数量超过 corePoolSize 时,多余的空闲线程的存活时间(非核心线程的闲置超时时间),超过这个时间就会被回收。 unit:指定keepAliveTime的单位,如TimeUnit.SECONDS workQueue:线程池中的任务队列(阻塞队列).常用的有三种队列,SynchronousQueue,LinkedBlockingDeque,ArrayBlockingQueue。 创建线程池对象 */ //无边界队列,无长度限制 ConcurrentLinkedQueue<String> clq = new ConcurrentLinkedQueue<>(); ThreadPoolExecutor pool = new ThreadPoolExecutor( 1, 3, 0, TimeUnit.MICROSECONDS, new LinkedBlockingQueue<>(3)); MyThread myThread = new MyThread(); System.out.println("==========线程池刚创建,添加任务前=========="); System.out.println("核心线程数:" + pool.getCorePoolSize());/*1*/ System.out.println("当前线程数:" + pool.getPoolSize());/*0*/ System.out.println("线程池中最大线程数:" + pool.getMaximumPoolSize());/*3*/ System.out.println("阻塞队列任务数:" + pool.getQueue().size());/*0*/ //pool执行任务 核心执行 pool.execute(myThread);/*当前线程数:1*/ pool.execute(myThread);/*当前线程数 == 核心线程数,阻塞队列添加任务:1*/ pool.execute(myThread);/*当前线程数 == 核心线程数,阻塞队列添加任务:2*/ pool.execute(myThread);/*当前线程数 == 核心线程数,阻塞队列添加任务:3*/ System.out.println("==========添加任务后=========="); System.out.println("核心线程数:" + pool.getCorePoolSize());/*1*/ System.out.println("当前线程数:" + pool.getPoolSize());/*1*/ System.out.println("线程池中最大线程数:" + pool.getMaximumPoolSize());/*3*/ System.out.println("阻塞队列任务数:" + pool.getQueue().size());/*3*/ //阻塞队列队列满了 创建新的线程(新的线程数+核心线程数<=最大线程数) pool.execute(myThread);/*当前线程数=1 <最大线程数3,创建临时线程,当前线程数会增加1变成:2*/ pool.execute(myThread);/*当前线程数=1 <最大线程数3,创建临时线程,当前线程数会增加1变成:3*/ System.out.println("==========再次添加任务后=========="); System.out.println("核心线程数:" + pool.getCorePoolSize());/*1*/ System.out.println("当前线程数:" + pool.getPoolSize());/*3*/ System.out.println("线程池中最大线程数:" + pool.getMaximumPoolSize());/*3*/ System.out.println("阻塞队列任务数:" + pool.getQueue().size());/*3*/ //阻塞队列满了 新的线程数+核心线程数>最大线程数 拒绝 报错 pool.execute(myThread);/*java.util.concurrent.RejectedExecutionException*/ //关闭线程池 pool.shutdown(); /*shutdown 线程溢出时,不能关闭线程池 不再接受新的任务,之前提交的任务等执行结束再关闭线程池 */ pool.shutdownNow(); /*shutdownNow 不再接受新的任务,试图停止池中的任务再关闭线程池,返回所有未处理的线程list 列表。 */ } } class MyThread implements Runnable { @Override public void run() { try { Thread.sleep(2000); System.out.println(Thread.currentThread().getName());//正常执行时输出 } catch (InterruptedException e) { e.printStackTrace(); } } }
11.3 Executor框架
-
Executor框架的重要核心接口和类:
-
Executor是一个可以提交可执行任务的工具,这个接口解耦了任务提交和执行细节(线程使用、调度等),Executor主要用来替代显示的创建和运行线程
-
ExecutorService提供了异步的管理一个或多个线程终止、执行过程(Future)的方法
-
Executors类提供了一系列工厂方法用于创建任务执行器,返回的任务执行器都实现了ExecutorService接口(绝大部分执行器完成了池化操作)
-
内置的常见工厂方法及生成的任务调度器特征:
-
有几种不同的方式将任务委托给一个 ExecutorService:
-
execute(Runnable)
-
接收一个java.lang.Runnable对象作为参数,并且以异步的方式执行它,使用这种方式没有办法获取执行Runnable之后的结果,如果你希望获取运行之后的返回值,就必须使用接收Callable参数的execute() 方法
-
-
submit(Runnable)
-
同样接收一个Runnable的实现作为参数,但是会返回一个Future 对象。这个Future对象可以用于判断Runnable任务是否结束执行
-
-
submit(Callable)
-
和方法submit(Runnable)比较类似,但是区别在于它们接收不同的参数类型。Callable的实例与Runnable的实例很类似,但是Callable的call()方法可以返回一个结果而方法Runnable.run()则不能返回结果
-
Callable的返回值可以从方法submit(Callable)返回的Future对象中获取
-
-
invokeAny(...)
-
接收一个包含Callable对象的集合作为参数。调用该方法不会返回Future对象,而是返回集合中某个Callable对象的结果,而且无法保证调用之后返回的结果是集合中的哪个Callable结果,只知道它是这些Callable中的一个
-
如果一个任务运行完毕或者抛出异常,方法会取消其它的Callable的执行
-
-
invokeAll(...)
-
会调用存在于参数集合中的所有 Callable 对象,并且返回一个包含Future对象的集合,可以通过这个返回的集合来管理每个Callable的执行结果
-
需要注意的是,任务有可能因为异常而导致运行结束,所以它可能并不是真的成功运行了。但是我们没有办法通过 Future 对象来了解到这个差异
-
-
11.4 newCachedThreadPool
-
newCachedThreadPool创建一个可缓存线程池,先查看池中有没有以前建立的线程,如果有,就直接使用。如果没有,就建一个新的线程加入池中,缓存型池子通常用于执行一些生存期很短的异步型任务。 线程池为无限大,当执行当前任务时上一个任务已经完成,会复用执行上一个任务的线程,而不用每次新建线程。
- 核心线程数:0 ,最大线程数:无限大 , 阻塞队列:不能存放数据的阻塞队列 , 60s
-
案例
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadPoolTest { public static void main(String[] args) { testNewCacheThreadPool(); } //newCachedThreadPool创建一个可缓存的线程池 private static void test01() { /* 先查看池中有没有以前建立的线程, 如果有,就直接使用。如果没有,就建一个新的线程加入池中 */ ExecutorService es1 = Executors.newCachedThreadPool(); for (int i = 1; i <= 5; i++) {//执行5次任务 int index = i; try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } es1.execute(new Runnable() {//execute参数 new一个实现Runnable接口的匿名类 @Override public void run() { System.out.println("第" + index + "个线程" + Thread.currentThread().getName()); } }); } es1.shutdown(); } }
11.5 newFixedThreadPool
-
newFixedThreadPool创建一个可重用固定个数的线程池,以共享的无界队列方式来运行这些线程。因为线程池大小为3,每个任务输出打印结果后sleep 2秒,所以每两秒打印3个结果。定长线程池的大小最好根据系统资源进行设置。
-
创建方式: Executors.newFixedThreadPool(int nThreads);//nThreads为线程的数量
- 核心线程数:固定数量 未整理笔记
-
案例
//newFixedThreadPool创建一个可重用固定个数的线程池 private static void test02() { //newFixedThreadPool创建一个可重用固定个数的线程池 ExecutorService es = Executors.newFixedThreadPool(3); //结果解析:由于设置最大线程数为3,所以在输出三个数后等待1s才继续输出 for (int i = 1; i <=20; i++) { int index = i; es.execute(new Runnable() { //执行任务 @Override public void run() { System.out.println("第"+index+"个线程"+Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); } }
11.6 newSingleThreadExecutor
-
newSingleThreadExecutor创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务。
-
创建方式:Executors.newSingleThreadExecutor() ;
-
案例
//newSingleThreadExecutor创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务。 private static void test03() { //newSingleThreadExecutor创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务。 ExecutorService es = Executors.newSingleThreadExecutor(); for (int i = 1; i <= 5; i++) { int index = i; es.execute(new Runnable() { //执行任务 @Override public void run() { System.out.println("第" + index + "个线程" + Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); } }
11.7 newScheduleThreadPool
-
newScheduleThreadPool创建一个定长线程池,支持定时及周期性任务执行
-
案例1
//newScheduleThreadPool创建一个定长线程池,支持定时及周期性任务执行 private static void test04() { //创建一个定长线程池,支持定时及周期性任务执行 ScheduledExecutorService ses = Executors.newScheduledThreadPool(3); //执行任务 ses.schedule(new Runnable() { @Override public void run() { System.out.println("延迟3秒"); } }, 3, TimeUnit.SECONDS); }
-
案例2
//周期性执行任务 private static void test05() { //创建一个定长线程池,支持定时及周期性任务执行 ScheduledExecutorService ses = Executors.newScheduledThreadPool(3); ses.scheduleAtFixedRate(new Runnable() { @Override public void run() { System.out.println("延迟1秒后每3秒执行一次任务" + Thread.currentThread().getName()); } }, 1, 3, TimeUnit.SECONDS); }
12.信号量Semaphore
-
信号量为多线程协作提供了更为强大的控制方法。信号量是对锁的扩展。锁一次都只允许一个线程访问一个资源,而信号量却可以指定多个线程,同时访问某一个资源。
-
例如:车位是共享资源,每辆车好比是一个线程,看门人是信号量。
-
案例:
//信号量 public class SemapDemo implements Runnable { // 创建信号量对象,只能5个线程同时访问 Semaphore semp = new Semaphore(5); @Override public void run() { try { semp.acquire();// 获取许可 Thread.sleep(5000); System.out.println(Thread.currentThread().getName() + ",完成"); // 访问完 释放 如果把下面语句注释掉 ,控制台只能打印5条数据,就一直阻塞 semp.release(); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { SemapDemo semapDemo = new SemapDemo(); //创建固定数目线程的线程池 ExecutorService es = Executors.newFixedThreadPool(20); //模拟20个客户端访问 for (int i = 0; i < 20; i++) { es.execute(semapDemo); //执行任务 } //退出线程池 es.shutdown(); } }
13.Lock对象
-
虽然我们现在已经可以理解同步代码块和同步方法的锁对象问题,但是我们并不能直接看到在哪里加了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5中提供了一个新的锁对象Lock(接口),提供了更为清晰的语义
-
JDK5中有一个Lock的默认实现:ReentrantLock:
-
可重入的独占锁。该对象与synchronized关键字有着相同的表现和更清晰的语义,而且还具有一些扩展的功能。可重入锁被最近的一个成功lock的线程占有(unlock后释放)。该类有一个重要特性体现在构造器上,构造器接受一个可选参数,是否是公平锁,默认是非公平锁
-
公平锁:
-
先来一定先排队,一定先获取锁
-
-
非公平锁:
-
不保证上述条件。非公平锁的吞吐量更高
-
-
- synchronized、ReentrantLock 都是可重入的锁,可重入锁相对来说简化了并发编程的开发。
-
案例
public class Resource3 { //创建一个锁对象 private Lock lock = new ReentrantLock(); public void f() { System.out.println(Thread.currentThread().getName() + "not synchronized in f()"); //锁定,进入同步块 lock.lock(); try { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + "synchronized in f()"); } try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } } catch (Exception e) { e.printStackTrace(); } finally { //释放锁,离开同步块 lock.unlock(); } } public void g() { System.out.println(Thread.currentThread().getName() + "not synchronized in g()"); //锁定,进入同步块 lock.lock(); try { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + "synchronized in g()"); } try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } } catch (Exception e) { e.printStackTrace(); } finally { //释放锁,离开同步块 lock.unlock(); } } public void h() { System.out.println(Thread.currentThread().getName() + "not synchronized in h()"); //锁定,进入同步块 lock.lock(); try { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + "synchronized in h()"); } try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } } catch (Exception e) { e.printStackTrace(); } finally { //释放锁,离开同步块 lock.unlock(); } } public static void main(String[] args) { final Resource3 rs = new Resource3(); new Thread(() -> rs.f()).start(); new Thread(() -> rs.g()).start(); rs.h(); } }
14.ThreadLocal
-
ThreadLocal的作用:
-
ThreadLocal用来解决多线程程序的并发问题
-
ThreadLocal并不是一个Thread,而是Thread的局部变量,当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本
-
从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思
-
-
ThreadLocal类中的方法:
-
void set(T value)
-
将此线程局部变量的当前线程副本中的值设置为指定值
-
-
void remove()
-
移除此线程局部变量当前线程的值
-
-
protected T initialValue()
-
返回此线程局部变量的当前线程的“初始值”
-
-
T get()
-
返回此线程局部变量的当前线程副本中的值
-
-
-
案例
public class ThreadLocalTest { ThreadLocal<Long> longLocal = new ThreadLocal<>(); ThreadLocal<String> stringLocal = new ThreadLocal<>(); public void set() { longLocal.set(Thread.currentThread().getId()); stringLocal.set(Thread.currentThread().getName()); } public long getLong() { return longLocal.get(); } public String getString() { return stringLocal.get(); } public static void main(String[] args) throws InterruptedException { final ThreadLocalTest test = new ThreadLocalTest(); test.set(); System.out.println("main:" + test.getLong()); System.out.println("main:" + test.getString()); Thread thread1 = new Thread(){ public void run() { test.set(); System.out.println("thread1:" + test.getLong()); System.out.println("thread1:" + test.getString()); }; }; thread1.start(); thread1.join(); System.out.println("main:" + test.getLong()); System.out.println("main:" + test.getString()); } }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)