面试之多线程案例(四)

1.单例模式

单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。

1.1单例模式的类型

单例模式包含两种类型:

  • 饿汉式:在类加载的时候已经创建好对象,等待被使用
  • 懒汉式:在真正需要使用对象的时候才去创建对象

1.2饿汉式创建单例对象

饿汉式在类加载的时候已经创建好该对象,在程序调用的时候直接返回该对象即可,不需要等到使用时再创建。

public class Singleton{
    
    private static final Singleton singleton = new Singleton();
    //此时已经实例化好了一个对象,内存中已经存在了,因此不会再存在多个Singleton对象了
    
    private Singleton(){}
    
    public static Singleton getInstance() {
        return singleton;
    }
}

1.3懒汉式创建单例对象

image.png

懒汉式创建单例对象就是在使用单例对象之前先判断是否已经被实例化了,如果已经被实例化,则可以直接使用,否则才开始实例化。

public class Singleton {
    
    private static Singleton singleton;
    
    private Singleton(){}
    
    public static Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
    
}

这就是一个懒汉式创建单例对象,此时已经可以进行使用。但是还是存在一些问题。比如并发操作时,两个线程同时判断该对象为空,那么两个线程都会实例化对象,所以就会创建两个对象,已经不满足单例模式。

image.png

此时的解决办法就是在方法上加锁或者对类对象加锁,如下:

public static synchronized Singleton getInstance() {
    if (singleton == null) {
        singleton = new Singleton();
    }
    return singleton;
}
// 或者
public static Singleton getInstance() {
    synchronized(Singleton.class) {   
        if (singleton == null) {
            singleton = new Singleton();
        }
    }
    return singleton;
}

此时这样加锁就规避了两个线程同时创建对象,当一个线程获取锁时,另一个线程需要阻塞等待,保证从始至终只创建一个对象。但是这样的话,在每次获取对象时候都需要获取锁,并发性能较差。

所以我们需要优化:首先判断对象是不是为空,如果为空才获取锁进行对象的实例化,如果首次判断不为空,那么直接可以使用对象,不用再获取锁。

所以直接在方法上加锁的方式不可取,因为无论如何每次都要获取锁

public static Singleton getInstance() {
    if (singleton == null) {  // 线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton
        synchronized(Singleton.class) { // 线程A或线程B获得该锁进行初始化
            if (singleton == null) { // 其中一个线程进入该分支,另外一个线程则不会进入该分支
                singleton = new Singleton();
            }
        }
    }
    return singleton;
}

 1.4加入volatile防止指令重排序

创建一个对象,在JVM中会经过三步:

(1)为singleton分配内存空间

(2)初始化singleton对象

(3)将singleton指向分配好的内存空间

指令重排序是指:JVM在保证最终结果正确的情况下,可以不按照程序编码的顺序执行语句,尽可能提高程序的性能

在这三步中,第2、3步有可能会发生指令重排现象,创建对象的顺序变为1-3-2,会导致多个线程获取对象时,有可能线程A创建对象的过程中,执行了1、3步骤,线程B判断singleton已经不为空,获取到未初始化的singleton对象,就会报NPE异常。文字较为晦涩,可以看流程图:image.png

使用volatile关键字可以防止指令重排序,volatile可以保证指令执行顺序与程序指明顺序一致,不会发生改变。

public class Singleton {
    
    private static volatile Singleton singleton;
    
    private Singleton(){}
    
    public static Singleton getInstance() {
        if (singleton == null) {  // 线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton
            synchronized(Singleton.class) { // 线程A或线程B获得该锁进行初始化
                if (singleton == null) { // 其中一个线程进入该分支,另外一个线程则不会进入该分支
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
    
}

2.线程池

线程池顾名思义,就是一次创建多条线程,放在一个池子里,用的时候拿出来一个,用完之后放回去。

在实际业务中需要用到许多线程,虽然创建线程相比于创建进程来说比较轻量级,但是频繁的创建销毁也会消耗很多的资源。线程池最大的好处就是减少每次启动,销毁线程的损耗。 

2.1jdk中默认线程池

  public static void main(String[] args) {
        // 1. 用来处理大量短时间工作任务的线程池,如果池中没有可用的线程将创建新的线程,如果线程空闲60秒将收回并移出缓存
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        // 2. 创建一个操作无界队列且固定大小线程池
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
        // 3. 创建一个操作无界队列且只有一个工作线程的线程池
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
        // 4. 创建一个单线程执行器,可以在给定时间后执行或定期执行。
        ScheduledExecutorService singleThreadScheduledExecutor = Executors.newSingleThreadScheduledExecutor();
        // 5. 创建一个指定大小的线程池,可以在给定时间后执行或定期执行。
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
        // 6. 创建一个指定大小(不传入参数,为当前机器CPU核心数)的线程池,并行地处理任务,不保证处理顺序
        Executors.newWorkStealingPool();
    }

2.2创建系统自带的线程池

ThreadPoolExecutor  threadPoolExecutor = new ThreadPoolExecutor(
                5,//核心线程数
                10,//最大线程数
                1,//临时线程存活的时间
                TimeUnit.SECONDS,//时间单位
                new LinkedBlockingQueue<>(20),//阻塞队列类型
                };

2.3线程池的工作流程

2.4拒绝策略

  1. ThreadPoolExecutor.AbortPolicy,这个策略是直接拒绝,也是默认的策略

  2. ThreadPoolExecutor.CallerRunsPolicy,将任务返回给调用者(调用的线程)

  3. ThreadPoolExecutor.DiscardOldestPolicy,放弃最早等待的任务

  4. ThreadPoolExecutor.DiscardPolicy,放弃最新的任务

3.死锁

3.1什么是死锁

多个线程同时被阻塞,它们中的一个或者全部都在等待某一个资源释放。由于线程被无限期的阻塞,导致程序不能正常终止

3.2死锁所要具备的条件

  1. 互斥条件:该资源任意时刻只有一个线程占用
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
  3. 不剥夺条件:线程已获得的资源在未使用完成之前其他线程不能强行剥夺,只有自己使用完毕之后才释放资源
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系

3.3如何避免死锁

只要破坏产生死锁四个条件的其中一个就可以打破死锁!

  • 破坏互斥条件:这是锁的基本条件,不能打破
  • 破坏不剥夺条件:占用部分资源的线程进一步申请资源时,如果申请不到,则释放它所有的资源
  • 破坏请求与保持条件:一次性申请全部所需资源
  • 破坏循环等待条件:靠按序申请资源来预防,按照某一顺序申请资源,释放资源则是反序释放。破坏循环等待条件
  • 锁排序法:可以设计一套获取锁的策略,先获取哪个锁,后获取哪个锁,按顺序获取就会避免死锁问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值