一、多线程的概念
多线程(Multi Threading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理或同时多线程处理器。在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理”。
1) 并发
在一个系统中,存在很多正在运行的进行,假如CPU只有一个,多个进行轮番执行,抢占CPU执行,宏观上程序是同时执行,微观上是多个程序获取CPU时间段,获取到的执行,没获取到的等待,这种叫并发。
2) 并行
在一个系统中,多个程序正在运行时,硬件上存在多个CPU,不同程序在不同CPU上运行,两个或者多个程序是同时,相互之间不再抢占CPU,这就并行。
3)进程
进程是一段正在执行的程序,是操作系统进行资源分配和调度的一个独立单位
4)线程
是进程的一个实体,是cup调度和分配的基本单位,是比进程更小的可以独立运行的基本单位
二、多线程的有缺点
优点:
1)提高程序执行效率——底层使用多线程技术,让整个任务执行速度提高,达到整体快速完成的目的。
2) 提高CPU使用率——进行中包含多个线程,多个线程轮番执行,每个线程可能有等待时间,但对CPU来说,一直在执行
缺点:
1) 容易产生资源竞争——假如有多台电脑连接同一台打印机,同时想打印资源时,相当于多线程同时请求,但只有一台电脑请求会被执行,其他需要等待
2) 多线程切换执行,切换时浪费资源——多线程在抢占使用同一个CPU时,会进行上下文切换(java底层线程私有区,程序计数器会记录当前线程的执行位置,下次再抢占到CPU时,会从该位置继续执行),上下文切换会消耗服务器资源。
3) 多线程可能会产生死锁
三、多线程的实现方式
多线程实现方式主要有三种:
1、继承Thread并重写run方法,实现简单但不可以继承其他类
2、实现 Runable接口并重写run方法。避免了单继承局限性,编程更加灵活,实现解耦
3、实现Callable接口并重写call方法,可以获取线程执行结果的返回值,并且可以抛出异常
4、使用线程池创建
3.1 继承Thread类
Thread 线程是程序中执行的线程。 Java虚拟机允许应用程序同时执行多个执行线程。
package com.aaa.day01_mutilThread;
/**
* @author :caicai
* @date :Created in 2022/7/18 14:03
* @description:多线程实现方式一:继承Thread
* @modified By:
* @version:
*/
public class MTExtendsThread_demo1 extends Thread{
/**
* @Author caicai
* @Description 继承Thread重写run方法 线程执行具体业务(任务)的方法
* @Date 14:08 2022/7/18
**/
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
// currentThread() Thread类中的静态方法 用来获取当前正在执行的线程
// native 修饰的方法是非Java代码编写的方法,无法查看源码
Thread thread = Thread.currentThread();
// getName()获取线程名称
System.out.println("线程"+thread.getName()+"正在执行打印:"+i);
}
}
}
class TestDemo1{
public static void main(String[] args) {
System.out.println("线程"+Thread.currentThread().getName()+"开始执行了");
// 实例化一个对象
MTExtendsThread_demo1 mtExtendsThread = new MTExtendsThread_demo1();
// 为一个线程设置名称
mtExtendsThread.setName("ThreadTest");
// start() 启动线程方法 导致此线程开始执行; java虚拟机调用此线程的run方法
mtExtendsThread.start();
// 此方式是方法的调用,不是多线程,自始自终都是main在执行
// mtExtendsThread.run();
System.out.println("线程"+Thread.currentThread().getName()+"执行完毕.......");
}
}
打印结果:
3.2 实现Runable接口
Runnable接口应由任何类实现,其实例将由线程执行。 该类必须定义一个无参数的方法,称为run
package com.aaa.day01_mutilThread;
/**
* @author :caicai
* @date :Created in 2022/7/18 14:26
* @description:多线程实现方式二:实现Runnable接口
* @description:Runnable接口应由任何类实现,其实例将由线程执行。该类必须定义一个无参数的方法,称为run
* @modified By:
* @version:
*/
public class MTImplRunnable_demo2 implements Runnable{
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
// 调用Thread类中的静态方法,用来获取当前正在执的线程
Thread thread = Thread.currentThread();
// getName() 获取线程名称
System.out.println("线程"+thread.getName()+"正在执行打印"+i);
}
}
}
class TestDemo2{
public static void main(String[] args) {
System.out.println("线程"+Thread.currentThread().getName()+"开始执行了");
// 实例化对象
MTImplRunnable_demo2 mtImplRunnable = new MTImplRunnable_demo2();
// 方法调用 并不能开启线程
// mtImplRunnable.run();
// 启动线程 借助Thread启动线程
// Thread thread = new Thread(mtImplRunnable);
// thread.setName("Runnable");
// thread.start();
new Thread(mtImplRunnable,"Runnable").start();
System.out.println("线程"+Thread.currentThread().getName()+"执行完毕.......");
}
}
打印结果:
3.3 实现Callable接口
Callable接口类似于Runnable ,因为它们都是为其实例可能由另一个线程执行的类设计的。 然而,A Runnable不返回结果,也不能抛出被检查的异常。
package com.aaa.day01_mutilThread;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @author :caicai
* @date :Created in 2022/7/18 14:47
* @description:多线程实现方式三:实现Callable接口
* @description:Callable接口类似Runnable,他们都是为其实例可能由另一个线程执行的类设计的,
* 然而 Runnable不返回结果,也不能抛出被检查的异常
* @modified By:
* @version:
*/
public class MTImplCallable_demo3 implements Callable<String> {
/**
* 使用Runnable 和 Callable 实现多线程的区别
* 1、多是多线程,实现接口不同,执行业务的方法不同, 一个是run方法 一个是call方法
* 2、call方法有返回值 run 方法没有返回值
* 3、call 有异常处理 通过Future里面的get方法获取异常,如果没有调用get,异常无法获取 run没有异常处理
**/
@Override
public String call() throws Exception {
for (int i = 1; i <= 10; i++) {
// 调用Thread类中的静态方法,用来获取当前正在执的线程
Thread thread = Thread.currentThread();
// getName() 获取线程名称
System.out.println("线程"+thread.getName()+"正在执行打印"+i);
}
String randomStr = UUID.randomUUID().toString();
System.out.println("业务执行中随意一个字符串:"+randomStr);
// 制造异常
// System.out.println(1/0);
return randomStr;
}
}
class TestDemo3{
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("线程"+Thread.currentThread().getName()+"开始执行了");
// 实例化对象
MTImplCallable_demo3 mtImplCallable = new MTImplCallable_demo3();
//借助于FutureTask类 初始化线程 提供方法来检查计算是否完成,等待其完成,并检索计算结果。 结果只能在计算完成后使用方法get进行检索,如有必要,阻塞,直到准备就绪。
// FutureTask 点击查看源码 发现间接实现Runnable 和 Future 接口 作用如下
// 1、如果Runnable 接口的子类,就可以通过Thread启动线程
// 2、如果Future 接口的子类,可以通过get方法,获取线程执行时业务的返回值和异常
FutureTask futureTask = new FutureTask(mtImplCallable);
// 启动线程
new Thread(futureTask,"Callable").start();
// 通过futureTask获取返回值和异常
Object returnValue = futureTask.get();
System.out.println("线程执行的返回值为:"+returnValue);
System.out.println("线程"+Thread.currentThread().getName()+"执行完毕.......");
}
}
打印结果:
3.4 继承Thread和实现Runnable的区别
继承thread,受java中单继承的影响,无法共享类成员变量, 实现Runable可以共享共享类的成员变量。
Thread 模拟现实中售票情况
package com.aaa.day01_mutilThread.anli.thread_runnable;
/**
* @author :caicai
* @date :Created in 2022/7/18 16:53
* @description:使用卖票例子,演示Thread与区别Runnable
* 继承thread,受java中单继承的影响,无法共享类成员变量
* @modified By:
* @version:
*/
public class SaleTicketA extends Thread{
// 定义售票数量
private int ticketNum = 20;
// 售票员名称
private String saleName;
// 使用构造给售票员赋值
public SaleTicketA(String saleName) {
this.saleName = saleName;
}
/**
* @Author caicai
* @Description 模拟多个窗口或者多态自动售票机卖票
* @Date 16:57 2022/7/18
**/
@Override
public void run() {
// 判断票是否卖完
while (ticketNum>0){
System.out.println("售票员:"+saleName+"售出一张,总票数还剩"+(--ticketNum)+"张");
}
}
}
class Test1{
public static void main(String[] args) {
// 实例化线程对象
SaleTicketA saleTicketA1 = new SaleTicketA("张三");
SaleTicketA saleTicketA2 = new SaleTicketA("李四");
SaleTicketA saleTicketA3 = new SaleTicketA("王五");
saleTicketA1.start();
saleTicketA2.start();
saleTicketA3.start();
}
}
Thread的结果显示:一张票售卖三次 明显是不符合实际情况的
Runable模拟现实中售票情况
package com.aaa.day01_mutilThread.anli.thread_runnable;
/**
* @author :caicai
* @date :Created in 2022/7/18 17:02
* @description:Runnable 实现Runable可以共享共享类的成员变量。
* @modified By:
* @version:
*/
public class SaleTicketB implements Runnable{
// 定义售票数量
private int ticketNum = 20;
// 模拟多个窗口或者多态自动售票机卖票
@Override
public void run() {
// 判断票是否卖完
while (ticketNum>0){
System.out.println("售票员:"+Thread.currentThread().getName()+"售出一张,总票数还剩:"+(--ticketNum)+"张");
}
}
}
class Test2{
public static void main(String[] args) {
// 实例化线程对象
SaleTicketB saleTicketB = new SaleTicketB();
new Thread(saleTicketB,"张三").start();
new Thread(saleTicketB,"李四").start();
new Thread(saleTicketB,"王五").start();
}
}
Runable方式结果打印:结果正常 符合实际情况
四、多线程的状态(线程的生命周期)
1)new--> 新建状态
创建一个线程对象后,该线程对象就处于新建状态,此时并没有调用该对象的start方法,所以它不能运行,与其他Java对象一样,仅仅由Java虚拟机为其分配了内存,没有表现出任何线程的动态特征。
2)Runable--> 就绪状态
当线程对象调用了start()方法后,该线程就进入就绪状态。处于就绪状态的线程位于线程队列中,此时它只是具备了运行的条件,能否获得CPU的使用权并开始运行,还需要等待系统的调度。
3)Running--> 运行状态
如果处于就绪状态的线程获得了CPU的使用权,并开始执行run()方法中的线程执行体,则该线程处于运行状态。一个线程启动后,它可能不会一直处于运行状态,当运行状态的线程使用完系统分配的时间后,系统就会剥夺该线程占用的CPU资源,让其他线程获得执行的机会。需要注意的是,只有处于就绪状态的线程才可能转换到运行状态。
4)Blocked--> 阻塞状态
阻塞状态是线程因为某种原因放弃CUP的使用权,暂时停止运行,直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况主要分为三种:
1、等待 —— 当线程调用了某个对象的wait()方法时,会使线程进入阻塞状态,如果想进入就绪状态就需要使用notify()或者notifyAll()方法唤醒该线程。
2、超时等待 —— 当线程调用了Thread的sleep(long millis)方法时,会使线程进入阻塞状态,在这种情况下,只需等到线程睡眠的时间到了后,线程就会自动进入就绪状态。
3、同步阻塞 —— 线程在获取synchronized 同步锁失败。即当线程试图获取某个对象的同步锁时,如果该锁被其他线程所持有,则当前线程会进入阻塞状态,如果想从阻塞状态进入就绪状态就必须获取到其他线程所持有的锁。
5)dead --> 死亡状态
如果线程调用stop()方法或nun()方法正常执行完毕,或者线程抛出一个未捕获的异常(Exception)错误(Error),线程就进入死亡状态。一旦进入死亡状态,线程将不再拥有运行的资格,也不能再转换到其他状态。