多线程 (进阶+初阶)

文章目录

  • 一、进程和线程是什么
    • 1.1程序
    • 1.2端口号和PID
    • 1.3进程和线程
      • 有进程实现并发编程为什么还要使用线程?
      • 两者区别(面试重点)
    • 1.4串行、并行、并发
  • 二、Java.lang.Thread
    • 2.1第一个多线程代码
    • 2.2 jconsole命令
    • 2.3创建线程的四种方法(重点)
      • 继承Thread类
      • 实现Runnable接口
      • 两种方式各自优势
      • 两种创健线程方式的不同写法
        • 匿名内部类继承Thread类和实现Runnable接口
        • Lambda表达式
      • 实现Callable接口
    • 2.4并发和串行时间对比
    • 2.5 练习
  • 三、Thread常用方法
    • 3.1构造方法
    • 3.2Thread类的核心属性
    • 3.3中断线程的两种方法
      • 通过共享变量中断线程
      • 通过方法调用中断线程
    • 3.4两种线程中断差别(易错点)
      • 两种中断通知
      • 总结
    • 3.5等待另一个线程
      • join()方法延伸
    • 3.6获取当前线程对象和休眠当前线程对象
    • 3.7线程的状态(面试重点)
      • NEW 和 RUNNABLE状态
      • 三种阻塞状态(面试重点)
      • yield()方法
  • 四、线程安全(面试重点)
    • 4.1线程不安全概念
    • 4.2Java的内存模型-JMM
    • 4.3线程安全的三大特性
      • 原子性
      • 可见性
      • 可见性带来的线程安全问题
      • 原子性带来的线程安全问题
      • 关于内存的问题
      • 指令重排
      • 总结(面试重点)
    • 4.4 synchronize关键字(面试重点)
      • monitor lock对象锁
      • mutex lock互斥(synchronize第一个特性)
      • Synchronized原理
      • 互斥关系的定义
      • 代码块刷新内存(第二个特性)
      • 上锁操作和单线程操作区别
      • 可重入(第三个特性)
      • 死锁(不可重入锁)
      • 可重入的实际过程
    • 4.5 synchronized修饰类中的成员方法
    • 4.6 synchronized修饰类中的静态方法
    • 4.7 synchronized修饰当前代码块
    • 4.8 synchronized修饰当前class对象(难点)
    • 4.9线程安全类(了解即可)
  • 五、volatile关键字(面试重点)
    • 5.1保证可见性
    • 5.2 内存屏障
      • volatile保证可见性为什么不保证原子性?
      • sleep()和yield()可能刷新内存
  • 六、线程间等待与唤醒机制(面试重点)
    • 6.1 wait()等待
    • 6.2 notify()唤醒
    • 6.3等待队列 阻塞队列
      • 面试题重点:sleep()和wait()的区别
  • 七、单例模式(面试重点)
      • 如何保证单例
    • 7.1饿汉式单例
    • 7.2懒汉式单例
      • hashMap的懒汉式加载示例
    • 7.3饿汉和懒汉的线程安全问题
      • 解决懒汉式线程安全问题
  • 八、阻塞队列
    • 8.1生产消费者模型
    • 8.2 定时器Timer(创建线程的一种方式)
  • 九、线程池
    • 9.1 线程池的作用
    • 9.2 线程池的核心方法
    • 9.3 Executors 线程池工具类
    • 9.4ThreadPoolExector核心参数(面试重点)
      • 单线程池的意义
    • 9.5线程池工作流程
  • 十、常用锁的策略
    • 10.1悲观锁 乐观锁
    • 10.2乐观锁的实现
    • 10.3读写锁
    • 10.4重量级锁和轻量级锁
    • 10.5公平锁和非公平锁
    • 10.6 CAS 的应用
    • 10.7 synchronized 锁升级原理
    • 10.8 juc包下常用子类 lock锁
    • 10.8 死锁
  • 十一、线程工具类 java.util.concurrent
    • 11.1 Semaphore 信号量
    • 11.2 CountDownLatch——大号的join()方法
  • 十二、面试问题

一、进程和线程是什么

1.1程序

多线程 (进阶+初阶)_第1张图片
多线程 (进阶+初阶)_第2张图片

1.2端口号和PID

多线程 (进阶+初阶)_第3张图片

1.3进程和线程

多线程 (进阶+初阶)_第4张图片
多线程 (进阶+初阶)_第5张图片

有进程实现并发编程为什么还要使用线程?

虽然多进程也能实现并发编程, 但是线程比进程更轻量:所以线程又叫轻量级进程;
创建线程比创建进程更快;
销毁线程比销毁进程更快;
调度线程比调度进程更快;

两者区别(面试重点)

多线程 (进阶+初阶)_第6张图片
多线程 (进阶+初阶)_第7张图片
Tset就是进程,main是主线程,此时多余的一个进程java.exe是idea自己
多线程 (进阶+初阶)_第8张图片
总结:
在这里插入图片描述

1.4串行、并行、并发

在这里插入图片描述

二、Java.lang.Thread

2.1第一个多线程代码

注意事项:
1.启动线程是start方法,不是run方法。通过start方法将线程启动以后,每个线程自动执行自己的run方法
2.Thread-0这些线程名字是默认的,可以修改
3.这四个线程同时执行,互不影响
多线程 (进阶+初阶)_第9张图片
多线程 (进阶+初阶)_第10张图片

2.2 jconsole命令

多线程 (进阶+初阶)_第11张图片

2.3创建线程的四种方法(重点)

多线程 (进阶+初阶)_第12张图片
多线程 (进阶+初阶)_第13张图片

继承Thread类

继承Thread的子类就是一个线程实体

// 定义一个Thread类,相当于一个线程的模板
class MyThread01 extends Thread {
    // 重写run方法
    // run方法描述的是线程要执行的具体任务
    @Overridepublic
     void run() {
        System.out.println("hello, thread.");
    }
}
 
