进程与多线程——初阶

目录

一、计算机基础知识

1.1 冯诺依曼体系:

1.2 什么是进程:

1.3、进程结构体里有哪些属性?(属性非常多,只挑几个核心的)

1.4 并发与并行:

1.5 进程状态

1.6 进程的优先级

1.7 进程的上下文

1.8 进程的记账信息

1.9 进程间通信

1.10 内存分配——内存管理

二、进程与线程

2.1 为何要有线程

2.2 进程与线程的区别

2.3 Java 的线程 和 操作系统线程 的关系

2.4 创建线程

其他变形

三、Thread 类及常见方法

3.1 Thread 的常见构造方法

3.2 Thread 的几个常见属性

3.3 中断一个线程

3.4 等待一个线程-join()

3.5 获取当前线程的引用

 四、线程的状态

4.1 线程的所有状态

4.2 线程的状态和状态转移的意义

 五、线程安全问题

六 synchronized 关键字 

6.1 synchronized 的特性

1. 互斥

6.2 synchronized 使用示例

七 volatile 关键字

7.1 内存可见性:

7.2 volatile 不保证原子性

7.3 指令重排序

八. wait 和notify

8.1 wait() 方法

8.2 notify() 方法 

8.3 notifyAll() 方法

8.4 wait 和sleep 的对比(面试题)

九 多线程案列

9.1 单例模式

9.1.1 饿汉模式

9.1.2 懒汉模式-单线程模式

9.1.3 懒汉模式-多线程版

 9.1.4 懒汉模式-多线程(改进)

9.2 阻塞队列 

9.2.1 什么是阻塞队列

9.2.2 生产者消费者模型

9.2.3 标准的阻塞队列

9.3 定时器

9.3.1 标准库中的定时器

9.3.2 实现定时器 

9.4 线程池

9.4.1 标准库中的线程池

 9.4.2 工厂模式

 9.4.3 线程池的构造

9.4.4 实现线程池

十. 对比线程和进程

10.1 线程的优点

10.2 进程与线程的区别


一、计算机基础知识

1.1 冯诺依曼体系:

进程与多线程——初阶_第1张图片

~ CPU中央处理器:进行算术运算和逻辑判断

~ 存储器:分为外存和内存,用于存储数据(使用二进制方式存储)

~ 输入设备:用户给计算机发号施令的设备

~ 输出设备:计算机给用户汇报结果的设备

针对存储空间

硬盘>内存>>CPU

针对数据访问速度

CPU>>内存>硬盘 

1.2 什么是进程

进程是操作系统对一个正在运行的程序的一种抽象,换言之,可以把进程看做程序的一次运行过程;同时,在操作系统中,进程又是操作系统进行资源分配的基本单位。(此处涉及到资源包括不限于内存,硬盘,cup等~~)

1.3、进程结构体里有哪些属性?(属性非常多,只挑几个核心的)

1.pid :每个进程需要有一个唯一的身份表示~~

2.内存指针:当前这个进程使用的内存是哪部分~~

3.文件描述符表:(文件:比如:硬盘上存储的数据,往往就是以文件为单位进行整理的)进程每 次打开一个文件,就会产生一个“文件描述符”(标识了这个被打开的文件)一个进程可能会打开很多文件,对应了一组文件描述符。把这些文件描述符放到一个顺序表这样的结构里,就构成文件描述符表 

1.4 并发与并行:

并行:同一时刻,两个核心,同时执行两个进程,此时这俩进程就是并行执行的

并发:一个核心,先执行进程1,执行一会之后,再去执行进程2,在执行一会之后,再去执行进程3...此时只要这里的切换速度足够快,进程1,2,3 就是“同时”执行

由于该操作完全是由操作系统自身控制的,程序员感知不到,所以很多时候就把并行+并发统称为并发

1.5 进程状态

简单认为,进程状态主要是这两个:

就绪态:该进程已经准备好,随时可以上CPU执行;

阻塞态:该进程暂时无法上CPU执行

1.6 进程的优先级

进程之间的调度不一定是“公平”的~~有的要优先调度

1.7 进程的上下文

上下文,就是描述了当前进程执行到哪里这样的“存档纪录”,进程在离开CPU 的时候就要把当前运行的中间结果“存档”,等到下次进程回来CPU上,再恢复之前的“存档”,从上次的结果继续往后执行~~

1.8 进程的记账信息

统计了每个进程,在CPU上执行了多久了,可以作为调度的参考依据

1.9 进程间通信

如上所述,进程是操作系统进行资源分配的最小单位,这意味着各个进程互相之间是无法感受到对方存在的,这就是操作系统抽象出进程这一概念的初衷,这样变带来了进程之间互相具备“隔离性”。但有时候,需要进程之间进行交互,相互配合,进行“信息交换”。

目前,主流操作系统提供的进程通信机制有如下:

1.管道

2.共享内存

3.文件

4.网络

5.信号量

6.信号

1.10 内存分配——内存管理

操作系统对内存资源的分配,采用的是空间模式——不同进程使用内存中的不同区域,互相之间不会干扰。

进程与多线程——初阶_第2张图片

进程与多线程——初阶_第3张图片

二、进程与线程

