进程与线程基本概念
进程与线程的区别
在现代操作系统中,进程是操作系统进行资源分配的基本单位(内存地址、文件 I/O 等),而线程是操作系统进行调度的基本单位,即CPU分配时间的单位。
线程是进程内部的一个执行单元。 每一个进程至少有一个主执行线程,它无需由用户去主动创建,是由系统自动创建的。用户根据需要在应用程序中创建其它线程,多个线程并发地运行于同一个进程中。
图解进程和线程的关心
Java 虚拟机 3:Java内存模型-内存区域划分以及对象创建的过程
上下文切换
上下文切换(有时也称做进程切换或任务切换)是指 CPU 从一个进程(或线程)切换到另一个进程(或线程)。上下文是指某一时间点 CPU 寄存器和程序计数器的内容。
CPU通过为每个线程分配CPU时间片来实现多线程机制。CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。
举例说明 线程 A - B:
- 先挂起线程A,将其在cpu中的状态保存在内存中。
- 在内存中检索下一个线程B的上下文并将其在 CPU 的寄存器中恢复,执行B线程。
- 当B执行完,根据程序计数器中指向的位置恢复线程A。
上下文切换通常是计算密集型的,意味着此操作会消耗大量的 CPU 时间,故线程也不是越多越好。如何减少系统中上下文切换次数,是提升多线程性能的一个重点课题。
并发与并行的区别
并发:一个时间段内有很多的线程或进程在执行,但何时间点上都只有一个在执行,多个线程或进程争抢时间片轮流执行
并行:一个时间段和时间点上都有多个线程或进程在执行。
CPU通过为每个线程分配CPU时间片来实现多线程机制。CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。
Java多线程入门类和接口
JDK提供了Thread类和Runnable接口来让我们实现自己的“线程”类。
- 继承Thread类,并重写run方法;
- 实现Runnable接口的run方法;
使用Runnable和Thread来创建一个新的线程。但是它们有一个弊端,就是run方法是没有返回值的。而有时候我们希望开启一个线程去执行一个任务,并且这个任务执行完成后有一个返回值。
JDK提供了Callable接口与Future接口为我们解决这个问题,这也是所谓的“异步”模型。
继承Thread类
1 | public class Demo { |
在程序里面调用了start()方法后,虚拟机会先为我们创建一个线程并进入就绪状态(Ready),然后等到这个线程第一次得到时间片时再自动调用run()方法。
为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
new 一个 Thread,线程进入了新建状态。调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 但是,直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结: 调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。
实现Runnable接口
接着我们来看一下Runnable接口(JDK 1.8 +):
1 |
|
可以看到Runnable是一个函数式接口,这意味着我们可以使用Java 8的函数式编程来简化代码。
1 | public class Demo { |
Callable、Future与FutureTask
使用Runnable和Thread来创建一个新的线程。但是它们有一个弊端,就是run方法是没有返回值的。而有时候我们希望开启一个线程去执行一个任务,并且这个任务执行完成后有一个返回值。
JDK提供了Callable接口与Future接口为我们解决这个问题,这也是所谓的“异步”模型。
Callable
Callable与Runnable类似,同样是只有一个抽象方法的函数式接口。不同的是,Callable提供的方法是有返回值的,而且支持泛型。
1 |
|
这里可以看一个简单的使用demo:
1 | // 自定义Callable |
Future
Future接口只有几个比较简单的方法:
1 | public abstract interface Future<V> { |
所以有时候,为了让任务有能够取消的功能,就使用Callable来代替Runnable。如果为了可取消性而使用 Future但又不提供可用的结果,则可以声明 Future<?>形式类型、并返回 null作为底层任务的结果。
FutureTask
FutureTask是实现的RunnableFuture接口的,而RunnableFuture接口同时继承了Runnable接口和Future接口:
1 | public interface RunnableFuture<V> extends Runnable, Future<V> { |
那FutureTask类有什么用?为什么要有一个FutureTask类?前面说到了Future只是一个接口,而它里面的cancel,get,isDone等方法要自己实现起来都是非常复杂的。所以JDK提供了一个FutureTask类来供我们使用。
1 | // 自定义Callable,与上面一样 |
线程组和线程优先级
线程组
Java中用ThreadGroup来表示线程组,我们可以使用线程组对线程进行批量控制。
ThreadGroup和Thread的关系就如同他们的字面意思一样简单粗暴,每个Thread必然存在于一个ThreadGroup中,Thread不能独立于ThreadGroup存在。执行main()方法线程的名字是main,如果在new Thread时没有显式指定,那么默认将父线程(当前执行new Thread的线程)线程组设置为自己的线程组。
ThreadGroup管理着它下面的Thread,ThreadGroup是一个标准的向下引用的树状结构,这样设计的原因是防止”上级”线程被”下级”线程引用而无法有效地被GC回收。
ThreadGroup源码中的成员变量:
1 | public class ThreadGroup implements Thread.UncaughtExceptionHandler { |
线程组的常用方法:
1.获取当前的线程组名字
1 | Thread.currentThread().getThreadGroup().getName() |
2.复制线程组
1 | // 获取当前的线程组 |
3.线程组统一异常处理示例
1 | public class ThreadGroupDemo { |
总结来说,线程组是一个树状的结构,每个线程组下面可以有多个线程或者线程组。线程组可以起到统一控制线程的优先级和检查线程的权限的作用。
线程的优先级
Java中线程优先级可以指定,范围是1~10。但是并不是所有的操作系统都支持10级优先级的划分(比如有些操作系统只支持3级划分:低,中,高),Java只是给操作系统一个优先级的参考值,线程最终在操作系统的优先级是多少还是由操作系统决定。
在 Thread 源码中和线程优先级相关的属性有 3 个:
线程的优先级可以理解为线程抢占 CPU 时间片的概率,优先级越高的线程优先执行的概率就越大,但并不能保证优先级高的线程一定先执行。在程序中我们可以通过 Thread.setPriority() 来设置优先级,setPriority() 源码如下:
1 | public final void setPriority(int newPriority) { |
使用方法Thread类的setPriority()来设定线程的优先级示例:
1 | public class Demo { |
Java中的优先级来说不是特别的可靠,Java程序中对线程所设置的优先级只是给操作系统一个建议,操作系统不一定会采纳。而真正的调用顺序,是由操作系统的线程调度算法决定的。
Java线程的状态及主要转化方法
Java线程的6个状态
线程的状态在 JDK 1.5 之后以枚举的方式被定义在 Thread 的源码中,它总共包含以下 6 个状态:
- NEW,新建状态,线程被创建出来,但尚未启动时的线程状态;
- RUNNABLE,就绪状态包括(Running 和 Ready),表示可以运行的线程状态,它可能正在运行,或者是在排队等待操作系统给它分配 CPU 资源;
- BLOCKED,阻塞等待锁的线程状态,表示处于阻塞状态的线程正在等待监视器锁,比如等待执行 synchronized 代码块或者使用 synchronized 标记的方法;
- WAITING,等待状态,一个处于等待状态的线程正在等待另一个线程执行某个特定的动作,比如,一个线程调用了 Object.wait() 方法,那它就在等待另一个线程调用 Object.notify() 或 Object.notifyAll() 方法;
- TIMED_WAITING,计时等待状态,和等待状态(WAITING)类似,它只是多了超时时间,比如调用了有超时时间设置的方法 Object.wait(long timeout) 和 Thread.join(long timeout) 等这些方法时,它才会进入此状态;
- TERMINATED,终止状态,表示线程已经执行完成。
Thread.State 源码如下:
1 | public enum State { |
BLOCKED 和 WAITING 的区别
虽然 BLOCKED 和 WAITING 都有等待的含义,但二者有着本质的区别:
BLOCKED 可以理解为当前线程还处于活跃状态,只是在阻塞等待其他线程使用完某个锁资源;
而 WAITING 则是因为自身调用了 Object.wait() 或着是 Thread.join() 又或者是 LockSupport.park() 而进入等待状态,只能等待其他线程执行某个特定的动作才能被继续唤醒,比如当线程因为调用了 Object.wait() 而进入 WAITING 状态之后,则需要等待另一个线程执行 Object.notify() 或 Object.notifyAll() 才能被唤醒。
调用 start() 和 run() 启动线程的区别
从执行的效果来说,start() 方法可以开启多线程,让线程从 NEW 状态转换成 RUNNABLE 状态,而 run() 方法只是一个普通的方法.
它们可调用的次数不同,start() 方法不能被多次调用,否则会抛出 java.lang.IllegalStateException;而 run() 方法可以进行多次调用,因为它只是一个普通的方法而已。
Thread 源码来看,start() 方法属于 Thread 自身的方法,并且使用了 synchronized 来保证线程安全,源码如下:
1 | public synchronized void start() { |
run() 方法为 Runnable 的抽象方法,必须由调用类重写此方法,重写的 run() 方法其实就是此线程要执行的业务方法,源码如下:
1 | public class Thread implements Runnable { |
总结:调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。
Thread常用方法 join() 和 yield() 的区别
join():使当前线程等待另一个线程执行完毕或超时之后再继续执行,内部调用的是Object类的wait方法实现的,比如:在一个线程中调用other.join(),这时候当前线程会让出执行权给other线程,直到other线程执行完或者过了超时时间之后再继续执行当前线程:
yield():yield在英语里有放弃的意思,同样,这里的yield()指的是当前线程愿意让出对当前处理器的占用,使同优先级或更高优先级的线程有执行的机会,yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。不可靠,因为线程调度器不一定会采纳 yield() 出让 CPU 时间片的建议
join
内部调用的是Object类的wait方法实现的, 源码如下:
1 | public final synchronized void join(long millis) |
例如,在未使用 join() 时,代码如下
1 | public class ThreadExampleWithJoin { |
从结果可以看出,在未使用 join() 时主子线程会交替执行。
然后我们再把 join() 方法加入到代码中,代码如下:
1 | public class ThreadExampleWithJoin { |
从执行结果可以看出,加 join() 方法之后,主线程会先等子线程执行完之后才继续执行。
如果使用 thread.join(2000),则运行结果如下
yield
yield在英语里有放弃的意思,同样,这里的yield()指的是当前线程愿意让出对当前处理器的占用,使同优先级或更高优先级的线程有执行的机会,yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。不可靠,因为线程调度器不一定会采纳 yield() 出让 CPU 时间片的建议
Thread 的源码可以知道 yield() 为本地方法
1 | public static native void yield(); |
yield() 方法表示给线程调度器一个当前线程愿意出让 CPU 使用权的暗示,但是线程调度器可能会忽略这个暗示。
比如我们执行这段包含了 yield() 方法的代码,如下所示:
1 | public class ThreadExampleWithYield { |
运行结果:
当我们把这段代码执行多次之后会发现,每次执行的结果都不相同,这是因为 yield() 执行非常不稳定,线程调度器不一定会采纳 yield() 出让 CPU时间片的建议。
Java线程间的通信方式
- 锁与同步
- 等待/通知机制 Java多线程的等待/通知机制是基于Object类的wait()方法和notify(), notifyAll()方法来实现的。
- 信号量 JDK提供了一个类似于“信号量”功能的类Semaphore
- 管道 基于“管道流”的通信方式。JDK提供了PipedWriter、 PipedReader、 PipedOutputStream、 PipedInputStream。
- ThreadLocal类