多线程编程
文章目录
前言
几乎所有的程序员都知道,现代操作系统进行资源分配的最小单元是进程,而操作系统进行运算调度的最小单元是线程,其实,在linux中线程也可以看作是一种轻量级的进程,那么线程是包含于进程之中的,是进程中实际的运作单位;同一进程中的多个线程共用同一块内存空间,而不同的线程又拥有独立的栈内存用以存放线程本地数据;
大家都知道,现在的计算机动辄就是多处理器核心的,而每一个线程同一时间只能运行在一个处理器上,那么如果程序采用单线程进行开发,那么就不能充分利用多核处理器带来的优势;所以为了充分利用多核处理器的资源来提高程序的执行性能,多线程编程变得越来越重要,比如对于计算密集型任务,使用一个线程可能需要100秒,但是,如果使用十个线程共同完成,那么需要的时间可能只有10秒左右;如果你是使用java开发程序的,那么你很幸运,因为Java是内置多线程编程模型的;但是,想要使用好多线程这把利刃,还需要掌握好多线程编程的基础知识,从而做到得心应手地使用多线程进行高性能程序的开发!
多线程的应用场景
- 程序中出现需要等待的操作,比如网络操作、文件IO等,可以利用多线程充分使用处理器资源,而不会阻塞程序中其他任务的执行
- 程序中出现可分解的大任务,比如耗时较长的计算任务,可以利用多线程来共同完成任务,缩短运算时间
- 程序中出现需要后台运行的任务,比如一些监测任务、定时任务,可以利用多线程来完成
自定义线程的实现
处于实用的角度出发,想要使用多线程,那么第一步就是需要知道如何实现自定义线程,因为实际开发中,需要线程完成的任务是不同的,所以我们需要根据线程任务来自定义线程,JDK为我们的开发人员提供了三种自定义线程的方式,供实际开发中使用,来开发出符合需求的多线程程序!
以下是线程的三种实现方式,以及对每种实现的优缺点进行分析,最后是对这三种实现方式进行总结;
方式一:继承Thread类
package com.thread;
//通过继承Thread类实现自定义线程类
public class MyThread extends Thread {
//线程体
@Override
public void run() {
System.out.println("Hello, I am the defined thread created by extends Thread");
}
public static void main(String[] args){
//实例化自定义线程类实例
Thread thread = new MyThread();
//调用start()实例方法启动线程
thread.start();
}
}
优点:实现简单,只需实例化继承类的实例,即可使用线程
缺点:扩展性不足,Java是单继承的语言,如果一个类已经继承了其他类,就无法通过这种方式实现自定义线程
方式二:实现Runnable接口
package com.thread;
public class MyRunnable implements Runnable {
//线程体
@Override
public void run() {
System.out.println("Hello, I am the defined thread created by implements Runnable");
}
public static void main(String[] args){
//线程的执行目标对象
MyRunnable myRunnable = new MyRunnable();
//实际的线程对象
Thread thread = new Thread(myRunnable);
//启动线程
thread.start();
}
}
优点:
- 扩展性好,可以在此基础上继承其他类,实现其他必需的功能
- 对于多线程共享资源的场景,具有天然的支持,适用于多线程处理一份资源的场景
缺点:构造线程实例的过程相对繁琐一点
方式三:实现Callable接口
package com.thread;
import java.util.concurrent.Callable;
import java.util.concurrent.executionException;
import java.util.concurrent.FutureTask;
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "Hello, I am the defined thread created by implements Callable";
}
public static void main(String[] args){
//线程执行目标
MyCallable myCallable = new MyCallable();
//包装线程执行目标,因为Thread的构造函数只能接受Runnable接口的实现类,而FutureTask类实现了Runnable接口
FutureTask<String> futureTask = new FutureTask<>(myCallable);
//传入线程执行目标,实例化线程对象
Thread thread = new Thread(futureTask);
//启动线程
thread.start();
String result = null;
try {
//获取线程执行结果
result = futureTask.get();
} catch (InterruptedException e) {
e.printstacktrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println(result);
}
}
优点:
- 扩展性好
- 支持多线程处理同一份资源
- 具备返回值以及可以抛出受检查异常
缺点:
- 相较于实现Runnable接口的方式,较为繁琐
小结
我们对这三种方式进行分析,可以发现:方式一和方式二本质上都是通过实现Runnable接口并重写run()方法,将接口实现类的实例传递给Thread线程类来执行线程体(run()方法中的实现),这里将Runnable接口实现类的实例作为线程执行目标,供线程Thread实例执行;对于方式三,其实也是这样的,由于Thread类只能执行Runnable接口实现类的执行目标,所以需要对Callable接口的实现类进行包装,包装成Runnable接口的实现类(通过实现了Runnable接口的FutureTask类进行包装),从而使得Thread类能够接收Callable接口实现类的实例,可见这里使用了适配器模式!
综上所述,三种实现方式都存在着一个使用范式,即首先实现线程执行目标对象(包含线程所要执行的任务),然后将目标对象作为构造参数以实例化Thread实例,来获得线程!本质上都是实现一个线程体,由Thread来执行线程体,达到开启线程执行任务的效果!但是,三种实现方式各有优缺点,使用时,应该结合具体需求来选用合适的实现方式进行开发!
线程的生命周期
经过上面的代码演示,我们知道了线程如何实现,但是如果我们想要更好地使用线程,还需要对程序运行中线程的状态以及状态之间的转换(即线程的生命周期)有所了解,这样才能在多线程程序运行出现问题时,分析问题产生的原因,从而快速准确地定位并解决问题!
首先,看一下Thread类中给出的关于线程状态的说明:
/**
* 线程生命周期中的的六种状态
* NEW:还没有调用start()的线程实例所处的状态
* RUNNABLE:正在虚拟机中执行的线程所处的状态
* BLOCKED:等待在监视器锁上的线程所处的状态
* WAITING:等待其它线程执行特定操作的线程所处的状态
* TIMED_WAITING:等待其它线程执行超时操作的线程所处的状态
* TERMINATED:退出的线程所处的状态
* 给定时间点,一个线程只会处于以下状态中的一个,这些状态仅仅是虚拟机层面的线程状态,并不能反映任何操作系统中线程的状态
*/
public enum State {
//还没有调用start()开启的线程实例所处的状态
NEW,
//正在虚拟机中执行或者等待被执行的线程所处的状态,但是这种状态也包含线程正在等待处理器资源这种情况
RUNNABLE,
// 等待在监视器锁上的线程所处的状态,比如进入synchronized同步代码块或同步方法失败
BLOCKED,
// 等待其它线程执行特定操作的线程所处的状态;比如线程执行了以下方法: Object.wait with no timeout、thread.join with no timeout、 LockSupport.park
WAITING,
// 等待其它线程执行超时操作的线程所处的状态;比如线程执行了以下方法: Thread.sleep、Object.wait with timeout
//Thread.join with timeout、LockSupport.parkNanos、LockSupport.parkUntil
TIMED_WAITING,
//退出的线程所处的状态
TERMINATED;
}
- 新建(New):当线程实例被new出来之后,调用start()方法之前,线程实例处于新建状态
- 可运行(Runnable):当线程实例调用start()方法之后,线程调度器分配处理器资源之前,线程实例处于可运行状态或者线程调度器分配处理器资源给线程之后,线程实例处于运行中状态,这两种情况都属于可运行状态
- 等待(Waitting):当线程处于运行状态时,线程执行了obj.wait()或Thread.join()方法、Thread.join、LockSupport.park以及Thread.sleep()时,线程处于等待状态
- 超时等待(Timed Waitting):当线程处于运行状态时,线程执行了obj.wait(long)、Thread.join(long)、LockSupport.parkNanos、LockSupport.parkUntil以及Thread.sleep(long)方法时,线程处于超时等待状态
- 阻塞(Blocked):当线程处于运行状态时,获取锁失败,线程实例进入等待队列,同时状态变为阻塞
- 终止(Terminated):当线程执行完毕或出现异常提前结束时,线程进入终止状态
线程的状态转换
上面也提到了,某一时间点线程的状态只能是上述6个状态中的其中一个;但是,线程在程序运行过程中的状态是会发生变化的,由一个状态转变为另一个状态,那么下面给出线程状态转换图帮助我们清晰地理解线程的状态转变过程:
上面我们已经对线程的实现以及线程的状态有了较为清晰的认识,那么通过上述内容,我们也可以发现其实有很多方法,我们并没有详细地介绍,比如start()、yield()、wait()、notify()、notifyAll()、sleep()、join()等等,这些方法大多来源于JDK中Thread类这一关键的线程类中,下面结合Thread类的源码看一下,多线程编程中经常遇到的方法有哪些,以及这些方法的用途;
线程类Thread源码
实例同步方法:join()
/**
* 等待调用此方法的线程执行结束
* @throws InterruptedException 如果任何线程中断了当前线程,将会抛出此异常,同时将中断标志位清除
*/
public final void join() throws InterruptedException {
join(0);
}
/**
* 最多等待millis毫秒,时间一到无论是否执行完毕,都会返回
* 如果millis为0,那么意味着一直等到线程执行完毕才会返回
* 此方法的实现是基于循环检测当前线程是否存活来判断是否调用当前实例的wait方法来实现的
* @param millis 等待时间
* @throws illegalargumentException 非法参数异常
* @throws InterruptedException 如果任何线程中断了当前线程,将会抛出此异常,同时将中断标志位清除
*/
public final synchronized void join(long millis) throws InterruptedException {
long base = system.currenttimemillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
/**
* 线程执行结束之前最多等待millis毫秒nanos纳秒
* 此方法基于循环判断isAlive返回值来决定是否调用wait方法来实现
* 随着一个线程终止,将会调用notifyAll方法
* 所以建议不要在当前实例上调用 wait、 notify、 notifyAll
*/
public final synchronized void join(long millis, int nanos)
throws InterruptedException {
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
millis++;
}
join(millis);
}
中断方法以及检测中断方法和判活方法:
/**
* 中断当前线程
* 如果当前线程阻塞在Object的wait()、wait(long)、wait(long, int),或者
* join()、join(long)、join(long, int)以及sleep(long)、sleep(long, int)等方法
* 那么将会清除中断标志位并受到一个中断异常
* 非静态方法
*/
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}
/**
* 检测当前线程是否已经被中断,此方法会清除当前线程的中断标志
* 也就是说,如果这个方法被连续调用两次,并且第一次调用之前,线程被中断过,那么第一次调用返回true,第二次返回false
* @return <code>true</code> 如果当前线程已经被中断,返回true,否则返回false
* 静态方法
*/
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
/**
* 检测当前线程是否已经被中断,此方法不会清除当前线程的中断标志
* 非静态方法
*/
public boolean isInterrupted() {
return isInterrupted(false);
}
/**
* 根据参数值决定是否在判断中断标志位之后清除标志位
* 实例方法
*/
private native boolean isInterrupted(boolean ClearInterrupted);
/**
* 检测一个线程是否还存活,存活指的是已经启动但还没有终止
* 实例方法
*/
public final native boolean isAlive();
结尾
到此为止,本文已经对线程的使用场景、实现方式以及生命周期、状态转换过程以及线程类常用的方法进行了介绍,但是上面只是从概念上对线程的相关知识进行叙述,但是实际开发中,我们使用多线程是为了解决实际问题,比如如何实现多个线程共同完成一个耗时长的任务或者如何实现多个线程交互完成一个大型任务,在这些实际应用线程的过程中,会遇到问题,下面将在多线程与高并发编程基础知识(下)一文中给出多线程开发中,面临的问题以及对这些问题进行分析,并介绍常用的解决方案;最后,希望读者在阅读之后,如果发现文中出现不正确的地方还请指出,大家一同探讨和学习,共同成长^^!
相关阅读
很多人在遇到事业瓶颈,或者从事一份自己不喜欢工作,又或者对未来迷茫的时候,总能看到很多人说,转码农,当程序员。诚然,转行当程序员不仅
我刚开始使用PLC时,也是一头雾水。仗着自己对硬件、工程知识的熟悉,开始了对软件的编写工作,期间走弯路、出故障是家常便饭,所以我非
C# 多线程 Parallel.ForEach 和 ForEach 效率问题研究
最近要做一个大数据dataTable循环操作,开始发现 运用foreach,进行大数据循环,并做了一些逻辑处理。在循环中耗费的时间过长。后来换
君子既知教之所由兴,又知教之所由废,然后可以为人师也。故君子之教,喻也。道而弗牵,强而弗抑,开而弗达。道而弗牵则和,强而弗抑则易,开而
随着互联网行业的发展,现在想要开发一个app已经不是难事了,软件开发行业的发展,让很多人也想通过app自学编程。不过app制作开发出来,