文章目录

  • 一、进程和线程是什么
    • 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端口号和PID

1.3进程和线程


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

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

两者区别(面试重点)



Tset就是进程,main是主线程,此时多余的一个进程java.exe是idea自己

总结:

1.4串行、并行、并发

二、Java.lang.Thread

2.1第一个多线程代码

注意事项:
1.启动线程是start方法,不是run方法。通过start方法将线程启动以后,每个线程自动执行自己的run方法
2.Thread-0这些线程名字是默认的,可以修改
3.这四个线程同时执行,互不影响

2.2 jconsole命令

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


继承Thread类

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

// 定义一个Thread类,相当于一个线程的模板
class MyThread01 extends Thread {// 重写run方法// run方法描述的是线程要执行的具体任务@Overridepublicvoid 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();}
}

实现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方法启动线程

两种方式各自优势

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

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

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


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

Lambda表达式

lambda表达式是建立在函数式接口,只有一个抽象方法!!!

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

实现Callable接口

  • 使用线程池实现Callable接口,FutureTast是Future子类

2.4并发和串行时间对比

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

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

结论:多线程的最大好处就是调高系统处理效率

2.5 练习


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

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


作用功能不同:
run方法的作用是描述线程具体要执行的任务;
start方法的作用是真正的去申请系统线程

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

三、Thread常用方法

3.1构造方法


3.2Thread类的核心属性

name可以重复ID不可以重复


CPU调度线程,哪个线程被先调度,由操作系统决定

3.3中断线程的两种方法

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

通过共享变量中断线程

sleep()方法属于静态方法,在哪个线程中使用,就在哪个线程中生效

通过方法调用中断线程

调用 interrupt() 方法来通知

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

两种中断通知

调用静态方法查看线程是否中断:Thread.interrupted()
中断状态会被自动清除,会从true改为false

下图中使用 成员方法判断线程是否中断:Thread.currentThread().interrupted()
它的作用是当线程被中断时threa.interrupt()之后,不改变中断状态,仅仅只是查看当前线程是否中断,不做修改

总结

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

2.try…catch只要捕获到中断异常,无论使用哪个判断方法(静态、成员)都会使线程中断状态从
true改为false

3.5等待另一个线程

等待一个线程也是一种线程间通信方式

加了join()方法相当于图中t1,t2,主线程三个线程变成了串行,而不是并行

join()方法延伸

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获取当前线程对象和休眠当前线程对象

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



1.New状态到Runnable状态只需要start()方法,start只是申请可以调度线程,并不能立即执行线程

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

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

4.超时等待时间到了就会还原状态:还原成ready状态,等待被系统调度

NEW 和 RUNNABLE状态


除了New和Terminated都是alive状态

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

wait()方法一般是和notify()方法搭配使用

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

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

4.1线程不安全概念

线程不安全:在多线程场景下,串行执行和并行执行的结果不一致,就称为线程不安全。实际运行结果与我们预期不符。

比如t1和t2各自两个线程的run方法是累加5w的数值,那么最终两个线程应该相加得到10w,但是图中并发执行t1和t2线程却每次得到结果都不同。

4.2Java的内存模型-JMM

JMM内存模型和JVM内存区域划分不是一个概念,JMM只是一个概念模型并不是真正存在的,而JVM内存区域划分是实际存在的

JMM:描述线程的工作内存和主内存

JMM内存模型:

方法中的局部变量不是共享变量

4.3线程安全的三大特性

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

原子性

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

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

可见性

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

比如图中:Counter类中的成员变量count就是共享变量,保存在堆上。可以被t1和t2线程同时访问,也就是说count这个变量保存在主内存中


此时执行increase方法可见性、原子性都不可以被保证:
count++操作:不是一个原子性操作,某个线程执行count++(原子性)这个操作时,其它线程是无法读取++后的值(可见性)

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

举个例子:演示不可见性,但不一定这个数值一定是图中所说的情况造成的,只是其中的一种可能性。

