目录
为什么要引入多线程?
为什么线程更轻?
线程和进程的关系
多线程的弊端
Thread类
用Thread类创建一个线程并启动它
用一段通过多线程体现并发执行的效果
start和run的区别
使用jdl自带的工具包jconsole查看当前java进程中的所有线程
调用栈
注意:
jave中创建线程的方法
1.继承Thread,重写run
2.实现Runnable接口
3.使用匿名内部类,继承Thread
4.使用匿名内部类,实现Runnable
5.使用Lambda表达式(最常用的)
Thread常见属性
前台线程和后台线程
isAlive()
isInterrupted()判断线程是否终止
interrupted
join()
sleep
当这个pcb回到就绪队列会被立即执行吗?
线程的状态
线程的几种状态:
多线程的意义
多进程编程已经可以利用cpu的多核资源,解决并发编程了
但是进程太重了
创建一个进程,开销比较大
调度一个进程,开销也比较大
销毁一个进程,开销还是比较大
进程是操作系统资源分配的基本单位,
进程主要重在资源的开销和回收上
因此引入线程,线程也叫轻量进程
解决并发编程的前提下,让创建,销毁,调度的速度更快一些,提高程序执行效率
轻在把申请资源和释放资源的操作省下了.
举个例子
比如说现在有10个人要去饭店吃饭,有俩种方案
一种是10个人分别开10间包间吃饭
一种是10个人开1间包间吃饭
显然第一种开销更大,多花了许多的包间费
第二种开销更小,少花了许多包间费
进程包含线程
一个进程包含1个或多个线程(不能1个都没有)
同一个进程中的线程之间,共用了进程的同一份资源(主要是内存(同1个进程中,线程1new的对象,线程2,3,4也可以用)和文件描述符(线程1打开的文件,线程2,3,4都可以直接使用))
线程是操作系统调度的基本单位
进程的调度相当于,每个进程只有1个线程这样的情况
如果1个线程有多个线程,每个线程都是独立在cpu上调度的
每个线程都有自己的执行逻辑.
一个核心上执行的是一个线程
如果一个进程有线程1和线程2
线程1可能在核心A上执行,
线程2可能在核心B上执行
一个线程也是通过PCB来描述的
1个进程里面可能对应一个PCB,也可能对应多个PCB,取决于这个进程中有多少个线程
PCB描述的特征里面,每个线程都有自己的调度属性(PCB的状态,上下文,优先级,记账信息)
各自记录各自的
但是同1个进程里面的PCB之间,pid是一样的,内存指针和文件描述符也是一样的
增加线程的数量,也不是可以一直提高速度
CPU核心数量有限,线程太多,不少的资源开销反而浪费在资源调度上面了
多线程的情况下,多个进程共享同一份资源空间,可能会发生冲突
线程1和线程2都想要同一份资源,此时就可能发生冲突,
可能会导致线程安全问题
在多进程中,就不会发生这种情况
如果一个线程抛异常,如果处理不好,可能就把整个线程带走了
这个进程中的其它线程也就挂了
Thread类不需要import导入别的包,它是在java.long下面的,默认已经导入了
class MyThread extends Thread{
@Override
public void run() {
System.out.println("hello world");
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
Thread t = new MyThread();
t.start();
}
}
这样做和直接在main方法中打印hello world有什么区别?
如果直接在main方法中打印hello world,我们java进程中主要就是一个线程.
(调用main方法中的线程)主线程
而通过start(),主线程调用start()创建了一个新的线程,新的线程调用run方法
class MyThread extends Thread{
@Override
public void run() {
while(true){
//为了方便观察,此处使用sleep休眠1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("hello thread");
}
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
Thread t = new MyThread();
t.start();
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("hello main");
}
}
}
start是真正创建了一个新的线程,线程是独立的执行流
run只是描述了线程干的活,并没有创建线程,如果直接在main方法中调用run,
此时没有创建新的线程,全是main一个人在干活
比如上面这个代码,把t.start,改成t.run结果截然不同
没有之前的main进程和t线程抢占式执行,因为此时只有main这1个进程.
new Thread对象操作,不创建线程(系统内核里的pcb)
调用start才创建pcb,才有真正的线程
PCB是一个数据结构,体现的是 进程/线程是如何实现的,如何被描述出来的
class MyThread extends Thread{
@Override
public void run() {
System.out.println("hello world");
}
}
class MyRunnable implements Runnable{
@Override
//Runnable 的作用,是描述一个"要执行的任务",run方法描述的是任务要干的活
public void run() {
System.out.println("hello thread");
}
}
public class ThreadDemo2{
public static void main(String[] args){
//描述了一个任务
Runnable runnable = new MyRunnable();
//把任务交给线程来执行
Thread t = new Thread(runnable);
t.start();
}
}
这样做的目的是为了解耦合,让线程和线程要做的任务分离开
如果将来要改代码,不用多线程,使用多进程,线程池......此时代码改动较少
public class ThreadDemo3 {
public static void main(String[] args) {
Thread t = new Thread(){
@Override
public void run() {
System.out.println("hello thread");
}
};
t.start();
}
}
1.创建了一个Thread子类(子类没有名字)
2.创建了子类的实例,并让t指向这个子类
public class ThreadDemo4 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable(){
@Override
public void run() {
System.out.println("hello thread");
}
});
t.start();
}
}
此处创建了一个类,实现了Runnable接口,同时创建了一个实例,并把这个实例传入到Thread的构造方法.
public class ThreadDemo5 {
public static void main(String[] args) {
Thread t = new Thread(()->{
System.out.println("hello world");
});
t.start();
}
}
把任务用Lambda表示
把Lambda传给Thread构造方法
Thread常见构造方法
Thread(Runnab tarfet,String name)
前台线程:
手动创建的线程,默认是前台的,包括main默认也是前台线程
前台线程会阻止进程结束,前台线程的工作没做完,进程是结束不了的
后台线程:
其它的jvm自带的线程都是后台线程
可以使用setDaemon把线程设置成后台线程
后台线程不会阻止进程结束,后台线程的工作没做完,进程也是可以结束的
t本来是一个前台线程,如果t线程的任务不执行完,这个程序是不会结束的,会一直打印hello world
但是我们现在 把t线程设置成后台线程,t线程的任务还没执行完,程序也是可以结束的
在真正调用start之前,调用t.isAlive就是false
调用start之后,调用isAlive就是true
如果内核里线程把任务执行完了,此时线程销毁,pcb也随之释放,
但是Thread t这个对象不一定被释放
调用isAlive为false
public class ThreadDemo6 {
public static void main(String[] args){
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello world");
}
},"baiyang");
t.start();
while(true){
try {
Thread.sleep(1000);
t.isAlive();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
线程终止:不是让线程立即终止,而是通知线程要终止了
线程是否终止,取决于线程的具体代码写法
此时线程可能立即终止
也可能等一会再终止
还可能忽略这个终止
1.使用标志位来控制线程是否要终止
public class ThreadDemo7 {
private static boolean flag = true;
public static void main(String[] args) {
Thread t = new Thread(()->{
while(flag){
System.out.println("hello world");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
flag = false;
}
}
2.使用Thread自带的标志位,来判断isInterrupted()
public class ThreadDemo8 {
public static void main(String[] args) {
Thread t = new Thread(()->{
while (!Thread.currentThread().isInterrupted()){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.interrupt();
}
}
上面这个代码的运行结果是这样的:
触发异常后,程序任然继续执行
使用interrput后,会执行如下三步操作
1.把线程内部的标志位boolean设置为true
2.如果线程在进行sleep,就会触发异常,把sleep唤醒
3.sleep在唤醒的时候,还会把刚才设置的标志位boolean,再设置为false(清空标志位)
这就导致了,当sleep异常被catch完了之后,循环继续执行
我们此时在打印异常后面,加一个break,结束,
就可以立即让线程t终止
我们还可以在catch后面加一些我们想要做的事
比如唤醒后,让线程等待1s,再打印个"彭于晏",才终止
所以interrupted清空标志位的目的是
为了让唤醒sleep之后,线程是立即终止
还是等一会再终止,还是忽略这个终止......
这个选择权交给我们自己
join:等待一个线程
线程是一个随机调度的过程,
等待线程做的事就是,控制俩个线程结束的顺序
public class ThreadDemo9 {
public static void main(String[] args) {
Thread t = new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("hello thread");
});
t.start();
System.out.println("join之前");
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("join之后");
}
}
如果开始执行join的时候,t线程已经执行完毕,此时join不会阻塞,会立即返回
join的几个版本
休眠这个线程多少毫秒
让线程休眠,本质上就是让这个线程不去参与调度了(不去cpu上执行了)
pcb是使用链表的数据结构来组织的
但实际上,并不是一个简单的链表,而是一系列以链表为核心的数据结构
一旦线程进入了阻塞状态,对应的PCB就进入了阻塞队列了,此时线程暂时无法参与调度
比如调用sleep(1000),
对应的线程pcb就要再阻塞队列中待1s
不一定,虽然是sleep(1000),但考虑到实际的调度开销,对应的线程无法在唤醒之后就被立即执行
它需要和其它在就绪队列的pcb一样,抢占cpu,实际时间间隔大于1s
状态是针对当前的线程调度描述的
线程是操作系统调度的基本单位,状态更适合线程
1.NEW 创建了Thread对象,但还是没有调用start(内核还没有创建对应的pcb)
2.TERMINATED 表示内核中的pcb已经执行完毕(内核里的pcb已经销毁),但是Thread对象还在
3.RUNNABLE 可以运行的,包括正在cpu上执行的,和在就绪队列,随时可以去cpu上执行的
4.WAITING
5.TIMED_WAITING
6.BLOCKED
4,5,6都是阻塞(线程pcb在阻塞队列中),这几种状态是不同原因的阻塞
public class ThreadDemo10 {
public static void main(String[] args) {
Thread t = new Thread(()->{
for (int i = 0; i < 100_0000; i++) {
//此处这个循环什么都不做
}
});
//启动之前获取一下t的状态,也就是NEW状态
System.out.println("start之前 "+t.getState());
t.start();
System.out.println("t线程正在执行中 "+t.getState());
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t结束之后 "+t.getState());
}
}
我们再在上面这个代码稍加改动
public class ThreadDemo10 {
public static void main(String[] args) {
Thread t = new Thread(()->{
for (int i = 0; i < 100_0000; i++) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//启动之前获取一下t的状态,也就是NEW状态
System.out.println("start之前 "+t.getState());
t.start();
for (int i = 0; i < 1000; i++) {
System.out.println("t线程正在执行中 "+t.getState());
}
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t结束之后 "+t.getState());
}
}
我们可以看到打印的t线程正在执行中的状态有,RUNNABLE,也有TIME_WAITING,
当t线程没有执行到sleep(10)的时候,就是RUNNABLE,比如正在执行for循环里面(int i = 0;i<100_0000;i++)这些
当t线程执行到sleep(10),t线程发生阻塞,此时再获取t线程的状态就是TIME_WAITING
程序分成
CPU密集,包含大量的 加减乘除 等运算,
IO密集型,涉及到大量的读写操作,比如读写文件,读写控制台,读写网络......
写个代码感受一下多线程的意义
假设当前有俩个变量a,b,需要把俩个变量a,b各自自增100亿次(典型的CPU密集型场景),
串行执行:可以一个线程,先针对a自增,然后再针对b自增
并发执行:还可以俩个线程,线程1对a进行自增,线程2对b进行自增
1.串行执行
public class ThreadDemo11 {
public static void main(String[] args) {
serial();
}
//串行执行
public static void serial(){
//为了衡量代码的执行速度,加上计时操作
//currentTimeMillis 获取当前系统的 ms 级时间戳
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");
}
}
2.并发执行
public class ThreadDemo11 {
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");
}
}
我们可以发先, 俩个线程并发执行,明显更快!
为什么不是刚好缩短到串行执行的一半?
t1和t2不一定都是分布在俩个cpu上执行
不能保证它俩一定是并行执行,也有可能是并发执行
另外,t1和t2在执行过程中,会经历很多次调度,
这些调度,有些是并发(在一个核心上执行),有些是并发(在俩个核心上执行的)
到底是多少次并发,多上次并行,取决于操作系统的配置,和当时程序运行的环境
此外,线程调度自身也是有时间消耗的
总结
因此多线程,在CPU密集的任务中,有非常大的作用,可以充分利用cpu的多核资源,加快程序运行效率
当然多线程在IO密集的任务中也是有作用的
不过使用多线程,也不一定就能提高效率
取决于,是否是多核cpu
当前核心是否空闲(如果这些cpu的核心都已经满载了,这个时候再多线程也没用,反而会花费更多的开销浪费在线程调度上)