Java面试题复习-基础篇

Java面试题

基础篇

排序算法

二分查找
  1. 前提:有已排序数组A(假设已经做好)
  2. 定义左边界L、右边界R、确定搜素范围、循环执行二分查找(3、4两步骤)
  3. 获取中间索引 M=Floor((L+R)/2)
  4. 中间索引的值 A[M] 与待搜索的值 T 进行比较
    1. A[M] == T 表示找到,返回中间索引
    2. 中间值右侧的其他元素都大于T,无需比较,中间索引左边去找,M-1 设置为右边界,重新查找
    3. 中间值左侧的其他元素都大于T,无需比较,中间索引左边去找,M-1 设置为左边界,重新查找
  5. 当 L>R 时,表示没有找到,应结束循环
package com.peggy.rs;

/**
 *
 * <br>1. 前提:有已排序数组A(假设已经做好)</br>
 * <br>2. 定义左边界L、右边界R、确定搜素范围、循环执行二分查找(3、4两步骤)</br>
 * <br>3. 获取中间索引 M=Floor((L+R)/2)</br>
 * <br>4. 中间索引的值 A[M] 与待搜索的值 T 进行比较<br/>
 *    <ul>1.A[M] == T 表示找到,返回中间索引</ul>
 *    <ul>2.中间值右侧的其他元素都大于T,无需比较,中间索引左边去找,M-1 设置为右边界,重新查找</ul>
 *    <ul>3.中间值左侧的其他元素都大于T,无需比较,中间索引左边去找,M+1 设置为左边界,重新查找</ul>
 * <br>5. 当 L>R 时,表示没有找到,应结束循环</br>
 */
public class BinarySearch {

    public static int binarySearch(int[] a,int t){
        int L=0 ,R=a.length-1,M;
        while (L<=R){
            M=(L+R)/2;
            if (a[M]==t){
                return M;
            }else{
                if (a[M]>t){
                    R=M-1;
                }else{
                    L=M+1;
                }
            }
        }
        return -1;
    }
}

@Test
public void binarySearchTest(){
    int a[]={1,2,4,6,7,99,100,344,745,999};
    int index= BinarySearch.binarySearch(a,99);
    System.out.println(index);
}

但这样存在一个问题,关于M=(L+R)/2整数溢出

@Test
public void binarySearchTest2(){
    int L=0,R=Integer.MAX_VALUE-1;
    int m=(L+R)/2;
    System.out.println(m);
    m=(10 + R) / 2;
    System.out.println(m);
}

可以看到数据是有溢出的

image-20220906200743658

解决方案一:

通过等式代换,执行效率低

(L+R)/2 == L/2+R/2 == L-L/2+R/2 == L-(L-R)/2

@Test
public void binarySearchTest3(){
    int L=0,R=Integer.MAX_VALUE-1;
    int m=L-(L-R)/2;
    //(L+R)/2 ===> L/2+R/2 ===> L-L/2+R/2 ===> L-(L-R)/2
    System.out.println(m);  
    m=10-(10-R)/2;
    System.out.println(m);
}

位移处理

@Test
public void binarySearchTest4(){
   int L=0,R=Integer.MAX_VALUE-1;
   int m=(L+R)/2;
   System.out.println(m);
   m=(10 + R)>>>1;
   System.out.println(m);
}

相关面试题

  1. 有一个有序表为1,5,8,11,19,22,31,35,40,45,48,49,50当二分查找值为48的结点时
    查找成功需要比较的次数
  • 京东实习生招聘
  1. 使用二分法在序列1,4,6,7,15,33,39,50,64,78,75,81,89,96中查找元素81时,需要经过()次比较
  • 美图点评校招
  1. 在拥有128个元素的数组中二分查找一个数,需要比较的次数最多不超过多少次
  • 北京易道博识社招

解题技巧:

  • 奇数二分去中间

  • 偶数二分取中间靠左

2 n = 128 或 128 / 2 / 2... 直到 1 2^n = 128 或128/2/ 2 ...直到1 2n=128128/2/2...直到1

问题转化为 l o g 2 128 ,如果手边有计算器,用 l o g 10 128 / l o g 10 2 问题转化为log_2128,如果手边有计算器,用log_{10}128 / log_{10}2 问题转化为log2128,如果手边有计算器,用log10128/log102

  • 是整数,则该整数即为最终结果
  • 是小数,则舍去小数部分,整数加一为最终结果
