Java面试题
基础篇
排序算法
二分查找
- 前提:有已排序数组A(假设已经做好)
- 定义左边界L、右边界R、确定搜素范围、循环执行二分查找(3、4两步骤)
- 获取中间索引 M=Floor((L+R)/2)
- 中间索引的值 A[M] 与待搜索的值 T 进行比较
- A[M] == T 表示找到,返回中间索引
- 中间值右侧的其他元素都大于T,无需比较,中间索引左边去找,M-1 设置为右边界,重新查找
- 中间值左侧的其他元素都大于T,无需比较,中间索引左边去找,M-1 设置为左边界,重新查找
- 当 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);
}
可以看到数据是有溢出的
解决方案一:
通过等式代换,执行效率低
(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,5,8,11,19,22,31,35,40,45,48,49,50当二分查找值为48的结点时
查找成功需要比较的次数
- 京东实习生招聘
- 使用二分法在序列1,4,6,7,15,33,39,50,64,78,75,81,89,96中查找元素81时,需要经过()次比较
- 美图点评校招
- 在拥有128个元素的数组中二分查找一个数,需要比较的次数最多不超过多少次
- 北京易道博识社招
解题技巧:
-
奇数二分去中间
-
偶数二分取中间靠左
2 n = 128 或 128 / 2 / 2... 直到 1 2^n = 128 或128/2/ 2 ...直到1 2n=128或128/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
- 是整数,则该整数即为最终结果
- 是小数,则舍去小数部分,整数加一为最终结果
插入排序
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;
}
基本实现
- 将数组分为两个区域,排序区域和未排序区域,每一轮从未排序的区域中取出第一个元素,插入到排序区域(需要保证顺序)
- 重复以上的步骤,直到整个数组有序
优化方式:
- 待插入元素进行比较时,遇到比自己小的元素,就代表找到了插入位置,无需进行后续比较
- 插入时可以直接移动元素,而不是交换元素
与选择排序比较
-
二者平均时间复杂度都是O(n2)
-
大部分情况下,插入都略优于选择
-
有序集合插入的时间复杂度为O(n)
-
插入属于稳定排序算法,而选择属于不稳定排序
第一题:
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
希尔排序
对于插入排序存在一个缺点,如果是较大的元素恰好在数组的最前面,那么这个较大的元素的移动次数就会很多
比如下面这一数组 a[ ] ={9,4,2,5,1,7,8,0}
可以发现插入排序此时对于9而言就需要移动7次,为了减少这样的排序次数,就有了改进的算法希尔排序
快速排序
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();
}
}
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);
}
牺牲了元素的一致性
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;
}
- ArrayList是fail-fast的典型代表,遍历的同时不能修改,尽快失败
- CopyonWriteArrayList 是 fail-safe 的典型代表,遍历的同时可以修改,原理是读写分离
ArrayList vs LinkedList
ArrayList
- 基于数组,需要连续内存
- 随机访问快(指根据下标访问)(基于数组,需要连续内存随机访问快(指根据下标访问)
- 尾部插入、删除性能可以,其它部分插入、删除都会移动数据,因此性能会低
- 可以利用cpu缓存,局部性原理
LinkedListLinkedList
- 基于双向链表,无需连续内存
- 随机访问慢(要沿着链表遍历)
- 头尾插入删除性能高
- 占用内存多
HashMap
HashMap在存取一对键值对时,先会对key值进行hash运算,然后对运算得到的hash值进行16取模,得到其桶下标记
而之所以这样设计的原因是,当我们要查找一个key取出该对应的值时,我们只需要将我们要取出的key值进行一次hash运算(会调用该key对象对于的hashcode方法返回hash值),然后将其得到的取余值定位到桶下标就可以快速的定位到对于的key
如图所示当我们添加的元素的hash码一样(hash碰撞)就会出现数组转链表的形式,我们知道链表的索引查找元素的效率较低,那么对于这种情况HashMap底层会怎么处理呢?
思路一:
缩减链表长度,并重新计算hash(hash值模数组长度)码,重新分配桶下标(增加长度为原来元素大小的2倍)
重新计算hash码(hash模32),桶下表进行分配
问题如果说链表中的8个hash码都是一样的,所有元素的原始hash值时一样的比如
那么这种情况就无法通过扩容重新分配桶下标。
为了解决上述所出现的问题,就需要另一种的解决方案,红黑树的树化。
思路二:当链表的长度超过8个,并且容量超过64时,将当前的链表转化为红黑树
对于红黑树的特点,右边的元素最小左边的元素最大
面试题
- 底层数据结构,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 ,也会退化为链表
- 在正常的情况下,链表的长度是不会超过8的,之所以有红黑树的存在,是为了避免恶意的DoS攻击,防止链表超长的情况下导致的性能下降。所以数化是偶然的情况
- 索引如何计算? hashcode都有了,为何还要提供hash()方法?数组容量为何是2的n次幂?
- 计算对象的hashCode(),再进行调用HashMap的 hash()方法进行二次哈希,最后&(capacity - 1)得到索引
- 二次hash()是为了综合高位数据,让哈希分布更为均匀
- 计算索引时,如果是2的n次幂可以使用位与运算代替取模,效率更高;扩容时 hash & oldCap =0的元素留在原来位置,否则新位置=旧位置+oldcap
- 但以上三种方式是为了配合容量为2的n次幂时的优化手段,例如Hashtable的容量就不是2的n次幂,并不能说哪种设计更优,应该是设计者综合了各种因素,最终选择了使用2的n次幂作为容量
为什么要进行二次hash
是为了解决数组中数据的经过一次hash之后取余,可能会产生余数相同的情况,这样就会导致我们的元素所会产生更多的超长链表。这样影响到查询的效率,而二次hash就可以将元素的分布更加均匀散列。
容量为什么是2的n次幂
通过上面的例子可以看到,如果将容量设置为2的n次幂可以使用按位与运算代替我们的取模运算,这样执行的效率更高,扩容时hash&oldCap==0的元素保留在原位,否则新的位置=旧位置+oldCap
讨论如果数组容量为2的n次幂这样设计会有什么缺点?
通过上图的演示中可以发现,如果说我们的是为追求更好的效率,我们使用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扩容机制演示
- 加载因子为何默认是0.75f
- 在空间占用与查询时间之间取得较好的权衡
- 大于这个值,空间节省了,但链表就会比较长影响性能③小于这个值,冲突减少了,但扩容就会更频繁,空间占用多
当负载因子为1的时候 当元素大于 16*1=16 时才会进行扩容会出现大量链表,降低了性能
当元素大于16个时触发扩容机制,这时重新分配桶下标(二次hash值&扩容后的大小) 分配元素
而当负载因子较小的时,就会出现大量的空间浪费
负载因子之所以选择0.75作为负载因子,是对空间占用与时间上的一个较好的权衡
- 多线程下会有啥问题?
- 扩容死链(1.7)
- 数据错乱( 1.7,1.8)
当两个线程同时操作一个对象时造成的数组丢失
扩容的链前后的表桶下标一直的数据迁移过程
结束循环完成链表的迁移
- 注意:
- 在整个迁移的过程中 a b 对象的只是改变的引用并未改变对象的本身,看似好像是将原来的对象复制了一份,其本身是引用的改变
多线程操作链表迁移
这里就出现一个问题,如果我们再去索引b元素后面的元素则会导致死循环,这就是1.7中的扩容并发死链问题
- key 能否为null,作为key 的对象有什么要求?
- HashMap 的key可以为null,但Map的其他实现则不然
- 作为key的对象,必须实现 hashCode和equals,并且 key的内容不能修改(不可变)
关于key的不可变的解释:
从图中可以看到我们的对stu对象的内部属性重新改变了,这就导致了我们的该对象内部的hash值发生了改变,所以是无法在原来的map中匹配到改变对象后key的桶下标。
- String对象的hashcode()如何设计的,为啥每次乘的是31
设计模式
单例模式
目标:
- 掌握单例模式常见五种实现方式
- 了解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);
}
可以看见我们通过类名点的方式调用内部的静态方法的时候,调用了该类的私有构造,这说明已经将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;
}
枚举类的饿汉式
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 起到的作用
可以发现如果我们不添加 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;
}
}