java基础方面:
java基本数据类型
类型基本数据
- 数值型
- 整数类型(byte,short,int,long)
- 浮点类型(float,double)
- 字符型(char)
- 布尔型(boolean)
引用数据类型
- 类(class)
- 接口(interface)
- 数组([ ])
访问修饰符 public,private,protected,以及不写(默认)时的区别
Java 支持 4 种不同的访问权限。
分类:
- private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)
- default (即缺省,什么也不写,不使用任何关键字): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。
- protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)。
- public : 对所有类可见。使用对象:类、接口、变量、方法
访问修饰符图
记住:在java中,数组和String字符串都不是基本数据类型,他们被当作类来处理,是引用数据类型!!!
使用final关键字修饰一个变量时,是引用不能变,还是引用的对象不能变?
这分为两种情况:
- 如果是基本数据类型,被final修饰的变量一旦初始化就不能改变。
- 如果是引用数据类型的变量,比如String等,初始化后不能指向另一个对象,但是内容可以改变。例如:
Final StringBuffer a=new StringBuffer("Hello"); a.append(" world!");
&和&&的区别(与)
- &&具有短路的功能,而&不具备短路功能。
- 当&运算符两边的表达式的结果都为true时,整个运算结果才为true。而&&运算符第一个表达式为false时,则结果为false,不再计算第二个表达式。
- &还可以用作位运算符,当&操作符两边的表达式不是boolean类型时,&表示按位与操作,我们通常使用0x0f来与一个整数进行&运算,来获取该整数的最低4个bit位,例如:0x31 & 0x0f的结果为0x01。
char变量中能不能存储一个中文汉字,为什么?
char类型可以存储一个中文汉字,因为java中使用的编码是Unicode,一个char类型是占两个字节,所以放一个中文是没问题的。
break和continue的区别?
break和continue都是用来控制循环语句的。
break用于完全结束一个循环,跳出循环体执行后面的语句。
contine用于跳过本次循环,继续下次循环。
String对象中的==比较运用:
public static void main(String[] args) {
String s1="java";
String s2="java";
System.out.println(s1==s2);
//比较结果为true,原因很简单,在编译的时候把String对象被放进常量池。
//再次出现“java”字符串的时候,JVM把s2引用指向了“java”对象,它为自己节省了内存开销。
String s3=new String("java");
System.out.println(s1==s3);
//结果为false,因为一但看到new关键字,JVM会在堆中为String分配空间。==比较的是引用,所以为false。
String s4="good";
String s=s1+s4;
System.out.println(s=="javagood");
//结果为false,因为在编译时刻,JVM确实会String s1 = "java"; 的String对象放在字符串常量池里
//而在运行时刻,s1+s4是在堆里创建,s引用当然不可能指向字符串常量池的对象
}
String s = "Hello";s = s + " world!";这两行代码执行后,原始的 String 对象中的内容变了没有?
没有!
因为String类被设计成不可变类,所以它的所有对象都是不可变对象。
在这段代码中,s原先指向一个String对象,内容是“Hello”,然后我们对S进行了“+”操作,那么s所指向的哪个对象是否发生改变呢?
没有!
因为这时s不指向原来那个对象了,而指向了另一个String对象,内容是“Hello world”,原来那个对象还存在于内存之中,只是s这个引用变量不再指向它了。
通过上面的说明,我们很容易导出另一个结论,如果经常对字符串进行各种各样的修改,或者说,不可预见的修改,那么使用 String 来代表字符串的话会引起很大的内存开销。因为 String 对象建立之后不能再改变,所以对于每一个不同的字符串,都需要一个 String 对象来表示。这时,应该考虑使用 StringBuffer/StringBuilder类,它允许修改,而不是每个不同的字符串都要生成一个新的对象。并且,这两种类的对象转换十分容易。同时,我们还可以知道,如果要使用内容相同的字符串,不必每次都 new 一个 String。
String, StringBuffer, StringBuilder 的区别。
String:字符串常量(final修饰,不可被继承),String是常量,当创建之后不能更改。(可以通过StringBuffer和StringBuilder创建String对象)
StringBuffer:字符串变量(线程安全),也是final类别的,不允许被继承,其中的绝大多数方法都进行了同步处理,包括常用的Append方法也做了同步处理(synchronized),其toString方法进行对象缓存和,以减少y元素的复制开销。
StringBuilder:字符串变量(非线程安全),方法除了没有使用同步处理(synchronized)修饰以外基本一致,不同之处在于toString的时候,会直接方会一个新对象。它是在单线程的环境下使用的,因为它的方法没有synchronized修饰,因此它的效率理论上也比StringBuffer要高。
自动装箱自动拆箱代码分析
![](https://img-blog.csdnimg.cn/20200323212746758.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3ByaW5jZTc3cWlxaXFx,size_16,color_FFFFFF,t_70)
Integer i=1;i+=1;做了哪些事情?
首先 Integer i = 1; 做了自动装箱(使用 Integer.valueOf() 方法将 int 装箱为 Integer 类型),接着 i += 1; 先将 Integer 类型的 i 自动拆箱成 int(使用Integer.intValue() 方法将 Integer 拆箱为 int),完成加法运行之后的 i 再装箱成 Integer 类型。
下面程序请写出运行结果:
Integer i1 = 100;
Integer i2 = 100;
Integer i3 = 200;
Integer i4 = 200;
System.out.println(i1==i2);//true
System.out.println(i3==i4);//false
Double i1 = 100.0;
Double i2 = 100.0;
Double i3 = 200.0;
Double i4 = 200.0;
System.out.println(i1==i2);//false
System.out.println(i3==i4);//false
//在Boolean中定义了2个静态成员属性
Boolean i1 = false;
Boolean i2 = false;
Boolean i3 = true;
Boolean i4 = true;
System.out.println(i1==i2);//true
System.out.println(i3==i4);//true
Integer a = 1;
Integer b = 2;
Integer c = 3;
Integer d = 3;
Integer e = 321;
Integer f = 321;
Long g = 3L;
Long h = 2L;
System.out.println(c==d);//true 比较cache
System.out.println(e==f);//false 比较引用
System.out.println(c==(a+b));//true 比较值
System.out.println(c.equals(a+b));//true 比较值
System.out.println(g==(a+b));//true //比较值
System.out.println(g.equals(a+b));//false //比较引用(对于包装器类型,equals方法并不会进行类型转换)
System.out.println(g.equals(a+h));//true比较引用
Integer a = 444;
int b = 444;
System.out.println(a==b); //true 比较值
System.out.println(a.equals(b));//true比较值
方法参数的传递,注意!
对于基本数据类型的参数,形势参数的改变不影响实际参数的值。
对于引用类型的参数,形势参数的改变,影响实际参数的值。
当一个对象被当作参数传递到一个方法后,此方法可以改变这个对象的属性,并可返回变化后的结果,那么这里是值传递还是引用传递?
是值传递,java语言的方法调用只支持参数的值传递,参数的值也就是该对象的内存地址。这个值(内存地址)被传递后,同一个内存地址指向堆内存当中的同一个对象,所以通过哪个引用去操作这个对象,对象的属性都是改变的。
是否可以从一个static方法内部发出对非static方法调用?
不可以,因为非static方法是要与对象关联在一起的,必须创建一个对象后,才可以在该对象上进行方法调用,而static方法调用时不需要创建对象,可以直接调用。也就是说,当一个static方法被调用时,可能还没有创建任何实例对象,如果从一个static方法中发出对非static方法调用,那个非static方法是关联到哪个对象上呢?这个逻辑无法成立!
抽象类和接口语法上有什么区别?
1、抽象类中可以有构造方法,接口中不能有构造方法。
2、抽象类中可以有普通的成员变量,接口中没有普通的成员变量。
3、抽象类中可以包含非抽象的普通方法,接口中所有的方法必须是抽象的,不能有非抽象的普通方法。
4、抽象类中的抽象方法的访问类型可以是public、protected,但是接口中的方法必须是public类型的,并且默认既为public abstract。
5、抽象类中可以包含j静态方法,接口中不能包含静态方法。
6、抽象类和接口中都可以包含静态成员变量,抽象类中的静态成员变量的访问类型可以任意,但接口定义的变量只能是public static final,并且默认的类型就是这个。
7、一个类可以实现多了接口,但只能继承一个抽象类。
成员变量和局部变量的区别:
- 成员变量有默认的初始化值。
- 局部变量没有默认的初始化值,定义、赋值后才能使用。
super和this:
子类中调用父类的成员用super,super代表父类对象。
子类中调用自己的成员用this,this代表当前对象。
构造方法是不会被继承的,但是不管有参还是无参的构造方法,在第一行都有个默认的super(),首先会调用父类的构造方法一下。但是记住,this和super在构造方法中不能同时出现,同时用。
final、 finally、 finalize区别
final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。
finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块
中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。
finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调
用,当我们调用System.gc() 方法的时候,由垃圾回收器调用finalize(),回收垃圾,一个对象是否可回收的
最后判断。
什么是多态,以及多态的用处?
作为参数传递的时候,可扩展性强,便于调用重写方法。
多态分为向上转型和向下转型:
- 向上转型:
父类类型 变量名=new 子类对象;(父类引用指向子类对象)
注意:只能调用重写方法,不能调用子类特有的方法!
- 向下转型:(向下转型之前一定要先有向上转型)
子类类型 子变量=(子类类型)父类类型 变量名;
注意:向下转型之后,可以调用子类特有的方法。
提问:
在继承或者多态中,假如子类,父类成员变量名相同,变量值取决于哪个,看创建对象时,等号左边是哪个对象,调用的就是谁的重名变量,多态也同理。
多态中成员方法的访问,重名方法调用时,看创建对象时,new的是谁,调用的谁的方法。
调用下面的方法,得到的返回值是什么?
1. public int getNum() {
2. try {
3. int a = 1 / 0;
4. return 1;
5. } catch (Exception e) {
6. return 2;
7. } finally {
8. return 3;
9. }
10.}
代码走到第3行的时候遇到了一个MathException,这时第4行的代码就不会执行了,代码直接跳转到catch语句中,走到第 6 行的时候,异常机制有一个原则:如果在catch中遇到了return或者异常等能使该函数终止的话那么有finally就必须先执行完finally代码块里面的代码然后再返回值。因此代码又跳到第8行,可惜第8行是一个return语句,那么这个时候方法就结束了,因此第6行的返回结果就无法被真正返回。如果finally仅仅是处理了一个释放资源的操作,那么该道题最终返回的结果就是2。因此上面返回值是3。
说出最常见的5个RuntimeException?
- java.lang.NullPointerException 空指针异常;出现原因:调用了未经初始化的对象或者是不存在的对象。
- java.lang.ClassNotFoundException 指定的类找不到;出现原因:类的名称和路径加载错误;通常都是程序试图通过字符串来加载某个类时可能引发异常。
- java.lang.NumberFormatException 字符串转换为数字异常;出现原因:字符型数据中包含非数字型字符。
- java.lang.IndexOutOfBoundsException 数组角标越界异常,常见于操作数组对象时发生。
- java.lang.IllegalArgumentException 方法传递参数错误。
- java.lang.ClassCastException 数据类型转换异常。
- java.lang.NoClassDefFoundException 未找到类定义错误。
- SQLException SQL 异常,常见于操作数据库时的 SQL 语句错误。
- java.lang.InstantiationException 实例化异常。
- java.lang.NoSuchMethodException 方法不存在异常。
ArrayList和LinkedList有什么区别?
ArrayList是实现了基于动态数组的数据结构,因为地址连续,一旦数据存储好了,查询操作效率会比较高(在内存里是连着放的)。因为地址连续, ArrayList要移动数据,所以插入和删除操作效率比较低。
LinkedList基于链表的数据结构,地址是任意的,所以在开辟内存空间的时候不需要等一个连续的地址,对于新增和删除操作add和remove,LinedList比较占优势。因为LinkedList要移动指针,所以查询操作性能比较低。
适用场景分析:
当需要对数据进行对此访问的情况下选用ArrayList,当需要对数据进行多次增加删除修改时采用LinkedList。
ArrayList与Vector的比较?
1、Vector的方法都是同步的,是线程安全的,而ArrayList的方法不是,由于线程的同步必然要影响性能。因此,ArrayList的性能比Vector好。
2、当Vector或ArrayList中的元素超过它的初始大小时,Vector会将它的容量翻倍,而ArrayList只增加50%的大小,这样。ArrayList就有利于节约内存空间。
3、大多数情况不使用Vector,因为性能不好,但是它支持线程的同步,即某一时刻只有一个线程能够写Vector,避免多线程同时写而引起的不一致性。
4、Vector可以设置增长因子,而ArrayList不可以。
适用场景分析:
1、Vector是线程同步的,所以它也是线程安全的,而ArrayList是线程异步的,是不安全的。如果不考虑到线程的安全因素,一般用ArrayList效率比较高。
2、如果集合中的元素的数目大于目前集合数组的长度时,在集合中使用数据量比较大的数据,用Vector有一定的优势。
wait和sleep的区别?
1、sleep()方法是属于Thread类中的,而wait()方法,则是属于Object类中的。
2、sleep()方法导致了程序暂停执行指定的时间,让出cpu给其他线程,但是他的监控状态依然保持着,当指定的时间到了又会自动恢复运行状态。所以在调用sleep()方法的过程中,线程不会释放对象锁。
3、调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。
什么是线程死锁?死锁如何产生?如何避免线程死锁?
线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。当线程进入对象的synchronized代码块时,便占有了资源,直到它退出该代码块或者调用wait方法,才释放资源,在此期间,其他线程将不能进入该代码块。当线程互相持有对方所需要的资源时,会互相等待对方释放资源,如果线程都不主动释放所占有的资源,将产生死锁。
死锁的产生的一些特定条件:
1、互斥条件:进程对于所分配到的资源具有排它性,即一个资源只能被一个进程占用,直到被该进程释放 。
2、请求和保持条件:一个进程因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
3、不剥夺条件:任何一个资源在没被该进程释放之前,任何其他进程都无法对他剥夺占用。
4、循环等待条件:当发生死锁时,所等待的进程必定会形成一个环路(类似于死循环),造成永久阻塞。
如何避免:
1、加锁顺序:
当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。当然这种方式需要你事先知道所有可能会用到的锁,然而总有些时候是无法预知的。
2、加锁时限:
加上一个超时时间,若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。但是如果有非常多的线程同一时间去竞争同一批资源,就算有超时和回退机制,还是可能会导致这些线程重复地尝试但却始终得不到锁。
3、死锁检测:
死锁检测即每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。
notify和notifyAll区别
他们的作用都是通知处于等待该对象的线程。
1、notifyAll使所有原来在该对象上等待被notify的线程统统退出wait的状态,变成等待该对象上的锁,一旦该对象被解锁,他们就会去竞争。
2、notify是通知其中一个线程,不会通知所有的线程。
枚举的作用和使用场景?
enum
例如枚举类:
枚举类是一个特殊的类,它和普通类一样可以使用构造方法、定义成员变量和方法,也可以实现多个接口,但不能继承类。
enum Color {
RED, BLUE, GREEN;
}
Color color = Color.RED;//取用枚举中的值
枚举的使用场景:
当变量只能从一堆固定的值中取出一个时,那么就应该使用枚举。
单例模式-控制实例数目,5种写法 :懒汉,恶汉,双重校验锁,枚举和静态内部类。
单例模式的定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点!**
使用场景:打印机,计算机中的打印机也是采用单例模式设计的,一个系统可以存在多个打印任务,但是只能又有个正在工作的任务。
在写单例模式代码之前,我们先了解两个知识点,关于类加载顺序和static关键字。
类加载顺序:(什么时候类会被加载,启动mian()方法的时候)
类加载顺序一般遵从下面的加载机制:
如果类还没有被加载:
● 先执行父类的静态代码块和静态变量初始化,静态代码块和静态变量的执行顺序跟代码中出现的顺序有关。
● 执行子类的静态代码块和静态变量初始化。
● 执行父类的实例变量初始化
● 执行父类的构造函数
● 执行子类的实例变量初始化
● 执行子类的构造函数
● 同时,加载类的过程是线程私有的,别的线程无法进入。
如果类已经被加载:
静态代码块和静态变量不再在重复执行,再创建类对象时,只执行与实例相关的变量初始化和构造方法。
static关键字:
一个类中如果有成员变量或者方法被static关键字修饰,那么该成员变量或方法将独立于该类的任何对象。它不依赖类特定的实例,被类的所有实例共享,只要这个类被加载,该成员变量或方法就可以通过类名去进行访问,它的作用用一句话来描述就是,不用创建对象就可以调用方法或者变量,这简直就是为单例模式的代码实现量身打造的。
下面将列举几种单例模式的实现方式,其关键方法都是static修饰的,并且,为了避免单例的类对象频繁的创建对象,我们可以用private的构造函数来确保单例类无法被外部实例化。懒汉和饿汉单例:
在程序的编写上,一般将单例模式分为两种,分别时饿汉式和懒汉式。
也就是立即加载和延迟加载两个概念。
单例模式三要素:
● 私有构造方法。
● 指向自己实例的私有静态引用。
● 以自己实例为返回值的静态公有方法。
饿汉式:(启动main方法时)在类加载时就完成了初始化,所以类加载比较慢,但获取对象的速度比较快。
public class Singleton1 {
//指向自己实例的私有静态引用,主动创建。
private static Singleton1 singleton1=new Singleton1();
//私有构造方法
private Singleton1(){
}
//以自己实例为返回值的静态公有方法,静态工厂方法。
private static Singleton1 getSingleton1(){
return singleton1;
}
}
懒汉式:在类加载的时候不初始化,等到第一次被使用时才初始化。
// 懒汉式单例
public class Singleton2 {
// 指向自己实例的私有静态引用
private static Singleton2 singleton2;
// 私有的构造方法
private Singleton2(){}
// 以自己实例为返回值的静态的公有方法,静态工厂方法
public static Singleton2 getSingleton2(){
// 被动创建,在真正需要使用时才去创建
if (singleton2 == null) {
singleton2 = new Singleton2();
}
return singleton2;
}
}
总之,从速度和反应时间角度来讲,饿汉式(又称立即加载)要好一些;从资源利用效率上说,懒汉式(又称延迟加载)要好一些。
单例模式的优势:
在内存中只有一个对象,节省内存空间。
避免频繁的创建销毁对象,可以提高性能。
避免对共享资源的多重占用,简化访问。
为整个系统提供一个全局访问点。
单例模式的使用场景:
● 有状态的工具类对象。
● 频繁访问数据库或文件对象。
单例模式的注意事项:
在使用单例模式时,我们必须使用单例类提供的公有工厂方法得到单例对象,而不是使用反射来创建,否则j将会实例化一个新对象。此外,在多线程环境下使用单例模式时,应该特别注意线程安全问题!
在单线程环境下,无论是饿汉式单例还是懒汉式单例,它们都能够正常工作。但是,在多线程环境下,情形就发生了变化:由于饿汉式单例天生就是线程安全的,可以直接用于多线程而不会出现问题;但懒汉式单例本身是非线程安全的,因此就会出现多个实例的情况,与单例模式的初衷是相背离的。
-
为什么说饿汉式单例天生就是线程安全的?
-
因为类的加载是按需加载,只被加载一次。
-
传统的懒汉式单例为什么是非线程安全的?
-
上面发生非线程安全的一个显著原因是,会有多个线程同时进入 if (singleton2 == null) {…} 语句块的情形发生。当这种这种情形发生后,该单例类就会创建出多个实例,违背单例模式的初衷。因此,传统的懒汉式单例是非线程安全的。
-
怎么修改传统的懒汉式单例,使其线程变得安全?
-
(1)同步延迟加载 — synchronized方法
-
// 线程安全的懒汉式单例 public class Singleton2 { private static Singleton2 singleton2; private Singleton2(){} // 使用 synchronized 修饰,临界资源的同步互斥访问 public static synchronized Singleton2 getSingleton2(){ if (singleton2 == null) { singleton2 = new Singleton2(); } return singleton2; } }/* Output(完全一致): 1104499981 1104499981 1104499981 1104499981 1104499981 1104499981 1104499981 1104499981 1104499981 1104499981
-
线程安全的单例的实现还有哪些,怎么实现?
-
从执行结果上来看,问题已经解决了,但是这种实现方式的运行效率会很低,因为同步块的作用域有点大,而且锁的粒度有点粗。同步方法效率低,那我们考虑使用同步代码块来实现。
-
(2)同步延迟加载 — synchronized块
-
// 线程安全的懒汉式单例 public class Singleton2 { private static Singleton2 singleton2; private Singleton2(){} public static Singleton2 getSingleton2(){ synchronized(Singleton2.class){ // 使用 synchronized 块,临界资源的同步互斥访问 if (singleton2 == null) { singleton2 = new Singleton2(); } } return singleton2; } }/* Output(完全一致): 16993205 16993205 16993205 16993205 16993205 16993205 16993205 16993205 16993205
-
该实现与上面synchronized方法版本实现类似,此不赘述。从执行结果上来看,问题已经解决了,但是这种实现方式的运行效率仍然比较低,事实上,和使用synchronized方法的版本相比,基本没有任何效率上的提高。
-
双重检查模式、Volatile关键字 在单例模式中的应用
-
使用双重检测同步延迟加载去创建单例的做法是一个非常优秀的做法,其不但保证了单例,而且切实提高了程序运行效率。对应的代码清单如下:
-
// 线程安全的懒汉式单例 public class Singleton3 { //使用volatile关键字防止重排序,因为 new Instance()是一个非原子操作,可能创建一个不完整的实例 private static volatile Singleton3 singleton3; private Singleton3() { } public static Singleton3 getSingleton3() { // Double-Check idiom if (singleton3 == null) { synchronized (Singleton3.class) { // 1 // 只需在第一次创建实例时才同步 if (singleton3 == null) { // 2 singleton3 = new Singleton3(); // 3 } } } return singleton3; } }/* Output(完全一致): 1104499981 1104499981 1104499981 1104499981 1104499981 1104499981 1104499981 1104499981
如上述代码所示,为了在保证单例的前提下提高运行效率,我们需要对 singleton3 进行第二次检查,目的是避开过多的同步(因为这里的同步只需在第一次创建实例时才同步,一旦创建成功,以后获取实例时就不需要同步获取锁了)。这种做法无疑是优秀的,但是我们必须注意一点:
必须使用volatile关键字修饰单例引用。 -
那么,如果上述的实现没有使用 volatile 修饰 singleton3,会导致什么情形发生呢? 为解释该问题,我们分两步来阐述:
(1)、当我们写了 new 操作,JVM 到底会发生什么?
首先,我们要明白的是: new Singleton3() 是一个非原子操作。代码行singleton3 = new Singleton3(); 的执行过程可以形象地用如下3行伪代码来表示:
-
memory = allocate(); //1:分配对象的内存空间 ctorInstance(memory); //2:初始化对象 singleton3 = memory; //3:使singleton3指向刚分配的内存地址
-
但实际上,这个过程可能发生无序写入(指令重排序),也就是说上面的3行指令可能会被重排序导致先执行第3行后执行第2行,也就是说其真实执行顺序可能是下面这种:
-
memory = allocate(); //1:分配对象的内存空间 singleton3 = memory; //3:使singleton3指向刚分配的内存地址 ctorInstance(memory); //2:初始化对象
这段伪代码演示的情况不仅是可能的,而且是一些 JIT 编译器上真实发生的现象。
-
1、线程 1 进入 getSingleton3() 方法;
2、由于 singleton3 为 null,线程 1 在 //1 处进入 synchronized 块;
3、同样由于 singleton3 为 null,线程 1 直接前进到 //3 处,但在构造函数执行之前,使实例成为非 null,并且该实例是未初始化的;
4、线程 1 被线程 2 预占;
5、线程 2 检查实例是否为 null。因为实例不为 null,线程 2 得到一个不完整(未初始化)的 Singleton 对象;
6、线程 2 被线程 1 预占。
7、线程 1 通过运行 Singleton3 对象的构造函数来完成对该对象的初始化。显然,一旦我们的程序在执行过程中发生了上述情形,就会造成灾难性的后果,而这种安全隐患正是由于指令重排序的问题所导致的。让人兴奋地是,volatile 关键字正好可以完美解决了这个问题。也就是说,我们只需使用volatile关键字修饰单例引用就可以避免上述灾难。
-
ThreadLocal 在单例模式中的应用
-
借助于 ThreadLocal,我们可以实现双重检查模式的变体。我们将临界资源线程局部化,具体到本例就是将双重检测的第一层检测条件 if (instance == null) 转换为 线程局部范围内的操作 。这里的 ThreadLocal 也只是用作标识而已,用来标识每个线程是否已访问过:如果访问过,则不再需要走同步块,这样就提高了一定的效率。对应的代码清单如下:
// 线程安全的懒汉式单例 public class Singleton4 { // ThreadLocal 线程局部变量 private static ThreadLocal<Singleton4> threadLocal = new ThreadLocal<Singleton4>(); private static Singleton4 singleton4 = null; private Singleton4(){} public static Singleton4 getSingleton4(){ if (threadLocal.get() == null) { // 第一次检查:该线程是否第一次访问 createSingleton4(); } return singleton4; } public static void createSingleton4(){ synchronized (Singleton4.class) { if (singleton4 == null) { // 第二次检查:该单例是否被创建 singleton4 = new Singleton4(); // 只执行一次 } } threadLocal.set(singleton4); // 将单例放入当前线程的局部变量中 } }/* Output(完全一致): 1028355155 1028355155 1028355155 1028355155 1028355155 1028355155 1028355155
java集合类面试题:
![](https://img-blog.csdnimg.cn/20200325211904581.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3ByaW5jZTc3cWlxaXFx,size_16,color_FFFFFF,t_70)
ArraryList和Vector的区别?
这两个类都实现了List接口,他们都是有序的集合(储存有序),底层是数组,我们可以按位置索引取出某个元素,允许元素重复或为null。
区别:
同步性:ArraryList是非同步的,Vector是同步的。
注意:即便需要同步的时候,我们可以使用Collections工具类来构建出同步的ArrayList而不用Vector。
扩容大小:Vector正常原来的一倍,ArrayList增长原来的0.5倍。
ArrayList和LinkedList的区别?
- ArrayList是实现了基于动态数组的数据结构,LinkedList是基于链表的数据结构。
- ArrayList索引读取速度快,从最后插入和删除速度快,但是头部插入或删除速度慢,因为要移动数据。
- LinkedList使用双向循环列表方式存储数据,所有头部插入或读取速度快,而中部插入速度慢。
共同点:
从储存结构和实现来讲j基本上是相同的,都是实现Map接口.。
区别:
同步性:HashMap是非同步的,HashTable是同步的。
是否允许为null:HashMap允许为null,HashTable不允许为null
contions方法:HashTable有contions方法,而HashMap把contions方法去掉了,改成了contionsValue和contionsKey
ArrayList集合加入1万条数据,应该怎么提高效率?
ArrayList的默认初始容量为10,要插入大量数据的时候需要不断扩容,而扩容是非常影响性能的。因此,现在明确了10万条数据了,我们可以直接在初始化的时候就设置ArrayList的容量!
单例
冒泡排序
泛型