/**
 * 继承Thread类并重写run方法创建一个线程
 * @author  rose
 * @created 2022-06-20
 */
public class Thread_demo01 {
    public static void main(String[] args) {
        // 实例化一个线程对象
        MyThread01 t = new MyThread01();
        // 真正的去申请系统线程,参与CPU调度
        t.start();
    }
}

多线程 (进阶+初阶)_第14张图片

实现Runnable接口

这个实现Runnable接口的子类,并不是真正的的线程实体,只是线程的一个核心工作任务。这是和第一种方法最大的区别

// 创建一个Runnable的实现类,并实现run方法
// Runnable主要描述的是线程的任务
class MyRunnable01 implements Runnable {
    @Overridepublic void run() {
        System.out.println("hello, thread.");
    }
}
/**
 * 通过实现Runnable接口并实现run方法
 * @author rose
 * @created 2022-06-20
 */
public class Thread_demo02 {
    public static void main(String[] args) {
        // 实例化Runnable对象
        MyRunnable01 runnable01 = new MyRunnable01();
        // 实例化线程对象并绑定任务
        Thread t = new Thread(runnable01);
        // 真正的去申请系统线程参与CPU调度
        t.start();
    }
}

1.创建线程任务对象
2.创建线程对象,并传入任务对象
3.调用Thread类的start方法启动线程
多线程 (进阶+初阶)_第15张图片

两种方式各自优势

1.两种方式创建线程最后都是通过Thread类的start方法启动线程
2.继承Thread类方法创建线程属于单继承,有局限性
3.实现Runnable接口的子类更加灵活,不仅实现Runnable接口,还可以继承其他类
4.调用当前线程的区别:继承Thread类,直接使用this就表示当前线程对象的引用;实现Runnable 接口, this 表示的是 MyRunnable 的引用. 需要使用 Thread.currentThread()
多线程 (进阶+初阶)_第16张图片

两种创健线程方式的不同写法

匿名内部类继承Thread类和实现Runnable接口

多线程 (进阶+初阶)_第17张图片
多线程 (进阶+初阶)_第18张图片

/**
 * @author hide_on_bush
 * @date 2022/7/12
 */
public class OtherMethod {
    public static void main(String[] args) {
        //1.匿名内部类继承Thread类
        Thread thread=new Thread(){
            @Override
            public void run() {
                System.out.println("这是匿名内部类继承Thread类");
                System.out.println(Thread.currentThread().getName());
            }
        };
        //2.匿名内部类实现Runnable接口
        Thread runThread=new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("这是匿名内部类实现Runnable接口");
                System.out.println(Thread.currentThread().getName());
            }
        });
        thread.start();
        runThread.start();
        System.out.println("这是主线程"+Thread.currentThread().getName());
    }
}

多线程 (进阶+初阶)_第19张图片

Lambda表达式

lambda表达式是建立在函数式接口,只有一个抽象方法!!!
多线程 (进阶+初阶)_第20张图片

//3.Lambad
        Thread lambadaThread=new Thread(()-> System.out.println("这是Lambda表达式实现Runnable接口"));

实现Callable接口

多线程 (进阶+初阶)_第21张图片

  • 使用线程池实现Callable接口,FutureTast是Future子类
    多线程 (进阶+初阶)_第22张图片

2.4并发和串行时间对比

理论上:并发执行速度是顺序执行的一倍,所以串行耗时应该是并发的一倍

实际上:线程的创建、销毁和调用也会耗时,所以实际的时间比理论实践多一点

结论:多线程的最大好处就是调高系统处理效率
多线程 (进阶+初阶)_第23张图片

2.5 练习

多线程 (进阶+初阶)_第24张图片
正解:可能先打印1也可能先打印2,具体先调度子线程输出还是先调度主线程输出由系统决定。
至于为什么多次试验都是 21的结果:子线程位于主线程中,当t.start时主线程已经在运行,所以往往都先跑主线程才看到子线程结果
多线程 (进阶+初阶)_第25张图片
多线程 (进阶+初阶)_第26张图片

public class Thread_2533 {
    public static void main(String[] args) throws InterruptedException {
        // 记录开始时间
        long start = System.currentTimeMillis();
 
        // 1. 给定一个很长的数组 (长度 1000w), 通过随机数的方式生成 1-100 之间的整数.
        int total = 1000_0000;
        int [] arr = new int[total];
        // 构造随机数,填充数组
        Random random = new Random();
        for (int i = 0; i < total; i++) {
            int num = random.nextInt(100) + 1;
            arr[i] = num;
        }
 
 
        // 2. 实现代码, 能够创建两个线程, 对这个数组的所有元素求和.
        // 3. 其中线程1 计算偶数下标元素的和, 线程2 计算奇数下标元素的和.
        // 实例化操作类
        SumOperator operator = new SumOperator();
        // 定义具体的执行线程
        Thread t1 = new Thread(() -> {
            // 遍历数组,累加偶数下标
            for (int i = 0; i < total; i += 2) {
                operator.addEvenSum(arr[i]);
            }
        });
 
        Thread t2 = new Thread(() -> {
            // 遍历数组,累加奇数下标
            for (int i = 1; i < total; i += 2) {
                operator.addOddSum(arr[i]);
            }
        });
 
        // 启动线程
        t1.start();
        t2.start();
        // 等待线程结束
        t1.join();
        t2.join();
 
        // 记录结束时间
        long end = System.currentTimeMillis();
        // 结果
        System.out.println("结算结果为 = " + operator.result());
        System.out.println("总耗时 " + (end - start) + "ms.");
    }
}
 
// 累加操作用这个类来完成
class SumOperator {
    long evenSum;
    long oddSum;
 
    public void addEvenSum (int num) {
        evenSum += num;
    }
 
    public void addOddSum (int num) {
        oddSum += num;
    }
 
