当前位置: 首页 > news >正文

Java 多线程(二)

目录

  • synchronized
    • 刷新内存
    • synchronized的特性可重入的
    • 出现死锁的情况
    • 如何避免死锁(重点,死锁的成因和解决)
  • volatile关键字
  • wait和notify
  • 多线程的代码案例
    • 饿汉模式和懒汉模式的线程安全问题
    • 指令重排序问题
    • 阻塞队列
      • 使用
      • 自己实现一个阻塞队列
      • 实现生产者消费者模型

synchronized

  1. synchronized除了修饰代码块之外,还可以修饰一个实例方法或者是修饰一个静态方法

synchronized修饰实例方法

class Counter{public int count;// 此方法是下面方法的简化版本synchronized public void increase(){count++;}// this作为了锁对象public void increase2(){synchronized(this){count++;}}
}
public class Demo14 {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Thread t1 = new Thread(()->{for(int i = 0;i < 50000;i++){counter.increase();}});Thread t2 = new Thread(()->{for(int i = 0;i < 50000;i++){counter.increase();}});t1.start();t2.start();t1.join();t2.join();System.out.println(counter.count);}
}

在这里插入图片描述
synchronized修饰静态方法

在这里插入图片描述
java的对象头:java的一个对象的内存空间中,除了你自己定义的属性之外,还有一些自带的属性,这些自带的属性就叫对象头
在对象头中,其中就有属性表示当前对象是否已经加锁
synchronized使用的是java对象头当中的锁

刷新内存

  1. synchronized刷新内存存疑,网上众说纷纭

synchronized的特性可重入的

  1. 可重入锁:指的是一个线程连续针对一把锁加锁两次,不会出现死锁满足这个要求就是可重入的,不满足就是不可重入

  2. 死锁:就是同一个线程对同一个对象加锁两次,第二次的加锁需要第一次的解锁,而第一次的解锁需要第二次的加锁完毕,所以两者相互矛盾
    在这里插入图片描述

  3. 死锁在日常的代码中出现,还不容易观察到,比如下面的代码,使用可重入锁可以解决死锁的问题

  4. 可重入锁:让锁记录一下,是哪个线程给它锁住的,后续再加锁的时候,如果加锁线程就是持有锁的线程就直接加锁成功
    在这里插入图片描述

  5. 提一个问题:
    不能释放锁,因为两个右大括号中可能有别的代码,解锁了就线程不安全了
    在这里插入图片描述

  6. 利用引用计数,记录释放锁的时机
    在这里插入图片描述

出现死锁的情况

  1. 两种情况:
    在这里插入图片描述
  2. 两个线程,两把锁
