第17章:多线程基础
总体内容
相关概念
程序
进程
在任务管理器中可以看到电脑现在启动的进程
线程
单线程多线程
并发并行
& 并发和并行可能同时存在
& 比如说:当一个cpu上有多个任务时,就并发了。一台计算机中只有2个cpu(A和B),有两个任务在A和B上并行执行,这时又来了第三个任务,第三个任务在A上执行,此时A上有两个任务,他们的执行就是并发的
& 现在的计算机一般是:2个cpu,每个cpu4个核,共8核
public class Excise {
public static void main(String[] args) {
//获取当前时间(currentRuntime)
//RunTime是单例模式的 private static Runtime currentRuntime = new Runtime();
Runtime runtime = Runtime.getRuntime();
//获取当前电脑cpu/核心数 (available可获得的Processors处理器)
int cpuNums = runtime.availableProcessors();
System.out.println(cpuNums);//8
}
}
线程的使用
创建线程
方式1:继承Thread类
代码演示
解读:
- 当类继承了Thread类时,该类就可以当作线程使用(Thread:线程)
- 重写run()方法,写上自己线程要实现的业务代码
- Thread类中的run()方法 实现了 Runnable接口中的run()方法
- 具体内容看代码上的标注吧,第三个问题在多线程机制里写
public class Excise {
public static void main(String[] args) {
//实例化Cat对象,该对象可以当作一个线程使用
Cat cat = new Cat();
//启动线程cat
cat.start();
}
}
//1. 当类继承了Thread类时,该类就可以当作线程使用(Thread:线程)
//2. 我们会重写run()方法,写上自己的业务代码
//3. Thread类中的run()方法 实现了 Runnable接口中的run()方法
/*
@Override
public void run() {
if (target != null) {
target.run();
}
}
*/
class Cat extends Thread {
@Override
public void run() {//重写run()方法,写上自己的业务代码
//2. 实现:当输出8次时,结束该进程->增加循环,次数到8时,跳出循环
int times = 0;
while (true) {
//1. 实现:该线程每隔一秒,在控制台输出“喵喵,我是小猫咪”
System.out.println("喵喵,我是小猫咪" + (++times));
//让该线程休眠一秒,Thread.sleep(1000)会报编译异常,用Ctrl+Alt+T
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (times == 8) {
break;
}
}
}
}
效果:每隔一秒输出一条信息,直到第8条退出
多线程机制
机制解读
1&. 当程序运行时,就启动了一个进程,进程执行到main()方法时,创建一个main线程,main线程中执行到cat.start()
时,会创建另外一个线程Thread-0(通过Thread.currentThread.getName()
可以获取当前线程的名字)
2&. 要注意的是:main线程在开始Thread-0线程后,不会阻塞,也就是主线程不会等子线程执行完才继续执行后面的内容,这时主线程和子线程交替执行(并发)
JConsole监控线程
① 启动程序
② 在终端中启动jconsole
③ 对本程序:Excise进行连接
④ 看进程内线程的执行情况
⑤ 解读:
jconsole可以监控进程内线程的执行情况,连接方法上面都有,这里对线程执行情况进行解读:
& 编写代码时设置了:main()中的循环为60次,Cat类的run()中的循环为80次
& 开始时,主线程和子线程交替执行
& 当主线程中的循环执行到60次时,主线程结束(销毁),子线程继续执行
& 当子线程中的循环执行到80次时,子线程结束,此时进程也结束了
& 注意:主线程结束,不会导致子线程结束
为什么用start()
解读问题:start()方法最终会调用cat的run()方法,那为什么不直接调用cat的run()方法,而是要执行cat.start()呢?
① cat.run()
run()是一个普通的方法,没有真正启动一个线程。 run()方法和主线程的其它普通方法串行执行,也就是主线程阻塞到run()方法这里,等待run()执行结束后,再继续执行后面的普通方法
② cat.start()
start()会为cat对象启动一个线程,然后调用cat的run方法,这时主线程和子线程并发(交替)执行–>多线程
读start()源码
public synchronized void start() {//①
start0();
}
//satrt0()是本地方法,由JVM调用,底层是C/C++实现
//真正实现多线程效果的是start0(),而不是run()
private native void start0();//②
方式2:实现Runnable接口
代码演示:线程
import java.util.Date;
public class Excise {
public static void main(String[] args) throws InterruptedException {
Dog dog = new Dog();
// dog.start();//这里不能掉哟个start()
//创建Thread对象,把dog对象(实现了Runnable接口),放入thread
Thread thread = new Thread(dog);
thread.start();
}
}
class Dog implements Runnable{
@Override
public void run() {
int count = 0;
while (true){
System.out.println("hi 小狗狗"+(++count)+" "+Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count==10){
break;
}
}
}
}
代码演示:代理模式
上面的那种方法,底层用了设计模式=>代理模式
代码模拟:实现Runnable接口 开发线程机制=>极简的Thread类
解读:
① 先模拟一个极简Thread类。Thread类实现了Runnable接口,所以我们编写一个ThreadProxy类也实现Runnable接口
② ThreadProxy类中的内容:target属性存放自定义线程对象,run()调用target的run(),start()调用start0(),start0()是jvm调用的,它把target的线程状态转换为可运行态,当分配到cpu时,target的run()方法被执行,这里简写为start0()调用run()方法
③ 编写自定义线程类。该类已经继承了一个类,所以只能通过实现Runnable接口来实现线程,重写run方法,写上该线程要做的逻辑
④ 调用自定义线程的run()。先声明线程,前面提到了为什么用start()调用run(),而不是直接调用run(),所以这里不再阐述。因为Runnable接口中没有start()方法,所以我们要通过Thread类来调用start()方法,首先我们把自定义线程对象传到Thread类中,再调用Thread类的start()方法
⑤ 接④来讲解流程。传入以后,target存放着自定义线程对象,然后start()调用start0(),start0()调用run(),run()调用target.run()
⑥ 这个模拟用到了静态代理模式
public class Excise {
public static void main(String[] args) throws InterruptedException {
Dog dog = new Dog();
ThreadProxy threadProxy = new ThreadProxy(dog);//自定义线程dog=>target
threadProxy.start();//start()调用start0(),start0()调用run(),run()调用target.run()
}
}
class Animal {
}
//因为java单继承机制,所以不能再继承Thread,只能实现Runnable接口
class Dog extends Animal implements Runnable {
@Override
public void run() {
System.out.println("小狗汪汪叫..");
}
}
//线程代理类,模拟一个极简的Thread类
class ThreadProxy implements Runnable {
private Runnable target = null;//属性,类型是Runnable
@Override
public void run() {//run方法:调用自定义线程类的run()方法
if (target != null) {
target.run();
}
}
public ThreadProxy(Runnable target) {//构造器:传入自定义线程类的对象
this.target = target;
}
public void start() {
start0();
}
private void start0() {
run();//调用代理类中的run()方法
}
}
继承Thread vs 实现Runnable接口
线程使用案例
多线程如何理解
实例:多线程执行(易)
使用实现Runnable方法来实现
public class Excise {
public static void main(String[] args) throws InterruptedException {
Hello hello = new Hello();
Hi hi = new Hi();
Thread thread = new Thread(hello);
thread.start();
Thread thread1 = new Thread(hi);
thread1.start();
}
}
class Hello implements Runnable {
private int count;
@Override
public void run() {
while (true) {
System.out.println("hello world" + (++count) + " 线程名为:" + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count == 10) {
break;
}
}
}
}
class Hi implements Runnable {
private int count;
@Override
public void run() {
while (true) {
System.out.println("hi" + (++count) + " 线程名为:" + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count == 5) {
break;
}
}
}
}
输出结果:
实例:多线程售票出现的问题
① 因为实现Runnable接口时,多个线程可以共享一个资源(都可以访问自定义线程类中的内容),所以自定义线程类中的属性不需要定义成静态属性
② 但是继承Thread类时,属性必须定义成静态,才能被多个线程(自定义线程类的对象)共同访问
③ 可能出现的问题:当票数=1时,三个线程共同判断票数是否≤0,此时三个线程都判断为false,继而都进行了票数–的操作,导致出现了负票数的现象
这时就需要引入同步和互斥了。
/**
* 使用多线程,模拟三个窗口同时售票(100张票)==>出现问题
*/
public class Excise {
public static void main(String[] args) throws InterruptedException {
//1、 继承Thread
//创建三个窗口(线程),因为三个都是SellTicket1的对象,所以访问同一个ticketNums
// SellTicket1 sellTicket1 = new SellTicket1();
// SellTicket1 sellTicket2 = new SellTicket1();
// SellTicket1 sellTicket3 = new SellTicket1();
// sellTicket1.start();//启动线程
// sellTicket2.start();
// sellTicket3.start();
//2、 实现Runnable接口
SellTicket2 sellTicket2 = new SellTicket2();
new Thread(sellTicket2).start();//启动三个线程,共享自定义线程类sellTicket2中的内容
new Thread(sellTicket2).start();
new Thread(sellTicket2).start();
}
}
class SellTicket1 extends Thread {
private static int ticketNums = 100;//设置成静态变量,只要是SellTicket类的对象,就都能访问
@Override
public void run() {
while (true) {
//判断票数量
if (ticketNums <= 0) {
System.out.println("售票结束...");
break;
}
//休眠 => 售票员休息
try {
Thread.sleep(70);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票 剩余票数 = " + (--ticketNums));
}
}
}
class SellTicket2 implements Runnable {
//这里不需要设置成静态变量,因为用实现Runnable方式时,线程可以共享SellTicket类中的内容
private int ticketNums = 100;
@Override
public void run() {
while (true) {
//判断票数量
if (ticketNums <= 0) {
System.out.println("售票结束...");
break;
}
//休眠 => 售票员休息
try {
Thread.sleep(70);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票 剩余票数 = " + (--ticketNums));
}
}
}
通知线程退出
代码演示
步骤:
- 在自定义线程类中,定义一个控制变量loop,默认值为ture。
private boolean loop = true;//设置一个控制变量(控制run()方法的终止)
- 将loop作为while的循环条件
while(loop){}
- 编写loop的set()方法。因为如果想在main线程中控制该线程的结束,main线程中必须能修改循环条件loop的值,所以得写一个set()方法
public void setLoop(boolean loop) { this.loop = loop; }
- 在main线程中设置loop值为false
t1.setLoop(false);//main线程中通知t1线程退出
解读:
- 前面写的案例都是让线程执行完毕后,自动退出
- 这个案例的目的是让 线程被动退出(线程1通知线程2退出)。
- 线程退出就是让run()方法结束,因为run()方法中有while循环,所以要让循环结束,这样run()方法也就结束了。所以使用变量来控制while循环,从而控制run()方法的结束,当run()结束时,这个线程也就结束了
public class Excise {
public static void main(String[] args) throws InterruptedException {
T t1 = new T();
t1.start();
//如果希望主线程控制子线程的终止,必须可以修改loop=>给loop写一个set()方法
//目的:让t1退出run()方法,从而终止t1线程-->通知的方式
//让主线程休眠10秒,再通知 t1线程退出
System.out.println("主线程休眠5秒");
Thread.sleep(5 * 100);
t1.setLoop(false);//main线程中通知t1线程退出
}
}
class T extends Thread {
private int count;
private boolean loop = true;//设置一个控制变量(控制run()方法的终止)
@Override
public void run() {
while (loop) {
System.out.println("线程运行" + (++count));
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void setLoop(boolean loop) {
this.loop = loop;
}
}
输出结果:
线程常用方法
线程中断
注意:interrupt()的使用,是中断线程,不是结束线程,一般用于中断线程休眠
下面是代码的输出结果
public class Excise {
public static void main(String[] args) throws InterruptedException {
T t = new T();
t.setName("王胖子");//1. setName()设置这个线程的名字
t.setPriority(Thread.MIN_PRIORITY);//2. 设置线程优先级:1
System.out.println(t.getPriority());
;//3. 获取线程优先级
t.start();//4. start()启动t线程
for (int i = 1; i <= 5; i++) {
Thread.sleep(1000);//5. sleep()主线程休眠
System.out.println("主线程正在输出" + i);
}
//6. 终止t线程的休眠。
//当调用interrupt()时,sleep就会抛出一个InterruptedException异常
//然后被catch捕获到,继而停止休眠,继续执行
t.interrupt();
}
}
class T extends Thread {
@Override
public void run() {
while (true) {
//输出100条语句
for (int i = 1; i <= 100; i++) {
System.out.println(Thread.currentThread().getName() + " 正在学习..." + i);//7. 获取当前线程的名字
}
//休眠20秒
try {
System.out.println(Thread.currentThread().getName() + " 休眠中...");
Thread.sleep(20 * 1000);
} catch (InterruptedException e) {
//当执行到interrupt()方法时,就会捕获一个中断异常,这一个代码块可以加自己的业务代码
//InterruptedException 是中断异常
System.out.println(Thread.currentThread().getName() + " 的休眠被中断...");
}
}
}
}
线程插队
- yield() 当资源充足时,就不会礼让了
① 两个人吃10个包子,A怕B不够吃,所以A礼让B会成功(cpu把资源先给B)
② 两个人吃一万个包子,A就没必要礼让B了,这时cpu就不会把资源先给B了 - join() 一定会插队成功:B插队,则会把资源先给B,B执行完后,再执行A
案例:
public class Excise {
public static void main(String[] args) throws InterruptedException {
T t = new T();
t.start();
for (int i = 1; i <= 5; i++) {
Thread.sleep(50);
System.out.println("主线程(小弟)吃了 " + i + " 个包子");//开始时,主线程和子线程交替执行
//实现:主线程执行3次后,让子线程先执行完,主线程再执行
if (i == 3) {
System.out.println("主线程(小弟) 让 子线程(老大) 先吃");
t.join();//把子线程 插到 主线程前面
System.out.println("子线程(老大) 吃完了 主线程(小弟) 接着吃");
}
}
}
}
class T extends Thread {
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("子线程(老大)吃了 " + i + " 个包子");
}
}
}
输出效果
线程插队练习
线程B插入到线程A中,让线程B先执行
① 在线程A中启动线程Bb.start();
② 在线程A中插入线程Bb.join()
③ 如果是实现Runnable接口的线程,需要通过Thread对象调用start()和join()
Thread thread = new Thread(new B());
thread.start();//启动子线程
thread.join();//将子线程插入,先执行子线程
public class Excise {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new T());
for (int i = 1; i <= 10; i++) {
System.out.println("Hi " + i);
if (i == 5) {
thread.start();//启动子线程
thread.join();//立即将子线程插入,先执行子线程
}
Thread.sleep(1000);
}
}
}
class T implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 10; i++) {//可以把for换成while+变量count
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Hello " + i);
}
}
}
守护线程
1Q. 为什么把一个线程设置成守护线程(Daemon):
① 前面写的都是用户线程,在主线程中启动一个子线程,当主线程退出后,如果子线程run()没有执行完,那么子线程会继续执行。
② 如果要实现:主线程退出时,子线程也跟着退出,则要把子线程设置成主线程的守护线程
2Q. 怎样把一个线程设置成守护线程
如果想监控其它进程/获取其它线程的信息,就可以为它做一个守护线程。
MyDaemon myDaemon = new MyDaemon();//1. 创建子线程
myDaemon.setDaemon(true);//2. 将子线程设置成主线程的守护线程
myDaemon.start();//3. 启动该子线程
完整代码:
public class Excise {
public static void main(String[] args) throws InterruptedException {
MyDaemon myDaemon = new MyDaemon();//创建子线程
myDaemon.setDaemon(true);//将子线程设置成主线程的守护线程
myDaemon.start();//启动该子线程
for (int i = 1; i <= 5; i++) {
System.out.println("主线程....." + i);
Thread.sleep(1000);
}
}
}
class MyDaemon extends Thread {
@Override
public void run() {
for (; ; ) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("子线程.....");
}
}
}
输出结果:
线程的生命周期
Runnable状态(可运行)可以细分为:Ready(就绪) 和 Running(运行)
所以可以细分成7种
写一个程序查看线程状态
public class Excise {
public static void main(String[] args) throws InterruptedException {
T t = new T();
System.out.println(t.getName() + " 的状态是:" + t.getState());
t.start();
while (Thread.State.TERMINATED != t.getState()) {//t线程的状态不为终止时 执行
System.out.println(t.getName() + " 的状态是:" + t.getState());
Thread.sleep(500);//主线程休眠
}
System.out.println(t.getName() + " 的状态是:" + t.getState());
}
}
class T extends Thread {
@Override
public void run() {
while (true) {
for (int i = 0; i < 3; i++) {
try {
Thread.sleep(1000);//子线程休眠
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("子线程输出.....");
}
break;
}
}
}
输出结果:
同步机制(Synchronized)
机制与使用
比如:去厕所,一个人进去把门锁了,等到出去时,又把门打开,下一个人才能进去
实例:解决多线程售票问题
解决票超卖的问题
- 把卖票的方法sell()写成同步方法(synchronized)。这里把卖票的代码单独写到了一个方法中,让run()调用sell()方法。使得同一时刻只有一个线程可以进入sell()中,该线程执行完后,其它线程才能有机会进入
- 添加属性loop来结束while循环。 因为把卖票代码提到另一个方法后,卖票结束时,只能return结束sell()方法,不能break结束while循环,就导致了while无法停止循环,已知重复调用sell(),输出"售票结束"
① 定义loop
② while的循环条件改为loop
③ 售票结束后,修改loop值
public class Excise {
public static void main(String[] args){
SellTicket3 sellTicket3 = new SellTicket3();
Thread thread1 = new Thread(sellTicket3);
Thread thread2 = new Thread(sellTicket3);
Thread thread3 = new Thread(sellTicket3);
thread1.setName("窗口1");//设置线程名
thread2.setName("窗口2");
thread3.setName("窗口3");
thread1.start();
thread2.start();
thread3.start();
}
}
//解决票超卖的问题:使用线程同步
class SellTicket3 implements Runnable {
private int ticketNums = 100;//实现Runnable接口,可以让多个线程访问该属性
//控制while循环的变量:很巧妙的让while停止循环
// (不控制的话,sell()中的return只会退出sell(),但没有停止while,还会一遍遍的调用sell)
private boolean loop = true;
//这里不直接给run()加同步了,因为run里面可能还有别的代码可以同时被访问,
// 所以把需要同步的代码,单独写在一个方法里
@Override
public void run() {
while (loop) {
sell();//同步方法
}
}
public synchronized void sell() {//同步方法,同一时刻只能有一个线程来执行sell方法
if (ticketNums == 0) {//判断票数量
System.out.println(Thread.currentThread().getName() + " 售票结束...");
loop = false;//售票结束后,让while循环也停止
return;
}
try {
Thread.sleep(70);//休眠 => 售票员休息
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 售出一张票 剩余票数 = " + (--ticketNums));
}
}
输出结果:
分析:
这个的"钥匙" 叫互斥锁
互斥锁
为同步方法/代码块加互斥锁:
① 静态同步方法/代码块的锁:类本身(类.class)
② 非静态同步方法/代码块的锁:this(当前对象)/同一其它对象
注意1:售票案例中是使用实现Runnable接口的方法,所以三个线程使用了同一个自定义线程对象,所以this指向相同的对象,但是继承Thread类就不是了
注意2:锁为相同的一个对象,是为了保证三个线程抢同一个锁,如果是不同的对象,那三个线程就不用抢了
说明:object是自定义线程类的属性,所以也是同一个对象啦
死锁
死锁的代码说明:
释放锁
本章作业
作业1
思路:
import java.util.Scanner;
public class Excise {
public static void main(String[] args) throws InterruptedException {
T1 t1 = new T1();
T2 t2 = new T2(t1);
t1.start();
t2.start();
}
}
/**
* 线程1:循环随机打印100以内的整数
*/
class T1 extends Thread {
private boolean loop = true;//控制while循环
@Override
public void run() {
//线程1:循环随机打印100以内的整数
while (loop) {
System.out.println((int) (Math.random() * 100 + 1));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " 退出!");
}
public void setLoop(boolean loop) {//修改loop
this.loop = loop;
}
}
/**
* 线程2:键盘读取到Q,就终止线程1
*/
class T2 extends Thread {
Scanner scanner = new Scanner(System.in);
private T1 t1;//关联线程1
public T2(T1 t1) {//构造器
this.t1 = t1;
}
@Override
public void run() {
//线程2:键盘读取到Q,就终止线程1
while (true) {
System.out.println("输入一个字符(输入Q停止输出):");
System.out.println("输入字符后马上回车!");
char key = scanner.next().charAt(0);
if (key == 'Q') {
t1.setLoop(false);//以通知的方式结束线程1(t1)
break;
}
}
System.out.println(Thread.currentThread().getName() + " 退出!");
}
}
输出结果:
作业2:取钱
思路:
public class Excise {
public static void main(String[] args) throws InterruptedException {
Card card = new Card();
Thread thread1 = new Thread(card);
Thread thread2 = new Thread(card);
thread1.setName("张三");
thread2.setName("王五");
thread1.start();
thread2.start();
}
}
class Card implements Runnable {
private int money = 10000;
@Override
public void run() {
while (true) {
//两个线程争夺 this对象锁(非公平锁=>得抢)
//争夺到就执行,执行完释放对象锁,继续争夺
//争夺不到就继续阻塞,准备继续争夺
synchronized (this) {
if (money < 1000) {
System.out.println("本卡余额不足...");
break;
}
money -= 1000;
System.out.println(Thread.currentThread().getName() + " 取了1000,当前余额 = " + money);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
输出结果=>没有出现超取现象(用到线程同步):