多线程
定义
进程:
指在系统中能独立运行并作为资源分配的基本单位,它是由一组机器指令、数据和堆栈等组成的,是一个能独立运行的活动实体。
进程的特性:
1.动态性:进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的。
2.并发性:任何进程都可以同其他进程一起并发执行。
3.独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位。
4.异步性:由于进程间的相互制约,使进程具有执行的间断性,即进程按各自独立的、不可预知的速度向前推进。
线程:线程是进程中的一个实体,作为系统调度和分派的基本单位。是指进程内独立执行某个任务的一个单元。Linux下的线程看作轻量级进程。
线程:
- 线程是进程内的一个相对独立的可执行的单元。若把进程称为任务的话,那么线程则是应用中的一个子任务的执行。
- 线程是被调度的基本单元,而进程不是调度单元。每个进程在创建时,至少需要同时为该进程创建一个线程。即进程中至少要有一个或一个以上的线程,否则该进程无法被调度执行。
- 进程是被分给并拥有资源的基本单元。同一进程内的多个线程共享该进程的资源,但线程并不拥有资源,只是使用他们。
- 线程是操作系统中基本调度单元,因此线程中应包含有调度所需要的必要信息,且在生命周期中有状态的变化。
- 由于共享资源,所以线程间需要通信和同步机制,且需要时线程可以创建其他线程,但线程间不存在父子关系。
总论:
- 线程就为了使操作系统能够有更好的并发而创建的,相当于只拥有少量资源的进程——轻型进程。在这种多线程操作系统中,进程是拥有系统资源的基本单位,包含多个线程,为其提供资源,而进程本身不再作为可执行的实体,当进程执行的时候,实际上是其中的某个线程在执行。
- 进程和线程都是一个时间段的描述,是CPU工作时间段的描述。
- 程序将单个任务按照功能分解成多个子任务来执行,每个子任务称为一个线程,多个线程共同完成主任务的运行过程,这样可以缩短用户等待时间,提高服务效率。
如何使用
线程的创建?
- 继承Thread,重写Run方法
public class MyThread extends Thread {
private String name;
public MyThread(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println("my name is "+name);
}
}
- 实现Runnable接口
public class MyRunnable implements Runnable {
private String name;
public MyRunnable(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println("he name is " + name);
}
}
但是这2中方式的运行代码是不一样的。
public static void main(String[] args) {
// 继承Thread
MyThread myThread = new MyThread("007");
myThread.start();
// 实现Runnable接口
MyRunnable myRunnable = new MyRunnable("008");
new Thread(myRunnable).start();
}
实现Runnable接口后还是需要在Thread中才能运行。
- 使用Callable和Future创建线程
(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。
(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("my nane is zhang");
return "zhangxi";
}
}
// 通过Callable和FutureTask
MyCallable callable = new MyCallable();
FutureTask<String> ft = new FutureTask<>(callable);
new Thread(ft).start();
try {
System.out.println(ft.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
其实这3中方式都差不多,本质上创建一个线程只能用new Thread 。运行就是start()方法。只不过执行体是Runable接口。
线程的生命周期?
Java线程具有五中基本状态
新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
2.同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
3.其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
怎么停止线程?
线程本质是一段代码段或者说是任务的执行。强行停止线程会有预想不到的异常出现,所以我们不能停止线程,但是可以从逻辑上去停止任务。最常用的方法就是设置标记位和使用 线程的interrupt()方法。
- 设置标记位
static class SubThread extends Thread {
private String name;
public volatile boolean isStop = false;
public SubThread(String name) {
this.name = name;
}
@Override
public void run() {
while (!isStop) {
System.out.println("my name is " + name);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
比较简单就是增加一个boolean 变量,当需要停止线程时,把此变量设置为true即可。唯一要注意的就是要用volatile关键字修饰,这样值一改变由于可见性可以立刻得知。
我们测试的时候会发现就算不加volatile也会停止,那是因为虚拟机运行的环境不同导致。
- 使用Thread的interrupt()方法。
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("start run=" + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
System.out.println("InterruptedException=" + e);
return;
}
}
}
}
public static void main(String[] args) {
MyThread myThread=new MyThread();
myThread.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
myThread.interrupt();
}
过程:
- Run打印100次,每次间隔500ms。
- 创建新线程运行Run,等待2000ms后调用interrupt()中断线程
- Run就会收到InterruptedException异常,我们再这里return退出代码,如果不处理,Run还是会继续跑的。
interrupt(),isInterrupted()和interrupted()有什么区别?
interrupt():就是通知中止线程的,使“中断状态”为true。
isInterrupted():就是打印中断状态的,然后不对中断状态有任何操作。
interrupted():检测运行这个方法的线程的中断状态,注意,是运行这个方法的线程,且会清除中断状态
线程中的其他方法
- join方法,等待线程执行完。
Thread01 thread01=new Thread01("王哈哈");
Thread02 thread02=new Thread02("丢雷吗");
thread01.start();
try {
thread01.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
thread02.start();
这里就是2个线程thread02要等到thread01执行完成才会执行。
- yield放,线程让步。
yield的意思是放弃,投降的意思。当前线程调用yield的时候,告诉虚拟机它愿意让其他的线程抢占自己的位置。者表明该线程没有紧急的事要做,但这只是一种暗示,并不能保证一定会发生。很少使用。
3 wait
wait 方法是属于 Object 类中的,wait过程中线程会释放对象锁,只有当其他线程调用 notify才能唤醒此线程。wait 使用时必须先获取对象锁,即必须在 synchronized 修饰的代码块中使用,那么相应的 notify 方法同样必须在 synchronized 修饰的代码块中使用,如果没有在synchronized 修饰的代码块中使用时运行时会抛出IllegalMonitorStateException的异常
wait():使一个线程处于等待状态,并且释放所持有的对象的lock。
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException异常。
notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。
Allnotity():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。
private static Object mObject = new Object();
public static void main(String[] args) {
Thread03 thread03 = new Thread03("王哈哈");
Thread04 thread04 = new Thread04("丢雷吗");
thread03.start();
thread04.start();
}
static class Thread03 extends Thread {
private String name;
public Thread03(String name) {
this.name = name;
}
@Override
public void run() {
synchronized (mObject) {
println("Thread03", "Thread03 start " + name);
try {
mObject.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
println("Thread03", "Thread03 end");
}
}
}
static class Thread04 extends Thread {
private String name;
public Thread04(String name) {
this.name = name;
}
@Override
public void run() {
synchronized (mObject) {
println("Thread04", "Thread04 start " + name);
mObject.notify();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
println("Thread04", "Thread04 end");
}
}
}
执行结果:
可以看到thread03调用wait后就进入了等待。在thread04中调用了notify 后 thread03 等 04 线程运行完了 再运行,如果不调用notify那么03线程就会一直阻塞。
源码原理
Thread类:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sqqvaimf-1578455380540)(467C788AEAA046E1BC081A3D55954BA6)]
Thread类很简单就是实现了Runnable接口。
我们再看构造函数:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8NLTiMRH-1578455380541)(6292A575938B4F038A452CFEEF69E5AA)]
看到也没特别的东西:参数
- ThreadGroup线程组:主要就是分组管理线程,和容器 ,树结构类似。
- Runnable实现类,线程运行时会回调此接口中的run方法。
- name 线程的名字,默认是"Thread-" + nextThreadNum()。
- stacksizd栈大小
- 权限控制。
在看start方法。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6QYwMLtQ-1578455380542)(C9B671D5E5DD46779DF35DED128F14E3)]
看注释:1.调用start方法后线程开始等待执行,当分配到cpu资源开始运行时,虚拟机会回调runnable的run方法。2.每次线程只能被调用一次start方法。
我接下来看看 什么回调了run方法。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3x1whOQG-1578455380543)(BD55013621684FD5988FF6B551559A09)]
看不到了。
我们知道,线程是 CPU 独立调度的单位,通过引入线程,实现时分复用,利用并发思想使得我们的程序运行的更加迅速。
主流的操作系统都提供了线程的实现,注意这句话,谁实现的线程?是操作系统,尽管本文侧重于介绍 Java 线程的实现原理,但是请大家清楚一点,实际上实现线程的老大哥,是运行在内核态的操作系统。
Java 语言提供了不同硬件和操作系统平台下对线程操作的统一处理,每个已经执行 start() 且还未结束的 java.lang.Thread 类的实例就代表了一个线程。但是正如我们刚刚所强调的那样,线程可是由操作系统来实现的啊,那么 Java 是如何面向开发者提供的线程统一操作呢?我们来简单的看一下 Thread 类的几个关键方法。
private static native void registerNatives();
public static native Thread currentThread();
public static native void yield();
public static native void sleep(long millis) throws InterruptedException;
public final native boolean isAlive();
有没有发现一个比较特殊的共同点?对,这些方法都是被 native 关键字所修饰的,你在阅读这些方法的源码的时候看不见这些方法具体实现的 Java 代码。因为在 Java 的 API 中,一个 native 方法往往意味着这个方法无法使用平台无关的手段来实现。
所以,还是那句话,实际上线程的实现与 Java 无关,由平台所决定,Java 所做的是将 Thread 对象映射到操作系统所提供的线程上面去,对外提供统一的操作接口,向程序员隐藏了底层的细节,使程序员感觉在哪个平台上编写的有关于线程的代码都是一样的。这也是 Java 这门语言诞生之初的核心思想,一处编译,到处运行,只面向虚拟机,实现所谓的平台无关性,而这个平台无关性就是由虚拟机为我们提供的。
操作系统实现线程主要有 3 种方式
用户级线程
内核级线程
用户级线程 + 内核级线程,混合实现
内核级线程:
内核线程就是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身。
程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程(Light Weight Process,LWP),轻量级进程就是我们通常意义上所讲的线程,由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。这种轻量级进程与内核线程之间 1 : 1 的关系称为一对一的线程模型,如下图所示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M5ZxfA4I-1578455380544)(8B3E06F0137C477B87B03B9F16BC0A54)]
由于有内核线程的支持,每个轻量级进程都成为一个独立的调度单元,即使有一个轻量级进程在系统调用中阻塞了,也不会影响整个进程继续工作,但是轻量级进程具有它局限性,主要有如下两点
线程的创建、销毁等操作,都需要进行系统调用,而系统调用的代价相对较高,需要在用户态和内核态之间来回切换。
每个轻量级进程都需要有一个内核线程的支持,因此轻量级进程要消耗一定的内核资源(比如内核线程的栈空间),因此一个系统支持轻量级线程的数量是有限的。
用户线程:
从广义上讲,一个线程只要不是内核线程,就可以认为是用户线程。从狭义上讲,用户线程指的是完全建立在用户空间的线程库上,系统内核不能感知线程存在的实现。用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。如果程序实现得当,这种线程不需要切换到内核态,因此操作可以是非常快速且低消耗的,并且可以支持规模更大的线程数量。这种进程与用户线程之间 1 : N 的关系称为一对多的线程模型,如下图所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L3wnLXVl-1578455380545)(469B938FAFF143898A82CDAE08E11022)]
使用用户线程的优势在于不需要内核支援,劣势也在于没有内核的支援,所有的线程操作都需要用户程序自己处理。线程的创建、切换和调度都是需要考虑的问题。因而使用用户线程实现的程序都比较复杂,除了以前在不支持多线程的操作系统中的多线程程序与少数有特殊需求的程序外,现在使用用户线程的程序越来越少了。
二者的对比:
关于用户级线程和内核级线程这两种线程模型的对比,个人认为主要可以从调度、开销、性能这三个角度来看待。
调度:对于用户级线程,操作系统内核不可感知,调度需要由开发者自己实现,内核级线程则与之相反,开发者可以做个甩手掌柜,将调度全权交由操作系统内核来完成。
开销:在前面介绍用户级线程的优点时,也提到了,在用户空间创建线程的开销相比之下会比内核空间小很多。
性能:用户级线程的切换发生在用户空间,这样的线程切换至少比陷入内核要快一个数量级,不需要陷入内核、不需要上下文切换、不需要对内存高速缓存进行刷新,这就使得线程调度非常快捷。
在早期的操作系统中有不支持线程的,都是使用用户线程来实现的,现在都支持线程了,大多数都使用轻量级进程去映射内核线程的手段来实现多线程技术,包括常见的 Windows 和 Linux 就这种一对一的线程模型。
Java 线程的实现
Java 线程在 JDK1.2之前,是基于称为“绿色线程”的用户线程实现的,而在 JDK 1.2 中,线程模型替换为基于操作系统原生线程模型来实现,因此,在目前的 JDK 版本中,操作系统支持怎样的线程模型,在很大程度上决定了 Java 虚拟机的线程是怎样映射的,这点在不同平台上没有办法达成一致,虚拟机规范中也并未限定 Java 线程需要使用哪种线程模型来实现。
举个例子,对于 Sun JDK 来说,它的 Windows 版与 Linux 版都是使用一对一的线程模型实现的,一条 Java 线程就是映射到一条轻量级进程之中,因为 Windows 和 Linux 系统提供的线程模型就是一对一的。
总结
- 多线程是为了充分利用CPU资源,简单的程序设计,提升程序的响应性。
- 线程的创建和java无关和平台有关。
- 多线程操作共享资源时需要同步。