public class Demo15 {public static void main(String[] args) {Object locker1 = new Object();Object locker2 = new Object();Thread t1 = new Thread(()->{synchronized (locker1){// 这里的sleep非常重要,要确保t1 和 t2 都分别拿到一把锁之后,再进行后续动作// 否则就会出现t1很快对两把锁都加锁了的情况,就不会出现死锁的情况了try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker2){System.out.println("t1加锁成功!");}}});Thread t2 = new Thread(()->{synchronized (locker2){try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker1){System.out.println("t2加锁成功!");}}});t1.start();t2.start();}
}

在这里插入图片描述
在这里插入图片描述
3. N个线程,M把锁(相当于2的扩展)
这样更容易出现死锁了
有一个经典的描述N个线程,M把锁的模型,哲学家就餐问题

在这里插入图片描述

如何避免死锁(重点,死锁的成因和解决)

  1. 死锁的原因:
    死锁有4个必要条件:
    在这里插入图片描述
    第4点也是(代码结构)
    第4点循环等待比如是哲学家就餐问题

  2. 第1和第2点是锁的基本特性,只要3和4出现了就会形成死锁

解决死锁问题:
1.对于1和2是synchronized本身的特性,解决不了
2.对于3,请求等待可以把代码改成并列执行的顺序,先执行1再执行2
3.对于4,循环等待,可以规定小的编号的先执行,再执行大的编号(先大后小也可以)
4.有的时候必须要获取多个锁再操作,就需要编号

解决死锁条件3

public class Demo15 {public static void main(String[] args) {Object locker1 = new Object();Object locker2 = new Object();Thread t1 = new Thread(()->{synchronized (locker1){// 这里的sleep非常重要,要确保t1 和 t2 都分别拿到一把锁之后,再进行后续动作// 否则就会出现t1很快对两把锁都加锁了的情况,就不会出现死锁的情况了try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}synchronized (locker2){System.out.println("t1加锁成功!");}});Thread t2 = new Thread(()->{synchronized (locker2){try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}synchronized (locker1){System.out.println("t2加锁成功!");}});t1.start();t2.start();}
}

解决死锁条件4

public class Demo15 {public static void main(String[] args) {Object locker1 = new Object();Object locker2 = new Object();Thread t1 = new Thread(()->{synchronized (locker1){// 这里的sleep非常重要,要确保t1 和 t2 都分别拿到一把锁之后,再进行后续动作// 否则就会出现t1很快对两把锁都加锁了的情况,就不会出现死锁的情况了try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker2){System.out.println("t1加锁成功!");}}});Thread t2 = new Thread(()->{synchronized (locker1){try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker2){System.out.println("t2加锁成功!");}}});t1.start();t2.start();}
}

volatile关键字

  1. volatile用来解决线程不安全的两点
    保证内存可见性
    禁止指令重排序

内存可见性出现的线程安全问题:

import java.util.Scanner;public class Demo16 {private static int isQuit = 0;public static void main(String[] args) {// 读Thread t1 = new Thread(()->{while(isQuit == 0){// 循环里面什么都不做}System.out.println("t1线程结束!");});t1.start();// 修改isQuit的值使线程t1结束Thread t2 = new Thread(()->{System.out.println("请输入 isQuit的值:");Scanner scanner = new Scanner(System.in);isQuit = scanner.nextInt();});t2.start();}
}

在这里插入图片描述
在这里插入图片描述

出现内存可见性的原因:是编译器优化出现的bug
在这里插入图片描述
volatile可以解决上述问题
在多线程的环境下,编译器对于是否要进行优化,判定不一定准,这就需要我们使用volatile关键字告诉编译器,不要优化了
让判断isQuit和从内存中读达到平衡,让判断的操作每次读取isQuit

只需要给isQuit前加上volatile就可以了

import java.util.Scanner;public class Demo16 {private volatile static int isQuit = 0;public static void main(String[] args) {// 读Thread t1 = new Thread(()->{while(isQuit == 0){// 循环里面什么都不做// 什么都不做会飞快的运行多次}System.out.println("t1线程结束!");});t1.start();// 修改isQuit的值使线程t1结束Thread t2 = new Thread(()->{System.out.println("请输入 isQuit的值:");Scanner scanner = new Scanner(System.in);isQuit = scanner.nextInt();});t2.start();}
}

编译器优化,加了sleep就不会触发内存可见性问题了,原因如下:
在这里插入图片描述

  1. 内存的可见性问题:
    在这里插入图片描述
    main memory:主存
    work memory:cpu寄存器 + 缓存

  2. volatile不能保证原子性

  3. synchronized可以保证内存可见性

  4. volatile可以解决的线程安全问题主要是可见性问题和有序性问题,但不能解决原子性问题。

wait和notify

  1. wait:等待,让指定线程进入阻塞状态
  2. notify:通知,唤醒阻塞状态的进程
    在这里插入图片描述
    下面的代码就是在t2线程中,t1进行join,t2线程执行后,t2线程要等待t1线程执行完毕之后才能继续执行
public class Demo17 {public static void main(String[] args) {Thread t1 = new Thread(()->{for(int i = 0;i < 5;i++){try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("t1线程执行完毕!");});Thread t2 = new Thread(()->{try {// join()在哪个线程中,哪个线程就会等待调用join()的线程先执行完毕,该线程才能继续执行t1.join();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("t2线程执行完毕!");});t1.start();t2.start();System.out.println("主线程执行完毕!");}
}

在这里插入图片描述

  1. wait在执行的时候要做三件事:
    释放当前的锁
    让线程进入阻塞
    当线程被唤醒的时候,重新获取到锁

释放锁的前提是先加上锁
wait和notify都是Object的方法
随便一个对象都可以使用wait和notify

wait的代码,等待

public class Demo18 {public static void main(String[] args) throws InterruptedException {Object object = new Object();synchronized(object) {System.out.println("调用wait之前");// 把wait放在synchronized里面来调用,确实是先拿到了锁object.wait();// 然后阻塞等待,等待到其他线程调用nofity唤醒System.out.println("调用wait之后");}}
}

在这里插入图片描述
notify的代码,等待唤醒

public class Demo19 {public static void main(String[] args) {// 必须对同一个对象进行等待唤醒Object object = new Object();Thread t1 = new Thread(()->{synchronized (object){System.out.println("wait开始!");try {object.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("wait结束!");}});Thread t2 = new Thread(()->{synchronized (object){try {Thread.sleep(3000);} catch (InterruptedException e) {throw new RuntimeException(e);}object.notify();System.out.println("wait唤醒!");}});t1.start();t2.start();}
}
  1. 使用wait和notify也可以避免线程饿死

线程饿死:让1号一直在获取锁和释放锁之间反复横跳
在这里插入图片描述
解决方法:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
5. wait还有一个超时时间的版本,避免了wait无休止的等待下去,比如:wait(3000)

多线程的代码案例

  1. 单例模式:非常经典的设计模式
    设计模式:就是程序员的棋谱,开发过程中会遇到很多经典场景,针对这些场景,大佬就提出了很多解决方案,按照解决方案来写就不会很差

2. 最容易考的两种模式:单例模式和工厂模式

  1. 单例:单个实例(对象),有些场景下,我们希望有的类,只能有一个对象,使用单例模式
  2. 让编译器强制要求,只能new一次对象
  3. 单例模式语法上没有要求,要通过编程技巧来达到

在这里插入图片描述
单例模式:
1.在类的内部,自己提供一个现有的实例
2.构造方法设置为private修饰的,不让其他人进行创建新的实例

class Singleton{// 自己提供一个现有的实例private static Singleton instance = new Singleton();// 通过这个方法来获取这个实例public static Singleton getInstance(){return instance;}// 把它设置为私有,这样外面的其他代码就无法new出这个实例了private Singleton(){}
}
public class Demo20 {public static void main(String[] args) {// 这里又有一个实例了,就不是单例了// Singleton singleton = new Singleton();Singleton s1 = Singleton.getInstance();Singleton s2 = Singleton.getInstance();System.out.println(s1 == s2);}
}

在类加载的时候创建,饿汉模式(比较急切)
在这里插入图片描述
懒汉模式(在第一次使用的时候,再去创建实例
和饿汉模式的区别是:在调用getInstance()的时候才创建出实例
也只有唯一实例

// 懒汉模式
class SingletonLazy{public static SingletonLazy instance = null;public static SingletonLazy getInstance(){if(instance == null){instance = new SingletonLazy();}return instance;}private SingletonLazy(){}
}
public class Demo21 {public static void main(String[] args) {}
}

懒汉模式其实在计算机当中是更加高效的,下面就是一个例子:
在这里插入图片描述

饿汉模式和懒汉模式的线程安全问题

  1. 线程安全问题造成的原因:如果多个线程同时修改同一个变量,会出现线程安全问题
    如果多个线程同时读取同一个变量,不会有线程安全问题

  2. 饿汉模式只读取了变量,是安全的

  3. 懒汉模式既读取了变量,又修改了变量是不安全的

不安全的原因:
在这里插入图片描述
如何保证懒汉模式是线程安全的?
需要再if的前面进行加锁,让它的操作变成原子的操作
在这里插入图片描述

在这里插入图片描述
一旦出现加锁,那就不是高性能了
在这里插入图片描述
我感觉是只加锁一次就行,后续,再使用这个获取实例的方法就不加锁,可以吗?

有什么办法既保证线程安全,又可以保证不怎么影响执行效率呢?

可以再加一个if
第一个if用来判定是否需要加锁
第二个if用来判定是否需要new对象,保证对象只new一次

// 懒汉模式
class SingletonLazy{public static SingletonLazy instance = null;public static SingletonLazy getInstance(){// 懒汉模式对类对象进行加锁// if和new的问题// 如果线程安全,就是已经new好了一个对象// 如果线程不安全(有不安全的风险),还没有newif(instance == null) {synchronized (SingletonLazy.class) {if (instance == null) {instance = new SingletonLazy();}return instance;}}}private SingletonLazy(){}
}
public class Demo21 {public static void main(String[] args) {}
}

在这里插入图片描述
在这里插入图片描述

指令重排序问题

  1. 指令重排序也是编译器的优化,编译器为了提高效率,会调整原有代码的执行顺序,前提是保证了逻辑是不变的
  2. 指令重排序可以再保证逻辑不变的前提下,提高代码的执行效率
  3. 指令重排序可能会对我们的代码产生影响,单线程下可以,多线程对逻辑不变会产生误判

在这里插入图片描述
4. 使用volatile可以解决指令重排序问题
5. 如果日常使用过程中少了1,2,3其中一个都会出现问题
在这里插入图片描述
6. 使用反射能否打破单例模式
使用序列化/反序列化能否打破单例模式
其实反射和序列化都是在特定场景下使用的,使用的比较克制

单例模式在面试过程中都是经常考到的
先写一个线程不安全的,再写一个加锁的,再写一个高性能,不要每次加锁的,最后写一个考虑指令重排序问题的

// 懒汉模式
class SingletonLazy{public static volatile SingletonLazy instance = null;public static SingletonLazy getInstance() {// 懒汉模式对类对象进行加锁// if和new的问题// 如果线程安全,就是已经new好了一个对象// 如果线程不安全(有不安全的风险),还没有newif (instance == null) {synchronized (SingletonLazy.class) {if (instance == null) {instance = new SingletonLazy();}}}return instance;}private SingletonLazy(){}
}
public class Demo21 {public static void main(String[] args) {}
}

阻塞队列

  1. 阻塞队列是多线程代码中比较常用到的一种数据结构
  2. 阻塞队列最大的意义就是可以用来实现生产者消费者模型
  3. 生产者消费者模型,一种常见的多线程代码编写方式

阻塞队列
1.线程安全的
2.带有阻塞特性的:
如果队列为空,再往队列中出元素,就会阻塞,就要一直阻塞到其他线程往队列中添加元素为止

如果队列为满,再往队列中入元素,就会阻塞,就要一直阻塞到其他线程往队列中取出元素为止

在这里插入图片描述
3. 生产者消费者模型的好处(意义)是什么?
可以降低资源的竞争,提高我们程序的效率

在这里插入图片描述

解耦合
比如服务器A和服务器B进行数据交互,A用来发送请求,B用来接收请求,A和B的耦合度比较高,如果B挂了的话,会影响到A,如果再加入一个C服务器,也和A交互,A的代码就需要进行修改

如果使用一个阻塞队列的话,A和B都把请求和响应放入阻塞队列中,A挂了不会影响B,再加入一个C也不需要修改A的代码

这样耦合度就降低了

在这里插入图片描述
2. 削峰填谷
峰:短时间内请求量比较多
谷:请求量比较少
有了这样的机制之后,就可以在有突发情况来临时,整个服务器系统让然可以正确执行

在这里插入图片描述

利用生产者消费者模型也可以得到解决
在这里插入图片描述

使用

阻塞队列
在这里插入图片描述

import jdk.nashorn.internal.ir.Block;import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;public class Demo22 {public static void main(String[] args) throws InterruptedException {BlockingQueue<String> queue = new LinkedBlockingQueue<>();queue.put("111");queue.put("222");queue.put("333");queue.put("444");System.out.println(queue.take());System.out.println(queue.take());System.out.println(queue.take());System.out.println(queue.take());// 最后一次会阻塞住System.out.println(queue.take());}
}

自己实现一个阻塞队列

  1. 基于一个普通的队列,加上线程安全,加上阻塞就可以了
    在这里插入图片描述
  2. 使用size来判空和判满
    普通的队列
    修改会产生线程安全问题,直接加锁
    使用wait和notify实现阻塞

彼此唤醒对方的wait,一个队列要么是空,要么是满

// 不写作泛型了,直接让队列存储String
class MyBlockingQueue{// 此处也可以使用构造方法,来指定数组的最大长度private String[] data = new String[1000];// 队列的起始位置// 都加上volatile,防止出现内存可见性问题private volatile int head = 0;// 队列的结束位置的下一个位置private volatile int tail = 0;// 队列中的有效元素个数private volatile int size = 0;// 入队列和出队列public void put(String elem) throws InterruptedException {synchronized (this) {while (size == data.length) {// 满了,再插入就阻塞了// 普通队列,直接returnthis.wait();}data[tail] = elem;tail++;// 如果自增之后来到了数组末尾,让它回到数组的开头,环形队列if (tail == data.length) {tail = 0;}size++;// 这个notify用来唤醒take中的waitthis.notify();}}public String take() throws InterruptedException {synchronized (this) {while (size == 0) {// 队列为空,直接返回// 队列为空,继续出队列就会出现阻塞this.wait();}String ret = data[head];head++;size--;if (head == data.length) {head = 0;}// 这个notify用来唤醒put中的waitthis.notify();return ret;}}
}

异常唤醒的
wait还可能被interrupt唤醒,interrupt会中断wait
在这里插入图片描述
在这里插入图片描述
处理异常唤醒的情况:
在这里插入图片描述
加上volatile
在这里插入图片描述

实现生产者消费者模型

  1. 用阻塞队列来实现生产者消费者模型
// 不写作泛型了,直接让队列存储String
class MyBlockingQueue{// 此处也可以使用构造方法,来指定数组的最大长度private String[] data = new String[1000];// 队列的起始位置// 都加上volatile,防止出现内存可见性问题private volatile int head = 0;// 队列的结束位置的下一个位置private volatile int tail = 0;// 队列中的有效元素个数private volatile int size = 0;// 入队列和出队列public void put(String elem) throws InterruptedException {synchronized (this) {while (size == data.length) {// 满了,再插入就阻塞了// 普通队列,直接returnthis.wait();}data[tail] = elem;tail++;// 如果自增之后来到了数组末尾,让它回到数组的开头,环形队列if (tail == data.length) {tail = 0;}size++;// 这个notify用来唤醒take中的waitthis.notify();}}public String take() throws InterruptedException {synchronized (this) {while (size == 0) {// 队列为空,直接返回// 队列为空,继续出队列就会出现阻塞this.wait();}String ret = data[head];head++;size--;if (head == data.length) {head = 0;}// 这个notify用来唤醒put中的waitthis.notify();return ret;}}
}public class Demo23 {public static void main(String[] args) {// 生产者和消费者分别用一个线程来表示(也可以用多个)MyBlockingQueue queue = new MyBlockingQueue();// 消费者Thread t1 = new Thread(()->{while(true){try {String result = queue.take();System.out.println("消费元素 = " + result);Thread.sleep(500);// 先不sleep,利用sleep控制生产和消费的速度} catch (InterruptedException e) {throw new RuntimeException(e);}}});// 生产者Thread t2 = new Thread(()->{int num = 1;while(true) {try {queue.put(num + " ");System.out.println("生产元素 = " + num);num++;// Thread.sleep(500);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t1.start();t2.start();}
}
http://www.dtcms.com/a/377421.html

相关文章:

  • TCGA(The Cancer Genome Atlas)数据库是癌症基因组学研究的重要资源,包含了多种癌症类型的基因组、转录组、表观基因组和临床数据
  • 单片机与PLC:定义、异同及替代可能性解析
  • 金融知识:投资和融资
  • 重学前端013 --- 响应式网页设计 CSS网格布局
  • hCaptcha 图像识别 API 对接说明
  • 大模型应用开发八股
  • Linux进程概念(上):进程基本概念和进程状态
  • 汽车EPAS ECU功能安全建模分析:Gamma框架+深度概率编程落地ISO 26262(含寿命预测案例)
  • 深入解析:ES6 中 class 与普通构造器的区别
  • 华清远见25072班网络编程学习day3
  • QT(3)
  • 具有区域引导参考和基础的大型语言模型,用于生成 CT 报告
  • 【QT】-怎么实现瀑布图
  • 【Leetcode hot 100】94.二叉树的中序遍历
  • 渗透测试真的能发现系统漏洞吗
  • 【芯片设计-信号完整性 SI 学习 1.2 -- loopback 回环测试】
  • Android App瘦身方法介绍
  • MySQL修改字段类型避坑指南:如何应对数据截断与转换错误?
  • Linux权限以及常用热键集合
  • 成品油加油站综合监管迈入 “云时代”!智慧物联网涉税数据采集平台推行工作全面推进
  • c primer plus 第五章复习题和练习题
  • C++设计模式,高级开发,算法原理实战,系统设计与实战(视频教程)
  • Spring 统一功能处理
  • ES6基础入门教程(80问答)
  • 第3讲 机器学习入门指南
  • InnoDB 逻辑存储结构:好似 “小区管理” 得层级结构
  • copyparty 是一款使用单个 Python 文件实现的内网文件共享工具,具有跨平台、低资源占用等特点,适合需要本地化文件管理的场景
  • C# 哈希查找算法实操
  • 一个C#开发的Windows驱动程序管理工具!
  • 环境变量