最后t1先执行完毕,将自己工作内存中count=5w写回主内存中对t2线程是不可见的,于是t2就会把自己工作内存中count=55659写回工作内存并将t1写回主内存的值覆盖

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

有很多种可能性,这里只是列举一种,总之最后的答案一定不是10w。

计划+2次,但最终只+了1次,本来t1已经将count=1写回了主内存,但是t2也让count=1写回了主内存,最后count的值还是1,正确操作之后应该是主内存中count=2才对。


类似火车的售票系统:
客户A在买票时,发现还有一张票,当他下订单时,在主内存中nums=1-1=0没票了,此时恰好客户B和A同时准备购买票,但是此时nums=0还没有写回主内存,导致B也买了这张票(超卖现象)。

关于内存的问题

从硬件角度来说:所谓的 “工作内存”, 则是指 CPU 的寄存器和高速缓存,比如执行a+10这个操作,第一步先从主内存中读取a变量的值,但是读取的值需要一个临时存储的地方,这个地方就叫寄存器。所谓"主内存"在硬件角度是真实存在的

CPU 访问自身寄存器的速度以及高速缓存的速度, 远远超过访问内存的速度(快了 3 - 4 个数量级,也就是几千倍,上万倍)
内存设备之所以这么多,还是出于成本考虑~

指令重排

如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题,可以少跑一次前台。这种叫做指令重排序

此时t1线程实例化对象还没有将name成员变量赋值,但是t2线程调用per.getName()方法,于是就出现name=null的情况

总结(面试重点)

4.4 synchronize关键字(面试重点)

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

monitor lock对象锁

synchronized的底层是使用操作系统的mutex lock实现的

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

互斥:多个线程想要获取同一个对象的锁(同一个对象很关键),同一时间只有一个线程可以获取到这个锁,其它线程进入阻塞状态等待


不管几个线程对这个对象进行处理,同一时刻只有一个线程拿到这个对象的锁,拿到这个锁就会执行increase()方法,另外线程就处于等待阻塞状态,哪个线程先拿到对象的锁不确定。

Synchronized原理


每个对象都有一块描述"锁"信息的内存,这个信息告诉其它线程当前该对象有没有被哪个线程持有

当某个对象被某个线程上锁时,其它对象想要获取当前对象锁就会进入一个阻塞队列,但这个队列不遵循FIFO队列先入先出的规矩

互斥关系的定义


到底是不是构成互斥关系,关键看锁的是不是同一个对象

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

获取锁到释放锁的中间过程保证了原子性和可见性,当其它线程再来获取锁时,保证获取到的共享变量的值一定是正确的值

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

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

可重入(第三个特性)

可重入:获取到"锁"的线程可以再次获取锁的操作

类似上了两次"锁"

死锁(不可重入锁)

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才真正释放锁(才能被别的线程获取到)

关于加两次锁的原因:只要进入一个synchronized代码块一次,就得上锁一次,计数器加一

至于加不加synchronized的问题:synchronized只是保证线程安全,对于代码编译不会产生任何问题,加不加看实际应用需求,比如你只是进行读操作,完全没有必要加synchronized。只有当修改共享变量,程序员为了保证线程安全会人为在代码中自己添加关键字。

4.5 synchronized修饰类中的成员方法

在成员方法上加synchronized关键字,锁的就是该类的实例化对象

counter1和counter2是两个是两个不同的对象,在t1线程中counter1对象调用同步代码块的方法上锁的对象是counter1对象,t2线程中同理,获取的是counter2对象的锁

互斥现象:t1和t2线程使用同一个对象调用同步代码块的成员方法。哪个线程先获取对象锁就先执行哪个线程

4.6 synchronized修饰类中的静态方法

锁的是一个类,不管这个类实例化多少个对象,同一时刻也只能一个线程访问


虽然看似不同对象调用静态代码块的方法,但是同一时刻只能有一个线程能获取这个锁

