介绍
- 多线程(英语:multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理(Multithreading)”。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程(台湾译作“执行绪”),进而提升整体处理性能。
进程和线程
进程(Process)
- 正在运行的程序,是一个程序的运行状态和资源占用(内存,CPU)的描述,通过进程ID区分。
- 进程是程序的一个动态过程,它指的是从代码加载到执行完毕的一个完成过程。
- 目前操作系统支持多进程多任务。
进程的特点:
- 独立性:不同的进程之间是独立的,相互之间资源不共享(举例:两个正在上课的教室有各自的财产,相互之间不共享)
- 动态性:进程在系统中不是静止不动的,而是在系统中一直活动的
- 并发性:多个进程可以在单个处理器上同时进行,且互不影响
线程(Thread)
线程就是一条执行路径。是进程的组成部分,一个进程可以有多个线程,每个线程去处理一个特定的子任务。
线程的特点: 线程的执行是抢占式的,多个线程在同一个进程中可以并发执行,其实就是CPU快速的在不同的线程之间切换,也就是说,当前运行的线程在任何时候都有可能被挂起,以便另外一个线程可以运行。
进程和线程的关系以及区别
- 一个程序运行后至少有一个进程
- 一个进程可以包含多个线程,但是至少需要有一个线程,否则这个进程是没有意义的
- 进程间不能共享资源,但线程之间可以
- 系统创建进程需要为该进程重新分配系统资源,而创建线程则容易的多,因此使用线程实现多任务并发比多进程的效率高
多线程的实现
多线程创建方式有三种:
1.继承Thread类 2.实现Runnable接口 3.实现Callable接口
继承Thread类
- 继承自Thread类,Thread类是所有线程类的父类,实现了对线程的抽取和封装
- 继承Thread类创建并启动多线程的步骤:
- 定义一个类,继承自Thread类,并
重写该类的run方法
,该run方法的方法体就代表了线程需要完成的任务,因此,run方法的方法体被称为线程执行体- 创建Thread子类的对象,即创建了子线程
- 用
线程对象的start方法
来启动该线程- 注意:
- 程序运行时会自动创建一个线程 ,这个线程叫
主线程
;可以通过主线程创建子线程。- 启动线程使用
start()方法
,不要直接调用run()方法。
实现Runnable接口
- 实现
Runnable接口
创建并启动多线程的步骤:
- 定义一个Runnable接口的实现类,并
重写该接口中的run方法
,该run方法的方法体同样是该线程的线程执行体- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象
调用线程对象的start方法
来启动该线程
public class RunnableDemo implements Runnable{ private int ticket = 100; @Override public void run() { while (ticket>0){ System.out.println(Thread.currentThread().getName()+"卖了,第"+ticket+"张票"); ticket--; } } } public class RunnableTest { public static void main(String[] args) { //创建对象 RunnableDemo rd = new RunnableDemo(); //创建线程对象 Thread w1 = new Thread(rd); Thread w2 = new Thread(rd); Thread w3 = new Thread(rd); Thread w4 = new Thread(rd); //启动线程 w1.start(); w2.start(); w3.start(); w4.start(); } }
两种实现方式的比较
- 继承
Thread
类的方式
- 没有资源共享,编写简单
- 如果要访问当前线程,除了可以通过
Thread.currentThread()方式
之外,还可以使用getName()获取线程名字
。弊端
:因为线程类已经继承了Thread类,则不能再继承其他类【单继承】- 实现
Runnable
接口的方式
- 可以多个线程共享同一个资源,所以非常适合多个线程来处理同一份资源的情况
- 资源类实现了Runnable接口。如果资源类有多个操作,需要把功能提出来,单独实现Runnable接口。
弊端
:编程稍微复杂,不直观,如果要访问当前线程,必须使用Thread.currentThread()
- 总结:实际上大多数的多线程应用都可以采用实现Runnable接口的方式来实现
【推荐使用匿名内部类】
调用start()与run()方法的区别
- 当调用start()方法时将创建新的线程,并且执行run()方法里的代码,但是如果直接调用run()方法,不会创建新的线程。
线程的第三种创建方式:(了解)
使用Callable接口实现多线程
public class MyCallable implements Callable<Integer>{ @Override public Integer call() throws Exception { int sum=0; for(int i=1;i<=100;i++){ Thread.sleep(100); sum+=i; } System.out.println("子线程执行了........."+Thread.currentThread().getName()); //System.out.println("子线程执行了........."+Thread.currentThread().getName()); return sum; } } public static void main(String[] args) throws Exception{ //1创建Mycallable对象(可调用的) MyCallable callable=new MyCallable(); //2创建一个任务实现Runable接口 FutureTask<Integer> task=new FutureTask<Integer>(callable); //3创建线程对象 Thread thread=new Thread(task); //4启动 thread.start(); //5获取返回值 Integer sum=task.get();//会等待子线程执行完毕,返回结果 System.out.println(sum); }
线程的常用方法
设置获取线程的名称
- 设置 (1)使用构造方法 (2)setName(“xxx”);//设置线程名字
- 获取 (1)super.getName();//获取线程名字 (2)Thread.currentThread().getName();(推荐)静态获取线程名字
线程休眠
- 使得当前正在执行的线程休眠一段时间,释放时间片,导致线程进入阻塞状态
- Thread.sleep(5000),5000的单位是毫秒,设置了sleep就相当于将当前线程挂起5s,这个操作跟线程的优先级无关,当对应的时间到了之后,还会再继续执行
设置线程优先级
setPriority();
- 可以通过设置优先级来改变线程抢到时间片的概率,优先级高的线程获得较多的执行机会。
- 默认情况下,每个线程的优先级都与创建它的父线程具有相同的优先级
- 例如:main线程具有普通优先级,则由main线程创建的子线程也有相同的普通优先级
- 注意:
- 优先级范围1~10,默认为5,对应的数值越大,说明优先级越高,这个方法的设置一定要在start之前
- 线程的优先级低并不意味着争抢不到时间片,只是抢到时间片的概率比较低而已
合并(加入)线程
- 在执行原来线程的过程中,如果遇到了合并线程,则
优先执行合并进来的线程,执行完合并进来的线程后,再回到原来的任务中,继续执行原来的线程
。- 特点:
- 线程合并,当前线程一定会释放cpu时间片,cpu会将时间片分给要
Join
的线程- 哪个线程需要合并就在当前线程中,添加要合并的线程
- join之前,一定要将线程处于准备状态start
public static void main(String[] args) throws Exception { //1创建线程对象 Demo demo=new Demo(); //2启动 demo.start(); //加入线程(阻塞了主线程,等join执行完完毕才继续执行) demo.join(); //3for for(int i=0;i<30;i++) { System.out.println("主线程--------------"+i); Thread.sleep(20); } }
后台线程
- 线程分为
前台(用户)线程
和后台(守护)线程
。- 后台线程:隐藏起来一直在默默运行的线程,直到进程结束,又被称为守护线程,JVM的垃圾回收线程就是典型的后台线程。
- 特征:如果所有的
前台线程都死亡,后台线程会自动死亡
。- 前台线程:
默认的线程
都是前台线程,如果前台不执行完毕,程序不会退出。
public static void main(String[] args) throws Exception{ //1创建线程对象 Demo demo=new Demo(); //设置线程为后台线程 demo.setDaemon(true); //2启动线程 demo.start(); for(int i=0;i<20;i++) { System.out.println("主线程:"+i); Thread.sleep(20); } }
线程让步
- 可以让当前正在执行的线程暂停,但它不会阻塞该线程,他只是将该线程转入就绪状态,完全可能出现的情况是:当某个线程调用了
yield方法
暂停之后,线程调度器又将其调度出来重新执行。
- 实际上,当某个线程调用了
yield方法
暂停之后,只有优先级与当前线程相同
,或者优先级比当前线程更高
的就绪状态的线程才会获得执行的机会。
public class YieldFunctionDemo01 { public static void main(String[] args) { YieldThread1 t0 = new YieldThread1("线程000"); //t0.setPriority(8); t0.start(); YieldThread1 t1 = new YieldThread1("线程111"); t1.start(); } }
线程中断 interrupt()
- 程序在等待过程中,可以使用interrupt方法打断。
public class InterruptDemo extends Thread{ @Override public void run() { System.out.println("子线程开始执行了............."); try { System.out.println("子线程开始休眠 30秒......."); Thread.sleep(30000); System.out.println("子线程正常苏醒了..."); } catch (InterruptedException e) { System.out.println("子线程被打醒了....."); } System.out.println("子线程结束了......."); } } public static void main(String[] args) throws Exception{ System.out.println("主线程执行了....."); InterruptDemo interruptDemo=new InterruptDemo(); interruptDemo.start(); System.out.println("30秒内按任意键结束子线程"); System.in.read();//程序会让用输入一个数据 interruptDemo.interrupt();//打断线程 System.out.println("主线程结束了............."); }
线程的生命周期
- 对于线程,当线程被创建,进入新生状态,调用start()方法 ,进入就绪状态(可执行状态),如果抢到cpu,进入运行状态,运行过程中出现了阻塞,进入阻塞状态,如果程序正常结束进入死亡状态。
New(新生):
线程被实例化,但是还没有开始执行Runnable(就绪):
没有抢到时间片Running(运行):
抢到了时间片,CPU开始处理这个线程中的任务Blocked(阻塞):
线程在执行过程中遇到特殊情况,使得其他线程就可以获得执行的机会,被阻塞的线程会等待合适的时机重新进入就绪状态Dead(死亡):
线程终止- a.run方法执行完成,线程正常结束【正常的死亡】
- b.直接调用该线程的stop方法强制终止这个线程
State枚举表示线程的状态(六种)
- NEW (新生)
- RUNNABLE (可运行、运行)
- BLOCKED (锁阻塞)
- WAITING (等待)
- TIMED_WAITING (限时等待)sleep(5000)
- TERMINATED (终止死亡)
线程五状态
线程七状态