线程详解
Posted 我又null了
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线程详解相关的知识,希望对你有一定的参考价值。
线程概述
运行一个音乐播放器播放一首歌,音乐播放器就是一个进程,在程序执行时,既有声音的输出,同时还有该歌曲的字幕展示,这就是进程中的两个线程
线程与进程
程序进入内存就变成了进程,进程就是处于运行中的程序
进程特征:
- 独立性:每个进程都有自己的私有地址,一个进程不能直接访问其他进程
- 动态性:进程有自己的生命周期和不同状态,而程序不具备
- 并发性:多个进程可以在单个处理器上并发执行,进程之间互不影响
并发: 进程在cpu中切换执行 并行:进程在cpu上一起执行
对于一个CPU而言,它在某个时间点只能执行一个程序,也就是说,只能运行一个进程,
CPU不断地在这些进程之间轮换执行,虽然CPU在多个进程间轮换执行,但是我们感觉到好像有多个进程在同时进行
线程是进程的执行单元,对于绝大多数的应用程序来说,通常仅要求有一个主线程,
但也可以在该进程内创建多条顺序执行流,这些顺序执行流就是线程(子线程),
每个线程也是相互独立的
线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,
但不拥有系统资源,它与父进程的其他线程共享该进程所有拥有的全部资源
一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行。
多线程的优势
- 进程中的线程之间的隔离程度要小。它们共享内存、文件句柄和其他的每个线程的状态
- 进程在执行过程中拥有独立的内存单元,而多个线程共享内存,提高运行效率
- 线程共享的环境包括:进程代码段、进程的公有数据
- 进程之间不能共享内存,但线程之间共享内存非常容易
- 系统创建进程是需要为该进程重新分配系统资源,但创建线程则代价小得多
线程的创建与启动
Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。
每个线程的作用是完成一定的任务,实际上就是执行一段程序代码。
Java使用线程执行体来代表这段程序代码。
Ø 继承Thread创建线程
-
定义Thread类的子类,并重写该类的run()方法,run()方法的方法体代表线程需要完成的任务。因此把run方法称为线程执行体。
- 创建Thread子类的实例,即创建了线程对象
- 调用线程对象的start()方法来启动该线程。
public class Test2 extends Thread{ // 重写run方法,run方法的方法体就是子线程的执行体 @Override public void run() { for (int i = 0; i < 100; i++) { // 继承Thread类后,从父类继承的getName方法可以获取当前线程的名称 System.out.println("线程名称:"+this.getName()+" "+i); } } public static void main(String[] args) { for (int i = 0; i < 100; i++) { // 调用Thread的currentThread()方法获取当前线程对象 // 这里就不能用this来获取name了 System.out.println("线程名称;"+Thread.currentThread().getName()+"="+i); //创建两个子线程,并运行 if (i == 20){ new Test2().start(); new Test2().start(); } } } }
线程是以抢占式的方式运行的,虽然只创建了两个线程实例,实际上有三个线程在运行
(两个子线程,一个主线程main)
通过setName(String name)的方式来为线程设置名称,也可以通过getName的方式来得到线程的名称。
在默认情况下,主线程的名称为main,用户启动的多线程的名称依次为Thread-0,Thread-1,Thread-3..Thread-n
实现Runnable接口创建线程
- 定义Runnable接口的实现类,并重写该接口的run方法
-
创建Runnable实现类的实例对象,并以此实例对象作为Thread的target来创建Thread类,该Thread对象才是真正的线程对象。
- 调用线程对象的start()方法来启动该线程
public class Test3 implements Runnable{ @Override public void run() { for(int i = 0;i < 100;i++) { // 实现了Runnable接口的类其本质并不是线程类,因此没有getName方法, // 因此需要通过Thread类来获取当前线程,仅仅是一个任务体,仍需交给Thread去执行 System.out.println("线程名称:"+Thread.currentThread().getName()+" "+i); } } public static void main(String[] args) { /*new一个实现了Runnable接口的实例,这个实例不是线程对象 * 不能test3.start()来运行子线程,执行run方法体 * 实际的线程对象要通过new Thread()来获取,只不过对于实现了Runnable接口的实现类的实例 * 作为参数传入到new Thread(test3).start()来执行子线程 * 意义是让线程对象来执行test3实例的run方法体 * */ Test3 test3 = new Test3(); new Thread(test3).start(); new Thread(test3).start(); } }
又因为Runnable是一个函数式接口,所以可以使用lamda表达式来进行代码编写
public class Test4 { public static void main(String[] args) { /* * 用lamda表达式的写法,{}里面写的就是run方法体 * 将runnable传入到 new Thread(runnable,"子线程1")里面,就表示了创建了子线程 * 并执行run方法体,第二个参数是为子线程起名字 * */ Runnable runnable = ()->{ for (int i = 0; i < 100; i++) { System.out.println("线程名字:"+Thread.currentThread().getName()+"="+i); } }; Test4 test4 = new Test4(); new Thread(runnable,"子线程1").start(); new Thread(runnable,"子线程2").start(); } }
通过对比上面两种创建线程的方式,继承Thread 和 实现Runnable接口,第一种主线程和子线程
分别执行一遍任务。第二种主线程和子线程共同完成一个任务。
Ø 使用Callable&Future创建线程
在Java 1.5开始,Java提供了Callable接口,该接口实际上可看成是Runnable接口的增强版,Callable接口提供了一个call()的方法可以作为线程的执行体,但call()方法比run()方法功能更加强大。
这是因为:
1. call()方法可以有返回值
2. call()方法可以声明抛出异常
因此我们可以提供一个Callable对象作为Thread的target,而该线程的线程执行体就是该Callable对象的call()方法。但是存在以下两个问题:
1. Callable接口是Java 5提供的一个新的接口而不是Runnable接口的子接口,所以Callable对象不能直接作为Thread类的target目标执行类。
2. call()方法还有一个返回值——call()方法并不是直接调用,它是作为线程执行体被调用的。如何获取call()方法的返回值?
为了解决以上两个问题,Java 1.5提供了Future接口来代表Callable接口里call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该实现类实现了Future接口,并实现了Runable接口——可以作为Thread类的target。
创建并启动有返回值的线程的步骤如下:
- 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且该call()方法有返回值
-
创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值。
- 使用FutureTask对象作为Thread对象的target创建并启动新线程
- 调用FutureTask对象的get()方法来获得子线程结束后的返回值
import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; /** * 实现Callable接口时指定的泛型为返回值的类型 */ public class Test5 implements Callable<Integer> { /* * 对于实现了Callable接口的类,重写的call方法就是子线程要执行的方法体 * 这个call方法与run方法的区别就是,call有返回值,可以声明抛出异常 * * 实现的Callable接口可以看做是Runnable接口的增强版,所以可以提供一个Callable对象作为target传给线程对象 * 但是问题就是,Callable接口不是Runnable接口的子接口,不能直接作为target * call方法有返回值 * * */ @Override public Integer call() throws Exception { for (int i = 0; i < 100; i++) { /*与实现了Runnable接口的run方法相似,也是不能使用this来获取name*/ System.out.println("当前线程名称:"+Thread.currentThread().getName()+" "+i); Thread.sleep(200); } return 100; } public static void main(String[] args) throws ExecutionException, InterruptedException { /*创建Callable对象,因为当前类实现了Callable接口 多态*/ Callable<Integer> callable = new Test5(); // 创建FutureTask对象,并将call对象封装在FutureTask内部,FutureTask的泛型为Callable FutureTask<Integer> futureTask = new FutureTask<>(callable); /*创建线程对象*/ new Thread(futureTask).start(); // 获取线程结束后的返回值 System.out.println("线程执行结束后的返回值:"+futureTask.get()); } }
Callable接口是一个函数式接口,所以可以用lamda表达式写法
import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; public class Test6 { public static void main(String[] args) throws Exception { /* * Callable接口也是一个函数式接口,所以可以用lamda表达式写法 * {} 里面写的就是call的方法体 * */ Callable<Integer> callable = ()->{ for (int i = 0; i < 100; i++) { System.out.println("线程名称:"+Thread.currentThread().getName()+"="+i); } return 100; }; // 创建FutureTask对象,并将call对象封装在FutureTask内部,FutureTask的泛型为Callable FutureTask<Integer> futureTask = new FutureTask<>(callable); /*创建线程对象 并将FutureTask封装好的Callable对象传入线程对象中*/ new Thread(futureTask).start(); // 获取线程结束后的返回值 System.out.println("线程执行结束后的返回值:"+futureTask.get()); } }
Ø 创建线程的三种方式比较
通过继承Thread类或实现Runnable、Callable接口都可以实现多线程,不过实现Runnable接口与实现Callable接口的方式基本相同,只是Callable接口里定义的方法有返回值,可以声明抛出异常,并且Callable需要FutureTask来进行封装成Thread可识别的target目标。因此可以将实现Runnable接口和实现Callable接口归纳为一种方式。这种方式与继承Thread方式之间的主要差别如下
线程的声明周期
当线程被创建并启动后,并不是一启动就进入了执行状态,也不是一直处于执行状态,
在线程的生命周期中,它要经历新建、就绪、运行、阻塞和死亡5种状态。
尤其是当线程启动以后,它不可能一直占用CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会在运行、阻塞之间切换。
新建状态:当new了一个线程之后,该线程就处于新建状态,此时它和其他的Java对象一样仅仅由Java虚拟机为其分配内存,并初始化其他成员变量的值
就绪状态:当线程对象调用了start方法之后,该线程就处于就绪状态,Java虚拟机会为这个线程对象创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于什么时候开始运行,则取决于JVM里的线程调度器的调度。
运行状态:处于就绪状态的线程获得了CPU,开始执行run方法的线程执行体,则该线程就处于运行状态
阻塞状态:
- 线程调用sleep()方法主动放弃所占用的处理器资源
- 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
- 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有
-
线程在等待某个通知(notify)
- 程序调用了线程的suspend()方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法
死亡状态:
- run()或call()方法执行完成,线程正常结束
- 线程抛出一个未捕获的Exception或者直接Error错误
- 直接调用该线程的stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用
当主线程结束时,其他线程不受任何影响,并不会随之结束。一旦子线程启动之后,它就拥有和主线程相同的地位
不要试图对一个已经死亡的线程调用start()方法使它重新启动,死亡就是死亡,该线程不可以再次作为线程执行。
控制线程
以上是关于线程详解的主要内容,如果未能解决你的问题,请参考以下文章