    public long result() {
        System.out.println("偶数和:" + evenSum);
        System.out.println("奇数和:" + oddSum);
        return evenSum + oddSum;
    }
}

多线程 (进阶+初阶)_第27张图片
作用功能不同:
run方法的作用是描述线程具体要执行的任务;
start方法的作用是真正的去申请系统线程

运行结果不同:
run方法是一个类中的普通方法,主动调用和调用普通方法一样,会顺序执行一次;
start调用方法后, start方法内部会调用Java 本地方法(封装了对系统底层的调用)真正的启动线程,并执行run方法中的代码,run 方法执行完成后线程进入销毁阶段。

三、Thread常用方法

3.1构造方法

多线程 (进阶+初阶)_第28张图片
多线程 (进阶+初阶)_第29张图片

3.2Thread类的核心属性

name可以重复ID不可以重复

多线程 (进阶+初阶)_第30张图片
CPU调度线程,哪个线程被先调度,由操作系统决定
多线程 (进阶+初阶)_第31张图片

3.3中断线程的两种方法

线程中断是线程间通信的一种重要方式,Java中线程的启动和终止,中断Java程序员说了不算,都是系统调度的,我们所谓的中断线程只是更改线程的状态而已,要想让线程终止,run方法执行结束,自然就终止了

通过共享变量中断线程

sleep()方法属于静态方法,在哪个线程中使用,就在哪个线程中生效
在这里插入图片描述

通过方法调用中断线程

调用 interrupt() 方法来通知
在这里插入图片描述
多线程 (进阶+初阶)_第32张图片

3.4两种线程中断差别(易错点)

两种中断通知

调用静态方法查看线程是否中断:Thread.interrupted()
中断状态会被自动清除,会从true改为false
多线程 (进阶+初阶)_第33张图片
下图中使用 成员方法判断线程是否中断:Thread.currentThread().interrupted()
它的作用是当线程被中断时threa.interrupt()之后,不改变中断状态,仅仅只是查看当前线程是否中断,不做修改
多线程 (进阶+初阶)_第34张图片

总结

1.当一个线程对象调用interrupt方法后(线程变为中断状态true),用类方法Thread.interrupted判断线程,会将true改为false,但是调用成员方法isInterrupted就只是查看查看线程状态,不会修改。

2.try…catch只要捕获到中断异常,无论使用哪个判断方法(静态、成员)都会使线程中断状态从
true改为false
多线程 (进阶+初阶)_第35张图片

3.5等待另一个线程

等待一个线程也是一种线程间通信方式
在这里插入图片描述
加了join()方法相当于图中t1,t2,主线程三个线程变成了串行,而不是并行
多线程 (进阶+初阶)_第36张图片

join()方法延伸

多线程 (进阶+初阶)_第37张图片

t1.start();
        // 主线程死等t1,直到t1执行结束主线程再恢复执行
        t1.join();
        // 此时走到这里,t1线程已经执行结束,再启动t2线程
        t2.start();
        // main -> 调用t2.join() 阻塞主线程,直到t2完全执行结束再恢复主线程的执行
        // 主线程只等t2 2000ms - 2s,若t2在2s之内还没结束,主线程就会恢复执行
        t2.join(2000);
        // t2线程也执行结束了,继续执行主线程
        System.out.println("开始学习JavaEE");
        System.out.println(Thread.currentThread().getName());
    }
}

3.6获取当前线程对象和休眠当前线程对象

多线程 (进阶+初阶)_第38张图片

3.7线程的状态(面试重点)

多线程 (进阶+初阶)_第39张图片
多线程 (进阶+初阶)_第40张图片
1.New状态到Runnable状态只需要start()方法,start只是申请可以调度线程,并不能立即执行线程
多线程 (进阶+初阶)_第41张图片

2.Runnable就两个状态:一个Ready和Running,可执行状态并不一定真正在执行中ing

3.调用join()、sleep()方法都会把线程置为超时状态

4.超时等待时间到了就会还原状态:还原成ready状态,等待被系统调度
多线程 (进阶+初阶)_第42张图片

NEW 和 RUNNABLE状态

多线程 (进阶+初阶)_第43张图片
除了New和Terminated都是alive状态
多线程 (进阶+初阶)_第44张图片

三种阻塞状态(面试重点)

wait()方法一般是和notify()方法搭配使用
多线程 (进阶+初阶)_第45张图片
多线程 (进阶+初阶)_第46张图片

yield()方法

调用yield()方法的线程会从运行态转换为就绪态,主动让出CPU资源,什么时候让出CPU以及什么时候再次被CPU调度都是OS决定!!!

public class YieldTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (true) {
                System.out.println(Thread.currentThread().getName());
                // 春鹏线程就会让出CPU,进入就绪态,等待被CPU继续调度
                Thread.yield();
            }
        },"春鹏线程");
        t1.start();
        Thread t2 = new Thread(() -> {
            while (true) {
                System.out.println(Thread.currentThread().getName());
            }
        },"云鹏线程");
        t2.start();
    }
}

多线程 (进阶+初阶)_第47张图片

四、线程安全(面试重点)

4.1线程不安全概念

线程不安全:在多线程场景下,串行执行和并行执行的结果不一致,就称为线程不安全。实际运行结果与我们预期不符。
多线程 (进阶+初阶)_第48张图片
比如t1和t2各自两个线程的run方法是累加5w的数值,那么最终两个线程应该相加得到10w,但是图中并发执行t1和t2线程却每次得到结果都不同。
多线程 (进阶+初阶)_第49张图片

4.2Java的内存模型-JMM

JMM内存模型和JVM内存区域划分不是一个概念,JMM只是一个概念模型并不是真正存在的,而JVM内存区域划分是实际存在的
在这里插入图片描述
JMM:描述线程的工作内存和主内存
在这里插入图片描述
JMM内存模型:
多线程 (进阶+初阶)_第50张图片
方法中的局部变量不是共享变量
多线程 (进阶+初阶)_第51张图片

