文章目录
线程介绍
线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。
每个正在系统上运行的程序都是一个进程。每个进程包含一到多个线程。线程是一组指令的集合,或者是程序的特殊段,它可以在程序里独立执行。也可以把它理解为代码运行的上下文。所以线程基本上是轻量级的进程,它负责在单个程序里执行多任务。通常由操作系统负责多个线程的调度和执行。
一个进程可以有很多线程,每条线程并行执行不同的任务。
总结:进程是所有线程的集合,每一个线程是进程中的一条执行路径。
线程的意义
为了解决负载均衡问题,充分利用CPU资源.为了提高CPU的使用率,采用多线程的方式去同时完成几件事情而不互相干扰.为了处理大量的IO操作时或处理的情况需要花费大量的时间等等,比如:读写文件,视频图像的采集,处理,显示,保存等
线程相比进程的优点:
- 线程在程序中是独立的,并发的执行流,但是,与分隔的进程相比,进程中的线程之间的隔离程度要小。它们共享内存,文件句柄和其他每个进程应有的状态。
- 线程比进程具有更高的性能,这是由于同一个进程中的线程都有共性:多个线程将共享同一个进程虚拟空间。线程共享的环境包括:进程代码段,进程的公有数据等。利用这些共享的数据等,线程很容易实现相互之间的通信。
- 当操作系统创建一个进程时,必须为进程分配独立的内存空间,并分配大量相关资源:但创建一个线程则简单很多,因此使用多线程来实现并发比使用多进程实现并发
使用多线程的影响:
- cpu在多个线程之间进行切换会牺牲一部分性能.
- 创建线程需要占用一定的内存空间
- 线程之间共享数据会产生线程安全问题
- 线程竞争资源会导致死锁
线程创建方式
Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。
Java可以以下四种方式来创建线程:
- 继承Thread类创建线程
- 实现Runnable接口创建线程
- 使用Callable和Future创建线程
- 使用线程池Executor框架
1、继承Thread类创建线程
通过继承Thread类来创建并启动多线程的一般步骤如下
- 定义Thread类的子类,并重写该类的run()方法,该方法的方法体就是线程需要完成的任务,run()方法也称为线程执行体。
- 创建Thread子类的实例,也就是创建了线程对象
public class ThreadExt extends Thread{
@Override
public void run() {
for(int i = 0;i<10;i++){
System.out.println(Thread.currentThread().getName() + " : " + i);
}
}
public static void main(String[] args) {
Thread thread = new ThreadExt();
thread.start();
}
}
2、实现Runnable接口创建线程
通常推荐使用实现Runnable接口创建线程的方式,原因是一个类实现了接口还可以继续继承,继承了Thread类则不能再继承其它类了。
通过实现Runnable接口创建并启动线程一般步骤如下:
- 定义Runnable接口的实现类,一样要重写run()方法,这个run()方法和Thread中的run()方法一样是线程的执行体
- 创建Runnable实现类的实例,并用这个实例作为Thread的target来创建Thread对象,这个Thread对象才是真正的线程对象
public class RunnableImpl implements Runnable{
@Override
public void run() {
for(int i = 0;i<10;i++){
System.out.println(Thread.currentThread().getName() + " : " + i);
}
}
public static void main(String[] args) {
//将实现Runnable接口的类传入Thread构造器创建,推荐使用此方式
Thread thread = new Thread(new RunnableImpl());
//当然你也可以使用匿名内部类的形式实现Runnable接口,一般不推荐这么做
thread = new Thread(()->{
for(int i = 0;i<10;i++){
System.out.println(Thread.currentThread().getName() + " : " + i);
}
});
//
thread.start();
}
}
3、 使用Callable和Future创建线程
和Runnable接口不一样,Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法功能要强大。
Java5提供了Future接口来代表Callable接口里call()方法的返回值,并且为Future接口提供了一个实现类FutureTask,这个实现类既实现了Future接口,还实现了Runnable接口,因此可以作为Thread类的target。在Future接口里定义了几个公共方法来控制它关联的Callable任务。
- 创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)。
- 使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值
- 使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)
- 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
public class CallableImpl implements Callable {
// 与run()方法不同的是,call()方法具有返回值
@Override
public Integer call() throws InterruptedException {
int sum = 0;
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " : " + i);
sum += i;
}
Thread.sleep(3000); //计算完成sum后,我们让线程休眠3秒
return sum;
}
public static void main(String[] args) {
Callable<Integer> callableImpl = new CallableImpl(); // 创建CallableImpl对象
FutureTask<Integer> ft = new FutureTask<>(callableImpl); //使用FutureTask来包装CallableImpl对象
Thread thread = new Thread(ft); //FutureTask对象作为Thread对象的target创建新的线程
thread.start(); //线程进入到就绪状态
System.out.println("主线程for循环执行完毕..");
try {
int sum = ft.get(); //取得新创建的新线程中的call()方法返回的结果
System.out.println("sum = " + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
4、 使用线程池Executor框架
Executor接口中之定义了一个方法execute(Runnable command),该方法接收一个Runable实例,它用来执行一个任务,任务即一个实现了Runnable接口的类。
关于线程池框架的详细介绍请参考这篇博文:Java线程池ThreadPoolExecutor的使用及其原理
下面是一个自定义线程池的示例:
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(// 自定义一个线程池
1, // 核心线程数
2, // 最大线程数
60, // 超过核心线程数的额外线程存活时间
TimeUnit.SECONDS, // 线程存活时间的时间单位
new ArrayBlockingQueue<>(3) // 有界队列,容量是3个
, Executors.defaultThreadFactory() // 线程工厂
, new ThreadPoolExecutor.AbortPolicy() //线程的拒绝策略
);
//执行一个任务
threadPool.execute(()->{
//线程执行的具体逻辑
//Runnable to do something.
System.out.println("hello world");
});
threadPool.shutdown();
threadPool.shutdownNow();
}
启动线程
- 使用
start()
方法开始执行线程 - 注意开启线程不是调用
run()
方法,直接使用run()
方法相当于在当前线程下执行了
其中start()
方法原码如下
/**
*使该线程开始执行;Java虚拟机调用此线程的run方法。
*结果是两个线程同时运行:
*当前线程(执行start方法)和另一个线程(执行其run方法)。
*多次启动线程是不合法的。
*特别是,线程一旦完成就不能重新启动执行。
**/
public synchronized void start() {
/**
*0状态值对应于状态“NEW”。
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/*通知组此线程即将启动
*以便将其添加到组的线程列表中
*组的未开始计数可以减少*/
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
守护线程
Java中有两种线程,一种是用户线程,另一种是守护线程。
- 用户线程是指用户自定义创建的线程,主线程停止,用户线程不会停止守护线程
- 当进程不存在或主线程停止,守护线程也会被停止。
使用setDaemon(true)方法设置为守护线程
多线程运行状态
线程从创建、运行到结束总是处于下面五个状态之一:新建状态、就绪状态、运行状态、阻塞状态及死亡状态。
新建状态
当用new操作符创建一个线程时, 例如new Thread(),线程还没有开始运行,此时线程处在新建状态。
线程中的代码此时并未被执行。
就绪状态
一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()方法返回后,线程就处于就绪状态。
处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程。因为在单CPU的计算机系统中,不可能同时运行多个线程,一个时刻仅有一个线程处于运行状态。因此此时可能有多个线程处于就绪状态。对多个处于就绪状态的线程是由Java运行时系统的线程调度程序(thread scheduler)来调度的。
运行状态
当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法.
阻塞状态
阻塞状态是指线程因为某些原因放弃CPU,暂时停止运行。当线程处于阻塞状态时,Java虚拟机不会给线程分配CPU。直到线程重新进入就绪状态,它才有机会转到运行状态。
阻塞状态可分为以下3种:
- 位于对象等待池中的阻塞状态(Blocked in object’s wait pool):当线程处于运行状态时,如果执行了某个对象的wait()方法,Java虚拟机就会把线程放到这个对象的等待池中,这涉及到“线程通信”的内容。
- 位于对象锁池中的阻塞状态(Blocked in object’s lock pool):当线程处于运行状态时,试图获得某个对象的同步锁时,如果该对象的同步锁已经被其他线程占用,Java虚拟机就会把这个线程放到这个对象的锁池中,这涉及到“线程同步”的内容。
- 其他阻塞状态(Otherwise Blocked):当前线程执行了sleep()方法,或者调用了其他线程的join()方法,或者发出了I/O请求时,就会进入这个状态。
死亡状态
有两个原因会导致线程死亡:
- run方法正常退出而自然死亡,
- 一个未捕获的异常终止了run方法而使线程猝死。
为了确定线程在当前是否存活着(就是要么是可运行的,要么是被阻塞了),需要使用isAlive方法。如果是可运行或被阻塞,这个方法返回true;
如果线程仍旧是new状态且不是可运行的, 或者线程死亡了,则返回false.
线程的常用方法
常用线程api方法 | |
---|---|
start() | 启动线程 |
currentThread() | 获取当前线程对象 |
getID() | 获取当前线程ID Thread-编号 该编号从0开始 |
getName() | 获取当前线程名称 |
sleep(long mill) | 休眠线程,单位毫秒 |
stop() | 停止线程, |
常用线程构造函数 | |
Thread() | 分配一个新的 Thread对象。自动生成的名称的格式为“Thread-”+n,其中n是整数。 |
Thread(String name) | 分配一个新的 Thread对象,指定线程名称name。 |
Thread(Runable r) | 分配一个新的 Thread对象 |
Thread(Runable r, String name) | 分配一个新的 Thread对象 指定线程名称name |
join()方法作用
当在当前线程中执行到t1.join()方法时,就认为当前线程会把把执行权让给t1
public static void main(String[] args) {
Thread t1 = new Thread(() ->{ System.out.println(Thread.currentThread().getName()); },"t1");
Thread t2 = new Thread(() -> {
try {
t1.join();
} catch (Exception e) {
// TODO: handle exception
}
System.out.println(Thread.currentThread().getName());
},"t2");
Thread t3 = new Thread(() -> {
try {
t2.join();
} catch (Exception e) {
// TODO: handle exception
}
System.out.println(Thread.currentThread().getName());
},"t3");
t3.start();
t2.start();
t1.start();
}
输出:
t1
t2
t3
Yield方法
Thread.yield()方法的作用:暂停当前正在执行的线程,并执行其他线程。(可能没有效果)
yield()让当前正在运行的线程回到可运行状态,以允许具有相同优先级的其他线程获得运行的机会。因此,使用yield()的目的是让具有相同优先级的线程之间能够适当的轮换执行。但是,实际中无法保证yield()达到让步的目的,因为,让步的线程可能被线程调度程序再次选中。
结论:大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。
线程调度
优先级
现代操作系统基本采用时分的形式调度运行的线程,线程分配得到的时间片的多少决定了线程使用处理器资源的多少,也对应了线程优先级这个概念。在JAVA线程中,通过一个int
priority来控制优先级,范围为1-10,其中10最高,默认值为5。下面是源码(基于1.8)中关于priority的一些量和方法。
优先级高的线程并不一定先执行
- java线程是通过映射到系统的原生线程上来实现的,所以线程的调度最终还是取决于操作系统,操作系统的优先级与java的优先级并不一一对应,如果操作系统的优先级级数大于java的优先级级数(10级)还好,但是如果小于得的话就不行了,这样会导致不同优先级的线程的优先级是一样的。
- 优先级可能会被系统自动改变,比如windows系统中就存在一个线程调度器,大致功能就是如果一个线程执行的次数过多的话,可能会越过优先级为他分配执行时间
线程调度器
线程调度器是一个操作系统服务,它负责为 Runnable 状态的线程分配 CPU 时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。同上一个问题,线程调度并不受到 Java 虚拟机控制,所以由应用程序来控制它是 更好的选择(也就是说不要让你的程序依赖于线程的优先级)。时间分片是指将可用的 CPU 时间分配给可用的 Runnable 线程的过程。分配 CPU 时间可以基于线程优先级或者线程等待的时间。
线程调度算法
计算机通常只有一个CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU的使用权才能执行指令.所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得CPU的使用权,分别执行各自的任务.在运行池中,会有多个处于就绪状态的线程在等待CPU,JAVA虚拟机的一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配CPU的使用权.
有两种调度模型:分时调度模型和抢占式调度模型。
分时调度模型是指让所有的线程轮流获得cpu的使用权,并且平均分配每个线程占用的CPU的时间片这个也比较好理解。
java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃CPU。
多线程中的上下文切换
在上下文切换过程中,CPU会停止处理当前运行的程序,并保存当前程序运行的具体位置以便之后继续运行。从这个角度来看,上下文切换有点像我们同时阅读几本书,在来回切换书本的同时我们需要记住每本书当前读到的页码。在程序中,上下文切换过程中的“页码”信息是保存在进程控制块(PCB)中的。PCB还经常被称作“切换桢”(switchframe)。“页码”信息会一直保存到CPU的内存中,直到他们被再次使用。
上下文切换是存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。上下文切换是多任务操作系统和多线程环境的基本特征。
线程间的通信
请参考:Java多线程通讯
面试题
- 进程与线程的区别?
- 为什么要用多线程?
- 多线程创建方式?
- 是继承Thread类创建线程好还是实现Runnable接口线程好?为什么?
- 多线程的应用场景?
- 现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行