线程的实例运用(单例模式、阻塞队列)以及关于生产者和消费者模型的介绍
文章目录
目录
前言
前篇文章介绍了进程和线程的关系以及线程的基础用法,此篇文章着重介绍线程的实例运用关于单例模式和阻塞队列,内容如有遗漏或者存在错误,欢迎大家评论区留言。
一、volatile关键字
在Java中,volatile关键字用于确保变量的可见性和有序性,但不保证原子性。它主要用于多线程环境下,解决共享变量的同步问题。volatile关键字通过插入内存屏障(Memory Barrier)来禁止指令重排序优化。编译器或处理器可能会对指令进行重排序以提高性能,但在多线程环境下,这种优化可能导致不可预期的结果。volatile确保了变量的读写操作按照代码顺序执行。
编译器会自动对重排序优化,从而达到提高性能的效果,在多线程中会导致部分数据不会实时更新导致编译出错,因此就需要加上volatile关键字保证读写操作正确执行从而实时更新数据
import java.util.Scanner;
public class demo14 {
public /*解决方法1volatile*/ static int isquit;
public static void main(String[] args) {
Thread t = new Thread(()->{
while(isquit == 0){
/*解决方法2
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}*/
}
System.out.println("t线程结束");
});
Thread t1 =new Thread(()->{
System.out.println("请输入isquit的值"); //通过t1线程改变isquit从而使t线程结束
Scanner scanner = new Scanner(System.in);
isquit = scanner.nextInt();
});
t.start();
t1.start();
}
}
针对上述问题主要有两种解决办法:
1.对变量isquit加上volatile关键字(推荐)
对于变量isquit加上关键字volatile以后会使得isquit的值获得实时更新,从而让另一个线程的while循环能够检测isquit值得变化
2.降低另一个线程的运行速度
由于另一个线程在高速执行while循环导致编译器不会读isquit值得的变化,可在线程t的while循环中加入sleep休眠或者别的操作从而减缓t线程的运行速度,使得编译器不得不强制性读写isquit的变化
二、wait与notify的阻塞与唤醒
wait方法会使得当前线程释放锁进入等待状态,直到别的线程调用notify方法或者notifyAll方法来唤醒wait
notify方法会随机唤醒一个相同对象上调用的wait的线程,被唤醒的线程会重新获取锁,成功唤醒后线程会从wait后面继续值行
注意:wait与notify方法都必须在锁中执行,且对应的wait与notify上的是同一个锁对象
public class demo15 {
public static void main(String[] args) {
Object object = new Object(); //初始化一个锁对象
Thread t1 = new Thread(()->{
synchronized (object){
for (int i = 0; i < 6; i++) {
System.out.println("t1线程工作");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if(i == 3){ //当线程执行到i等于三的时候线程t1会阻塞
System.out.println("线程t1阻塞");
try {
//线程1等待,同时会把object钥匙抛出,括号内还可以填时间,设置其最多等待时间
object.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 2; i++) { //在线程t2走到锁之前会与t1线程同时进行
System.out.println("t2线程工作");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
synchronized (object){ //当t1抛出object以后才会进行
for (int i = 0; i < 3; i++) {
System.out.println("t2线程工作");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
object.notify(); //会唤醒wait操作,但同时锁只有一把,所以只有当t2操作运行结束释放锁的时候才会运行到线程t1的wait操作
System.out.println("线程t1开始工作");
}
});
t1.start();
t2.start();
}
}
注意:此处的代码中,t1线程会优先进行锁竞争,当t2线程进入到锁之后会等待t1线程释放锁之后才会进行锁里面的代码块。同时,当t2线程唤醒t1线程之后t1线程也不会马上执行,因为存在锁竞争关系,会等t2线程把锁进程执行完毕后并且自动释放锁之后才会继续执行t1线程的wait后半部分
三、线程的实例运用(重点)
1.单例模式
主要针对目标类有且只有一个实例对象,通过单例模式共享一个实例减少资源占用,提高性能。单例模式的实现是需要通过代码从源头上限制程序猿来new对象
单例模式的实现三步骤:
1.要在目标类当中优先实例化一个私有全局对象
2.为这个对象提供一个公共的供给方法
3.将这个类的构造方法设置为私有的防止实例化新对象
饿汉代码实现:
饿汉模式:类加载时即创建实例,简单但可能造成资源浪费。
class Singletomhungry{
//实例化一个私有全局对象
private static Singletomhungry instance = new Singletomhungry();
//共有方法提供该对象
public static Singletomhungry getSingletomhungry(){
return singletomhungry;
}
//将构造方法私有化,防止创建新的对象
private Singletomhungry(){};
}

如果此时针对单例模式类强行实例化对象则会报错,提示该类的构造方法是私有的无法使用,也就实现了单例模式无法实例化的作用

应该通过类名访问的方式直接给对应的对象赋予insantance相关参数
懒汉模式的实现:
懒汉模式:延迟加载,仅在首次调用时创建实例,兼顾性能和线程安全。
class Singletomlazy{
private static volatile/*要点1*/ Singletomlazy instance = null;
public static Singletomlazy getInstance(){
/*要点2*/if(instance == null){ //判断是否需要加锁
/*要点3*/synchronized (Singletomlazy.class){
if(instance == null){ //判断是否需要new一个对象
instance = new Singletomlazy();
}
}
}
return instance;
}
private Singletomlazy(){};
}
懒汉模式的实现大体上还是那三步骤
针对懒汉模式在多线程的情况下会出现以下问题:
1.指令重排序问题,多线程情况下会出现误判----------->关键字volatile解决
2.多线程情况下针对同一变量修改值会出现线程安全问题----------->加锁synchronized
在getInstance方法中,需要加上两层if判断条件,第一个适用于判断是否需要加锁。第二个则是判断是否需要给instance对象赋值,如果instance对象为空,上述两个判断if都会进入。一旦给instance赋值以后,以后if判断都不会在经过了。
2.阻塞队列
阻塞队列是多线程中一种常见的数据结构,其作用便是线程安全通信、流量控制与缓冲、降低代码耦合度。阻塞队列最大的意义便是实现了生产者消费者模型
Java中可以通过提供的BlockingQueue<String>接口实现阻塞队列,还可以LinkedBlockingQueue与ArrayBlockingQueue这两类具体实现,前者是链表的形式,后者是数组形式。BlockingQueue<String>中还有put入队方法和take出队方法用于实现数据的储存和取出

队列中的元素是先进先出的顺序,而且此处只放入了三个元素,但是输出了四个并没有报错,这就是阻塞队列的优势具有流量控制与缓冲的作用
通过分析可得之,想要实现阻塞队列就是在普通队列的基础上实现线程安全和阻塞操作即可
普通队列实现
class MyBlockingQueue{
String[] data = new String[5];
private int head = 0;
private int tail = 0;
private int sight = 0;
public void put(String elem){
if(head == data.length){
return;
}
data[head] = elem;
head++;
sight++;
if(head == data.length){
head = 0;
}
}
public String take(){
if(sight == 0){
return null;
}
String elem = data[tail];
tail++;
sight--;
if(tail == data.length){
tail = 0;
}
return elem;
}
}
通过数组实现一个普通队列,再通过写put方法与take方法实现储存和释放元素

线程安全和阻塞操作
当实现普通队列以后,阻塞队列的实现就要开始考虑线程安全和阻塞操作了
针对线程安全就需要加上synchronized锁和考虑多线程指令重排序,而阻塞操作的实现则需要用wait与notify的等待与唤醒实现
class MyBlockingQueue{
String[] data = new String[100]; //数组最大容量是5
private volatile int head = 0; //防止出现指令重排序问题,成员变量都加上了volatile
private volatile int tail = 0;
private volatile int sight = 0;
private final Object locker = new Object();
public void put(String elem) throws InterruptedException {
synchronized (locker){ //加锁防止出现线程安全问题
while(sight == data.length){ //重点1:要将if改为while
locker.wait(); //重点2:wait与notify的位置
}
data[head] = elem;
head++;
sight++;
if(head == data.length){
head = 0;
}
locker.notify();
}
}
public String take() throws InterruptedException {
synchronized (locker){
while(sight == 0){ //重点1
locker.wait(); //重点2
}
String elem = data[tail];
tail++;
sight--;
if(tail == data.length){
tail = 0;
}
locker.notify();
return elem;
}
}
}
重点1:
原本判断条件是if改为while的原因是:唤醒wait方法不仅可以通过notify唤醒,
还可以通过interrupt唤醒,如果是通过interrupt唤醒会导致此处还没有take释放
操作就直接进行put的后续操作,后续会出现bug,因此就需要反复验证
使用wait唤醒的时候,往往都是通过while作为唤醒判定方式,目的就是为了能够多次反复确认该数组满足为满或者为空
重点2:
当满足数组为空或者为满的情况下,要实现阻塞操作就需要在对应的地方加上wait等待操作,要等另一个方法存储或者释放元素后才能唤醒wait操作从而继续执行,因此就应该在put和take方法最后加上notify唤醒操作
两个方法关系图大致如下:
take 和 put只有一边能阻塞
如果put阻塞了,其他线程调用put都会阻塞,只有通过take方法才能唤醒
如果take阻塞了,其他线程调用take也都会阻塞,只有通过put方法才可以唤醒
生产者和消费者的体现
生产者-消费者模型是一种经典的多线程协作模式,用于解决生产者和消费者之间的数据共享和同步问题。生产者负责生成数据,消费者负责处理数据,两者通过共享缓冲区进行通信。
public class test {
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); //休眠延时
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
//生产者
Thread t2 = new Thread(()->{
int num = 1;
while(true){
try {
queue.put(num + ""); //存储元素,由于put内必须是字符串类型,因此要加""
System.out.println("生产元素:" + num);
num++;
//Thread.sleep(500); //休眠延时作用
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t1.start();
t2.start();
}
}
分别定义俩线程用来模拟生产者和消费者,生产者目的就是储存元素并且打印日志用于显示生产了哪些元素,而消费者目的就是释放元素并且打印日志显示释放了哪些元素。在生产者和消费者当中二选一加入休眠操作进行延时模拟两线程执行速度不统一
1.当在消费者当中加入休眠时:

此时的情况就是生产者速度远大于消费者,由于数组上限为100,因此前100个元素在一瞬间基本就已经生产完毕,而消费者在消费完第一个元素以后会进入0.5s延时。直到生产者生产的元素到101的时候,此时数组中的元素已经占满了100个会进入阻塞等待状态,等到消费者消耗掉元素的时候生产者才可以继续生产。此时就成功模拟出了消费者和生产者的缓冲区通信
2.当在生产者当中加入休眠时:
此时的情况就是消费者速度远大于生产者,当生产者生产出一个元素的时候会进入0.5s延时,而此时消费者会进行多次消耗,但由于数组中此时只存在1个元素,消费者也只能消费掉一个元素。当消费者进行第二次消费的时候,由于数组为空会进入wait等待状态,直到生产者生产出下一个元素才会唤醒消费者从而接着释放元素,因此最终实现的情况就是生产者生产一个消费者紧接着消费一个依次进行
总结
重点掌握如何实现单例模式和阻塞队列,看百遍不如实际动手敲一遍。关于线程的实例基本都要考虑在多线程运行情况下的线程安全问题和指令重排序问题,在各种基本框架实现下,再加入解决方法才可解决实际生产问题
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)