4.3线程安全的三大特性

一段代码要保证线程安全问题就要满足三大特性:原子性,可见性,防止指令重排

原子性

int a=10就是一个原子性操作,要么没赋值,要么赋值成功

a+=10就是非原子性操作:先读取a的值,再执行a+10,最后将结果赋值给a,这里面包含三个原子性操作
在这里插入图片描述

可见性

对于共享变量的修改可以及时的被其他线程看到就叫可见性

比如图中:Counter类中的成员变量count就是共享变量,保存在堆上。可以被t1和t2线程同时访问,也就是说count这个变量保存在主内存中
多线程 (进阶+初阶)_第52张图片
多线程 (进阶+初阶)_第53张图片
此时执行increase方法可见性、原子性都不可以被保证:
count++操作:不是一个原子性操作,某个线程执行count++(原子性)这个操作时,其它线程是无法读取++后的值(可见性)
在这里插入图片描述

可见性带来的线程安全问题

举个例子:演示不可见性,但不一定这个数值一定是图中所说的情况造成的,只是其中的一种可能性。
多线程 (进阶+初阶)_第54张图片
最后t1先执行完毕,将自己工作内存中count=5w写回主内存中对t2线程是不可见的,于是t2就会把自己工作内存中count=55659写回工作内存并将t1写回主内存的值覆盖
多线程 (进阶+初阶)_第55张图片

原子性带来的线程安全问题

有很多种可能性,这里只是列举一种,总之最后的答案一定不是10w。
多线程 (进阶+初阶)_第56张图片
计划+2次,但最终只+了1次,本来t1已经将count=1写回了主内存,但是t2也让count=1写回了主内存,最后count的值还是1,正确操作之后应该是主内存中count=2才对。
多线程 (进阶+初阶)_第57张图片
多线程 (进阶+初阶)_第58张图片
类似火车的售票系统:
客户A在买票时,发现还有一张票,当他下订单时,在主内存中nums=1-1=0没票了,此时恰好客户B和A同时准备购买票,但是此时nums=0还没有写回主内存,导致B也买了这张票(超卖现象)。
多线程 (进阶+初阶)_第59张图片
多线程 (进阶+初阶)_第60张图片

关于内存的问题

从硬件角度来说:所谓的 “工作内存”, 则是指 CPU 的寄存器和高速缓存,比如执行a+10这个操作,第一步先从主内存中读取a变量的值,但是读取的值需要一个临时存储的地方,这个地方就叫寄存器。所谓"主内存"在硬件角度是真实存在的
多线程 (进阶+初阶)_第61张图片
CPU 访问自身寄存器的速度以及高速缓存的速度, 远远超过访问内存的速度(快了 3 - 4 个数量级,也就是几千倍,上万倍)
内存设备之所以这么多,还是出于成本考虑~
多线程 (进阶+初阶)_第62张图片

指令重排

如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题,可以少跑一次前台。这种叫做指令重排序
多线程 (进阶+初阶)_第63张图片
此时t1线程实例化对象还没有将name成员变量赋值,但是t2线程调用per.getName()方法,于是就出现name=null的情况
多线程 (进阶+初阶)_第64张图片

总结(面试重点)

多线程 (进阶+初阶)_第65张图片

4.4 synchronize关键字(面试重点)

synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时,其他线程如果也执行到同一个对象 synchronized 就会阻塞等待
多线程 (进阶+初阶)_第66张图片

monitor lock对象锁

synchronized的底层是使用操作系统的mutex lock实现的
多线程 (进阶+初阶)_第67张图片

mutex lock互斥(synchronize第一个特性)

互斥:多个线程想要获取同一个对象的锁(同一个对象很关键),同一时间只有一个线程可以获取到这个锁,其它线程进入阻塞状态等待
多线程 (进阶+初阶)_第68张图片
多线程 (进阶+初阶)_第69张图片
不管几个线程对这个对象进行处理,同一时刻只有一个线程拿到这个对象的锁,拿到这个锁就会执行increase()方法,另外线程就处于等待阻塞状态,哪个线程先拿到对象的锁不确定。
多线程 (进阶+初阶)_第70张图片

Synchronized原理

多线程 (进阶+初阶)_第71张图片
每个对象都有一块描述"锁"信息的内存,这个信息告诉其它线程当前该对象有没有被哪个线程持有
多线程 (进阶+初阶)_第72张图片
当某个对象被某个线程上锁时,其它对象想要获取当前对象锁就会进入一个阻塞队列,但这个队列不遵循FIFO队列先入先出的规矩
多线程 (进阶+初阶)_第73张图片

互斥关系的定义

多线程 (进阶+初阶)_第74张图片
到底是不是构成互斥关系,关键看锁的是不是同一个对象
多线程 (进阶+初阶)_第75张图片

代码块刷新内存(第二个特性)

获取锁到释放锁的中间过程保证了原子性和可见性,当其它线程再来获取锁时,保证获取到的共享变量的值一定是正确的值
多线程 (进阶+初阶)_第76张图片

上锁操作和单线程操作区别

同一个类中,可能存在很多方法,但是只有加上关键字Synchronized的方法它是互斥的,同一时间只能被一个线程操作(类似单线程),其它没有加关键字的方法仍然是并发执行!单线程是所有方法只有一个线程在运行多线程 (进阶+初阶)_第77张图片

可重入(第三个特性)

可重入:获取到"锁"的线程可以再次获取锁的操作
多线程 (进阶+初阶)_第78张图片
类似上了两次"锁"
多线程 (进阶+初阶)_第79张图片

死锁(不可重入锁)

public class Reentrant {
   private class Counter{
       int val;
       //锁的是当前Counter对象
       synchronized void increase(){
           val++;
       }
       //还是锁的当前Counter对象
        void increaseDouble(){
           //在方法内部再次调用increase方法
           increase();
       }
   }
}

