目录
背景知识:
什么是进程?
什么是线程?
线程与进程的区别:
Thread类及常用方法:
循环打印的例子:
start() 和 run() 的区别:
通过监视窗口查看线程:
创建线程:
1.继承 Thread 类, 重写 run() 方法:
2.实现 Runnable 接口:
3.使用匿名内部类,继承 Thread 类:
4.使用匿名内部类,实现 Runnable:
5.使用 Lambda 表达式:
Thread类常见构造方法:
Thread 的几个属性:
getId():
getName():
getSate():
isAlive():
中断一个线程
等待一个线程
获取当前线程引用
休眠当前线程
线程的状态
1.NEW:
2.RUNNABLE:
3.TERMINATED:
4.阻塞状态:
5.线程状态转换流程图:
实例验证多线程效率问题:
背景知识:
什么是进程?
进程是计算机操作系统对一个正在运行的程序的一种抽象,是操作系统内部进行资源分配和调度的基本单位。
在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;
在当代面向线程设计的计算机结构中,进程是线程的容器。
程序是指令、数据及其组织形式的描述,进程是程序的实体。
上面这句话换言之就是,计算机内部要管理任何现实事物,都需要将其抽象成一组有关联的数据,在java中就使用类/对象类描述这种特征。
class PCB {//进程的唯一标识:pid;//进程关联的程序信息,如哪个程序,加载到内存的区域;//使用的资源;//进度调度信息
}
如上面的例子:这样的一个 PCB 对象,就代表了一个运行着的程序,即“进程”; 操作系统再通过各种数据结构将每个 PCB 对象组织起来,方便对这些进程进行管理。
什么是线程?
使用线程的主要目的是为了解决“并发编程”问题。每一个线程就是一个“执行流”,每个线程按照一定的顺序来执行代码,多个线程可以同时执行多份代码;
这里举个例子就非常好理解了:我们在餐厅买饭时,档口的工作有很多种,有负责后勤备材料的、有负责烹饪的还有负责打包收费的,如果这些工作都让张三一个人来完成,那么对于他而言工作量自然很大,并且工作效率也有一定影响,那么此时张三就可以再叫来李四和王五来帮忙,这样他们分别负责意见事务,如此一来,三个人各自有不同的任务,但又密不可分,好比一个任务交给了三个执行流来执行,我们就把这种分配叫做多线程模式。同时,洗菜烹饪收费这三件事是有先后顺序的,对应到线程里也就是每个执行流要排队执行,至于为什么要排队执行,这个问题涉及到优化线程随机调度,后面将会仔细讲解。
既然刚开始提到“多线程为了解决‘并发编程’问题”,现在我们展开分析其中道理:
计算机的核心计算逻辑是由 CPU 来控制的,随着技术进展,CPU 的核心也在快速升级,对于并发问题,结合多进程,多核CPU可以起到一定的效果,但是并非 CPU 核心多了程序就一定能排的快了,核心再多不能让部分核心很忙,部分又在闲置,所以这还需要结合具体的程序代码,合理地将多核心运用起来才行。
再谈到多进程吧,其实多进程模式已经可以解决并发问题,并且可以利用起来CPU多核资源了,但是多进程的缺点太明显,那就是“太重”——消耗资源多、速度较慢。为了中和这种缺陷,就产生了“多线程模式”。
线程也可以叫做“轻量级进程”,不仅可以解决并发编程问题,还能使创建、销毁、调度等速度得到显著提升管。
线程与进程的区别:
- 包含关系:
进程包含线程,一个进程可以包含多个线程(不能没有),是一对多的关系,但一个线程不能同时存在于多个进程中;- 公用关系:
同一个进程中的多个线程之间,公用了进程的同一份资源(即内存 和 文件描述符表);
内存:指在 线程1 中 new 的对象在于其处于同一进程的其他线程也能使用;
文件描述符表:指在 线程1 中打开的文件在于其处于同一进程的其他线程也能直接使用,因为操作系统内核是利用文件描述符表来访问文件的,打开现存文件或新建文件时,内核会返回一个文件描述符。读写文件也需要使用文件描述符来指定待读写的文件;- 随机调度:
一个核心上执行一个线程,如果你的一个进程中有两个线程,那么操作系统在调度的时候,那个线程在哪个核心上执行都是有可能的;- PCB的组织:
线程也是通过PCB来描述的,一个线程可能对应一个PCB或者多个,PCB中得到的“状态、上下文、优先级、记账信息等”都是每个线程各自的记录,但是在同一个进程中的PCB之间,“pid、内存指针、文件描述符表”是一样的。- 基本单位:
不要拘泥于教材,站在“轻量级进程”角度而言,操作系统和CPU在实际调度的时候,是以线程为基本单位进行调度的,每个线程也都有自己的执行逻辑;实际中只要涉及到“调度”就基本上和进程没关系了,可以理解为,进程专门负责资源分配,线程主要来接管和调度相关的一切内容;
下面这个生动的例子可以帮助我们更清晰的区别多进程与多线程:
由此图不难提出疑问:既然线程解决了两面性的问题,那线程一定就能更快嘛?
就上面吃蛋糕而言,许多人在一起吃,相当于多个线程来处理同一个任务,就会出现三种情况:
1.桌子大小有限,不够所有人坐(对应CPU核心有限,不能将所有线程调度起来);
2.人太多,有的人正在吃,有的人吃完了又要去拿,推推搡搡导致正在吃的人不能专心吃(对应有的线程正在处理中,其他线程又要对其产生各种干扰);
3.线程太多,在线程调度上开销较高;
4.两个人看上同一块蛋糕(对应线程安全问题);
5.张三把李四的蛋糕抢走了,李四直接生气干扰大家都吃不了了(对应一个线程抛异常,如果处理不好,很可能导致整个进程的崩溃);
看来多线程模式也存在诸多问题,那么下面就要对所有问题一一展开讲解。
Thread类及常用方法:
每个执行流都需要一个对象来描述,而Thread对象就是用来描述执行流的,此时JVM再将这些Thread对象组织起来,作一系列的调度与管理。
Thread类是在java.lang下的,所以用的时候不需要导包。
循环打印的例子:
先写一个交替打印的例子来直观感受一下吧:
首先定义一个类 MyThread 继承 Thread 类,内部方法打印“hello thrad”,接着 main 方法内部实例出一个 t 对象,这个对象就是用来描述线程的,即称为 t 线程,调用 t.start() 方法启动 t 线程,同时 main 方法内部也要打印“hello main”,如此一来,main 方法和MyThread类内部的 run 方法都要打印不同的内容,那么就会因为线程的随机调度和抢占式执行导致“hello Thread” 和 “hello main”形成交替打印。至于先打印哪个,顺序是不一定的,取决于操作系统内部具体调度策略。
class MyThread extends Thread {@Overridepublic void run() {while (true) {System.out.println("hello thread");}}
}
public class ThreadDemo01 {public static void main(String[] args) {Thread t = new MyThread();t.start();while (true) {System.out.println("hello main");}}
}
深入理解:
从微观角度来看:main() 方法是一个主线程,start() 方法在这里创建了一个新的线程,新的线程负责执行 t.run() 方法,也就是调度操作系统的API,通过操作系统内部创建新的PCB,并且把要执行的指令交给这个PCB,当PCB被调度到CPU上执行时,也就可以执行到线程中 run() 方法了;
从宏观角度来看:如果直接在main()方法内部写一个打印,想要实现交替不是非常容易实现,那么此时程序的进程中就只有一个线程main;如果主线程调用 t,start() ,创建出一个新的线程,新的线程再调用 t.run(),如果run()方法执行完毕,这个新的线程也就自然销毁了。
start() 和 run() 的区别:
执行 start() 就是真正创建一个线程;run() 只是描述了线程要干的活,如果没有创建新的线程,而是直接在 main() 内部调用 run(),那就是 main() 把所有的活都干了。
通过监视窗口查看线程:
在 JDK 中自带了一个可以监视线程的窗口:jconsole
可以直接搜索,也可以在自己电脑JDK的文件夹内找:
选择连接刚才运行的程序,接着同意不安全连接
学会了使用 jconsole 的使用,就能在后续调试过程中方便找出 bug 所在地。
创建线程:
创建线程有很多种写法:
1.继承 Thread 类, 重写 run() 方法:
class MyThreads extends Thread {@Overridepublic void run() {while (true) {System.out.println("my thread");}}
}
2.实现 Runnable 接口:
Runnable 的作用是描述一个要执行的任务,借助了“解耦合”的思想,将 线程 和 线程要干的活 之间分开,这样写的好处在于,如果某一天不需要使用多线程,而是用多进程,或者说线程池、协程时,此时代码改动就会小一些,错误的概率的可以稍作降低。
class MyRunnable implements Runnable {@Overridepublic void run() {System.out.println("hello myRunnable");}
}
public class ThreadDemo03 {public static void main(String[] args) {//用 runnable 来描述一个任务Runnable runnable = new MyRunnable();//将任务交给 t 线程来执行Thread t = new Thread(runnable);t.start();}
}
3.使用匿名内部类,继承 Thread 类:
public class ThreadDemo04 {public static void main(String[] args) {Thread t = new MyThread() {@Overridepublic void run() {System.out.println("匿名内部类");}};t.start();}
}
4.使用匿名内部类,实现 Runnable:
public class ThreadDemo05 {public static void main(String[] args) {Thread t = new Thread(new Runnable() {@Overridepublic void run() {System.out.println("run");}});t.start();}
}
5.使用 Lambda 表达式:
把任务用 lambda 表达式来描述,直接把 Lambda 传给 Thread 构造方法;
public class ThreadDemo06 {public static void main(String[] args) {Thread t = new Thread(() -> {System.out.println("run");});t.start();}
}
Thread类常见构造方法:
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用Runnable描述的“任务”创建对象 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnable target,String name) | 使用Runnable描述的“任务”创建对象,并命名 |
Thread(ThreadGroup group,Runnable target) | 分组管理线程 |
其实给线程起名字,调试时便于观察;
举个例子:
public class ThreadDemo07 {public static void main(String[] args) {Thread t = new Thread(() -> {while (true) {System.out.println("run");}}, "myThread");t.start();}
}
Thread 的几个属性:
属性 | 获取方法 | 解释 |
ID | getId() | ID是线程的唯一标识,不同的线程不会重复 |
名称 | getName() | 构造方法里的名字 |
状态 | getState() | 当前线程所处的状态,下面将仔细介绍 |
优先级 | getPriority() | 优先级高的线程理论上来说更容易被调度到,这个可以获取也可以设置,但一般设置了没有用 |
是否后台线程 | isDeamon() | 后台线程(也叫守护线程)JVM会在一个进程的所有非后台线程结束后,才会结束运行。前台线程不结束,进程就走不完;后台线程没做完,进程是可以结束的,也可以使用setDeamon手动设置为后台线程 |
是否存活 | isAlive() | 简单的理解为 run 方法是否运行结束了。start之前为false;start之后为true |
是否被中断 | isInterrupt() | 下面将大篇幅讲解 |
getId():
public class ThreadDemo07 {public static void main(String[] args) {Thread t = new Thread(() -> {System.out.println("run");}, "myThread");t.start();System.out.println(t.getId());;}
}
getName():
public class ThreadDemo07 {public static void main(String[] args) {Thread t = new Thread(() -> {System.out.println("run");}, "myThread");t.start();System.out.println(t.getName());;}
}
getSate():
public class ThreadDemo07 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {System.out.println("run");}, "myThread");System.out.println(t.getState());t.start();System.out.println(t.getState());Thread.sleep(5000);System.out.println(t.getState());}
}
线程的这三种状态将在下面的内容中大篇幅介绍.
isAlive():
public class ThreadDemo07 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {for (int i = 0; i < 5; i++) {try {Thread.sleep(1000); //保证线程至少执行 5s - 便于观察} catch (InterruptedException e) {e.printStackTrace();}System.out.println("running");}}, "myThread");//①观察 start 之前是否存活System.out.println(t.isAlive());t.start();//②观察 start 之后线程执行过程中是否存活System.out.println(t.isAlive());//③至少等待 8s,观察线程 t线程执行完后是否存活Thread.sleep(8000);System.out.println(t.isAlive());}
}
此处再强调一遍:只有 start 之后才创建真正的线程,start 之后才真正开始做任务,调用 start,就会让内核创建一个 PCB,此时这个 PCB 才表示一个真正的线程。isAlive是在判断当前系统里这个线程是不是真的存在了;
- 调用 start 之前,isAlive为 false;
- 调用 start 之后,isAlive为 true;
如果内核里的线程将 run 里面的活干完了,此时线程就会销毁,PCB也随之消失,但是Thread 创建出来的这个 t 对象还不一定被释放,此时的 isAlive 也是false;
- 如果 t 的 run 还没启动,isAlive 就是 false;
- 如果 t 的 run 启动中,isAlive 就是 true;
- 如果 t 的 run 执行完毕,isAlive 就是 false;
上面这一部分,也就可以称之为“启动一个线程”;
中断一个线程
首先列出中断线程相关的方法:
方法 | 说明 |
public void interrupt() | 中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,否则设置标志位 |
public static boolean interrupted() | 判断当前线程的中断标志位是否设置,调用后清除标志位 |
public boolean isInterrupted() | 判断对象关联的线程的标志位是否设置,调用后不清除标志位 |
代码示例:
public class ThreadDemo09 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {while (!Thread.currentThread().isInterrupted()) {System.out.println("running");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});t.start();Thread.sleep(3000);t.interrupt();}
}
执行结果:
从结果来看,我们会疑惑:既然我都让线程终止了,为什么它报异常后又执行下去了呢?
我们一步步来看:
其实,该程序在执行时,sleep遇到异常后虽然配合进程终止了,但随后又清除了标志位,将原来的 false 改为了 true。(同样,wait / join 在线程阻塞挂起时也会有这样的效果);
因此,我们这里就要强调:中断线程的含义,并非真正地让进程立即停止,而只是一个“通知停止”的效果,至于线程是否真正停止,何时停止,取决于线程内代码的具体实现;
为了解决这一问题,将上述代码修改一下再来看:
①第一种: 将catch的语句修改成结束语句即可
或者:
②第二种:让它遇到异常后稍微等待一会儿再结束
还有其他各种处理方法,具体为什么要这么设计?其实就是为了唤醒之后,线程到底是否要终止?何时终止?选择权交给了程序员自己。
以上紫色字体内容甚为重要!!!
等待一个线程
相关方法:
方法 | 说明 |
public void join() | 等待某个线程结束,“死等” |
public void join(long millis) | 等待某个线程结束,最多等millis毫秒 |
public void join(long millis,int nanos) | 精度更高 |
代码实例:
public class ThreadDemo10 {public static void main(String[] args) {Thread t = new Thread(() -> {for (int i = 0; i < 3; i++) {System.out.println("running");}try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}});t.start();System.out.println("join之前");try {t.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("join之后");}
}
原本执行完 start 后,t 线程和 main 线程分头行动,并发执行,但是 t.join() 就起到了等待作用,使 main 线程等待 t 线程执行结束再继续执行自己,在这段代码中也就体现在打印了三次“running”之后,t 线程阻塞三秒,之后 main 继续执行。
获取当前线程引用
这个方法在中断线程中使用过.这是一个静态方法
方法 | 说明 |
public static Thread currentThread() | 返回当前线程对象的引用 |
休眠当前线程
相关方法:
方法 | 说明 |
public static void sleep(long millis) throws InterruptedException | 休眠当前线程 millis 毫秒 |
public static void sleep(long millis,int nanos) throws InteruptedException | 更高精度的休眠 |
休眠线程本质上就是让这个线程不参与调度了,可以理解为,有两个队列,一个是执行队列,另一个是阻塞队列(后面介绍),被sleep的线程就会从执行队列进入到阻塞队列中(这个状态也叫作“hang”)。
一旦线程进入阻塞状态,对应PCB也就进入阻塞队列了,此时就暂时无法参与调度,比如调用 sleep(1000),对应的线程PCB就要在阻塞队列中等待 1000ms ,试想:当这个 PCB 回到了就绪队列,会立即被调度吗?其实虽然是 sleep(1000),但考虑到还有地调度的开销,实际时间就要大于 1000ms.
线程的状态
通过下面这段代码我们可以观察到线程的所有状态:
public class ThreadDemo11 {public static void main(String[] args) {for (Thread.State state : Thread.State.values()) {System.out.println(state);}}
}
进入Thread.State内部就可以看到六种状态:
1.NEW:
创建了 Thread 对象,但是还没有调用 start ,内核还没有创建对应PCB;
2.RUNNABLE:
可运行的,它有两种情况:正在CPU上执行的,或者在就绪队列中,随时可以去CPU上执行的;
3.TERMINATED:
表示内核中的PCB已经执行完毕,但是 Thead 对象依然在;
4.阻塞状态:
WAITIING、TIMED_WAITING、BLOCKED都是线程PCB在阻塞队列中,下面会有介绍;
5.线程状态转换流程图:
通过运行代码来观察:
public class ThreadDemo12 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {});System.out.println("start之前:" + t.getState());t.start();System.out.println("执行中:" + t.getState());t.join();System.out.println("执行后:" + t.getState());}
}
实例验证多线程效率问题:
CPU密集问题:假设有两个变量,需要把这两个变量都自增1000亿次。
两种处理方法:
第一种,串行执行,使用一个线程,先对a自增,再对b自增:
public class ThreadDemo13 {public static void main(String[] args) {serial();}public static void serial() {//获取时间long beg = System.currentTimeMillis();long a = 0;for (long i = 0; i < 100_0000_0000L; i++) {a++;}long b = 0;for (long i = 0; i < 100_0000_0000L; i++) {b++;}long end = System.currentTimeMillis();System.out.println("执行时间:" + (end - beg) + " ms");}
}
第二种,并发执行,两个线程同时分别对a和b自增:
public class ThreadDemo14 {public static void main(String[] args) {concurrency();}public static void concurrency() {Thread t1 = new Thread(() -> {long a = 0;for (long i = 0; i < 100_0000_0000L; i++) {a++;}});Thread t2 = new Thread(() -> {long b = 0;for (long i = 0; i < 100_0000_0000L; i++) {b++;}});long beg = System.currentTimeMillis();t1.start();t2.start();try {t1.join();t2.join();} catch (InterruptedException e) {e.printStackTrace();}long end = System.currentTimeMillis();System.out.println("并发执行时间:" + (end - beg) + " ms");}}
可见,多线程在这种 CPU 密集型的任务中,有非常大的作用,可以充分利用 CPU 的多核资源,从而加快程序的运行效率;
除此之外还需要注意,并非使用多线程就一定能提高效率:一方面要看是否多核、另一方面还要满足核心空闲才行。
这一篇幅重点介绍了多线程的基础知识,后续文章讲逐一展开多线程实际终点问题“线程安全”“Synchronized锁的使用”“单例模式”“锁策略”“接口、原子类”等面试常考知识点