插入排序

849589-20171015225645277-1151100000

public static int[] selectionSort(int[] a) {

    for (int i = 1; i < a.length; i++) {
        int t = a[i];//待插入的元素
        int j = i - 1; //代表已排序区域的元素的索引
        while (j >= 0) {
            if (t > a[j]) {
                a[j + 1] = a[j];
            } else {
                break;
            }
            j--;
        }
        a[j + 1] = t;
    }
    return a;
}

基本实现

  1. 将数组分为两个区域,排序区域和未排序区域,每一轮从未排序的区域中取出第一个元素,插入到排序区域(需要保证顺序)
  2. 重复以上的步骤,直到整个数组有序

优化方式:

  1. 待插入元素进行比较时,遇到比自己小的元素,就代表找到了插入位置,无需进行后续比较
  2. 插入时可以直接移动元素,而不是交换元素

与选择排序比较

  1. 二者平均时间复杂度都是O(n2)

  2. 大部分情况下,插入都略优于选择

  3. 有序集合插入的时间复杂度为O(n)

  4. 插入属于稳定排序算法,而选择属于不稳定排序

image-20220907015112110

第一题:

18,23

18,19,23

9,18,19,24

第二题:

9,18,23,19,23,15

9,15,23,19,23,18

9,15,18,23,19,23

9,15,18,19,23,23

希尔排序

849589-20171015230936371-1413523412

对于插入排序存在一个缺点,如果是较大的元素恰好在数组的最前面,那么这个较大的元素的移动次数就会很多

比如下面这一数组 a[ ] ={9,4,2,5,1,7,8,0}

可以发现插入排序此时对于9而言就需要移动7次,为了减少这样的排序次数,就有了改进的算法希尔排序

快速排序

1d43762b58681c0ace7caa3fa93b33da

1.每一轮排序选择一个基准点(pivot)进行分区

  • 让小于基准点的元素的进入一个分区,大于基准点的元
    素的进入另一个分区
  • 当分区完成时,基准点元素的位置就是其最终位置

2.在子分区内重复以上过程,直至子分区元素个数少于等于1,这体现的是分而治之的思想(divide-and-conquer)

实现方式

1.单边循环快排(lomuto洛穆托分区方案)

  • 选择最右元素作为基准点元素
  • j指针负责找到比基准点小的元素,一旦找到则与i进行交换
  • i指针维护小于基准点元素的边界,也是每次交换的目标索引
  • 最后基准点与i交换,i即为分区位置

2.双边循环快排((并不完全等价于hoare霍尔分区方案)

  • 选择最左元素作为基准点元素
  • j指针负责从右向左找比基准点小的元素,i指针负责从左向右找比基准点大的元素,一旦找到二者交换,直至i,j相交
  • 最后基准点与i(此时i与j相等)交换,i即为分区位置

集合篇

ArrayList
  • ArrayList的扩容机制
  • Iterator的fail-fast、fail-safe机制
关于ArrayList初始容量

无参构造

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

public ArrayList() {
    //默认分配一个空数组初始容量为0
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

整数有参构造

private static final Object[] EMPTY_ELEMENTDATA = {};

public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        //将整数的大小作为数组初始容量
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}

集合有参构造

public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elementData.getClass() != Object[].class)
            //根据集合的大小创建一个初始容量
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // replace with empty array.
        this.elementData = EMPTY_ELEMENTDATA;
    }
}
扩容分析

add方法的扩容分析

public void add(int index, E element) {
    rangeCheckForAdd(index);
    
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    System.arraycopy(elementData, index, elementData, index + 1,size - index);
    elementData[index] = element;
    size++;
}
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    //新的容量为旧的容量的1.5倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

addAll方法的扩容分析

public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray();
    int numNew = a.length;
    ensureCapacityInternal(size + numNew);  // Increments modCount
    System.arraycopy(a, 0, elementData, size, numNew);
    size += numNew;
    return numNew != 0;
}
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}
  • ArrayList() 会使用长度为零的数组
  • ArrayList(int initialCapacity) 会使用指定容量的数组
  • public ArrayList(Collection<? extends E> c) 会使用c的大小作为数组容量add(Object o)首次扩容为10,再次扩容为上次容量的1.5倍
  • addAll(Collection c)没有元素时,扩容为Math.max(10,实际元素个数),有元素时为Math.max(原容量1.5倍,实际元素个数)