如果increaseDouble()方法不上锁的情况:线程1就会阻塞在这里,等待自己释放锁之后才能再次进入increaseDouble()方法中的increase()方法,那么程序永远不会停止,线程1一直阻塞在这个位置,我们把这种情况叫做死锁!

可重入的实际过程

如果某个线程加锁的时候,发现锁已经被人占用,但是恰好占用的正是自己,那么仍然可以继续获取到锁, 并让计数器自增,解锁的时候计数器递减为 0 的时候,且持有线程为null才真正释放锁(才能被别的线程获取到)
多线程 (进阶+初阶)_第80张图片
关于加两次锁的原因:只要进入一个synchronized代码块一次,就得上锁一次,计数器加一
多线程 (进阶+初阶)_第81张图片
至于加不加synchronized的问题:synchronized只是保证线程安全,对于代码编译不会产生任何问题,加不加看实际应用需求,比如你只是进行读操作,完全没有必要加synchronized。只有当修改共享变量,程序员为了保证线程安全会人为在代码中自己添加关键字。
多线程 (进阶+初阶)_第82张图片

4.5 synchronized修饰类中的成员方法

在成员方法上加synchronized关键字,锁的就是该类的实例化对象
多线程 (进阶+初阶)_第83张图片
counter1和counter2是两个是两个不同的对象,在t1线程中counter1对象调用同步代码块的方法上锁的对象是counter1对象,t2线程中同理,获取的是counter2对象的锁
多线程 (进阶+初阶)_第84张图片
互斥现象:t1和t2线程使用同一个对象调用同步代码块的成员方法。哪个线程先获取对象锁就先执行哪个线程多线程 (进阶+初阶)_第85张图片

4.6 synchronized修饰类中的静态方法

锁的是一个类,不管这个类实例化多少个对象,同一时刻也只能一个线程访问
多线程 (进阶+初阶)_第86张图片
多线程 (进阶+初阶)_第87张图片
虽然看似不同对象调用静态代码块的方法,但是同一时刻只能有一个线程能获取这个锁
多线程 (进阶+初阶)_第88张图片

4.7 synchronized修饰当前代码块

同步代码块之前的代码都可以并发执行,this相当于锁的是当前对象。三个线程中都是同一个对象调用同一个方法,所以在同一时刻只能有一个线程能进入这个方法
多线程 (进阶+初阶)_第89张图片
当不同的对象调用increase4()方法就不构成互斥了,因为此时每个线程中是不同的对象在调用同步代码块
多线程 (进阶+初阶)_第90张图片
同步代码块的意义就在于锁的粒度更细,方法中的其他代码仍然是多线程并发执行,如果锁的内容太多,多线程效率不高

4.8 synchronized修饰当前class对象(难点)

多线程 (进阶+初阶)_第91张图片
上述锁Counter.class对象锁的是Counter的同一个实例化对象,只有是Counter类同一个对象才会上锁

下图中锁的是Reentrant主类.class这个对象,class对象全局唯一,不管是Counter的哪个对象都构成互斥,同一个时刻只能一个线程进入
多线程 (进阶+初阶)_第92张图片
多线程 (进阶+初阶)_第93张图片

4.9线程安全类(了解即可)

多线程 (进阶+初阶)_第94张图片
锁的是成员方法,只要是同一个Vector对象都是互斥

多线程 (进阶+初阶)_第95张图片
ConcurrentHashMap和CopyOnWriteArrayList都属于java.util.concurrent并发工具包下的
多线程 (进阶+初阶)_第96张图片

五、volatile关键字(面试重点)

5.1保证可见性

读:直接从主内存中读取共享变量,无论当前工作内存中是否有该值

写:工作内存修改后的共享变量会立即刷新到主内存中,并且其它正在读取主内存的线程会等待,直到写操作结束

直接访问工作内存(实际是CPU的寄存器或者 CPU 的缓存),速度非常快, 但是可能出现数据不一致的情况,加上volatile,强制读写内存,速度是慢了,但是数据变的更准确了
多线程 (进阶+初阶)_第97张图片
代码示例:flag变量在没有volatile关键字修饰时,t1线程中一直处于死循环,因为 t1 线程一直读取自己的工作内存 flag=0 , t2 线程中即使改变了 flag 的值(对于t1线程来说是不可见的),t1线程也一直处于死循环,没有及时读取主内存中更新后的flag值。

但是在共享变量 flag 之前加上关键字 volatile ,线程 t1 会立即退出循环,因为 volatile 保证可见性,保证 t1 线程每次强制读取主内存中 flag 的数据
多线程 (进阶+初阶)_第98张图片
synchronized 同样可以保证可见性,去掉 flag 前的关键字volatile,给counter对象上锁,在线程 t2 中修改 flag 的值同样可以立即终止线程 t1
多线程 (进阶+初阶)_第99张图片
volatile 和 synchronized 有着本质的区别:synchronized 能够保证原子性,volatile 保证的是内存可见性不保证原子性,比如在之前写的t1线程累加5w和t2线程累加5w的例子中,即使给共享变量加上关键字volatile,同样得不到正确答案10w
在这里插入图片描述

5.2 内存屏障

多线程 (进阶+初阶)_第100张图片
多线程 (进阶+初阶)_第101张图片

volatile保证可见性为什么不保证原子性?

java语言是无法保证原子性的,要保证原子性只能采取上锁等方式,保证哦同一时间只有一个线程操作就能让不原子性操作变得原子性
在这里插入图片描述

sleep()和yield()可能刷新内存

  • 一定保证共享变量的可见性的关键字就是synchronized关键字、volatile关键字、final关键字

  • 此外final可以保证可见性原因:final关键字修饰的变量为常量,必须在初始化时赋值且不能更改,所以保证了天然的可见性
    多线程 (进阶+初阶)_第102张图片

