面试之多线程案例(四)

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创建系统自带的线程池

面试之多线程案例(四)_第1张图片

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

2.3线程池的工作流程

面试之多线程案例(四)_第2张图片

2.4拒绝策略

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

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

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

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

你可能感兴趣的:(面试,面试,java,单例模式)