迭代器fail-fast、fail-safe
  • fail-fast 在遍历的过程中一旦发现其他人对当前的数据进行修改,立刻抛出异常
  • fail-safe 发现遍历的同时其它人来修改,应当能有应对策略,例如牺牲一致性来让整个遍历运行完成
public class TestArrayList {
    private static void failFast(){
        ArrayList<String> arrayList = new ArrayList<>();
        arrayList.add("A");
        arrayList.add("B");
        arrayList.add("C");
        arrayList.add("D");
        arrayList.add("E");

        for (String str:arrayList) {
            System.out.println(str);
        }

        System.out.println(arrayList);
    }

    public static void main(String[] args) {
        failFast();
    }
}

image-20220907101845257

image-20220907101946363

private static void failSafe() {
    CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
    list.add("1");
    list.add("2");
    list.add("3");
    list.add("4");
    list.add("5");

    for (String s : list) {
        System.out.println(s);
    }
    System.out.println(list);
}

image-20220907102836801

牺牲了元素的一致性

image-20220907102914186

fail-fast源码分析

public Iterator<E> iterator() {
    return new Itr();
}
int cursor;       // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
// expectedModCount初始创建迭代器的次数
// modCount集合中当前元素的个数
int expectedModCount = modCount; 
Itr() {}
public E next() {
    //每一次遍历下一个元素都会调用checkForComodification()
    checkForComodification();
    int i = cursor;
    if (i >= size)
        throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    cursor = i + 1;
    return (E) elementData[lastRet = i];
}
final void checkForComodification() {
    //将当前的集合中的元素个数与迭代器的迭代次数做比较
    if (modCount != expectedModCount)
        //如果不一致(元素被修改)则抛出并发修改异常
        throw new ConcurrentModificationException();
}

fail-safe源码分析

public Iterator<E> iterator() {
    return new COWIterator<E>(getArray(), 0);
}
private COWIterator(Object[] elements, int initialCursor) {
    cursor = initialCursor;
    snapshot = elements;
}

image-20220907105731244

image-20220907110003386

image-20220907111734992

image-20220907112016388

  • ArrayList是fail-fast的典型代表,遍历的同时不能修改,尽快失败
  • CopyonWriteArrayList 是 fail-safe 的典型代表,遍历的同时可以修改,原理是读写分离

ArrayList vs LinkedList