六、线程间等待与唤醒机制(面试重点)

等待和唤醒也是一种线程间通信的方式,Object类方法表示任意实例化对象都具有wait()和notify()方法
在这里插入图片描述

6.1 wait()等待

使用wait()方法会释放锁,线程会进入等待队列
在这里插入图片描述
带有时间参数timeout的wait方法,时间参数单位是ms
在这里插入图片描述

6.2 notify()唤醒

1.方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。

2.如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 “先来后到”)

3.在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
多线程 (进阶+初阶)_第103张图片
注意事项:
1.如果没有搭配synchronized使用notify和wait方法会报错
2.唤醒等待线程也需要使用同一个对象的notify方法
多线程 (进阶+初阶)_第104张图片
多线程 (进阶+初阶)_第105张图片
调用notify方法唤醒在等待的线程,一定要等notify方法中同步代码块里执行完毕才可以执行被唤醒的线程。
多线程 (进阶+初阶)_第106张图片
修改 NotifyTask 中的 run 方法,,把 notify 替换成 notifyAll:
虽然是同时唤醒 3 个线程,,但是这 3 个线程需要竞争锁,所以并不是同时执行

6.3等待队列 阻塞队列

多线程 (进阶+初阶)_第107张图片
例如图中t3先获取到锁,所以t1和t2首先进入阻塞队列,t3调用wait方法就释放对象锁,t1和t2就去竞争这个对象锁,t3就会进入等待队列。当t1 t2 t3都进入等待队列,调用notifyall方法,则三个线程都被唤醒,但不是立即并发执行这三个线程,从等待队列同时将三个队列置入阻塞队列去竞争锁,不遵循先来先获取锁的原则
多线程 (进阶+初阶)_第108张图片
多线程 (进阶+初阶)_第109张图片

面试题重点:sleep()和wait()的区别

相同点:其实本质上两者并没有什么联系,但是可以同时让线程放弃执行一段时间

不同点:
sleep():线程Thread类提供的方法,不需要搭配关键字,并且调用方法时不需要释放锁

wait():Object类提供的方法,必须搭配synchronized关键字使用,调用方法会释放锁
多线程 (进阶+初阶)_第110张图片

七、单例模式(面试重点)

设计模式:软件开发中也有很多常见的 “问题场景”,针对这些问题场景,大佬们总结出了一些固定的套路
多线程 (进阶+初阶)_第111张图片

如何保证单例

不管外部调用或者不调用这个对象,只要类加载到JVM,唯一对象就会产生
多线程 (进阶+初阶)_第112张图片
在这里插入图片描述在这里插入图片描述
构造方法私有化,可以保证对象产生的数量
多线程 (进阶+初阶)_第113张图片
单例对象使用静态变量static的原因:在类的内部只用一次构造方法。外部访问成员变量需要通过对象,但是外部没有这个对象,此时外部想要获取这个唯一对象就需要把这个对象设置为静态变量,获取的方式:通过get方法
多线程 (进阶+初阶)_第114张图片

7.1饿汉式单例

这个类只要一加载就产生唯一对象(饥不择食),不管外部是否需要这个对象,只要类加载到JVM,唯一对象就产生
多线程 (进阶+初阶)_第115张图片

/**
 * 饿汉式单例模式
 * @author hide_on_bush
 * @date 2022/7/18
 */
public class SingleTon {
    //只产生一个对象
    private static SingleTon singleTon=new SingleTon();
    //私有化构造方法
    private SingleTon(){

    }
    //get方法返回唯一对象
    public static SingleTon getSingleTon() {
        return singleTon;
    }
}

无论是用多少个引用都是同一个对象,并且构造方法私有化,无法通过new产生实例化对象
多线程 (进阶+初阶)_第116张图片

7.2懒汉式单例

懒汉式单例更常见的原因:按需分配,只有需要实例化才产生对象,节省内存空间
多线程 (进阶+初阶)_第117张图片

hashMap的懒汉式加载示例

hashMap产生对象时都没有初始化table数组,只有第一次调用put方法才初始化数组大小,节省空间
多线程 (进阶+初阶)_第118张图片

7.3饿汉和懒汉的线程安全问题

饿汉式是天然的线程安全,JVM加载类时就创建了这个唯一对象;但是懒汉式就不一样,假设同时三个线程并行执行调用get方法,会发现对象为null,此时可能三个线程都会同时创建三个不同的对象
多线程 (进阶+初阶)_第119张图片
多线程 (进阶+初阶)_第120张图片

解决懒汉式线程安全问题

1.直接在方法上加锁:效率不高,锁的粒度太粗
多线程 (进阶+初阶)_第121张图片
2.优化 double - check

此时假设三个线程同时执行到同步代码块,当t1获取到这个锁进入同步代码块创建对象后释放锁,t2和t3还是会从开始时竞争锁的位置开始执行,还是会再次创建两个不同的对象,所以不可行
多线程 (进阶+初阶)_第122张图片
1.有三个线程,开始执行get方法,通过外层的 if (singleTon = null) 知道了实例还没
有创建的消息,于是三个线程开始竞争同一把锁
2.其中线程1率先获取到锁,此时线程1通过里层的 if (single= null) 进一步确认实例是
否已经创建,如果没创建,就把这个实例创建出来
3. 当线程1 释放锁之后,线程2 和线程3也拿到锁,也通过里层的 if (instance == null) 来
确认实例是否已经创建,发现实例已经创建出来了,就不再创建了
4.后续的线程,不必加锁,直接就通过外层 if (instance = null) 就知道实例已经创建了,从
而不再尝试获取锁了,降低了开销
多线程 (进阶+初阶)_第123张图片
3.使用volatile关键字
加锁 / 解锁是一件开销比较高的事情,而懒汉模式的线程不安全只是发生在首次创建实例的时候, 因此后续使用的时候,不必再进行加锁了,外层的 if 就是判定下看当前是否已经把 instance 实例创建出来了