4.7 synchronized修饰当前代码块

同步代码块之前的代码都可以并发执行,this相当于锁的是当前对象。三个线程中都是同一个对象调用同一个方法,所以在同一时刻只能有一个线程能进入这个方法

当不同的对象调用increase4()方法就不构成互斥了,因为此时每个线程中是不同的对象在调用同步代码块

同步代码块的意义就在于锁的粒度更细,方法中的其他代码仍然是多线程并发执行,如果锁的内容太多,多线程效率不高

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


上述锁Counter.class对象锁的是Counter的同一个实例化对象,只有是Counter类同一个对象才会上锁

下图中锁的是Reentrant主类.class这个对象,class对象全局唯一,不管是Counter的哪个对象都构成互斥,同一个时刻只能一个线程进入

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


锁的是成员方法,只要是同一个Vector对象都是互斥


ConcurrentHashMap和CopyOnWriteArrayList都属于java.util.concurrent并发工具包下的

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

5.1保证可见性

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

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

直接访问工作内存(实际是CPU的寄存器或者 CPU 的缓存),速度非常快, 但是可能出现数据不一致的情况,加上volatile,强制读写内存,速度是慢了,但是数据变的更准确了

代码示例:flag变量在没有volatile关键字修饰时,t1线程中一直处于死循环,因为 t1 线程一直读取自己的工作内存 flag=0 , t2 线程中即使改变了 flag 的值(对于t1线程来说是不可见的),t1线程也一直处于死循环,没有及时读取主内存中更新后的flag值。

但是在共享变量 flag 之前加上关键字 volatile ,线程 t1 会立即退出循环,因为 volatile 保证可见性,保证 t1 线程每次强制读取主内存中 flag 的数据

synchronized 同样可以保证可见性,去掉 flag 前的关键字volatile,给counter对象上锁,在线程 t2 中修改 flag 的值同样可以立即终止线程 t1

volatile 和 synchronized 有着本质的区别:synchronized 能够保证原子性,volatile 保证的是内存可见性不保证原子性,比如在之前写的t1线程累加5w和t2线程累加5w的例子中,即使给共享变量加上关键字volatile,同样得不到正确答案10w

5.2 内存屏障


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

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

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

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

  • 此外final可以保证可见性原因:final关键字修饰的变量为常量,必须在初始化时赋值且不能更改,所以保证了天然的可见性

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

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

6.1 wait()等待

使用wait()方法会释放锁,线程会进入等待队列

带有时间参数timeout的wait方法,时间参数单位是ms

6.2 notify()唤醒

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

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

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

注意事项:
1.如果没有搭配synchronized使用notify和wait方法会报错
2.唤醒等待线程也需要使用同一个对象的notify方法


调用notify方法唤醒在等待的线程,一定要等notify方法中同步代码块里执行完毕才可以执行被唤醒的线程。

修改 NotifyTask 中的 run 方法,,把 notify 替换成 notifyAll:
虽然是同时唤醒 3 个线程,,但是这 3 个线程需要竞争锁,所以并不是同时执行

6.3等待队列 阻塞队列


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

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

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

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

wait():Object类提供的方法,必须搭配synchronized关键字使用,调用方法会释放锁

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

设计模式:软件开发中也有很多常见的 “问题场景”,针对这些问题场景,大佬们总结出了一些固定的套路

如何保证单例

不管外部调用或者不调用这个对象,只要类加载到JVM,唯一对象就会产生


构造方法私有化,可以保证对象产生的数量

单例对象使用静态变量static的原因:在类的内部只用一次构造方法。外部访问成员变量需要通过对象,但是外部没有这个对象,此时外部想要获取这个唯一对象就需要把这个对象设置为静态变量,获取的方式:通过get方法

7.1饿汉式单例

这个类只要一加载就产生唯一对象(饥不择食),不管外部是否需要这个对象,只要类加载到JVM,唯一对象就产生