ArrayList

  • 基于数组,需要连续内存
  • 随机访问快(指根据下标访问)(基于数组,需要连续内存随机访问快(指根据下标访问)
  • 尾部插入、删除性能可以,其它部分插入、删除都会移动数据,因此性能会低
  • 可以利用cpu缓存,局部性原理

LinkedListLinkedList

  • 基于双向链表,无需连续内存
  • 随机访问慢(要沿着链表遍历)
  • 头尾插入删除性能高
  • 占用内存多
HashMap

HashMap在存取一对键值对时,先会对key值进行hash运算,然后对运算得到的hash值进行16取模,得到其桶下标记

image-20220907133948700

而之所以这样设计的原因是,当我们要查找一个key取出该对应的值时,我们只需要将我们要取出的key值进行一次hash运算(会调用该key对象对于的hashcode方法返回hash值),然后将其得到的取余值定位到桶下标就可以快速的定位到对于的key

如图所示当我们添加的元素的hash码一样(hash碰撞)就会出现数组转链表的形式,我们知道链表的索引查找元素的效率较低,那么对于这种情况HashMap底层会怎么处理呢?

image-20220907134616378

思路一:

缩减链表长度,并重新计算hash(hash值模数组长度)码,重新分配桶下标(增加长度为原来元素大小的2倍)

image-20220907135134620

重新计算hash码(hash模32),桶下表进行分配

image-20220907135427883

问题如果说链表中的8个hash码都是一样的,所有元素的原始hash值时一样的比如

image-20220907135646120

那么这种情况就无法通过扩容重新分配桶下标。

为了解决上述所出现的问题,就需要另一种的解决方案,红黑树的树化。

思路二:当链表的长度超过8个,并且容量超过64时,将当前的链表转化为红黑树

image-20220907194644842

对于红黑树的特点,右边的元素最小左边的元素最大

面试题

  • 底层数据结构,1.7与1.8有何不同?
    • 1.7数组+链表,1.8数组+(链表|红黑树)
  • 为何要用红黑树,为何一上来不树化,树化阈值为何是8,何时会树化,何时会退化为链表?为何要用红黑树,为何一上来不树化,树化阈值为何是8,何时会树化,何时会退化为链表?
    • 在正常的情况下,链表的长度是不会超过8的,之所以有红黑树的存在,是为了避免恶意的DoS攻击,防止链表超长的情况下导致的性能下降。所以数化是偶然的情况
      • hash 表的查找,更新的时间复杂度是0(1),而红黑树的查找,更新的时间复杂度是0(logv(2) n),TreeNode占用空间也比普通Node的大,如非必要,尽量迹是使用链表。
      • hash值如果足够随机,则在 hash 表内按泊松分布,在负载因子0.75的情况下,长度超过8的链表出现概率是0.00000006,选择8就是为了让树化几率足够小
    • 树化两个条件:链表长度超过树化阈值;数组容量>=64
    • 退化情况1:在扩容时如果拆分树时,树元素个数<=6则会退化链表,
    • 退化情况2: remove树节点时,若root、root.left、root.right、root.left.left有一个为null ,也会退化为链表
  • 索引如何计算? hashcode都有了,为何还要提供hash()方法?数组容量为何是2的n次幂?
    • 计算对象的hashCode(),再进行调用HashMap的 hash()方法进行二次哈希,最后&(capacity - 1)得到索引
    • 二次hash()是为了综合高位数据,让哈希分布更为均匀
    • 计算索引时,如果是2的n次幂可以使用位与运算代替取模,效率更高;扩容时 hash & oldCap =0的元素留在原来位置,否则新位置=旧位置+oldcap
    • 但以上三种方式是为了配合容量为2的n次幂时的优化手段,例如Hashtable的容量就不是2的n次幂,并不能说哪种设计更优,应该是设计者综合了各种因素,最终选择了使用2的n次幂作为容量

image-20220907202515219

为什么要进行二次hash

是为了解决数组中数据的经过一次hash之后取余,可能会产生余数相同的情况,这样就会导致我们的元素所会产生更多的超长链表。这样影响到查询的效率,而二次hash就可以将元素的分布更加均匀散列。

容量为什么是2的n次幂

image-20220908120233121

image-20220908120846305

通过上面的例子可以看到,如果将容量设置为2的n次幂可以使用按位与运算代替我们的取模运算,这样执行的效率更高,扩容时hash&oldCap==0的元素保留在原位,否则新的位置=旧位置+oldCap

讨论如果数组容量为2的n次幂这样设计会有什么缺点?

image-20220908122222080

通过上图的演示中可以发现,如果说我们的是为追求更好的效率,我们使用2的n次幂做为数组容量,如果是为了追求好hash分布性则采用质数作为数组的容量(默认是性能优先)

面试题:

  • 介绍一下put方法流程,1.7与1.8有何不同?
    • HashMap是懒惰创建数组的,首次使用才创建数组计算索引(桶下标)
    • 如果桶下标还没人占用,创建Node占位返回
    • 如果桶下标已经有人占用
      • 已经是TreeNode走红黑树的添加或更新逻辑
      • 是普通Node,走链表的添加或更新逻辑,如果链表长度超过树化阈值,走树化逻辑
    • 返回前检查容量是否超过阈值,一旦超过进行扩容
    • 不同
      • 链表插入节点时,1.7是头插法,1.8是尾插法
      • 1.7是大于等于阈值(元素个数为数组容量的3/4)且没有空位时才扩容,而1.8是大于阈值就扩容
      • 1.8在扩容计算Node索引时,会优化

1.7扩容机制演示

image-20220908141646052

image-20220908141634824

  • 加载因子为何默认是0.75f
    • 在空间占用与查询时间之间取得较好的权衡
    • 大于这个值,空间节省了,但链表就会比较长影响性能③小于这个值,冲突减少了,但扩容就会更频繁,空间占用多

当负载因子为1的时候 当元素大于 16*1=16 时才会进行扩容会出现大量链表,降低了性能

image-20220908142123526

当元素大于16个时触发扩容机制,这时重新分配桶下标(二次hash值&扩容后的大小) 分配元素

image-20220908142144675

而当负载因子较小的时,就会出现大量的空间浪费

image-20220908142630460

image-20220908142649912

负载因子之所以选择0.75作为负载因子,是对空间占用与时间上的一个较好的权衡

  • 多线程下会有啥问题?
    • 扩容死链(1.7)
    • 数据错乱( 1.7,1.8)

image-20220908143059312

当两个线程同时操作一个对象时造成的数组丢失

扩容的链前后的表桶下标一直的数据迁移过程

image-20220908143731034

image-20220908143916746

image-20220908143936111

结束循环完成链表的迁移

  • 注意:
    • 在整个迁移的过程中 a b 对象的只是改变的引用并未改变对象的本身,看似好像是将原来的对象复制了一份,其本身是引用的改变

image-20220908143942190

多线程操作链表迁移

image-20220908144653869

image-20220908144730932

image-20220908144832521

image-20220908145007907

image-20220908145026469

image-20220908145140084

image-20220908145239787

这里就出现一个问题,如果我们再去索引b元素后面的元素则会导致死循环,这就是1.7中的扩容并发死链问题

  • key 能否为null,作为key 的对象有什么要求?
    • HashMap 的key可以为null,但Map的其他实现则不然
    • 作为key的对象,必须实现 hashCode和equals,并且 key的内容不能修改(不可变)

关于key的不可变的解释:

image-20220908145851992

从图中可以看到我们的对stu对象的内部属性重新改变了,这就导致了我们的该对象内部的hash值发生了改变,所以是无法在原来的map中匹配到改变对象后key的桶下标。

  • String对象的hashcode()如何设计的,为啥每次乘的是31
    • image-20220908150256185

image-20220908150737144

image-20220908150749876

设计模式

单例模式

目标:

  • 掌握单例模式常见五种实现方式
  • 了解jdk中有哪些地方体现了单例模式

实现方式

  • 饿汉式
  • 枚举饿汉式
  • 懒汉式
  • 双检索懒汉式
  • 内部类懒汉式
饿汉式
//饿汉模式
public class HungryMode {

    private HungryMode(){
        System.out.println("hungryMode创建");
    }
    private static HungryMode HUNGRYMODE = new HungryMode();
    public static HungryMode getHungryMode(){
        return  HUNGRYMODE;
    }
}
@Test
public void TestHungryMode(){
    HungryMode hungryMode1=HungryMode.getHungryMode();
    HungryMode hungryMode2=HungryMode.getHungryMode();
    System.out.println(hungryMode1);
    System.out.println(hungryMode2);
}

image-20220908155030820

可以看见我们通过类名点的方式调用内部的静态方法的时候,调用了该类的私有构造,这说明已经将HubgryMode对象创建好了。先有对象,在调用了getHungryMode()方法

破坏单例模式的方式

  • 反射破坏单例

    通过反射机制可以直接创建该私有化的类的实体对象,这样得到的对象必定不再与之前通过类静态调用创建的对象相同,破坏了单例模式,那么对于这样通过反射破坏单例的方式,我们可以预防吗?答案是肯定的

    //饿汉模式
    public class HungryMode {
    
        private HungryMode(){
            if(HUNGRYMODE!=null){
                throw new RuntimeException("创建对象失败");
            }
            System.out.println("hungryMode创建");
        }
    
        
        private static HungryMode HUNGRYMODE = new HungryMode();
        public static HungryMode getHungryMode(){
            return  HUNGRYMODE;
        }
    
    }
    

    在私有构造方法中添加一个对于 HUNGRYMODE 的判断,如果HUNGRYMOD不为空说明已经创建过一个对象,则抛出异常

  • 反序列化破坏单例

    通过反序列化破坏单例这就要求我们的必须实现Serializable接口

    //饿汉模式
    public class HungryMode implements Serializable {
    
        private HungryMode(){
            System.out.println("hungryMode创建");
        }
    
    
        private static HungryMode HUNGRYMODE = new HungryMode();
        public static HungryMode getHungryMode(){
            return  HUNGRYMODE;
        }
    
    }
    

    通过反序列化,调用readObject()将字节流转换为对象这个过程中是不调用 构造方法的

    解决方法:

    • 重新readReslove()方法,并返回结果
    //饿汉模式
    public class HungryMode implements Serializable {
    
        private HungryMode(){
            System.out.println("hungryMode创建");
        }
    
    
        private static HungryMode HUNGRYMODE = new HungryMode();
        public static HungryMode getHungryMode(){
            return  HUNGRYMODE;
        }
        
        //如果在序列化中发现重新了readReslove()方法,就会将返回值作为序列化创建的返回对象
        public Object readResolve(){
            return HUNGRYMODE;
        }
    }
    
  • Unsafe破坏单例

枚举类型创建单例模式
enum Sex{
   MALE,FEMALE;
}

image-20220911114510499

枚举类的饿汉式

public enum Singleton{
    INSTANCE;
    prvate Singlenton(){
        System.out.println("private Singleton()")
    }
}
  • 枚举类不会不受反序列化的影响,反序列化中的 ObjectInputStream会对枚举类型做特殊的处理,如果是枚举类型,会自动的将我们的枚举中的实例自动的返回,而不是调用我们的反序列化的数组生成对象
  • 反射对于枚举类型也是无法破坏
懒汉模式
//懒汉模式
public class IdelerMode {
    private IdelerMode(){
        System.out.println("private IderlerMode");
    }

    private static IdelerMode INSTANCE=null;

    private static IdelerMode getIdelerMode(){
        if(INSTANCE==null){
            INSTANCE= new IdelerMode();
        }
        return INSTANCE;
    }
    
}

懒汉模式在多线程下存在的问题

​ 在多线程下,当我们的第一个线程1在执行的过程当中INSTANCE==null为空,线程1此时也执行到INSTANCE==null部分此时,如果此时线程1没有来得及创建对象,线程2中的判断INSTANCE==null是成立的,也对调用IINSTANCE=new IndelerMode()创建对象,这样就会导致新对象的产生。

​ 那么如何解决呢?

解决懒汉模式下的多线程创建对象问题

只需要在 private static IdelerMode getIdelerMode()添加一个synchronize关键字即可

//懒汉模式
public class IdelerMode {
    private IdelerMode(){
        System.out.println("private IderlerMode");
    }

    private static IdelerMode INSTANCE=null;

    private static synchronized IdelerMode getIdelerMode(){
        if(INSTANCE==null){
            INSTANCE= new IdelerMode();
        }
        return INSTANCE;
    }

}

这样虽然可以解决基本的问题,但是这样的性能却不是特别好的,因为我们的创建对象是在线程第一次调用getInstance()获取的对象的,如果我们已经创建好的对象此时INSTANCE不再是空,当我们再次调用时,就不需要有synchronized线程锁的存在,为了解决这一问题,提升性能我们有以下的解决方案

懒汉式线程锁的优化

//懒汉单例 -DCL
public class IdelerMode2 implements Serializable {
    private IdelerMode2(){
        System.out.println("private InderMode2()");
    }
    private static volatile IdelerMode2 INSTANCE= null;
    
    public static IdelerMode2 getIdelerMode2(){
        if(INSTANCE==null){
            synchronized (IdelerMode2.class){
                if(INSTANCE==null){
                    INSTANCE=new IdelerMode2();
                }
            }
        }
        return INSTANCE;
    }
}
  • 通过上面的代码可以看到,每次当线程调用getIdelerMode2()的时候会先进行判断,如果发现INSTANCE==null此时,多个线程才会竞争同一把锁。如果说在锁内部没有INSTANCE==null的再次判断则会导致,线程会直接创建新的对象,并新对象覆盖就对象。

关于 Volatile 关键字

private static volatile IdelerMode2 INSTANCE= null;中,关键字 volatile 起到的作用

image-20220911124531904

image-20220911125359032

可以发现如果我们不添加 volatile 关键字,在多线程下,就会出现指令的重排序。

当我们的线程1执行到了 INSTANCE=对象部分,而我们的线程2在执行 INSTANCE==null,但是我们只是线程1所执行的 INSTANCE=对象已经分配了地址,INSTANCE此时不等于null,但是Singeton4()的对象并没有创建,这时线程2就会直接将INSTANCE返回

推荐的一种实现 懒汉式单例-内部类

//懒汉单例 -DCL
class IdelerMode3 implements Serializable {
    
    private static class Holder{
        static IdelerMode3  INSTANCE= new IdelerMode3();
    }
    
    public static IdelerMode3 getIdelerMode2(){
        return Holder.INSTANCE;
    }
       
}

JDK中的单例模式

Runtime类

image-20220911130514361

System类

image-20220911130617924

Collections类

image-20220911130907650

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值