从零了解多线程(万字详解)

目录

为什么要引入多线程?

为什么线程更轻?

线程和进程的关系

多线程的弊端

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类

Thread类不需要import导入别的包,它是在java.long下面的,默认已经导入了

用Thread类创建一个线程并启动它

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的区别

start是真正创建了一个新的线程,线程是独立的执行流

run只是描述了线程干的活,并没有创建线程,如果直接在main方法中调用run,
此时没有创建新的线程,全是main一个人在干活

比如上面这个代码,把t.start,改成t.run结果截然不同

 

 

没有之前的main进程和t线程抢占式执行,因为此时只有main这1个进程.

使用jdl自带的工具包jconsole查看当前java进程中的所有线程

 

 

 

调用栈 

 

 注意:

new Thread对象操作,不创建线程(系统内核里的pcb)
调用start才创建pcb,才有真正的线程
PCB是一个数据结构,体现的是 进程/线程是如何实现的,如何被描述出来的

jave中创建线程的方法

1.继承Thread,重写run

class MyThread extends Thread{
    @Override
    public void run() {
            System.out.println("hello world");
    }
}

2.实现Runnable接口

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();
    }
}

这样做的目的是为了解耦合,让线程和线程要做的任务分离开
如果将来要改代码,不用多线程,使用多进程,线程池......此时代码改动较少

3.使用匿名内部类,继承Thread

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指向这个子类

4.使用匿名内部类,实现Runnable

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的构造方法.

5.使用Lambda表达式(最常用的)

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)

 

 Thread常见属性

前台线程和后台线程

前台线程:
手动创建的线程,默认是前台的,包括main默认也是前台线程
前台线程会阻止进程结束,前台线程的工作没做完,进程是结束不了的

后台线程:
其它的jvm自带的线程都是后台线程
可以使用setDaemon把线程设置成后台线程
后台线程不会阻止进程结束,后台线程的工作没做完,进程也是可以结束的

t本来是一个前台线程,如果t线程的任务不执行完,这个程序是不会结束的,会一直打印hello world
但是我们现在 把t线程设置成后台线程,t线程的任务还没执行完,程序也是可以结束的

isAlive()

在真正调用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);
            }
        }
    }
}

 

isInterrupted()判断线程是否终止

线程终止:不是让线程立即终止,而是通知线程要终止了
线程是否终止,取决于线程的具体代码写法

此时线程可能立即终止
也可能等一会再终止
还可能忽略这个终止

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();
    }
}

 

interrupted

上面这个代码的运行结果是这样的:

触发异常后,程序任然继续执行

使用interrput后,会执行如下三步操作

1.把线程内部的标志位boolean设置为true
2.如果线程在进行sleep,就会触发异常,把sleep唤醒
3.sleep在唤醒的时候,还会把刚才设置的标志位boolean,再设置为false(清空标志位) 

这就导致了,当sleep异常被catch完了之后,循环继续执行

我们此时在打印异常后面,加一个break,结束,
就可以立即让线程t终止

我们还可以在catch后面加一些我们想要做的事
比如唤醒后,让线程等待1s,再打印个"彭于晏",才终止

 

所以interrupted清空标志位的目的是
为了让唤醒sleep之后,线程是立即终止
还是等一会再终止,还是忽略这个终止......
这个选择权交给我们自己

join()

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的几个版本

sleep

 

休眠这个线程多少毫秒

让线程休眠,本质上就是让这个线程不去参与调度了(不去cpu上执行了)
 

pcb是使用链表的数据结构来组织的
但实际上,并不是一个简单的链表,而是一系列以链表为核心的数据结构

一旦线程进入了阻塞状态,对应的PCB就进入了阻塞队列了,此时线程暂时无法参与调度

比如调用sleep(1000),
对应的线程pcb就要再阻塞队列中待1s

当这个pcb回到就绪队列会被立即执行吗?

不一定,虽然是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的核心都已经满载了,这个时候再多线程也没用,反而会花费更多的开销浪费在线程调度上)

猜你喜欢

转载自blog.csdn.net/qq_62712350/article/details/128440173
今日推荐