/*** 饿汉式单例模式* @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产生实例化对象

7.2懒汉式单例

懒汉式单例更常见的原因:按需分配,只有需要实例化才产生对象,节省内存空间

hashMap的懒汉式加载示例

hashMap产生对象时都没有初始化table数组,只有第一次调用put方法才初始化数组大小,节省空间

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

饿汉式是天然的线程安全,JVM加载类时就创建了这个唯一对象;但是懒汉式就不一样,假设同时三个线程并行执行调用get方法,会发现对象为null,此时可能三个线程都会同时创建三个不同的对象

解决懒汉式线程安全问题

1.直接在方法上加锁:效率不高,锁的粒度太粗

2.优化 double - check

此时假设三个线程同时执行到同步代码块,当t1获取到这个锁进入同步代码块创建对象后释放锁,t2和t3还是会从开始时竞争锁的位置开始执行,还是会再次创建两个不同的对象,所以不可行

1.有三个线程,开始执行get方法,通过外层的 if (singleTon = null) 知道了实例还没
有创建的消息,于是三个线程开始竞争同一把锁
2.其中线程1率先获取到锁,此时线程1通过里层的 if (single= null) 进一步确认实例是
否已经创建,如果没创建,就把这个实例创建出来
3. 当线程1 释放锁之后,线程2 和线程3也拿到锁,也通过里层的 if (instance == null) 来
确认实例是否已经创建,发现实例已经创建出来了,就不再创建了
4.后续的线程,不必加锁,直接就通过外层 if (instance = null) 就知道实例已经创建了,从
而不再尝试获取锁了,降低了开销

3.使用volatile关键字
加锁 / 解锁是一件开销比较高的事情,而懒汉模式的线程不安全只是发生在首次创建实例的时候, 因此后续使用的时候,不必再进行加锁了,外层的 if 就是判定下看当前是否已经把 instance 实例创建出来了

volatile保证防止指令重排

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

八、阻塞队列


和普通队列的区别:阻塞队列线程安全

内部实现原理:

8.1生产消费者模型

/*** 生产者 - 消费者模型* @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接口的子类;第二个参数指定多长时间之后执行 (单位为毫秒)

九、线程池

9.1 线程池的作用

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

9.2 线程池的核心方法

  • 线程池核心类 ThreadPoolExecutor
  • 线程池核心方法 submit()提交任务,参数中可以接收 Runnable 接口和 Callable 接口

9.3 Executors 线程池工具类

  • Executors 本质上是 ThreadPoolExecutor 类的封装,提供了四种常用创建线程池的静态方法
  • 定时器线程池使用的是 Schedule类 和其他线程池调用的类不同,调用的是schedule()方法,其它线程池调用 submit()方法
  • 线程池接口关系:

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

corePoolSize:核心池线程数量(正式工)
maximumPoolSize:线程池最大线程数量(正式工+临时工)
keepAlive:线程池临时线程允许空闲时间
workQueue:工作队列,实质上就是个阻塞队列,线程从中取出执行任务
hander:拒绝策略,当任务数量超出线程池负荷时怎么办

固定线程池:没有临时工,最大线程数量==核心池线程数量

动态缓存池:核心线程为0,每当有新任务进来都是临时创建线程
工作队列(阻塞队列)几乎用不上,最大线程数能达到40多亿

单线程池:只有一个线程,所以需要无解界限的工作队列
固定大小延迟线程池:

单线程池的意义

单独创建一个线程:执行完一次任务就需要销毁
单线程池:将任务不断提交到阻塞队列中,线程只需要不断调度工作队列中的任务即可

9.5线程池工作流程

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

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

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

  4. 判断当前maximumSize是否达到最大值:
    未达到上限:创建临时线程执行此任务(临时工)
    达到上限:执行拒绝政策

十、常用锁的策略

10.1悲观锁 乐观锁

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

10.2乐观锁的实现

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

  • 乐观锁的实现案例:设当前余额为 100. 引入一个版本号 version,初始值为 1 并且我们规定 “提交版本必须大于记录当前版本才能执行更新余额”

    线程 1 在自己的工作内存中减去50(100-50),线程 2 在自己的工作内存中减去20(100-20)

    版本号:记录更新的次数。当线程1先将50写回主内存中时,版本号+1,表示更新了一次主内存。

    线程 2 也想将自己工作内存的80写回主内存,但此时发现主内存版本号等于2,线程 2 自己的版本号等于1,两者版本号不相等,无法将80写回主内存,写入失败。不满足 “提交版本必须大于记录当前版本才能执行更新” 的乐观锁策略。就认为这次操作失败

    解决策略:
    1.直接报错
    2.CAS策略:线程2先读取主内存版本号为2,再将主内存的新数据写回自己的工作内存进行操作(减20操作),最后将30写回主内存,并且版本号+1等于3

10.3读写锁

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

JDK内置的读写锁:ReentrantReadWriteLock
读加锁和读加锁之间,不互斥
写加锁和写加锁之间,互斥
读加锁和写加锁之间,互斥

10.4重量级锁和轻量级锁


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

10.5公平锁和非公平锁

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

10.6 CAS 的应用

  • CAS可以理解为乐观锁的一种实现
  1. 应用1: 原子类,原子类可以保证线程安全,原子类常用方法
  • incrementAndGet 相当于 ++i ;getAndIncrenment 相当于 i++ ;获取原子类的值直接调用get()方法

  • 内部实现原理
  1. 使用 CAS 机制实现自旋锁

  1. CAS引发的ABA问题

10.7 synchronized 锁升级原理

  • Synchronized 初始使用乐观锁策略,当发现锁竞争比较频繁的时候, 就会自动切换成悲观锁策略

10.8 juc包下常用子类 lock锁

  • synchronized 和 lock 的区别

10.8 死锁

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

11.1 Semaphore 信号量

  • 信号量就是一个计数器,表示可用资源数量
  • 信号量就类似于停车场的停车位
  • 代码案例,创建信号量(可用资源数量)为 5 ,再创建 20 个线程,同一时间只有 5 个线程可以获取这 5 个资源。

  • 假设创建资源数量为 6 ,如果每个线程同时获取 2 个资源,也必须同时释放 2 个资源,此时只有 3 个线程同一时刻获取这 6 个资源

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

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

十二、面试问题

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

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

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

  • java对象锁,synchronized

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

  • 线程池常见子类和常用方法

  • 线程池核心参数、线程池关闭流程、volatile关键字保证可见性无法保证原子性

  • 线程池工作流程

多线程 (进阶+初阶)相关推荐

  1. 学习笔记:C++初阶【C++入门、类和对象、C/C++内存管理、模板初阶、STL简介、string、vector、list、stack、queueu、模板进阶、C++的IO流】

    文章目录 前言 一.C++入门 1. C++关键字 2.命名空间 2.1 C语言缺点之一,没办法很好地解决命名冲突问题 2.2 C++提出了一个新语法--命名空间 2.2.1 命名空间概念 2.2.2 ...

  2. 《看漫画学Python》1、2版分享,初阶+进阶一起学

    前言 学习Python的小伙伴大部分应该都知道<看漫画学Python:有趣.有料.好玩.好用(全彩版)>这本书! 但是刚开始接触Python的朋友都会有一个共同的烦恼,自学好无聊,好枯燥, ...

  3. C语言初阶第三篇:换我心,为你心,始知C语言 从C语言选择语句到循环语句保姆教学来喽

    换我心,为你心,始知C语言 老铁们,咱们上一篇文章把字符串的知识点给过了一遍,今天就要开始真正的程序语句的学习了,也就是选择语句和循环语句,今天的内容比较简单,所以写的也不多,废话不多说,咱们开始吧! ...

  4. 二叉树前中后序遍历+刷题【中】【数据结构/初阶/C语言实现】

    文章目录 1. 二叉树基础操作 1.1 二叉树遍历 1.1.1 前序遍历 前序遍历(Pre-Order Traversal) 1.1.2 中序遍历 中序遍历(In-Order Traversal) 1 ...

  5. C语言——结构体(初阶版)

    1.定义和使用结构体变量 结构体的基础知识 结构是一些值的集合,这些值称为成员变量.结构的每个成员可以是不同类型的变量. 自己建立结构体类型 结构的成员可以是标量.数组.指针,甚至是其他结构体. st ...

  6. 前端三剑客之 HTML - JavaEE初阶 - 细节狂魔

    文章目录 前言 后端 && 前端的部分历史 - java 关于网站搭建 正文开始! HTML 怎么编写一个HTML的代码? 小拓展: 快速编写 HTML 代码的小技巧 浏览器的开发者工 ...

  7. 【设计经验传承】图标设计初阶要先型

    UI最重要组建之一就是图标,随着扁平化设计的发展趋势,越来越注重图标的简洁与寓意表达,平面图标已占主导地位.每位设计师所处的阶段所关注的要点是不一样的,我把图标设计分为2个阶段–初阶与高阶,这样分是为 ...

  8. JavaEE初阶系列 -开头篇:计算机是如何工作的(为下一篇的线程做铺垫)

    文章目录 前言 计算机的发展史 冯诺依曼体系 CPU 中央处理器: 进行算术运算和逻辑判断. CPU 基本工作流程 逻辑门 门电路(Gate Circuit) - 模电数电知识 1.与门:可以针对两个 ...

  9. MySQL初阶 - 易错知识点整理(待更新)

    MySQL初阶 - 易错知识点整理(待更新) Note:这里根据 CSDN Mysql技能树 整理的易错题,可参考MySQL 有这一篇就够,MySQL详细学习教程(建议收藏),MySQL 菜鸟教程 文 ...

最新文章

  1. “一切都是消息”--iMSF(即时消息服务框架)之【请求-响应】模式(点对点)...
  2. java web问题
  3. [转] Windows Server 2012 Beta Cluster (Hyper-V 3.0)-iSCSI篇
  4. asp 判断数组等于_如何在 ASP.NET Core MVC 中处理 404 错误
  5. 软件测试岗位职责和划分
  6. 推荐系统之GBDT+LR
  7. linux开发板访问互联网 笔记本win10中虚拟机
  8. 编译原理——将代码翻译成四元式序列
  9. Android Studio属性动画,Android Studio 三种方式建立动画效果
  10. Vue2.5学习笔记(三)动画
  11. 中国首个数字化糖尿病逆转项目在宁波正式启动
  12. lwIP配置宏整理(部分)
  13. linux服务器安全与配置,Linux系统服务器安装后的安全配置
  14. 图像视频伪造检测,针对DeepFake技术检测效果不佳
  15. 95 后大学生利用漏洞免费吃肯德基获刑,他冤么?
  16. 分页的php处理,分页处理的PHP类
  17. 一、Linux下MySQL安装和卸载图文教程详解
  18. 罗斯蒙特3051差压变送器
  19. 只会玩VR游戏?你还可以用虚拟现实向女友求婚
  20. HTML中margin重合问题

热门文章

  1. 重学计算机网络(三) - DHCP IP的孽缘
  2. 在 HTML5 中捕获音频和视频
  3. Linux下逻辑测试语句参数和流程控制语句 if语句
  4. FFMPEG保存H264流到AVI文件中形成录像
  5. ROS中的TF坐标变换工具及实现、Rviz查看(十四)C++、python
  6. AST实战|手把手教你还原ob混淆:ob混淆代码特征
  7. alter database命令
  8. Content type 'multipart/form-data;boundary=--------------------------258075776767858126421870;chars
  9. android Retrofit下载图片
  10. 177本名著浓缩成了177句话!经典收藏!太有道理了!