关于JAVA多线程的学习笔记,这一篇主要整理的是一些多线程中常见的概念,线程状态以及Thread类的常用方法
1.多线程相关的重要概念
1.1 线程、进程
1.1.1 基本定义
- 进程:计算机上所有可运行的软件,通常也包括操作系统,被组织成若干顺序进程,简称进程。进程保存了程序每个时刻的运行状态,为进程的切换提供可能。一个进程对应一个程序,每个进程对应一定的内存地址空间
- 线程:轻量级进程,进程里的“进程”,一个线程负责一个子任务,一个进程中可以有多个线程。
1.1.2 区别
进程 | 线程 |
---|---|
计算机资源分配的基本单位 | 线程是CPU调度的基本单位 |
独立的地址空间 | 线程共享进程的地址空间 |
开销大 | 开销小(轻量级) |
1.2 多线程、并发、并行
1.2.1 多线程
举例:比如用浏览器,同时进行浏览网页、播放视频、下载资源、听音乐等操作,多个线程同时执行不同的子任务
1.2.2 并发(concurrency)
同时处理多个事务的能力。并发是把CPU运行时间划分成若干个时间段,每个时间段再分配给各个线程执行,当一个线程在运行时,其它线程处于挂起状。从宏观角度是同时进行的,但从微观角度并不是同时进行。
1.2.3 并行(parallel)
多个cpu实例或者多台机器同时处理一段逻辑(并行是同一时刻当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,是真正意义上的不同线程在同一时刻同时执行)
1.2.4 区别
- 并发就像一个人(CPU)喂两个小孩(程序)吃饭,表面上是两个小孩在吃饭,实际是一个人在喂。
- 并行就是两个人喂两个小孩子吃饭。
1.3 线程安全
1.3.1 概念
线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。
2. 线程状态
2.1 状态
查看源码,JAVA线程一共有6种状态
状态 | 含义 |
---|---|
NEW | 尚未启动的线程处于这种状态 |
RUNNABLE | 正在JAVA虚拟机中执行的线程处于这种状态 |
BLOCKED | 受阻塞并等待某个监视器锁的线程处于这种状态 |
WAITING | 无限期地等待另一个线程来执行某一特定操作的线程处于这个状态 |
TIMED_WAITING | 等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态 |
TERMINATED | 已退出的线程处于这种状态 |
2.1.1 NEW、RUNNABLE、TERMINATED
这三个状态比较好理解,就是从创建到运行到最后完成的生命流程
2.1.2 WAITING、TIMED_WAITING、BLOCKED
这三个状态,相对比较容易混乱,这里进行一下区分:
- BLOCKED:是出现在某一个线程在等待锁的时候。其他的线程占有了锁,而当前的线程等待其他线程释放锁。
- WAITING:等待,这里看起来跟上一个blocked被堵塞没有什么不同,查看源码,我们可以看到这个状态对应的三种情况:发现都是一个等待被其他线程唤醒的情况。BLOCKED是等待其他线程释放进程,而WAITING是等待其他线程用notify()等方式唤醒线程。
/**
* <ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </ul>
*/
- TIMEWAITIG:直接翻译,就是时间等待的意思,我们查看它的源码,可以发现就像它的字面翻译一样,它就是处于一个等待时间执行完的状态
/**
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
2.2 线程状态的转换
这里用一副图介绍一下:
再举个例子解释一下:
- 比如现在有三个线程,同时到了到了同步块之前,其中两个进程调用了wait(),则一个线程保持了RUNABLE的状态,但另外两个线程进入了WAITING的状态,第一个线程完成了同步块的执行,并调用了notifyall()的函数,此时另外两个线程被唤醒,其中一个线程进入同步块,状态位RUNNABLE状态,另一个进入BLOCKED状态
3.Thread类介绍
3.1 实现多线程的两种方法
方法一:继承Thread类,并实现了run方法
public class MyThread extends Thread {
@Override
public void run() {
super.run();
}
}
方法二:实现Runable接口,实现run方法
public class MyThread1 implements Runnable{
@Override
public void run() {
}
}
举例:
// MyThread 继承了 Thread类
MyThread myThread = new MyThread();
myThread.start();
// MyThread 实现了 Runnable接口
Runnable myThread1 = new MyThread1();
Thread thread = new Thread(myThread1);
thread.start();
这里就有一个问题,为什么两种方法调用的时候不一样,我们这里查看 Runnable 的源码
// Runnable.class
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
只有一个方法,且为抽象函数,因此实现该接口,实际上只是实现了一个方法的普通类,并非新的线程。我们再看 Thread 类:其本身就实现了Runnable的接口,查看它的源码,发现有几个native方法,(关于native,https://blog.csdn.net/Applying/article/details/81572167,我得上一篇博客里面有提及,可能能帮助您理解一下)也有部分是Java实现的方法。这里查看Thread的start方法
// Thread.class
private ThreadGroup group;
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) {
}
}
}
它通过将向ThreadGroup中加入一个新的进程,来创建一个新的进程。
这里就要注意一下,Thread也有run方法,但
// Thread.class
private Runnable target;
@Override
public void run() {
if (target != null) {
target.run();
}
}
这里注意一点,在一些面试题里也会问到:在Thread中直接调用run函数,是不能启动新的线程的。具体的原因,上面也已经解释过了
两种方法的对比:
使用继承Thread类的方式来开发多线程应用程序在设计上有局限性的,因为JAVA的单根继承,所以推荐使用实现Runnable接口的方式进行
3.2 Thread类常用方法
3.2.1 start()
开启一个新的线程,并将线程加入当前的线程组中
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
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 */
}
}
}
这里要注意一点,多个顺序执行的start()方法,顺序不代表线程启动的顺序。如下面的例子,执行的结果,并不会是“0 1 2 3 4 5 6 7 8 9”,某次执行的结果是:“1 4 2 6 0 5 3 9 7 8”
public class MyThread extends Thread {
private int i;
@Override
public void run() {
super.run();
System.out.println(i);
}
public MyThread(int i) {
super();
this.i = i;
}
}
public class AA {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
MyThread myThread = new MyThread(i);
myThread.start();
}
}
}
3.2.2 currentThread()
返回当前的线程,获取当前线程的实例,并进行各种操作,例如定义线程名,获取线程名等。
3.2.3 sleep()
在指定的毫秒数内让当前“正在执行的线程”休眠(暂停执行),执行完的线程进入TIMEWAITING的状态,等待时间走完再次被唤醒
3.2.4 getId()
获得线程的唯一标识
3.2.5 停止线程
Java有三种方法终止正在运行的线程:
- 使用退出标志,使线程正常退出,即run方法完成后线程终止
- stop(),这是一种已经被弃用作废的方法,不推荐。这种方法是暴力停止的方式,可能使一些请理性的工作得不到完成
- 使用interrupt()中断线程
3.2.6 interrupt()
通过查看源码,我们可以发现调用该方法仅仅是在当前线程中打了一个停止的标志,并不是真的停止线程。而且还有两个配套的方法
- this.interrupted():测试当前线程是否已经是中断状态,执行后具有将状态标志置为false的功能
- this.isInterrupted():测试线程是否已经是中断状态,但不清除状态标记
- 意味着,一个被标记的线程,连续两次执行interrupted(),第二次执行的时候,会返回false,因为第一次执行的时候,标志已经被清除。具体原因,我们可以看下面的源码,两个函数的实现,差别只有在是否重置停止标志
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
public boolean isInterrupted() {
return isInterrupted(false);
}
/**
* Tests if some Thread has been interrupted. The interrupted state
* is reset or not based on the value of ClearInterrupted that is
* passed.
*/
private native boolean isInterrupted(boolean ClearInterrupted);
3.2.7 如何正确停止线程
既然stop()不推荐,interrupt()只能打标记,那如何正确停止线程呢。常见的有两个方法
- 异常法:利用interrupt()来对线程进行标记,并通过两个判断状态的函数进行判断,如果true,抛出异常,并在catch块中对异常信息进行相关的处理
- 使用return停止线程:思路类似于异常法,利用interrupt()进行打标记,然后判断并返回,但更推荐上面的方法,因为使用异常流可以更好、更方便地控制程序的运行流程。
3.2.8 暂停线程
suspend()方法暂停线程,resume()方法恢复线程的执行。但这两个方法,已经被弃用了,因为在暂停的时候,并不会释放资源,容易造成死锁,我们可以看到源码中的说明:
/**
* @deprecated This method has been deprecated, as it is
* inherently deadlock-prone. If the target thread holds a lock on the
* monitor protecting a critical system resource when it is suspended, no
* thread can access this resource until the target thread is resumed. If
* the thread that would resume the target thread attempts to lock this
* monitor prior to calling <code>resume</code>, deadlock results. Such
* deadlocks typically manifest themselves as "frozen" processes.
* For more information, see
* <a href="{@docRoot}/../technotes/guides/concurrency/threadPrimitiveDeprecation.html">Why
* are Thread.stop, Thread.suspend and Thread.resume Deprecated?</a>.
*/
@Deprecated
public final void suspend() {
checkAccess();
suspend0();
}
那应该如何暂停跟启动呢?个人觉得方法有两个:
- 使用sleep(),让线程暂停一段时间,等待时间过后自动唤醒,不过sleep也不会释放资源
- 使用wait(),让方法暂停,并释放资源,等待其他事情完成后,其他线程利用notify()将其唤醒,结束暂停状态
3.2.9 yield()
作用为放弃当前的CPU资源,将它让给其他的任务去占用CPU执行时间。但放弃的时间不确定,有可能刚刚放弃,就获取CPU时间片
3.2.10 线程的优先级
setPriority()方法可以设置优先级。线程的优先级,一共分为1~10这10个等级,不在范围内的,会抛出异常。而且有3个常量预置定义优先级的值:
/**
* The minimum priority that a thread can have.
*/
public final static int MIN_PRIORITY = 1;
/**
* The default priority that is assigned to a thread.
*/
public final static int NORM_PRIORITY = 5;
/**
* The maximum priority that a thread can have.
*/
public final static int MAX_PRIORITY = 10;
关于优先级问题注意几个点:
- 线程优先级具有继承性
- CPU尽量将资源让给优先级比较高的线程
- 优先级具有随机性,也就是优先级较高的线程不一定会每一次都先执行完