2.1 为何要有线程

首先,“并发编程”成为刚需

(1)单核CPU的发展遇到了瓶颈,要想提高算力,就需要多核CPU,而并发编程更能充分利用多核CPU资源

(2)有些任务场景需要“等待IO”,为了等待IO的时间能够做一些其他的工作,也需要用到并发编程。

其次,虽然多进程也能实现并发编程,大师线程比进程更轻量

(1)创建线程比创建进程更快

(2)销毁线程比销毁进程更快

(3)调度线程比调度进程更快

最后,线程虽然比进程轻量,但是人们还不满足,于是又有了“线程池”和“协程”

2.2 进程与线程的区别

(1)进程是包含线程的,每个进程至少有一个线程存在,即主线程

(2)进程和进程之间不共享内存空间和文件描述符表,同一个进程的线程之间共同享用一个内存空间和文件描述符表

(3)进程是系统分配资源的最小单位,线程是系统调度的最小单位。

(4)进程之间具有独立性,一个进程挂了,不会影响到别的进程;同一个进程里的多个进程之间,一个线程挂了,可能会把整个进程带走,影响到其他线程。

2.3 Java 的线程 和 操作系统线程 的关系

线程是操作系统中的概念,操作系统内核实现了线程这样的机制,并且对用户提供了一些API供用户使用(例如 Linux 的 pthread 库)

Java 标准库中的Tread 类可以视为是对操作系统提供的API进行了进一步的抽象和封装。

2.4 创建线程

使用 jconsole 命令观察线程

E:\Program Files\Java\jdk1.8.0_192\bin  //此时是我的 jconsole.exe 所在的文件目录

进程与多线程——初阶_第4张图片

方法 1 继承 Thread 类

(1)继承Thread 来创建一个线程类

class  myThread extends Thread{
    @Override
    public void run() {
        while (true) {
            System.out.println("hello t");
        }
    }
}

(2)创建 MyThread 类的实例

 Thread thread=new myThread();

(3)调用start 方法启动线程

   thread.start();//线程开始运行

方法 2 实现 Runnable 接口

(1)实现 Runnable

class MyRunnable implements  Runnable{
    @Override
    public void run() {
        System.out.println("r");
    }
}

(2)创建 Thread 类实例,调用 Thread 的构造方法时将 Runnable 对象作为target 参数

    Thread t=new Thread(new MyRunnable());

(3)调用 start 方法

t.start();

其他变形

匿名内部类创建 Thread 子类对象

public class ThreadDemo3 {
    public static void main(String[] args) {
        Thread t=new Thread(){
            @Override
            public void run() {
                System.out.println("hello kaikai");
            }
        };
        t.start();
    }
}

匿名内部类创建 Runnable 子类对象

public class ThreadDemo4 {
    public static void main(String[] args) {
        Thread t=new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello kaikai");
            }
        });
        t.start();
    }
}

lambda 表达式创建 Runnable 子类对象

public class ThreadDemo5 {
    public static void main(String[] args) {
        Thread t=new Thread(()->{
            System.out.println("hello kaikai");
        });
        t.start();
    }
}

run() 方法是入口方法,可以简单理解为主线程中的main方法,可以把想要执行的语句放入run() 方法中

三、Thread 类及常见方法

3.1 Thread 的常见构造方法

进程与多线程——初阶_第5张图片

 创建的Thread 对象没有命名,默认是Thread-0,Thread-1,Thread-2....

3.2 Thread 的几个常见属性

进程与多线程——初阶_第6张图片

 1)ID 是线程的唯一标示符,不同的线程不会重复

2)名称是我们在构造方法中所设置的名字

3)状态标示线程当前所处的一个状态

4)优先级高的线程理论上来说更容易被调度到

5)关于后台线程,需要记住一点: JVM会在一个线程的所有非后台线程结束后,才会结束运行。(创建的线程默认是前台的,可以通过setDaemon() 设置成后台的

6)是否存活,即简单的理解,为run 方法是否运行结束了

7)线程的中断

public class ThreadDemo5 {
    public static void main(String[] args) {
        Thread t=new Thread(()->{
                System.out.println("hello kaikai");
        },"我的名字");
        t.start();
        try{
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //上述t线程没有进行任何循环和sleep,意味着里面的代码会迅速执行完毕,
        //main 线程如果sleep 结束,此时 t 基本就是已经执行完了的状态,此时t对象还在
        //但是在系统中对应的线程已经结束了
        System.out.println(t.isAlive());
    }
}

此处说“基本”是比较留有余地的,以为可能存在极端情况,主线程sleep 结束了,t 线程仍然没有去CPU 上执行,会存在,概率很小。系统对于线程的调度,是随机的,假设在你的机器上有很多很多的线程,此时CPU调度一圈,消耗的时间就可能非常长,此时就可能导致某个线程隔离很久也没有调度上去。

setDaemon() 

public class ThreadDemo6 {
    public static void main(String[] args) {
        Thread t=new Thread(()->{
            while (true){
            }
        });
        //线程设置为后台,进程不会结束
        //t.setDaemon(false);
        //线程设置为前台,就会结束
        t.setDaemon(true);
        t.start();
    }
}

3.3 中断一个线程

(1)通过设置变量来控制线程中断

public class ThreadDemo7 {
    public static boolean flag=true;
    public static void main(String[] args) {
        //注意,此时不能将变量设为局部变量,变量捕获,若为局部变量,必须为final修饰的常量或变量的值不能修改!!
        //bollean flag=true;
        Thread t=new Thread(()->{
            while (flag){
                System.out.println("hello t");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t线程结束");
        });
        t.start();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag=false;
    }
}

(2)通过使用 Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替自定义标志位。

Thread 内部包含了一个 boolean 类型的变量作为线程是否被中断的标记

