多线程基础
一、概念
1.1程序、进程与线程的概念和区别
1.1.1 程序:
是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
1.1.2进程:
正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程。进程作为资源分配的单位, 系统在运行时会为每个进程分配不同的内存区域。
1.1.3线程:
进程可进一步细化为线程,是一个程序内部的一条执行路径,是任务分配的基本单元。若一个进程同一时间并行执行多个线程,就是支持多线程的
1.2.并行和串行的区别
并行: 多个CPU同时执行多个任务。比如:多个人同时做不同的事;
并发: 一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事。
1.3 线程的底层创建流程:
1.3.1 JAVA 层面:当我们创建一个线程并执行start()方法后这个线程才会被创建和执行。java.lang.Thread.start()方法会调用本地方法start0();
1.3.2 JVM 层面:接下来start0()方法会调用JVM_StartThread()方法:
1.3.3 操作系统方面:pthread.h 中定义pthread_create()方法。
1.4 阻塞
1.4.1 概念
线程在运行的过程中因为某些原因而发生阻塞,阻塞状态的线程的特点是:该线程放弃CPU的使用,暂停运行,只有等到导致阻塞的原因消除之后才回复运行。或者是被其他的线程中断,该线程也会退出阻塞状态,同时抛出InterruptedException。 正在执行的进程由于发生某时间(如I/O请求、申请缓冲区失败等)暂时无法继续执行。此时引起进程调度,OS把处理机分配给另一个就绪进程,而让受阻进程处于暂停状态,一般将这种状态称为阻塞状态。
1.4.2 一般引发的原因
线程中的阻塞、Socket客户端的阻塞、Socket服务器端的阻塞。
重点讲解线程中的阻塞:
A、线程执行了Thread.sleep(int millsecond);方法,当前线程放弃CPU,睡眠一段时间,然后再恢复执行
B、线程执行一段同步代码,但是尚且无法获得相关的同步锁,只能进入阻塞状态,等到获取了同步锁,才能回复执行。
C、线程执行了一个对象的wait()方法,直接进入阻塞状态,等待其他线程执行notify()或者notifyAll()方法。
D、线程执行某些IO操作,因为等待相关的资源而进入了阻塞状态。比如说监听system.in,但是尚且没有收到键盘的输入,则进入阻塞状态。
1.5 挂起
1.5.1 概念
挂起进程在操作系统中可以定义为暂时被淘汰出内存的进程,机器的资源是有限的,在资源不足的情况下,操作系统对在内存中的程序进行合理的安排,其中有的进程被暂时调离出内存,当条件允许的时候,会被操作系统再次调回内存,重新进入等待被执行的状态即就绪态,系统在超过一定的时间没有任何动作。
1.5.2 一般引发原因
A、终端用户的请求。当终端用户在自己的程序运行期间发现有可疑问题时,希望暂停使自己的程序静止下来。亦即,使正在执行的进程暂停执行;若此时用户进程正处于就绪状态而未执行,则该进程暂不接受调度,以便用户研究其执行情况或对程序进行修改。我们把这种静止状态成为“挂起状态”。
B、父进程的请求。有时父进程希望挂起自己的某个子进程,以便考察和修改子进程,或者协调各子进程间的活动。
C、负荷调节的需要。当实时系统中的工作负荷较重,已可能影响到对实时任务的控制时,可由系统把一些不重要的进程挂起,以保证系统能正常运行。
D、操作系统的需要。操作系统有时希望挂起某些进程,以便检查运行中的资源使用情况或进行记账。
E、对换的需要。为了缓和内存紧张的情况,将内存中处于阻塞状态的进程换至外存上。
1.6 关于阻塞和挂起的总结
1.6.1共同点:
1.6.1.1 进程都暂停执行
1.6.1.2 进程都释放CPU,即两个过程都会涉及上下文切换
1.6.2不同点:
1.6.2.1 对系统资源占用不同:虽然都释放了CPU,但阻塞的进程仍处于内存中,而挂起的进程通过“对换”技术被换出到外存(磁盘)中。
1.6.2.2 发生时机不同:阻塞一般在进程等待资源(IO资源、信号量等)时发生;而挂起是由于用户和系统的需要,例如,终端用户需要暂停程序研究其执行情况或对其进行修改、OS为了提高内存利用率需要将暂时不能运行的进程(处于就绪或阻塞队列的进程)调出到磁盘
1.6.2.3 恢复时机不同:阻塞要在等待的资源得到满足(例如获得了锁)后,才会进入就绪状态,等待被调度而执行;被挂起的进程由将其挂起的对象(如用户、系统)在时机符合时(调试结束、被调度进程选中需要重新执行)将其主动激活
二、线程的创建方式
2.1 构造器
线程构造器有9个,常用的有以下四个:
- Thread(): 创建新的Thread对象;
- Thread(String threadname): 创建线程并指定线程实例名;
- Thread(Runnable target): 指定创建线程的目标对象,它实现了Runnable接口中的run方法;
- Thread(Runnable target, String name): 创建新的Thread对象
2.2 创建新执行线程的四种方法
2.2.1 方式一: 继承Thread类
/**
* 多线程的创建,方式一:继承于Thread类
* 1. 创建一个继承于Thread类的子类
* 2. 重写Thread类的run() --> 将此线程执行的操作声明在run()中
* 3. 创建Thread类的子类的对象
* 4. 通过此对象调用start()
* <p>
* 例子:遍历100以内的所有的偶数
*/
//1. 创建一个继承于Thread类的子类
class MyThread extends Thread {
//2. 重写Thread类的run()
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
public class ThreadTest {
public static void main(String[] args) {
//3. 创建Thread类的子类的对象
MyThread t1 = new MyThread();
//4.通过此对象调用start():①启动当前线程 ② 调用当前线程的run()
t1.start();
//问题一:我们不能通过直接调用run()的方式启动线程。
// t1.run();
//问题二:再启动一个线程,遍历100以内的偶数。不可以还让已经start()的线程去执行。会报IllegalThreadStateException
// t1.start();
//我们需要重新创建一个线程的对象
MyThread t2 = new MyThread();
t2.start();
//如下操作仍然是在main线程中执行的。
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i + "***********main()************");
}
}
}
}
2.2.2 方式二 实现Runnable接口
-
定义子类,实现Runnable接口。
-
子类中重写Runnable接口中的run方法。
-
通过Thread类含参构造器创建线程对象。
-
将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。
-
调用Thread类的start方法:开启线程, 调用Runnable子类接口的run方法。
package atguigu.java; /** * 创建多线程的方式二:实现Runnable接口 * 1. 创建一个实现了Runnable接口的类 * 2. 实现类去实现Runnable中的抽象方法:run() * 3. 创建实现类的对象 * 4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象 * 5. 通过Thread类的对象调用start() * * * 比较创建线程的两种方式。 * 开发中:优先选择:实现Runnable接口的方式 * 原因:1. 实现的方式没有类的单继承性的局限性 * 2. 实现的方式更适合来处理多个线程有共享数据的情况。 * * 联系:public class Thread implements Runnable * 相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。 */ //1. 创建一个实现了Runnable接口的类 class MThread implements Runnable{ //2. 实现类去实现Runnable中的抽象方法:run() @Override public void run() { for (int i = 0; i < 100; i++) { if(i % 2 == 0){ System.out.println(Thread.currentThread().getName() + ":" + i); } } } } public class ThreadTest1 { public static void main(String[] args) { //3. 创建实现类的对象 MThread mThread = new MThread(); //4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象 Thread t1 = new Thread(mThread); t1.setName("线程1"); //5. 通过Thread类的对象调用start():① 启动线程 ②调用当前线程的run()-->调用了Runnable类型的target的run() t1.start(); //再启动一个线程,遍历100以内的偶数 Thread t2 = new Thread(mThread); t2.setName("线程2"); t2.start(); } }
2.2.3 继承方式和实现方式的联系与区别
联系 :
public class Thread extends Object implements Runnable
区别
- 继承Thread:线程代码存放Thread子类run方法中。
- 实现Runnable:线程代码存在接口的子类的run方法。
注意:
start方法可启动多线程;run方法只是thread的一个普通方法调用,还是在主线程里执行,是不会开启多线程的。
2.3 实现Callable接口,覆写call()方法
2.3.1 当线程有返回值时,只能通过实现Callable实现多线程 ,Callable接口在juc(java.util.concurrent
)包下。 此方法是1.5版本推出来的。
public class ThreadBasic {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 先取得Callable对象
MyCallable myCallable = new MyCallable();
// 将Callable对象转换为FutureTask对象
FutureTask<String> futureTask = new FutureTask<>(myCallable);
// 由于FutureTask对象实现了RunnableFuture接口,
// 而RunnableFuture接口又是Runnable接口的子接口,
// 因此可以将FutureTask对象当做Runnable接口的实现类传给Thread
Thread thread1 = new Thread(futureTask);
Thread thread2 = new Thread(futureTask);
// 一切的一切,最终的最终,开启线程还得是Thread的start方法
thread1.start();
thread2.start();
// 打印线程的返回值
// 由于FutureTask间接实现了Future接口
// 因此可以使用FutureTask的get()方法获取线程的返回值
System.out.println(futureTask.get());
}
}
class MyCallable implements Callable<String> {
private int ticket = 10;
@Override
public String call() throws Exception {
while(this.ticket>0){
System.out.println(Thread.currentThread().getName()+",剩余票数:"+this.ticket--);
}
return "票卖完了";
}
}
FutureTask(Callable实现多线程的核心类)即是Runnable的子类,又是RunnableFuture的子类,又可以传入Callable(FutureTask的构造方法)——连通三者,因此FutureTask的作用为:
a)将Callable转为FutureTask
b)将FutureTask转为Thread(因为FutureTask间接实现了Runnable接口,相当于是Runnable)
c)使用Thread的start()方法启动线程——兜兜转转启动线程还是只能使用Thread的start()方法
2.4 线程池
线程池的是一个重要部分,后期会单独介绍。
总结:1、以上几种创建线程的方法,最后都调用了start()方法,具体流程:start(判断当前线程是不是首次创建,Java方法)->调用start0()方法(JVM)->通过JVM进行资源调度,系统分配->回调run()方法(Java方法)执行线程的具体操作任务。
2、由于start()方法调用了JVM进行系统调度、系统分配等一系列操作,因此创建一个线程只能由start()来完成,而若直接调用run()方法,相当于是在调用一个普通方法。
总结:结合构造器以及市面上对多线程的解读,java创建多线程都使用了Runnable ,市面上的解读都是对Runnable 接口的异化。
三、多线程的状态与生命周期
3.1 源码
/**
* A thread state. A thread can be in one of the following states:
* <ul>
* <li>{@link #NEW}<br>
* A thread that has not yet started is in this state.
* </li>
* <li>{@link #RUNNABLE}<br>
* A thread executing in the Java virtual machine is in this state.
* </li>
* <li>{@link #BLOCKED}<br>
* A thread that is blocked waiting for a monitor lock
* is in this state.
* </li>
* <li>{@link #WAITING}<br>
* A thread that is waiting indefinitely for another thread to
* perform a particular action is in this state.
* </li>
* <li>{@link #TIMED_WAITING}<br>
* A thread that is waiting for another thread to perform an action
* for up to a specified waiting time is in this state.
* </li>
* <li>{@link #TERMINATED}<br>
* A thread that has exited is in this state.
* </li>
* </ul>
*
* <p>
* A thread can be in only one state at a given point in time.
* These states are virtual machine states which do not reflect
* any operating system thread states.
*
* @since 1.5
* @see #getState
*/
public enum State {
/**
* Thread state for a thread which has not yet started.
*/
NEW,
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
RUNNABLE,
/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
*/
BLOCKED,
/**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
* <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>
*
* <p>A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called <tt>Object.wait()</tt>
* on an object is waiting for another thread to call
* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
* that object. A thread that has called <tt>Thread.join()</tt>
* is waiting for a specified thread to terminate.
*/
WAITING,
/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
* <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>
*/
TIMED_WAITING,
/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
TERMINATED;
}
3.2 从Thread.State 从这个枚举类里看,jvm线程有6个状态:
NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED
下图详细描述了线程生命周期各个状态以及状态之间之间的转换时机
3.2.1.新建
New一个线程,还没有调用start(); 时处于新建状态。
3.2.2.Runnable状态
线程对象调用start();方法时,它会被线程调度器来执行(也就是交给操作系统执行),操作系统执行的时候,这整个状态叫Runnable状态,Runnable内部又有两个状态:
a) Ready就绪状态
扔到CPU的等待队列里去等待CPU运行
b) Running状态
等真正扔到CPU上去运行的时候叫Running.(调用yield方法,从Running–>Ready,线程调度器选中执行的时候 Ready—>Running)
3.2.3.Terminaled结束状态
线程顺利执行完了进入Terminaled结束状态(不可以再回到new状态调用start,这就算结束了)
Runnable还有一些其他状态:
3.2.4.TimedWaiting等待
按照时间等待,等待时间结束了自己就到了Running状态。Thread.sleep(time);o.wait(time);t.join(time);LockSupport.parkNanos()
都是关于时间等待的方法
3.2.5.Waiting等待
在运行过程中,如果调用了:
o.wait();t.join();LockSupport.park();进入waiting状态;
调用 o.notify(); o.notifyAll(); LockSupport.unpark();又回到Running状态
3.2.6.Block阻塞
同步代码块没有获得锁就会阻塞状态,获得锁就是就绪状态
追问1:这些状态,哪些是JVM管理的,哪些是操作系统管理的?
这些状态全是由JVM管理的,因为JVM管理的时候也要通过操作系统,所以,哪个是JVM哪个是操作系统,他俩分不开。JVM是跑在操作系统上的一个普通程序。
追问2:线程什么时候会被挂起?挂起是否也是一个状态?
Running的时候,在一个CPU上会跑很多个线程,CPU会隔一段时间执行这个线程一下,在隔一段时间执行那个线程一下,这是CPU内部的一个调度,把这个状态线程扔出去,从Running扔出去,就叫线程被挂起,CPU控制它。
四、线程重要方法
4.1 start()
启动线程。
4.2 run()
线程执行方法体。
注意:调用start()方法时会执行run()方法,直接执行run方法并没有开启新线程。
接下来讲解一下线程中断的概念,首先明白线程的中断是一个状态或者枚举,线程中断是一个执行动作的行为。
4.3 interrupt()
中断线程,将会设置该线程的中断状态位,即设置为true,中断的结果线程是死亡、还是等待新的任务或是继续运行至下一步,就取决于这个程序本身。线程会不时地检测这个中断标示位,以判断线程是否应该被中断(中断标示值是否为true)。它并不像stop方法那样会中断一个正在运行的线程。
4.4 isInterrupted()
4.5 interrupted()
判断某个线程是否已被发送过中断请求,请使用Thread.currentThread().isInterrupted()方法(因为它将线程中断标示位设置为true后,不会立刻清除中断标示位,即不会将中断标设置为false),而不要使用thread.interrupted()(该方法调用后会将中断标示位清除,即重新设置为false)。
对于处于sleep,join等操作的线程,如果被调用interrupt()后,会抛出InterruptedException,然后线程的中断标志位会由true重置为false,因为线程为了处理异常已经重新处于就绪状态。
中断状态位,即设置为true,中断的结果线程是死亡、还是等待新的任务或是继续运行至下一步,就取决于这个程序本身。线程会不时地检测这个中断标示位,以判断线程是否应该被中断(中断标示值是否为true)。它并不像stop方法那样会中断一个正在运行的线程。
4.4 isInterrupted()
4.5 interrupted()
判断某个线程是否已被发送过中断请求,请使用Thread.currentThread().isInterrupted()方法(因为它将线程中断标示位设置为true后,不会立刻清除中断标示位,即不会将中断标设置为false),而不要使用thread.interrupted()(该方法调用后会将中断标示位清除,即重新设置为false)。
对于处于sleep,join等操作的线程,如果被调用interrupt()后,会抛出InterruptedException,然后线程的中断标志位会由true重置为false,因为线程为了处理异常已经重新处于就绪状态。