一、线程概念
1、线程是什么
一个线程就是一个 “执行流”。每个线程之间都可以按照顺讯执行自己的代码。多个线程之间 “同时” 执行着多份代码。
进程包含了线程,一个线程对应一个PCB,一个进程对应一组PCB。
2、为什么要有线程
首先, “并发编程” 成为 “刚需”。
- 单核 CPU 的发展遇到了瓶颈。 要想提高算力, 就需要多核 CPU。 而并发编程能更充分利用多核 CPU 资源。
- 有些任务场景需要 “等待 IO”, 为了让等待 IO 的时间能够去做一些其他的工作, 也需要用到并发编程。
其次, 虽然多进程也能实现并发编程, 但是线程比进程更轻量。
- 创建线程比创建进程更快。
- 销毁线程比销毁进程更快。
- 调度线程比调度进程更快。
最后, 线程虽然比进程轻量, 但是人们还不满足, 于是又有了 “线程池”(ThreadPool) 和 “协程”(Coroutine)
3、进程与线程之间的区别
描述进程与线程的区别主要考虑以下四个方面:
1、首先说明进程与线程之间的关系 : 进程中包含一条或多条线程;
2、第二从系统管理与分配资源和CPU调度的角度来分析:进程是系统分配资源的最小单位,线程是CPU调度的最小单位;
3、第三从资源使用角度来分析:进程之间不能共享资源,进程中的线程之间共享进程的所有资源;
单独介绍一下线程的特点:线程的创建、销毁、调度效率比进程更高,并且有自己独立的执行任务。
1、进程包含线程
2、线程比进程更轻量化,创建更快,销毁也更快
3、同一个进程的多个线程之间共用一份内存/文件资源,进程和进程之间,则是独立的内存/文件资源。
4、进程是资源分配的基本单位,线程是调度执行的基本单位。
4、 Java 的线程和操作系统线程
线程是操作系统中的概念。操作系统内核实现了线程这样的机制, 并且对用户层提供了一些API供用户使用(例如 Linux 的 pthread 库)。
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装。
二、创建线程
创建线程共有5种写法:
1、继承Thread,重写run方法
2、实现Runnable,重写run方法
3、继承Thread,使用匿名内部类
4、实现Runnable,使用匿名内部类
5、使用lambda表达式
1、方法一:继承Thread类
- 继承 Thread 来创建一个线程类
class MyThread extends Thread {
@Override
public void run() {
System.out.println("这里是线程运行的代码");
}
}
- 创建 MyThread 类的实例
MyThread t = new MyThread();
- 调用 start 方法启动线程
t.start(); // 线程开始运行
package threading;
class MyThread extends Thread{
@Override
public void run(){
System.out.println("hello thread");
}
}
//演示多线程的基本创建方式
public class Demo1 {
public static void main(String[] args) {
MyThread t=new MyThread();
t.start();//另外启动一个线程来执行Thread中的run方法
}
}
在这个代码中,只有一个进程,两个线程,一个是main方法对应的主线程(是JVM创建的,main就相当于线程的入口方法),另一个线程是MyThread,run就是这个新线程的入口方法。
并发执行的意思是两边同时执行,各自执行各自的。
完整代码:
package threading;
class MyThread extends Thread{
@Override
public void run(){
while(true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* @author Susie-Wen
* @version 1.0
* @description:
* @date 2022/8/18 9:30
*/
//演示多线程的基本创建方式
public class Demo1 {
public static void main(String[] args) {
MyThread t=new MyThread();
t.start();
while(true){
System.out.println("hello mian");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
2、方法二:实现Runnable接口
- 实现 Runnable 接口
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("这里是线程运行的代码");
}
}
- 创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数
Thread t = new Thread(new MyRunnable());
- 调用 start 方法
t.start(); // 线程开始运行
完整代码:
package threading;
class MyRunnabbe implements Runnable{
@Override
public void run(){
while(true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//通过重写Runnable来实现创建线程
public class Demo2 {
public static void main(String[] args) throws InterruptedException {
MyRunnabbe runnabbe=new MyRunnabbe();
Thread t=new Thread(runnabbe);
t.start();
while(true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
把任务提取出来,目的仍然是为了解耦合,前面继承Thread写法,就把线程要完成的工作和线程本身耦合在一起了,假设未来要对这个代码进行调整(不用多线程了,用其他方式)代码改动就比较大。
而Runnable这种写法,就只是需要把Runnable传给其他的实体即可。
如果想搞多线程,都干一样的活,这个时候也更适合使用Runnable。
3、其他类型
- 匿名内部类创建 Thread 子类对象
// 使用匿名类创建 Thread 子类对象
Thread t1 = new Thread() {
@Override
public void run() {
System.out.println("使用匿名类创建 Thread 子类对象");
}
};
- 匿名内部类创建 Runnable 子类对象
匿名内部类的实例,作为构造方法的参数。
// 使用匿名类创建 Runnable 子类对象
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("使用匿名类创建 Runnable 子类对象");
}
});
- lambda 表达式创建 Runnable 子类对象
// 使用 lambda 表达式创建 Runnable 子类对象
Thread t3 = new Thread(() -> System.out.println("使用匿名类创建 Thread 子类对象"));
Thread t4 = new Thread(() -> {
System.out.println("使用匿名类创建 Thread 子类对象");
});
4、多线程的优势
package threading;
/**
* @author Susie-Wen
* @version 1.0
* @description:
* @date 2022/8/18 14:03
*/
public class Demo5 {
public static final long COUNT=100_0000_0000L;
public static void main(String[] args) throws InterruptedException {
serial();
concurrency();
}
//串行执行任务
public static void serial(){
//计算毫秒级别的时间戳
long begin=System.currentTimeMillis();
long a=0;
for(long i=0;i<COUNT;i++){
a++;
}
a=0;
for(long i=0;i<COUNT;i++){
a++;
}
long end=System.currentTimeMillis();
System.out.println("串行执行时间间隔:"+(end-begin)+"ms");
}
//并发执行任务
public static void concurrency() throws InterruptedException {
long begin=System.currentTimeMillis();
Thread t1=new Thread(()->{
long a=0;
for(long i=0;i<COUNT;i++){
a++;
}
});
Thread t2=new Thread(()->{
long a=0;
for(long i=0;i<COUNT;i++){
a++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();//join阻塞等待线程的结束
long end=System.currentTimeMillis();
System.out.println("并行执行时间间隔:"+(end-begin)+"ms");
}
}
代码运行结果:
同样的操作:
1、一个线程串行执行:6638ms
2、两个线程并发执行:3448ms
并发执行=微观上的并行+并发
这两个线程执行过程中,多少次是并行的(确实快了),多少次是并发的(总时间并没有减少,反而因为调度开销变长了)
- 如果计算量大,计算的久,创建线程的开销就更不明显(忽略不计)。
- 如果计算量小,计算的快,创建线程的开销影响更大,多线程的提升,就更不明显。
5、jconsole查看java进程
在相应的路径下找到:jconsole.exe(用来辅助调试程序的工具),双击打开。
此时就可以查看:
三、Thread 类及常见方法
Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关联。
用我们上面的例子来看,每个执行流,也需要有一个对象来描述,类似下图所示,而 Thread 类的对象就是用来描述一个线程执行流的,JVM 会将这些 Thread 对象组织起来,用于线程调度,线程管理。
1、 Thread 的常见构造方法
方法 | 说明 |
---|---|
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用 Runnable 对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnable target, String name) | 使用 Runnable 对象创建线程对象,并命名 |
【了解】Thread(ThreadGroup group,Runnable target) | 线程可以被用来分组管理,分好的组即为线程组,这个目前我们了解即可 |
public class Demo6 {
public static void main(String[] args) {
Thread t=new Thread(()->{
while(true){
System.out.println("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"这是俺的线程");
t.start();
}
}
2、 Thread 的几个常见属性
属性 | 获取方法 |
---|---|
获取ID | getId() |
获取线程的名称 | getName() |
状态,获取线程的状态 | getState() |
优先级,获取线程的优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
1、getId()
:这个属性获取的Id是Java中给Thread对象安排的身份标识,和操作系统内核中的PCB的pid,以及和操作系统提供的线程API中的线程id都不是一回事。身份标识可以有多个,在不同的环境下,使用不同的标识。
对于一个线程来说:
(1)在JVM中有一个id
(2)在操作系统的线程API有个id
(3)内核PCB中还有一个id
这三个id的效果是一样的,都是为了区分身份,但是是在不同的环境当中使用的。
2、getName()
:这里获取的name是刚才在构造方法里面指定的name
3、isDaemon()
:判断是否是后台线程,因为默认创建的线程是"前台线程",前台线程会组织进程退出,如果main运行完了,前台线程还没完,则进程不会退出。如果main等其他的前台线程执行完了,这个时候,即使后台线程没执行完,进程也会退出。
测试属性:
package threading;
public class Demo7 {
public static void main(String[] args) {
Thread t=new Thread(()->{
while(true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"这是俺的线程");
t.start();
System.out.println(t.getId());
System.out.println(t.getName());
System.out.println(t.getPriority());
System.out.println(t.getState());
System.out.println(t.isDaemon());
System.out.println(t.isAlive());
System.out.println(t.isInterrupted());
}
}
3、启动一个线程start
只用调用start才会真正创建线程,不调用start,就没有创建线程(在内核里创建PCB)。
- 覆写 run 方法是提供给线程要做的事情的指令清单。
- 线程对象可以认为是把 李四、王五叫过来了。
- 而调用 start() 方法,就是喊一声:”行动起来!“,线程才真正独立去执行了。
start()和run()的区别:
1、作用功能不同:
run方法的作用是描述线程具体要执行的任务; start方法的作用是真正的申请系统线程;
2、运行结果不同:
run方法是一个类中的普通方法,主动调用和调用普通方法一样,会顺序执行一次; start调用方法后, start方法内部会调用Java本地方法(封装了对系统底层的调用)真正的启动线程,并执行run方法中的代码,run 方法执行完成后线程进入销毁阶段。
- 直接调用run,则并没有创建线程,只是在原来的线程中运行的。
- 调用start,则是创建了线程,在新线程中执行代码(和原来的线程是并发的)。
调用 start 方法, 才真的在操作系统的底层创建出一个线程。
run方法执行完了,线程就提前结束了,有没有办法让线程提前一点结束呢?有的,就是通过线程中断的方式来进行的。(本质仍然是让run方法尽快结束,而不是run执行一半强制结束)
这就取决于run里面具体是如何实现的了。
1、直接自己定义一个标志位,作为线程是否结束的标记。
2、还可以使用标准库里面自带的一个标志位。
4、中断一个线程interrupt
李四一旦进到工作状态,他就会按照行动指南上的步骤去进行工作,不完成是不会结束的。但有时我们需要增加一些机制,例如老板突然来电话了,说转账的对方是个骗子,需要赶紧停止转账,那张三该如何通知李四停止呢?这就涉及到我们的停止线程的方式了。
目前常见的有以下两种方式:
- 通过共享的标记来进行沟通
- 调用 interrupt() 方法来通知
在Java中,中断线程并非是强制的,而是由线程自身的代码来进行判定处理的。
线程自身能怎么处理呢?
1、立即结束线程
2、不理会
3、稍后理会
public class Demo10 {
public static void main(String[] args) {
Thread t=new Thread(()->{
while(!Thread.currentThread().isInterrupted()){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
//方式一:立即结束线程
break;
//方式二:啥都不做,不理会,使得线程继续执行
//方式三:线程稍后处理
/* Thread.sleep(1000);
break;*/
}
}
});
t.start();
try{
Thread.sleep(3000);
}catch(InterruptedException e){
e.printStackTrace();
}
t.interrupt();
System.out.println("设置让t线程结束");
}
}
【1】使用自定义的变量来作为标志位
- 需要给标志位上加 volatile 关键字
public class ThreadDemo {
private static class MyRunnable implements Runnable {
public volatile boolean isQuit = false;
@Override
public void run() {
while (!isQuit) {
System.out.println(Thread.currentThread().getName()
+ ": 别管我,我忙着转账呢!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()
+ ": 啊!险些误了大事");
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target, "李四");
System.out.println(Thread.currentThread().getName()
+ ": 让李四开始转账。");
thread.start();
Thread.sleep(10 * 1000);
System.out.println(Thread.currentThread().getName()
+ ": 老板来电话了,得赶紧通知李四对方是个骗子!");
target.isQuit = true;
}
}
【2】使用 Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替自定义标志位
Thread 内部包含了一个 boolean 类型的变量作为线程是否被中断的标记
方法 | 说明 |
---|---|
public void interrupt() | 中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,否则设置标志位 |
public static boolean interrupted() | 判断当前线程的中断标志位是否设置,调用后清除标志位 |
public boolean isInterrupted() | 判断对象关联的线程的标志位是否设置,调用后不清除标志位 |
public class ThreadDemo {
private static class MyRunnable implements Runnable {
@Override
public void run() {
// 两种方法均可以
while (!Thread.interrupted()) {
//while (!Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread().getName()
+ ": 别管我,我忙着转账呢!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println(Thread.currentThread().getName()
+ ": 有内鬼,终止交易!");
// 注意此处的 break
break;
}
}
System.out.println(Thread.currentThread().getName()
+ ": 啊!险些误了大事");
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target, "李四");
System.out.println(Thread.currentThread().getName()
+ ": 让李四开始转账。");
thread.start();
Thread.sleep(10 * 1000);
System.out.println(Thread.currentThread().getName()
+ ": 老板来电话了,得赶紧通知李四对方是个骗子!");
thread.interrupt();
}
}
thread 收到通知的方式有两种:
- 如果线程因为调用 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException 异常的形式通
知,清除中断标志。
- 当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码的写法. 可以选择
忽略这个异常, 也可以跳出循环结束线程
- 否则,只是内部的一个中断标志被设置,thread 可以通过
- Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志
- Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志
这种方式通知收到的更及时,即使线程正在 sleep 也可以马上收到。
【3】观察标志位是否清除
标志位是否清除, 就类似于一个开关
标志位是否清除, 就类似于一个开关:
Thread.isInterrupted() 相当于按下开关, 开关自动弹起来了. 这个称为 “清除标志位”。
Thread.currentThread().isInterrupted() 相当于按下开关之后, 开关弹不起来, 这个称为"不清除标志位"。
- 使用 Thread.isInterrupted() , 线程中断会清除标志位
public class ThreadDemo {
private static class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.interrupted());
}
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target, "李四");
thread.start();
thread.interrupt();
}
}
true // 只有一开始是 true,后边都是 false,因为标志位被清
false
false
false
false
false
false
false
false
false
- 使用 Thread.currentThread().isInterrupted() , 线程中断标记位不会清除
public class ThreadDemo {
private static class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().isInterrupted());
}
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target, "李四");
thread.start();
thread.interrupt();
}
}
true // 全部是 true,因为标志位没有被清
true
true
true
true
true
true
true
true
true
5、等待一个线程join
有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。例如,张三只有等李四转账成功,才决定是否存钱,这时我们需要一个方法明确等待线程的结束。
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
Runnable target = () -> {
for (int i = 0; i < 10; i++) {
try {
System.out.println(Thread.currentThread().getName()
+ ": 我还在工作!");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ": 我结束了!");
};
Thread thread1 = new Thread(target, "李四");
Thread thread2 = new Thread(target, "王五");
System.out.println("先让李四开始工作");
thread1.start();
thread1.join();
System.out.println("李四工作结束了,让王五开始工作");
thread2.start();
thread2.join();
System.out.println("王五工作结束了");
}
}
方法 | 说明 |
---|---|
public void join() | 等待线程结束 |
public void join(long millis) | 等待线程结束,最多等 millis 毫秒 |
public void join(long millis, int nanos) | 同理,但可以更高精度 |
关于这里最多等待几毫秒的方法:
服务器开发经常要处理客户端的请求,根据请求计算生成响应,就需要用到“等待时间”,称为"超时时间",客户端发了请求过来,等待响应,这个等待就不能无限的等,可以根据需要,约定1000ms,500ms,如果时间之内响应没有回来,客户端会直接提示“等待超时”。
(在实际开发当中会很少使用无限等待的方法,大多数都是指定了最大的等待时间,避免程序因为死等而出现卡死这样的情况)
6、获取当前线程引用
方法 | 说明 |
---|---|
public static Thread currentThread(); | 返回当前线程对象的引用 |
public class ThreadDemo {
public static void main(String[] args) {
Thread thread = Thread.currentThread();
System.out.println(thread.getName());
}
}
7、 休眠当前线程
sleep:指定休眠的时间,让线程休息一会。
因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的。
方法 | 说明 |
---|---|
public static void sleep(long millis) throws InterruptedException | 休眠当前线程 millis毫秒 |
public static void sleep(long millis, int nanos) throws InterruptedException | 可以更高精度的休眠 |
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println(System.currentTimeMillis());
Thread.sleep(3 * 1000);
System.out.println(System.currentTimeMillis());
}
}
四、Thread的状态
1、观察线程的所有状态
线程的状态是一个枚举类型 Thread.State
public class ThreadState {
public static void main(String[] args) {
for (Thread.State state : Thread.State.values()) {
System.out.println(state);
}
}
}
- NEW: 安排了工作, 还未开始行动
- RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.
- BLOCKED: 这几个都表示排队等着其他事情
- WAITING: 这几个都表示排队等着其他事情
- TIMED_WAITING: 这几个都表示排队等着其他事情
- TERMINATED: 工作完成了
2、线程状态和状态转移的意义
public class Demo11 {
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(()->{
for(int i=0;i<10000_0000;i++){
//啥都不干
}
});
//t开始之前,得到的就是NEW
System.out.println(t.getState());
t.start();
// Thread.sleep(50);
//t正在工作,得到的是RUNNABLE
System.out.println(t.getState());
t.join();
//t结束之后,得到的状态就是terminate
System.out.println(t.getState());
}
}
- 刚把李四、王五找来,还是给他们在安排任务,没让他们行动起来,就是 NEW 状态;
- 当李四、王五开始去窗口排队,等待服务,就进入到 RUNNABLE 状态。该状态并不表示已经被银行工作人员开始接待,排在队伍中也是属于该状态,即可被服务的状态,是否开始服务,则看调度器的调度;
- 当李四、王五因为一些事情需要去忙,例如需要填写信息、回家取证件、发呆一会等等时,进入BLOCKED 、 WATING 、 TIMED_WAITING 状态;
- 如果李四、王五已经忙完,为 TERMINATED 状态。
所以,之前我们学过的 isAlive() 方法,可以认为是处于不是 NEW 和 TERMINATED 的状态都是活着的。
3、观察线程的状态和转移
【1】关注 NEW 、 RUNNABLE 、 TERMINATED 状态的转换
- 使用 isAlive 方法判定线程的存活状态
public class ThreadStateTransfer {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
for (int i = 0; i < 1000_0000; i++) {
}
}, "李四");
System.out.println(t.getName() + ": " + t.getState());;
t.start();
while (t.isAlive()) {
System.out.println(t.getName() + ": " + t.getState());;
}
System.out.println(t.getName() + ": " + t.getState());;
}
}
【2】关注 WAITING 、 BLOCKED 、 TIMED_WAITING 状态的转换
public static void main(String[] args) {
final Object object = new Object();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (object) {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}, "t1");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (object) {
System.out.println("hehe");
}
}
}, "t2");
t2.start();
}
使用 jconsole 可以看到 t1 的状态是 TIMED_WAITING , t2 的状态是 BLOCKED
修改上面的代码, 把 t1 中的 sleep 换成 wait:
public static void main(String[] args) {
final Object object = new Object();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (object) {
try {
// [修改这里就可以了!!!!!]
// Thread.sleep(1000);
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "t1");
...
}
使用 jconsole 可以看到 t1 的状态是 WAITING
【3】结论:
- BLOCKED 表示等待获取锁, WAITING 和 TIMED_WAITING 表示等待其他线程发来通知
- TIMED_WAITING 线程在等待唤醒,但设置了时限; WAITING 线程在无限等待唤醒
【4】yield() 让出 CPU
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
可以看到:
1. 不使用 yield 的时候, 张三李四大概五五开
2. 使用 yield 时, 张三的数量远远少于李四
结论:
yield 不改变线程的状态, 但是会重新去排队.
4. 多线程带来的的风险-线程安全 (重点)
4.1 观察线程不安全
while (true) {
System.out.println("张三");
// 先注释掉, 再放开
// Thread.yield();
}
}
}, "t1");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("李四");
}
}
}, "t2");
t2.start();
- 不使用 yield 的时候, 张三李四大概五五开
- 使用 yield 时, 张三的数量远远少于李四
结论:yield 不改变线程的状态, 但是会重新去排队。
五、多线程带来的的风险-线程安全
线程安全的意思,是在多线程各种随机的调度顺序下,代码没有bug,都能符合预期的方式来执行。
如果在多线程随机调度下代码出现bug,此时就认为是线程不安全的。
1、观察线程不安全
//演示线程不安全
class Counter{
int count=0;
public void increase(){
count++;
}
}
public class Demo12 {
private static Counter counter=new Counter();
public static void main(String[] args) throws InterruptedException {
//两个线程,每个线程都针对这个counter来进行5W次自增。
//预期结果是10W
Thread t1=new Thread(()->{
for(int i=0;i<50000;i++){
counter.increase();
}
});
Thread t2=new Thread(()->{
for(int i=0;i<50000;i++){
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("counter:"+counter.count);
}
}
出现的问题:结果发现每次counter的数量是不同的,并且都小于10W。
原因:
进行count++操作的底层是三条指令在CPU上完成。
1、把内存的数据读取到CPU寄存器中。load
2、把CPU的寄存器中的值,进行+1。add
3、把寄存器当中的值写回到内存中。save
由于当前是两个线程修改一个变量,由于每次修改的三个步骤(不是原子的),由于线程之间的调度顺序是不确定的,因此两个线程在真正执行这些操作的时候,就可能有多种执行的排列顺序。
由于在调度过程中,出现“串行执行”两种情况的次数,和其他情况的次数不确定,因此得到的结果就是不确定的值。
考虑极端的情况:
1、如果两个线程之间的调度全是串行执行,则结果就是10W。
2、如果两个线程之间的调度全都是其他情况,一次串行执行都没有,则结果就是5W。
因为最终的结果是5W~10W之间的数字。
六、写作业
问题一:请回答以下代码的输出, 并解释原因:
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("1");
}
});
t.start();
System.out.println("2");
}
答案:此题可能输出结果为 2 1 或 1 2。由于打印1 和 打印2分别在不同的线程中执行,具体先执行哪个线程是由系统决定的,且无法预测,所以两种情况都有可能。
问题二:
编写代码, 实现多线程数组求和. 给定一个很长的数组 (长度 1000w), 通过随机数的方式生成 1-100 之间的整数. 实现代码,能够创建两个线程, 对这个数组的所有元素求和. 其中线程1 计算偶数下标元素的和, 线程2 计算奇数下标元素的和. 最终再汇总两个和,进行相加 记录程序的执行时间.
public class Thread_2533 {
public static void main(String[] args) throws InterruptedException {
// 记录开始时间
long start = System.currentTimeMillis();
// 1. 给定一个很长的数组 (长度 1000w), 通过随机数的方式生成 1-100 之间的整数.
int total = 1000_0000;
int [] arr = new int[total];
// 构造随机数,填充数组
Random random = new Random();
for (int i = 0; i < total; i++) {
int num = random.nextInt(100) + 1;
arr[i] = num;
}
// 2. 实现代码, 能够创建两个线程, 对这个数组的所有元素求和.
// 3. 其中线程1 计算偶数下标元素的和, 线程2 计算奇数下标元素的和.
// 实例化操作类
SumOperator operator = new SumOperator();
// 定义具体的执行线程
Thread t1 = new Thread(() -> {
// 遍历数组,累加偶数下标
for (int i = 0; i < total; i += 2) {
operator.addEvenSum(arr[i]);
}
});
Thread t2 = new Thread(() -> {
// 遍历数组,累加奇数下标
for (int i = 1; i < total; i += 2) {
operator.addOddSum(arr[i]);
}
});
// 启动线程
t1.start();
t2.start();
// 等待线程结束
t1.join();
t2.join();
// 记录结束时间
long end = System.currentTimeMillis();
// 结果
System.out.println("结算结果为 = " + operator.result());
System.out.println("总耗时 " + (end - start) + "ms.");
}
}
// 累加操作用这个类来完成
class SumOperator {
long evenSum;
long oddSum;
public void addEvenSum (int num) {
evenSum += num;
}
public void addOddSum (int num) {
oddSum += num;
}
public long result() {
System.out.println("偶数和:" + evenSum);
System.out.println("奇数和:" + oddSum);
return evenSum + oddSum;
}
}
问题三: 在子线程执行完毕后再执行主线程代码 有20个线程,需要同时启动。 每个线程按0-19的序号打印,如第一个线程需要打印0
请设计代码,在main主线程中,等待所有子线程执行完后,再打印 ok