 进程与多线程——初阶_第7张图片

package threading;

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: zhao3
 * Date: 2023-05-28
 * Time: 22:46
 */
public class ThreadDemo10 {
    public static void main(String[] args) {
        Thread t=new Thread(()->{
            //currentThread 是获取到当前线程实例
            //此处 currentThread 得到的对象时 t
            //isInterrupted 就是 t 对象里自带的一个标志位
            //两种方法都可以
            while(!Thread.currentThread().isInterrupted()){
            //while (!Thread.interrupted()){
                    System.out.println("hello t");
                    try {
                        Thread.sleep(1000);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                        //break;
                    }
                }
        });
        t.start();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //把 t内部的标志位给设置成true
        t.interrupt();
    }
}

进程与多线程——初阶_第8张图片注:如果设置 interrupt 的时候,恰好,sleep 刚醒,这个时候赶巧执行到下一轮循环的条件判断就直接结束了 。但是这种概率十分低。毕竟sleep 的时间占据了循环体的 99.9999%的时间.

此时,发现 3s 时间到调用 t.interrupt 方法的时候,线程并没有真的结束,而是打印了异常信息,又继续执行了。此时就要谈及到 interrupt 方法的作用:

1.设置标志位为true

2.如果线程正在阻塞中(比如在执行 sleep),此时就会把阻塞状态唤醒,通过抛出异常的方式让sleep 立即结束。

注意!!! 一个非常重要的问题!!!

当sleep 被唤醒的时候 ,sleep 会自动的把 isInterruped 标志位给清空(true-> false),这就导致下次循环,循环仍然可以继续执行

如果需要结束循环,就得在catch中搞个break。

thread 收到通知的方式有两种:

1. 如果线程因为调用 wait/jion/sleep 等方法而阻塞挂掉,则以 InterruptedException 异常的形式通知,清除中断标志

· 当出现 InterruptedException 的时候,要不要结束线程取决于catch 中代码的写法。可以选择忽略这个异常,也可以跳出循环结束线程。

2.否则,只是内部的一个中断标志被设置,thread 可以通过

· Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志位(即标志位变为false)

· Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志

这种方式通知收到的更及时,即使线程正在 sleep 也可以马上收到。

3.4 等待一个线程-join()

有时我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。例如,张三只有等李四转账成功,才决定是否存钱,这时我们需要一个方法明确等待线程的结束。

进程与多线程——初阶_第9张图片

    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(()->{
            System.out.println("hello t");
        });
        t.start();
        //若t线程没有执行完毕,主线程就处于阻塞等待状态
        //t.join();
        System.out.println("hello world");
    }

 谁调用join(),谁就先执行

3.5 获取当前线程的引用

这个方法在上面的线程的中断是我们已经使用过了

进程与多线程——初阶_第10张图片

public class ThreadDemo10 {
    public static void main(String[] args) {
        Thread thread=Thread.currentThread();
        System.out.println(thread.getName());
    }
}

//打印结果:
//main

 四、线程的状态

4.1 线程的所有状态

(1)NEW:系统中的线程还没有创建出来,只是有个Thread对象

(2)TERMINATED:系统中的线程已经执行完了,Thread对象还在

(3)RUNNABLE:就绪状态(就绪状态又分为两种:

   ①正在  CPU 上运行

   ②准备好随时可以去 CPU 上运行)

(4)TIMED_WAITING 指定时间等待 .sleep方法

(5)WAITING: 使用wait出现的状态

(6)BLOCKED: 等待锁出现的状态

    public static void main(String[] args) {
        Thread t=new Thread(()->{
            while (true){

            }
        });
        System.out.println(t.getState());
        t.start();
        System.out.println(t.getState());
    }
//NEW
//RUNNABLE



    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(()->{
        });
        System.out.println(t.getState());
        t.start();
        //sleep是为了确保线程执行完毕
        Thread.sleep(1000);
        System.out.println(t.getState());
    }
