JavaEE-多线程编程&单例模式

一、等待通知

系统内部,线程之间是抢占式执行的,随即调度,程序可以通过手动干预的方式,能够让线程一定程度的按咱们想要的顺序执行,无法主动让某个线程被调度,但可以主动让某个线程等待。等待通知可以安排线程之间的执行顺序。

举个栗子:当t1线程要在队列获取元素,由于此时队列是空的无法进行工作,它只能频繁的进行获取释放锁的操作,导致其他线程不能得到cpu分配资源,线程中调度是无序的,这种情况很可能出现,称为------线程饿死(不会像死锁那样卡死,但是可能会卡一下,影响程序效率)

++等待通知机制可以解决上述问题++ :++条件判断是否能执行当前逻辑,不能就主动wait阻塞等待,把执行的机会让给别的线程,避免该线程进行一些无意义的重试++ ,等时机成熟时(其他线程通知-notify),阻塞被唤醒。代码实现:

java 复制代码
public static void main(String[] args) {
        Object locker = new Object();
        Thread t1 = new Thread(()->{
            synchronized (locker){
                System.out.println("t1等待前");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("t1等待后");
        });

当我们执行这样的逻辑时,线程就会在执行完第一句输出语句后通过wait阻塞等待,注意:++因为wait操作被执行时是先解锁然后阻塞等待,解锁的前提是有锁,所以需要在操作前先加锁。++此时可以通过jconsole来查看线程状态:

可以看出此时是WAITING状态。再写另一个线程来唤醒它:

java 复制代码
Thread t2 = new Thread(()->{
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            synchronized (locker){
               System.out.println("t2唤醒前");
               locker.notify();
               System.out.println("t2唤醒后");
           }
        });

此时工作台可以从输出顺序看到执行过程

*(1)t1线程获取锁 (2)t1线程阻塞等待且解锁 (3)t2线程获取锁 (4)t2线程唤醒t1线程,执行完逻辑后释放锁 (5)t1重新获取锁,从上次被阻塞的地方继续执行。t1的状态变化是:++WAITING->RUNNABLE->BLOCKED++*处于blocked状态是因为唤醒后需要等t1先释放锁。

注意:notify一次只能唤醒一个线程,而且是随机的。不过notify也有可以唤醒所有线程的方法:

java 复制代码
locker.notifyAll();

wait也有一个带参数的版本,无参数版本采用的是死等战术,等不到唤醒程序就一直等,带参数版本和join差不多,过了参数时间就不会再阻塞状态。

二、单例模式

单例模式是一种经典的设计模式,相比其他的设计模式算是比较简单的设计模式,也是面试中常考的设计模式。

单例模式->单个实例,整个进程中有且只有一个对象,这样的对象就成为单例(instance),那么如何保证进程中只有一个实例呢?

需要让编译器帮我们进行检查,通过编码上的技巧,使编译器自动发现我们是否创建了多个实例,并尝试创建多个实例时,直接编译报错。

单例模式有很多种写法,本篇文章主要介绍两种:饿汉模式&懒汉模式。

1、饿汉模式

先看这样的一串代码:

java 复制代码
class Singleton{
    public static Singleton instance = new Singleton();
}

static成员初始化时机是在类加载的时候,可以简单理解为++JVM一启动就立即加载,成员也就立即创建了。++static修饰的类属性是类对象的,每个类的类对象在JVM中只有一个,里面的静态成员只有一个,初始化也只执行一次,当后续需要这个类的实例时可以通过方法来获取已经创建好的实例,而不是再创建新的,这个方法为:

java 复制代码
 public static Singleton getInstance() {
        return instance;
    }

那么如果其他线程想通过此类创建新的对象该怎么办呢?

当类之外的代码想尝试创建新的对象时一定会调用构造方法,所以++将构造方法的权限设置为private时就会无法调用,编译报错++。如下:

java 复制代码
private Singleton(){
     /  
    }

当类一加载静态成员就被创建了,就像饿的人看见吃的会想赶紧吃的感觉一样,所以这种模式可以被称为"饿汉模式"。

2、懒汉模式

在计算机中,懒往往是一个褒义词,代表着高效率。相对于饿汉模式一加载类就创建对象,懒汉则是当第一次需要使用对象才会去创建,就把创建实例的代价省下来了,按照这个思路来创建类:

java 复制代码
class SingletonLazy{
    public static SingleLazy instance = null;

    public static SingleLazy getInstance() {
        if(instance == null){
            instance = new SingleLazy();
        }
        return instance;
    }
}

先将静态成员的引用指向空,当需要创建实例时判断当前引用是否为空,为空时再创建新的,不为空就直接返回实例。懒的本质就是偷懒,能少做就少做,懒->缓。

如果代码中存在多个单例类,都使用懒汉模式的话这些实例会在程序启动时扎堆的创建,可能把程序启动时间拖慢,如果使用饿汉模式的话,调用时机是分散的,化整为0,让用户感受不到卡顿

多线程模式下分析懒汉模式与饿汉模式

思考:当多个线程同时getInstance时这两种模式是否会引起线程不安全问题?

饿汉模式安全,但懒汉模式是不安全的。

++饿汉模式安全的原因++ :创建实例的时机是java进程启动时,比主线程还早创建,因此在其他线程调用getInstance时实例肯定已经创建好了,每个线程只做了一件事,就是读取上述静态变量的值,多个线程读取一个变量,安全。

而懒汉模式与其不同,懒汉模式的关键操作代码是这些

第一行是:"读",查看一下实例引用的地址的是否为空,而第二行是赋值,也就是修改操作,上述操作在多线程环境下容易出现问题,比如会产生下面这种执行顺序

++假定最初instance引用为空,t1判断引用为空,t2判断引用为空,t1创建实例对象,由于t2已经判定完是否为空,所以也会创建实例对象。++

上述代码t2创建的引用会覆盖掉t1的引用的地址,进一步t1的instance没有指向了就会被GC回收掉

解决办法:++可以通过加锁的方式来保证懒汉模式下getInstance是安全的,当t1线程进入判定语句时t2需阻塞等待,t1创建完实例释放锁后t2才能获取锁,开始判定操作,此时的instance就已经指向了地址不为空了。++初步优化后的代码:

java 复制代码
class SingletonLazy{
    public static SingletonLazy instance = null;

    public static SingletonLazy getInstance() {
        synchronized (SingletonLazy.class){
            if(instance == null){
                instance = new SingletonLazy();
            }
        }
        return instance;
    }
}

但是懒汉模式只有在初次调用getInstance时会涉及到线程安全问题,一旦实例创建好了后面再调用都是只读操作,不涉及线程安全问题,而后续调用明明没有线程安全问题还要加锁,增加了没必要的开销

解决办法:++在加锁前再判断一次当前调用是否为第一次调用,如果是第一次调用再去获取锁,判定条件还是看instance是否为空即可。++

别忘了上篇文章提到的volatile,二次优化后的代码:

java 复制代码
class SingletonLazy{
    public static volatile SingletonLazy instance = null;
    public static SingletonLazy getInstance() {
        if(instance == null){
            synchronized (SingletonLazy.class){
                if(instance == null){
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
}

通过双重if避免了重复创建对象。

下篇文章更新多线程编程经典案例二------阻塞队列

感谢观看

道阻且长,行则将至

相关推荐
风铃儿~5 分钟前
Spring AI 入门:Java 开发者的生成式 AI 实践之路
java·人工智能·spring
斯普信专业组11 分钟前
Tomcat全方位监控实施方案指南
java·tomcat
忆雾屿21 分钟前
云原生时代 Kafka 深度实践:06原理剖析与源码解读
java·后端·云原生·kafka
武昌库里写JAVA34 分钟前
iview Switch Tabs TabPane 使用提示Maximum call stack size exceeded堆栈溢出
java·开发语言·spring boot·学习·课程设计
gaoliheng00642 分钟前
Redis看门狗机制
java·数据库·redis
我是唐青枫1 小时前
.NET AOT 详解
java·服务器·.net
Su米苏1 小时前
Axios请求超时重发机制
java
本郡主是喵2 小时前
并发编程 - go版
java·服务器·开发语言
南风lof3 小时前
源码赏析:Java线程池中的那些细节
java·源码阅读