volatile保证防止指令重排
多线程 (进阶+初阶)_第124张图片
加volatile关键字原因:假设此时程序中有两个线程,t1先获取锁执行同步代码块,t2刚开始执行会卡在第一个if语句,但是t1执行同步代码块会产生一个对象,那么此时t2会看到singleTon这个对象不等于null,可能会直接return 这个唯一对象,但是t1线程初始化对象还没有结束,返回的可能是一个不完整的对象,有了volatile关键字修饰才能保证JVM执行完new操作再返回对象
多线程 (进阶+初阶)_第125张图片

八、阻塞队列

多线程 (进阶+初阶)_第126张图片
和普通队列的区别:阻塞队列线程安全
多线程 (进阶+初阶)_第127张图片
内部实现原理:多线程 (进阶+初阶)_第128张图片

8.1生产消费者模型

多线程 (进阶+初阶)_第129张图片

/**
 * 生产者 - 消费者模型
 * @author hide_on_bush
 * @date 2022/9/20
 */
public class Consumer_Producer {
    public static void main(String[] args) {
        //阻塞队列
        BlockingQueue<Integer> blockingQueue=new LinkedBlockingQueue<>();
        Thread consumer=new Thread(()->{
            try {
                int val=blockingQueue.take();
                System.out.println("消费元素:"+val);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        },"消费者");
        Random random=new Random();
        Thread producer=new Thread(()->{
            try {
                int val= random.nextInt(100);
                blockingQueue.put(val);
                System.out.println("生产者放入一个元素:"+val);
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        },"生产者");
        consumer.start();
        producer.start();
    }
}

阻塞队列的大小一般由构造方法传入
在这里插入图片描述

8.2 定时器Timer(创建线程的一种方式)

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

schedule 包含两个参数:第一个参数指定即将要执行的任务代码,是一个new TimerTask任务,就是Runnable接口的子类;第二个参数指定多长时间之后执行 (单位为毫秒)
多线程 (进阶+初阶)_第130张图片

九、线程池

9.1 线程池的作用

  • 池的作用:就是让一些对象可以被重复多次利用,并且减少频繁创建和销毁对象带来的开销问题,提高时间和空间的利用率。
    多线程 (进阶+初阶)_第131张图片
  • 线程池中的线程和临时创建线程的区别:
  1. 临时创建的线程:需要创建、启动等一系列操作
  2. 线程池中的线程:都是Runnable状态,可以马上拿来用,提高时间空间的利用率
    多线程 (进阶+初阶)_第132张图片

9.2 线程池的核心方法

  • 线程池核心类 ThreadPoolExecutor
  • 线程池核心方法 submit()提交任务,参数中可以接收 Runnable 接口和 Callable 接口
    多线程 (进阶+初阶)_第133张图片

9.3 Executors 线程池工具类

  • Executors 本质上是 ThreadPoolExecutor 类的封装,提供了四种常用创建线程池的静态方法
    多线程 (进阶+初阶)_第134张图片
  • 定时器线程池使用的是 Schedule类 和其他线程池调用的类不同,调用的是schedule()方法,其它线程池调用 submit()方法
    多线程 (进阶+初阶)_第135张图片
  • 线程池接口关系:
    多线程 (进阶+初阶)_第136张图片
    多线程 (进阶+初阶)_第137张图片

9.4ThreadPoolExector核心参数(面试重点)

corePoolSize:核心池线程数量(正式工)
maximumPoolSize:线程池最大线程数量(正式工+临时工)
keepAlive:线程池临时线程允许空闲时间
workQueue:工作队列,实质上就是个阻塞队列,线程从中取出执行任务
hander:拒绝策略,当任务数量超出线程池负荷时怎么办
多线程 (进阶+初阶)_第138张图片
固定线程池:没有临时工,最大线程数量==核心池线程数量
多线程 (进阶+初阶)_第139张图片
动态缓存池:核心线程为0,每当有新任务进来都是临时创建线程
工作队列(阻塞队列)几乎用不上,最大线程数能达到40多亿
多线程 (进阶+初阶)_第140张图片
单线程池:只有一个线程,所以需要无解界限的工作队列多线程 (进阶+初阶)_第141张图片
固定大小延迟线程池:
多线程 (进阶+初阶)_第142张图片

单线程池的意义

单独创建一个线程:执行完一次任务就需要销毁
单线程池:将任务不断提交到阻塞队列中,线程只需要不断调度工作队列中的任务即可
多线程 (进阶+初阶)_第143张图片

9.5线程池工作流程

  1. 调用submit()方法提交一个线程任务

  2. 判断当前任务数量是否大于核心池线程数量
    若小于:无论当前是否有空闲线程都会创建一个新线程执行任务,而后将该线程保存到corePoolSize(招聘一个正式工)
    若大于:会再次判断工作队列中是否已满

  3. 判断工作队列是否已满时:
    工作队列未满:将任务入队,等待线程调度
    工作队列已满:判断当前线程池数量maximumSize是否已经达到最大值(正式工+临时工)

  4. 判断当前maximumSize是否达到最大值:
    未达到上限:创建临时线程执行此任务(临时工)
    达到上限:执行拒绝政策
    多线程 (进阶+初阶)_第144张图片

十、常用锁的策略

10.1悲观锁 乐观锁

  • 两种锁没有优劣之分,都有使用的场景。线程冲突不严重使用乐观锁,避免多次加锁解锁操作。线程冲突严重时使用悲观锁,避免线程多次访问共享变量失败带来cpu空转,造成资源浪费
    多线程 (进阶+初阶)_第145张图片

10.2乐观锁的实现

  • 乐观锁的一个重要功能就是要检测出数据是否发生访问冲突. 我们可以引入一个 “版本号” 来解决.

  • 乐观锁的实现案例:设当前余额为 100. 引入一个版本号 version,初始值为 1 并且我们规定 “提交版本必须大于记录当前版本才能执行更新余额”
    多线程 (进阶+初阶)_第146张图片
    线程 1 在自己的工作内存中减去50(100-50),线程 2 在自己的工作内存中减去20(100-20)
    多线程 (进阶+初阶)_第147张图片
    版本号:记录更新的次数。当线程1先将50写回主内存中时,版本号+1,表示更新了一次主内存。
    多线程 (进阶+初阶)_第148张图片
    线程 2 也想将自己工作内存的80写回主内存,但此时发现主内存版本号等于2,线程 2 自己的版本号等于1,两者版本号不相等,无法将80写回主内存,写入失败。不满足 “提交版本必须大于记录当前版本才能执行更新” 的乐观锁策略。就认为这次操作失败
    多线程 (进阶+初阶)_第149张图片
    解决策略:
    1.直接报错
    2.CAS策略:线程2先读取主内存版本号为2,再将主内存的新数据写回自己的工作内存进行操作(减20操作),最后将30写回主内存,并且版本号+1等于3
    多线程 (进阶+初阶)_第150张图片

10.3读写锁

数据的读取一般不会发生线程安全问题,只有更新数据(CURD)的时候才可能发生线程安全问题。

JDK内置的读写锁:ReentrantReadWriteLock
读加锁和读加锁之间,不互斥
写加锁和写加锁之间,互斥
读加锁和写加锁之间,互斥
多线程 (进阶+初阶)_第151张图片

10.4重量级锁和轻量级锁

多线程 (进阶+初阶)_第152张图片
轻量级锁采用的自旋锁:获取线程失败不阻塞,不让出CPU。如果获取锁失败, 立即再尝试获取锁,无限循环,直到获取到锁为止,第一次获取锁失败, 第二次的尝试会在极短的时间内到来,一旦锁被其他线程释放,就能第一时间获取到锁
多线程 (进阶+初阶)_第153张图片

10.5公平锁和非公平锁

Synchronized锁就是典型的非公平锁
操作系统内部的线程调度就可以视为是随机的,如果不做任何额外的限制, 锁就是非公平锁,如果要想实现公平锁,就需要依赖额外的数据结构,来记录线程们的先后顺序
多线程 (进阶+初阶)_第154张图片

10.6 CAS 的应用

  • CAS可以理解为乐观锁的一种实现
    在这里插入图片描述
  1. 应用1: 原子类,原子类可以保证线程安全,原子类常用方法
    多线程 (进阶+初阶)_第155张图片
  • incrementAndGet 相当于 ++i ;getAndIncrenment 相当于 i++ ;获取原子类的值直接调用get()方法
    多线程 (进阶+初阶)_第156张图片
    多线程 (进阶+初阶)_第157张图片
  • 内部实现原理
    多线程 (进阶+初阶)_第158张图片
  1. 使用 CAS 机制实现自旋锁

多线程 (进阶+初阶)_第159张图片

  1. CAS引发的ABA问题
    多线程 (进阶+初阶)_第160张图片
    多线程 (进阶+初阶)_第161张图片

10.7 synchronized 锁升级原理

  • Synchronized 初始使用乐观锁策略,当发现锁竞争比较频繁的时候, 就会自动切换成悲观锁策略
    多线程 (进阶+初阶)_第162张图片

10.8 juc包下常用子类 lock锁

多线程 (进阶+初阶)_第163张图片

  • synchronized 和 lock 的区别
    多线程 (进阶+初阶)_第164张图片

10.8 死锁

十一、线程工具类 java.util.concurrent

11.1 Semaphore 信号量

  • 信号量就是一个计数器,表示可用资源数量
    多线程 (进阶+初阶)_第165张图片
  • 信号量就类似于停车场的停车位多线程 (进阶+初阶)_第166张图片
  • 代码案例,创建信号量(可用资源数量)为 5 ,再创建 20 个线程,同一时间只有 5 个线程可以获取这 5 个资源。
    多线程 (进阶+初阶)_第167张图片
    多线程 (进阶+初阶)_第168张图片
  • 假设创建资源数量为 6 ,如果每个线程同时获取 2 个资源,也必须同时释放 2 个资源,此时只有 3 个线程同一时刻获取这 6 个资源
    多线程 (进阶+初阶)_第169张图片

11.2 CountDownLatch——大号的join()方法

  • 构造 CountDownLatch 实例,初始化 10 表示有 10 个任务需要完成,每个任务执行完毕,都调用 latch.countDown()方法,同时在 CountDownLatch 内部的计数器同时自减,主线程中使用 latch.await();阻塞等待所有任务执行完毕,相当于计数器为 0 了.
    多线程 (进阶+初阶)_第170张图片

十二、面试问题

  • start()方法和run()方法的区别?

1、线程中的start()方法和run()方法的主要区别在于,当程序调用start()方法,将会创建一个新线程去执行run()方法中的代码。但是如果直接调用run()方法的话,会直接在当前线程中执行run()中的代码,注意,这里不会创建新线程。这样run()就像一个普通方法一样。

2、另外当一个线程启动之后,不能重复调用start(),否则会报IllegalStateException异常。但是可以重复调用run()方法。

多线程 (进阶+初阶)_第171张图片

  • java对象锁,synchronized
    多线程 (进阶+初阶)_第172张图片

  • 如果一个线程连续使用start()方法启动会怎么样?

多线程 (进阶+初阶)_第173张图片

  • 线程池常见子类和常用方法
    多线程 (进阶+初阶)_第174张图片

  • 线程池核心参数、线程池关闭流程、volatile关键字保证可见性无法保证原子性
    多线程 (进阶+初阶)_第175张图片
    在这里插入图片描述

  • 线程池工作流程
    多线程 (进阶+初阶)_第176张图片

你可能感兴趣的:(java,jvm)