//NEW
//TERMINATED


    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(()->{
            while(true){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        System.out.println(t.getState());
        t.start();
        Thread.sleep(1000);
        System.out.println(t.getState());
    }
//NEW
//TIMED_WAITING

4.2 线程的状态和状态转移的意义

进程与多线程——初阶_第11张图片

 五、线程安全问题

class Counter{
    int count=0;
    public void add(){
        this.count++;
    }

    public int getCount() {
        return count;
    }
}
public class ThreadDemo11 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter=new Counter();
        Thread t1=new Thread(()->{
            for (int i=0;i<50000;i++){
                counter.add();
            }
        });
        Thread t2=new Thread(()->{
            for (int i=0;i<50000;i++){
                counter.add();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.getCount());
    }
}

以上代码我们的预期结果应该是100 000,但结果好像是随机值,这就涉及到多线程的安全问题了

count++操作,本质上是三个CPU指令构成

1.load,把内存中的数据读取的CPU寄存器中

2.add,就是把寄存器中的值,进行+1 运算

3.save,把寄存器中的值写回到内存中

以下只是运行出现的几种情况(并不全),无论是那种情况,只有两种方式得到的结果是100000的,那就是t1 save执行完后再执行 t2,或者执行玩t2的 save 后在执行 t1。

进程与多线程——初阶_第12张图片

六 synchronized 关键字 

6.1 synchronized 的特性

1. 互斥

synchronized 会起到互斥效果,某个线程执行到某个对象的synchronized 中时,其他线程如果也执行到同一个对象, synchronized 就会阻塞等待.

进入 synchronized 修饰的代码块,相当于 加锁

退出 synchronized 修饰的代码块,相当于 解锁

synchronized 用的锁是存在 java 对象里头的.

可以粗略理解成, 每个对象在内存中存储的时候,都有一块内存表示当前 "锁定" 状态(类似于厕所的 "有人/无人")

如果是 "无人" 状态,那么就可以使用,使用时需要设为 " 有人" 状态

如果是 "有人" 状态,那么其他人无法使用,只能排队(阻塞等待)

什么是阻塞等待?

针对每一把锁,操作系统内部都维护了一个等待队列,当这个锁被某个线程占有的时候,其他线程尝试进行加锁,就加不上,就会阻塞等待, 一直等到之前的线程解锁之后,由操作系统唤醒一个新的线程,再来获取到这个锁.

注意:

•  上一个线程解锁之后,下一个线程并不是立即就能获取到锁, 而是要靠操作系统来"唤醒". 这也就是操作系统线程调度的一部分工作

•   假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 在尝试获取锁, 此时 B 和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则.

6.2 synchronized 使用示例

1. 直接修饰普通方法: 锁的 demo1 对象

public class demo1 {
    public synchronized void count(){     
    }
}

2. 修饰静态方法: 锁的是demo1类的对象

public class demo1 {
    public synchronized static void count(){     
    }
}

3. 修饰代码块: 明确指定锁哪个对象

锁当前对象 

public class demo1 {
    public void count(){
        //this 可以换成其他对象,只要是 object 的子类
        //此时 this 是谁,就对谁加锁
        synchronized (this){

        }     
    }
}

锁类对象

public class demo1 {
    public void method(){
            synchronized(SynchonizedDemo.class){

        }     
    }
}

七 volatile 关键字

7.1 内存可见性:

 在以下代码中:

1.  创建两个线程 t1 和 t2

2.  t1 中包含一个循环, 这个循环以 flag == 0 为循环条件.

3.  t2 中从键盘读入一个整数, 并把这个整数赋值给 flag.

4.  预期当用户输入非 0 的值的时候, t1 线程结束

public class demo1 {
    public static int flag=0;

    public static void main(String[] args) {
        Thread t1=new Thread(()->{
           while (flag==0){
               //do nothing
           }
        });
        Thread t2=new Thread(()->{
            Scanner scanner=new Scanner(System.in);
            flag=scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}


但是当用户执行输入非 0 的值时, t1 线程循环并不会结束(这显然是一个bug)

这就涉及到了编译器优化, jvm 在执行代码时,能够智能的调整你的代码执行逻辑, 保证程序结果不变的前提下(编译器对于 "程序结果不变" 单线程下判定是非常准确地!!!,但是多线程就不一定,可能导致,调整之后,效率提高了,但是结果变了!!! 编译器出现了误判,引起了bug),通过加减语句,通过语句变换,通过一系列操作,让整个程序执行的效率大大提升.

此处执行到while 循环, flag==0, 就要经历以下两个步骤

•  load 从内存读取数据到CPU 寄存器

  cmp 比较寄存器里的值是否是 0

注意: 此处的两个操作, load 的时间开销远远高于 cmp. 读取内存虽然比读硬盘快(块几千倍), 但是, 读寄存器, 比读内存又要快(块几千倍)

编译器在执行代码时就发现:

•  load 的开销很大

•  每次 load 的结果都一样

此时编译器就做了一个非常大胆的操作,把load 给优化掉了(去掉了),只要第一次执行 load 才真正执行了. 后续都只 cmp ,不 load(反复利用之前寄存器 load 过的值).

正确的修改方式:

volatile public static int flag=0;

代码读取到 volatile 修饰的变量的时候, 会强制从主内存中读取 volatile 变量的最新值到线程的工作内存中

7.2 volatile 不保证原子性

volatile 使用的场景是一个线程读,一个线程写的情况

synchronized 则是多个线程写

7.3 指令重排序

为了代码的执行效率,jvm会在执行代码前,修改代码的执行逻辑,来提高代码的执行效率.由于指令重排序在多线程中是随机的,无法用代码很好的表示,以下简单用语言叙述

假设有两个线程:

t1:

Student s=new Student();

此时大体可以分成三个步骤:

1. 申请内存空间

2. 调用构造方法(初始化内存数据)

3. 把对象的引用赋值给s (内存地址的赋值)

t2:

if(s!= null)

s.learn;coga

假设 t1 按照 1 3 2 的顺序执行,当 t1 执行完1 3 后,即将执行2 的时候, t2 开始执行,由于 t1 的 3 已经执行过了,这个引用已经非空了! t2 就尝试调用 s.learn. 但是由于 t1 还是个毛坯房, 没有初始化过,此时的 learn 会成啥样, 就不知道了, 很可能产生 bug.

以上场景可以使用 volatile解决,给 s 使用 volatile进行修饰,创建就会禁止指令重排序. 也可以通过加锁解决(创建对象时,对代码进行加锁)

八. wait 和notify

完成这个协调工作, 主要涉及到三个方法:

wait()/wait(long timeout): 让当前线程进入等待状态

notify()/notifyAll(): 唤醒在当前对象上的等待的线程.

注意: wait, notify, notifyAll 都是 Object 类的方法

8.1 wait() 方法

wait 做的事情:

使当前执行代码的线程进行等待. (把线程放到等待队列中)

释放当前的锁

满足一定条件时被唤醒, 重新尝试获取这个锁

wait 要搭配 synchronized 来使用, 脱离synchronized 使用 wait 会直接抛出异常.

wait 结束等待的条件: 

其他线程调用该对象的 notify 方法

wait 等待时间超时 (wait 方法提供的有一个带 timeout 参数的版本,来指定等待时间)

其他线程用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常

    public static void main(String[] args)throws InterruptedException {
        Object object=new Object();
        synchronized (object){
            System.out.println("等待");
            object.wait();
            System.out.println("等待结束");
        }
    }

 这样在执行到object.wait()之后就一直等待下去, 那么程序肯定不能这么一直等待下去了. 这个时候就需要使用到了另外一个方法唤醒: notify().

8.2 notify() 方法 

notify 方法是唤醒等待的线程.

方法notify() 也要在同步方法或同步块中调用

•  如果有多个线程等待, 则有线程调度器随机挑选出一个呈 wait 状态的线程.(不遵循"先来后到")

在 notify() 方法后,当前线程不会马上释放该对象锁, 要等到执行 notify() 方法的线程将程序执行完, 也就是退出同步代码块之后才会释放对象锁.

代码示例:

 public static void main(String[] args)throws InterruptedException {
        Object object=new Object();
        Thread t1=new Thread(()->{
            synchronized (object){
                try {
                    System.out.println("wait 开始");
                    object.wait();
                    System.out.println("wati 结束");
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t1.start();
        Thread.sleep(1000);
        Thread t2=new Thread(()->{
            synchronized (object){
                System.out.println("notify 开始");
                object.notify();
                System.out.println("notify 结束");
            }
        });
        t2.start();
    }

8.3 notifyAll() 方法

notify 方法只是唤醒某一个等待线程,使用 notifyAll 方法可以一次唤醒所有等待线程.

范例:

class  WaitTask implements Runnable{
    private  Object object;
    public WaitTask(Object object){
        this.object=object;
    }
    @Override
    public void run() {
        synchronized (object){
            System.out.println("wait 开始前");
            try {
                object.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("wait 开始后");
        }
    }
}
class NotifyTask implements Runnable{
    private Object object;
    public NotifyTask(Object object){
        this.object=object;
    }
    public void run(){
        synchronized (object){
            System.out.println("notify 开始前");
            object.notifyAll();
            System.out.println("notify 开始后");
        }
    }
}
public class Demo1 {
    public static void main(String[] args)throws InterruptedException {
        Object object=new Object();
        Thread t1=new Thread(new WaitTask(object));
        Thread t2=new Thread(new WaitTask(object));
        Thread t3=new Thread(new WaitTask(object));
        Thread t4=new Thread(new NotifyTask(object));
        t1.start();
        t2.start();
        t3.start();
        Thread.sleep(1000);
        t4.start();
    }
}

注意: 虽然是同时唤醒 3 个线程, 但是这3个线程需要竞争锁,所以并不是同时执行,而让然是有先有后执行的.

8.4 wait 和sleep 的对比(面试题)

wait 和sleep 唯一相同的点就是都可以让线程放弃执行一段时间

不同点:

• 初心不同, wait 解决的事线程之间的顺序控制 , sleep 单纯是让当前线程休眠一会

•  wait 需要搭配 synchronized 使用, sleep 不需要

wait 是 Object 的方法, sleep 是Thread 的静态方法

九 多线程案列

9.1 单例模式

单例模式能保证某个类在程序中只存在唯一一份实例, 而不是创建出多个实例.

单例模式具体的实现方式, 分成"恶汉" 和 "懒汉" 两种.

9.1.1 饿汉模式

class Singleton{
    private static Singleton singleton=new Singleton();

    public Singleton getSingleton(){
        return singleton;
    }
    //设为private 进制外部访问 new 实例.
    private Singleton(){}
}

9.1.2 懒汉模式-单线程模式

类加载的时候不创建示例. 第一次使用的时候才创建实例

class  LazySingleton{
    private static LazySingleton singleton;
    public static LazySingleton getInstance() {
        if(singleton==null){
            singleton=new LazySingleton();
        }
        return singleton;
    }
}

9.1.3 懒汉模式-多线程版

上面的懒汉模式的实现是线程不安全的.

线程安全问题在首次创建实例时, 如果多个线程同时调用 getInstance 方法.就可能导致创建出多个实例.

一点实例创建好了. 后面再多线程环境调用 getInstance 就不再有线程安全问题了(不会再次创建 Instance).

加上 synchronized 就可以改善这里的线程安全问题:

   public synchronized static LazySingleton getInstance() {
        if(singleton==null){
            singleton=new LazySingleton();
        }
        return singleton;
    }

 9.1.4 懒汉模式-多线程(改进)

使用双重 if 判定, 降低锁竞争的频率

给 instance 加上了 volatile

class  LazySingleton{
    private volatile static LazySingleton singleton;
    public static LazySingleton getInstance() {
        if(singleton==null){
            synchronized (LazySingleton.class) {
                if (singleton == null) {
                    singleton = new LazySingleton();
                }
            }
        }
        return singleton;
    }
}

理解双重 if / volatile 

加锁/ 解锁是一件开销比较高的事情, 二懒汉模式的线程不安全只是发生在首次创建示例的时候.因此后续使用的时候,不必在进行加锁了.

外层的 if 就是判定下看当前是否已经把  singleton 实例创建了, 内层是多个线程在等待锁的过程, 防止获得到锁后多次创建.

volatile 是防止在创建 singleton 实例时发生指令重排序:

1. 创建内存(买房子)

2. 调用构造方法 (装修)

3. 把内存地址赋给引用(拿到钥匙)

若发生指令重排序,导致 2 和 3 的顺序发生了变化, 此时若线程 t1 执行完三后, 系统调度线程 t2 ,再去判断条件,发现条件不成立,非空,此时 t2 拿到的就是一个没有装修毛坯房,接下来 t2 再调用 singleton 的方法,就会出行bug.

9.2 阻塞队列 

9.2.1 什么是阻塞队列

阻塞队列是一种特殊的队列, 也遵守"先进先出" 的原则.

阻塞队列是一种线程安全的数据结构,并且具有以下特性:

• 当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素

• 当队列空的时候, 继续出队列就会阻塞, 直到有其他线程往队列中插入元素

阻塞队列的一个典型应用场景就是"生产这消费者模型". 这是一中非常典型的开发模型

9.2.2 生产者消费者模型

生产者消费者模式就是通过一个容器解决生产者和消费者的强耦合问题.

生产者和消费者彼此之间不直接通讯, 而通过阻塞队列来进行通讯, 所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产这要数据,而是直接从阻塞队列里取

(1) 阻塞队列就相当于一个缓冲区, 平衡了生产者消费者的处理能力.

比如在"秒杀" 场景下,服务器同一时刻可能会收到大量的支付请求. 如果直接处理这些请求, 服务器可能扛不住(每个支付请求的处理都需要比较复杂的流程).这个时候就可以把这些请求都放到一个阻塞队列中,然后再由消费者线程慢慢的来处理每个支付请求.

这样可以有效进行"削峰" ,防止服务器被突然到来的一波请求直接冲垮

同时也可以在用户量较少时,从阻塞队列中获取访问,按照正常速度执行.也就是"填谷"

(2) 阻塞队列也能使生产者和消费者之间解耦

比如过年一家人一起包饺子.一般都是分工明确, 比如一个人负责擀饺子皮, 其他人负责包, 擀饺子皮的人就是"生产者",包饺子的人就是"消费者".

擀饺子皮的人不关心包饺子的人是谁(能报就行,无论是手工包,借助工具包,还是机器包),包饺子的人也不关心擀饺子皮的人是谁(有饺子就行,无论是用擀面杖的,还是从超市买的).而盛放饺子皮的容器就是阻塞队列.

这样即使擀饺子皮的人罢工了,包饺子的人仍可以从容器中取饺子皮正常工作;又或者包饺子的人罢工了,擀饺子皮的人仍可以把擀好的饺子皮放到容器里,正常工作.

9.2.3 标准的阻塞队列

 在 Java 标准库中内置了阻塞队列. 如果我们需要在一些程序中使用阻塞队列, 直接使用标准库中的就可.

• BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue/ArrayBlockingQueue.

• put 方法用于阻塞式的入队列,take 用于阻塞式的出队列

• BlockingQueue 也有 offer, poll , peek 等方法, 但是这些方法不带有阻塞特性.

生产者消费者模型

public static void main(String[] args) throws InterruptedException {
        BlockingDeque blockingDeque=new LinkedBlockingDeque<>();
        Thread customer=new Thread(()->{
           while (true){
               try {
                   int value=blockingDeque.take();
                   System.out.println("消费元素: "+value);
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
           }
        });
        customer.start();

        Thread producer=new Thread(()->{
            int count=1;
           while (true){
               try {
                   blockingDeque.put(count);
                   System.out.println("生产元素: "+count);
                   count++;
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
           }
        });
        producer.start();
        producer.join();
        customer.join();
    }

阻塞队列实现

package ThreadDemo;

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: zhao3
 * Date: 2023-08-10
 * Time: 21:34
 */

class MyBlockingQueue{
    private volatile int []items=new int[1000];
    private volatile int head=0;
    private volatile int tail=0;
    private volatile int size=0;
    public synchronized void put(int value) throws InterruptedException {
        //避免 interrupt一类的原因导致wait中断
        while (size==items.length){
            this.wait();
        }
        items[tail]=value;
        tail++;
        if(tail==items.length){
            tail=0;
        }
        size++;
        this.notify();
    }
    public synchronized int take() throws InterruptedException {
        while (size==0){
            this.wait();
        }
        int value=items[head];
        head++;
        if(head==items.length){
            head=0;
        }
        size--;
        this.notify();
        return value;
    }

}
public class Demo4 {
    public static void main(String[] args) throws InterruptedException {
        MyBlockingQueue queue=new MyBlockingQueue();
        Thread customer=new Thread(()->{
            while (true){
                try {
                    int value=queue.take();
                    System.out.println("消费元素: "+value);
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        Thread producer =new Thread(()->{
            int count=1;
            while (true){
                try {
                    queue.put(count);
                    System.out.println("生产元素: "+count);
                    count++;
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        producer.start();
        customer.start();
    }
}

9.3 定时器

9.3.1 标准库中的定时器

• 标准库中提供了一个Timer类, Timer 类的核心方法为 schedule

• schedule 包含两个参数, 第一个参数指定要执行的任务代码,第二个参数指定多长时间后执行

 public static void main(String[] args) {
     Timer timer = new Timer();
     timer.schedule(new TimerTask() {
         @Override
         public void run() {
             System.out.println("hello kaikai");
         }
     }, 2000);
     System.out.println("hello kai1");
 }

• TimerTask() 本质是Runnable,实现了Runnable接口

• run 方法的执行是靠Timer 内部的线程在时间到了之后执行的! ! !

• Timer 里头内置了线程.(还是前台线程) 会阻止进程结束.

9.3.2 实现定时器 

定时器的构成:

• 一个带优先级的阻塞队列

• 队列中的每个元素是一个 Task 对象

• Task 中带有一个时间属性,队首元素就是即将执行的对象

• 同时有一个线程一直扫描队首元素,队首元素是否需要执行

代码实现:

class  MyTimer{
    Object lock=new Object();
    PriorityBlockingQueue blockingQueue=new PriorityBlockingQueue<>();
    public void schedule(Runnable runnable,long time){
        MyTask task=new MyTask(runnable,time);
        blockingQueue.put(task);
        synchronized (lock) {
            lock.notify();
        }
    }
    public MyTimer(){
        Thread t=new Thread(()->{
            while (true) {
                synchronized (lock) {
                    try {
                        MyTask task = blockingQueue.take();
                        long curTime = System.currentTimeMillis();
                        if (task.time <= curTime) {
                            task.runnable.run();
                        } else {
                            blockingQueue.put(task);
                            lock.wait(task.time - curTime);
                        }
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
            }
        });
        t.start();
    }
}

class MyTask implements Comparable{
    Runnable runnable;
    long time;
    public MyTask(Runnable runnable,long time){
        this.runnable=runnable;
        this.time=System.currentTimeMillis()+time;
    }

    @Override
    public int compareTo(MyTask o) {
        return (int)(this.time-o.time);
    }
}

9.4 线程池

线程池最大的好处就是减少每次启动, 销毁线程的损耗.

实现线程池:

• 核心操作为 submit,将任务加入线程池中

• 使用 Worker 类描述一个工作线程,使用 Runnable 描述一个任务

• 使用一个 BlockingQueue 组织所有的任务

• 每个 worker 线程要做的事情: 不停地从BlockingQueue 中取任务并执行任务

• 指定一个线程池中的最大线程数: maxWorkerCount ,当当前线程数超过这个最大值时;就不再新增线程了.

9.4.1 标准库中的线程池

使用Executors.newFixedThreadPool(10) 能创建固定包含 10 个线程的线程池

返回值类型为 ExecutorService

通过 ExecutorService.submit 可以注册一个任务到线程池中.

    public static void main(String[] args) {
        ExecutorService pool= Executors.newFixedThreadPool(10);
        pool.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello world");
            }
        });
    }

Executors 创建线程的几种方式

• newFixedThreadPool: 创建固定线程数的线程池

• newCachedThreadPool: 创建线程数目动态增长的线程池

• newSingleThreadExecutor: 创建只包含单个线程的线程池

• newScheduledThreadPool: 设定延迟时间后执行命令, 或者定期执行命令,进阶版的Timer

Executors 本质上是 ThreadPoolExecutor 类的封装

创建线程池时,并不是直接new ExecutorService 对象, 而是通过 Executors 类,里面的静态方法, 完成的对象构造.

上述对象的创建,就涉及到了工厂模式.

 9.4.2 工厂模式

工厂模式就是在创建对象时,不再是直接 new ,而是使用方法(通常是静态方法) 协助我们把对象创建出来.而工厂模式就是来填构造方法的坑的.

class Point{
    double x;
    double y;
    public Point(double x,double y){
        this.x=x;
        this.y=y;
    }
    public Point(double r,double a){
        this.x=r;
        this.y=a;
    }
}

Point 在直角坐标系中可以用x,y 来表示坐标,也可以用r,α来表示坐标,但当我们用不同的方式来创建对象时, 由于参数列表相同,无法构成重载.于是我们只能再创建一个类来对 Point类进行封装,并提供不同的方法来帮助我们创建不同的Point对象.

class Points{
    public static Point newPointByXY(double x,double y){
        return new Point(x,y);
    }
    public static Point newPointByra(double r,double a){
        return new Point(r,a);
    } 
}

Executor(接口) --> ExecutorService(接口) --> AbstractExecutorService(类) --> ThreadPoolExecutor(类)

Executors 对ThreadPoolExecutor 进一步封装

 9.4.3 线程池的构造

进程与多线程——初阶_第13张图片

(1) 核心线程数与最大线程数: 核心线程数相当于我们公司的正式员工,最大线程数相当于我们公司的正式员工加实习生,正式员工不能够随意开除(销毁), 而实习生可以随意开除(销毁), 当公司业务过多时,就会大量找实习生(创建临时线程),反之就会裁掉(销毁临时线程).创建线程池时,当核心线程数和最大线程数相等时,就会创建一个固定大小的线程池,如果 maxmumPoolSize 无限大,说明线程池可以无限创建线程.

(2) 存活时间: 当公司业务不紧张时, 过了一定时间(存活时间)就会裁掉实习生(销毁临时线程).

(3) 阻塞队列: 线程池要管理很多任务, 这些任务就是通过阻塞队列来组织的!!程序员可以手动的指定给线程池一个队列,此时程序员就可以很方便的控制/获取队列中的信息了. submit 方法其实就是把任务放到该队列中 

(4) 拒绝策略: 如果线程池满了,继续往里添加任务,如何进行拒绝.ThreadPoolExcutor中的静态内部类,继承自 RejectedExecutionHandler 接口

• ThreadPoolExcutor.AbortPolicy: 如果满了,继续添加任务,添加操作直接抛出异常

• ThreadPoolExcutor.CallerRunsPolicy: 添加的线程自己负责执行这个任务

• ThreadPoolExcutor.DiscardOldestPolicy: 丢弃最老的任务

• ThreadPoolExcutor.DiscardPolicy: 丢弃最新的任务

代码示例:​​​​​​​

    public static void main(String[] args) {
        ExecutorService pool=new ThreadPoolExecutor(1,2,1000,
                TimeUnit.MILLISECONDS,
                new SynchronousQueue(),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
        for(int i=0;i<3;i++){
            pool.submit(new Runnable(){
                @Override
                public void run() {
                    System.out.println("hello");
                }
            });
        }
    }

9.4.4 实现线程池

class MyThreadPool{
    BlockingDeque blockingDeque=new LinkedBlockingDeque<>();
    public void submit(Runnable runnable) {
        try {
            blockingDeque.put(runnable);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
    //实现一个固定大小容量的线程池
    public MyThreadPool(int n){
        for(int i=0;i{
                try {
                    while (true) {
                        Runnable runnable=blockingDeque.take();
                        runnable.run();
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
            t.start();
        }
    }
}
public class Demo8 {
    public static void main(String[] args) {
        MyThreadPool threadPool=new MyThreadPool(10);
        for(int i=0;i<1000;i++){
            //i 在此处不是一个常量活不可修改的变量,匿名内部类会变量捕获,编译不通过
            int num=i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello"+num);
                }
            });
        }
    }
}

不同的程序,线程做的活不一样

1. CPU 密集型任务, 主要做一些计算工作,要在CPU 上运行

2. IO 密集型任务, 主要是等待IO操作(等待读写硬盘,读写网卡),不咋吃 CPU

极端情况下,如果你的线程全是CPU~~,线程数就不应该超过 CPU核心数(逻辑核心,比如电脑现在8核16线程,就是8个物理核心,16个逻辑核心).如果你的线程全是 IO,线程就可以设置很多,远远超出CPU核心数.实践中很少有这么极端的情况,具体通过测试方式(可以通过纪录程序的运行时间来确定)来确定线程池的大小.

十. 对比线程和进程

10.1 线程的优点

1. 创建一个线程的代价要比创建一个进程小的多

2. 与进程之间的切换相比, 线程之间的切换需要操作系统做的工作要少很多

3. 线程占用的资源要比进程少很多

4. 能充分利用处理器的可并行数量

5. 在等待慢速 I/O 操作结束的同时, 程序可执行其他计算机任务

6. 计算密集型应用, 为了能在多处理器系统上运行, 将计算分解到多个线程中实现

7. I/O 密集型应用, 为了提高性能,将I/O 操作重叠, 线程可以同时等待不同的I/O 操作

10.2 进程与线程的区别

1. 进程是系统进行资源分配和调度的最小单位, 线程是程序执行的最小单位

2. 进程有自己的内存地址空间,线程只独享指令流执行的必要资源, 如寄存器和栈

3. 由于同一进程的各线程之间共享内存和文件资源, 可以不通过内核进行直接通信

4. 线程的创建, 切换及终止效率更高

你可能感兴趣的:(JAVA,EE初阶,服务器,运维)