1 引言
线程是进程中可独立执行的最小单位,也是CPU资源分配的基本单位。
线程的四个基本属性:
属性 | 描述 |
编号id | 线程的唯一标识 |
名称 | 线程的名字,默认“Thread-编号id”,可自定义 |
类别 | 分为守护线程和用户线程,可以通过 |
优先级 | 表示希望哪个线程优先执行,Java中优先级取值范围是1~10,默认5 |
2 Java线程对象Thread常用的方法
1 start()
表示启动线程,让CPU调度器调度开启线程持行run()方法。而直接调用run()方法只是运行当前run方法,不会开启线程。
附上源码:
public synchronized void start() {
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) {
}
}
}
private native void start0();
通过源码,不难发现start()
方法实际上是调用start0()
方法启动线程的,而start0()
是native
方法。
简单说下native的原理及实现机制:
native修饰的表示本地方法,一个Native Method就是一个java调用非java代码(如C代码)的接口,Java中的Native Method并不提供实现体。
当一个类第一次被使用到时,这个类的字节码会被加载到内存,并且只会会被载一次。在这个被加载的字节码的入口维持着一个该类所有方法描述符的list,这些方法描述符包含这样一些信息:方法代码存于何处,它有哪些参数,方法的描述符(public之类)等等。
如果一个方法描述符内有native,这个描述符块将有一个指向该方法的实现的指针。这些实现在一些DLL文件内,但是它们会被操作系统加载到Java程序的本地方法栈。当一个带有本地方法的类被加载时,其相关的DLL并未被加载,因此指向方法实现的指针并不会被设置。只有当本地方法被调用时,这些DLL才会被加载,这是通过调用java.system.loadLibrary()实现的。
此外,不可不说的是,本地方法意味着和平台有关,因此使用了native的程序可移植性都不太高,并且本地方法是有开销的,它丧失了Java的很多优势。
2 sleep()
线程暂停阻塞等待一段时间,时间过了就继续。该方法是不释放锁的。
附源码:
public static native void sleep(long millis) throws InterruptedException
同样的,sleep()
也是本地方法,底层也是通过C代码调用操作系统实现的
3 yield()
t.yield()
表示t线程主动让出CPU执行权进入就绪态
,所有线程重新争抢CPU(可能出现又是t线程抢到CPU执行权)。
附源码:
public static native void yield();
4 join()
t2线程正在运行,这时在t2线程中调用 t1.join()
,这是让线程t2等待线程t1执行结束再运行。
此方法可以让线程按顺序持行。自己线程中调用自己的join()方法是无实际意义的。
附源码:
public final void join() throws InterruptedException {
join(0);
}
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;
}
}
}
join(long timeout)
方法底层调用的是Object
类的wait(long timeout)
方法。
public final native void wait(long timeout) throws InterruptedException;
Object
类的wait()
方法是释放锁的。
3 Java线程运行状态
过程分析:
- new 的状态(NEW)
- start() 之后线程到达就绪状态等待CPU调度,CPU调度了 就是运行状态。(RUNNABLE)
- 调用了 TImedWaiting 就回到就绪状态,结束之后自动到运行状态(TIMED_WAITING)
- 调用 Waiting 就绪然后持行相应的方法到运行(WAITING)
- 调用 Blocked 等待进入同步代码快的锁。(BLOCKED)
- 结束状态Teminated(TERMINATED)
线程被挂起:正在CPU中调度的线程,CPU把该线程丢出去,持行其他线程,这就是线程被挂起。之后又回来持续该线程。(每个CPU每次只能持行一个线程)
4 Java线程调度原理
我们知道,在任意时刻,操作系统的CPU 只能执行一条机器指令,并且每个线程只有获取到 CPU 的使用权后,才可以执行指令(也就是说在任意时刻,只有一个线程占用 CPU,处于运行的状态
)。
多线程并发运行实际上是指多个线程轮流获取 CPU 使用权,分别执行各自的任务。
线程的调度由 JVM 负责。
线程调度模型分为两类:时间片轮转调度模型和抢占式调度模型。JVM采用的是抢占式调度模型。
- 时间片轮转调度模型
时间片轮转调度模型是让所有线程轮流获取 CPU 使用权,并且为每个线程分配相同的占用 CPU 的时间片,时间片用完了则线程自动放弃CPU。
- 抢占式调度模型
JVM 采用的是抢占式调度模型,根据优先级来选择占用CPU,优先级越高线程被持行的几率就越大,如果线程的优先级都一样,那就随机选择一个线程,并让该线程占用 CPU。也就是如果我们同时启动多个线程,并不能保证它们能轮流获取到均等的时间片。
如果我们的程序想干预线程的调度过程,最简单的办法就是给每个线程设定一个优先级。
5 线程创建的四种方式
5.1 继承Thread类创建线程
/**
* @author Carson
* @date 2020/4/3 10:44
*/
public class MyThread {
public static void main(String[] args) {
new ThreadGenerator().start();
}
}
class ThreadGenerator extends Thread {
@Override
public void run() {
System.out.println("Starting thread>>>>>>>>>>>>>>>>>>>>>>");
}
}
该方式的优缺点在于:
- 优点:编写简单
- 缺点:线程类已经继承了
Thread
,不能再继承别的类。
5.2 实现Runnable接口创建线程
/**
* @author Carson
* @date 2020/4/3 10:44
*/
public class MyThread {
public static void main(String[] args) {
new Thread(new ThreadGenerator()).start();
}
}
class ThreadGenerator implements Runnable {
@Override
public void run() {
System.out.println("Starting thread>>>>>>>>>>>>>>>>>>>>>>");
}
}
当然,在使用Java8及更高版本时,可以直接使用Lambda表达式创建:
/**
* @author Carson
* @date 2020/4/3 11:22
*/
public class MyThread {
public static void main(String[] args) throws Exception {
new Thread(() -> {
System.out.println("Starting thread>>>>>>>>>>>>>>>>>>>>>>");
}).start();
}
}
5.3 使用Callable和Future创建线程
/**
* @author Carson
* @date 2020/4/3 10:44
*/
public class MyThread {
public static void main(String[] args) throws Exception {
ThreadGenerator threadGenerator = new ThreadGenerator();
FutureTask futureTask = new FutureTask(threadGenerator);
new Thread(futureTask).start();
}
}
class ThreadGenerator implements Callable {
@Override
public Object call() throws Exception {
System.out.println("Starting thread>>>>>>>>>>>>>>>>>>>>>>");
return true;
}
}
同样的,也可以直接使用Lambda表达式创建:
/**
* @author Carson
* @date 2020/4/3 11:22
*/
public class MyThread {
public static void main(String[] args) {
FutureTask<Boolean> futureTask = new FutureTask<>(() -> {
System.out.println("Starting thread>>>>>>>>>>>>>>>>>>>>>>");
return true;
});
new Thread(futureTask).start();
}
}
实现Callable
接口和实现Runnable
接口的不同点在于:
- call()方法可以有返回值
- call()方法可以声明并抛出异常
5.4 使用线程池,例如用Executor框架
/**
* @author Carson
* @date 2020/4/3 11:22
*/
public class MyThread {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(6);
/* execute方法不关心返回值。 */
executorService.execute(new ThreadGenerator());
/* submit方法有返回值-->Future */
executorService.submit(new ThreadGenerator());
}
}
class ThreadGenerator implements Runnable {
@Override
public void run() {
System.out.println("Starting thread>>>>>>>>>>>>>>>>>>>>>>");
}
}
在开发中推荐使用线程池的方式,用线程池管理线程既高效风险也较低。