文章目录
1.基本概念
1.1.进程和线程
进程是我们代码的一次运行过程,是CPU进行资源分配的最小单位。线程属于进程的一部分,是CPU进行调度的最小单位。在每个进程中至少得有一个线程,同一个进程中的所有线程共享当前进程中的资源。
在Java中,每一个启动的JVM虚拟机就是一个进程,其中堆、方法区就是线程共享的部分,而栈,程序计数器则是线程私有的。
1.2.并行和并发
- 并发:多个事件在很短的时间间隔类交替执行,这个交替的时间短到难以察觉,让我们主观的认为是同时发生的。
- 并行:多个事件同时发生。
并行存在的前提一定是多核CPU,只有多个核心的存在才能达到多个事件同时运行的目的,而并发通过单个CPU时间片的切换,给不同的线程分配调度的时间就可以实现了。
1.3.多线程的特点
也可以说是多线程的作用或使用的方式,包括异步和并行。
- 异步:不用阻塞主流程,让主流程可以继续执行,另起一个线程做主流程之外的任务。例如申请账号成功后,发送短信通知到用户,这个发送短信的操作就可以异步执行。
- 并行:不是指的CPU层面的并行,而是通过多个线程去同时执行多个耗时的操作。例如需要做一个复杂的数据报表是,启动多个线程同时从不同的数据表中查询数据,最后再将结果汇总到一起。
1.4.多线程的优缺点
优点:可以提高CPU的利用率,将多核CPU的特点发挥到极致。比如当一个线程必须阻塞的时候,另外的线程可以不用等待,继续执行。
缺点:创建更多的线程意味着更多的内存消耗和更频繁的上下文切换,上下文切换会耗费CPU的资源。此外,多线程并发还需要考虑对共享变量操作的线程安全问题。
1.5.线程的上下文切换
在并发编程中,线程的数量往往是大于CPU的核心数的,也就是说会出现多个核心共用一个CPU核心的情况,在这种情况下,为了让用户感觉多个线程是在同时执行的,CPU资源的分配采用了时间片分配的策略。
给每个线程分配一定的执行时间,过了执行时间后就会保存当前执行的状态并让出CPU资源,让其他的线程获取CPU时间片,并加载上次让出资源时的状态。
这种让出资源,并让其它线程加载资源继续执行的过程,就是一次线程的上下文切换。
2.Java线程的创建和启动
Java中一共有3中方式可以创建线程:
- 继承Thread类并重写run方法。
- 实现Runnable接口并重写run方法。
- 实现Callable接口并重写call方法,这种方式也叫做FutureTask。
当然还有一种方式就是使用线程池工具——Executors,但这种方式实际上使用的还是Runnble和Callable。
三种方式的特点:
继承Thread类的方式,子类就不能再继承其它的类了。
Runable接口可以让子类继承其它的类,更加灵活。
实现Callable的方式,相对于Runnable可以通过阻塞的方式获取返回值,并且可以跑出异常。
2.1.代码示例
2.1.1.继承Thread类
public class ThreadDemo extends Thread {
@Override
public void run() {
System.out.println("继承Thread类");
}
public static void main(String[] args) throws InterruptedException {
ThreadDemo threadDemo = new ThreadDemo();
threadDemo.start();
}
}
-- 打印结果
继承Thread类
2.1.2.实现Runnable
public class RunnableDemo implements Runnable {
@Override
public void run() {
System.out.println("实现Runnable接口");
}
public static void main(String[] args) {
// runnable外还是需要包裹一层Thread
Thread runnableDemo = new Thread(new RunnableDemo());
runnableDemo.start();
}
}
-- 打印结果
实现Runnable接口
2.1.3.实现Callable
public class CallableDemo implements Callable<String> {
@Override
public String call() {
System.out.println("实现callable接口");
return "callable返回值";
}
public static void main(String[] args) throws Exception {
// Callable的启动要复杂一点,需要包裹一层FutureTask,可以通过阻塞获取返回值
FutureTask<String> futureTask = new FutureTask<>(new CallableDemo());
Thread callableDemo = new Thread(futureTask);
callableDemo.start();
// 阻塞获取返回值
String returnMsg = futureTask.get();
System.out.println(returnMsg);
}
}
-- 打印结果
实现callable接口
callable返回值
2.2.Java线程启动时发生了什么
Java中本身是没有线程的,线程的启动实际上是JVM对操作系统的请求。在源码中,start方法中实际上是调用了native方法,使用JNI做线程的启动。
public synchronized void start() {
……
boolean started = false;
try {
start0();
started = true;
} finally {
……
}
}
private native void start0();
启动过程的简图如下:
2.3.为什么不使用run()方法
通过上面2.2.的图示可以看到start()方法做了什么事,它实际上就是将一个新的线程从NEW状态转为RUNNABLE状态(下面3.1会说到线程的各种状态)。处于RUNNABLE状态的线程会等待CPU的资源分配,获取资源后会自动调用run方法。
run()方法就是一个普通的Java函数,直接调用它就是外部的线程调用了thread对象中的一个方法而已,是同步执行的,达不到多线程并发执行的目的。
3.线程的生命周期
3.1.线程的状态
在OS中线程有5种状态,在Java中有6种,分别是NEW , RUNNABLE , WAITING , TIMED_WAITING , BLOCKED , TERMINATED ,其中NEW状态是Java独有的 。
在Threa.class中定义了一个线程状态的枚举State,可以明确的看到描述。
public enum State {
/**
* 线程已创建但未启动。
*/
NEW,
/**
* 线程已在虚拟机中启动,等待操作系统调度后就可以运行。
*/
RUNNABLE,
/**
* 等待监视器锁,也就是等待进入synchronized块。
*/
BLOCKED,
/**
* 线程阻塞,等待其它线程将它唤醒,等待的方法包括Object.wait(),LockSupport.park()Thread.join()
* 另外一个线程唤醒它需要使用对应的唤醒方法,Object.notify()或Object.notifyAll(),LockSupport.unPark()
* Thread.join()会阻塞到执行join方法的线程运行结束后,才唤醒当前线程。
*/
WAITING,
/**
* 与WAITING类似,只是在Waiting的几个方法中传入等待时间的参数,没有显示的唤醒情况下,运行时间超出设置的等待时间也会唤醒。
* 此外还有Thread.sleep(long);
*/
TIMED_WAITING,
/**
* 线程执行完毕后的状态
*/
TERMINATED;
}