这1个月有点忙,面试了10多个小厂和2、3个大厂,给我的感觉就是基础不牢,地动山摇。一般的面试的逻辑就是面向简历,深挖细节。字节的一面问了我一个半小时,反思一下,真的基础非常的重要,一些中小厂可能会额外提到行业看法面、个人世界观面等,本文主要还是针对专业技术面更多。 话不多说,基于我现在被问到的一些情况,也查看了全网诸多的面试题,我总结了一些在Android面中的Java题。 由于本人见识非常有限,写的博客难免有错误或者疏忽的地方,希望各位人才们指点一二。
努力不辜,时光不负。继续冲冲冲!
一、Java概述
1.JVM、JRE和JDK的关系
- JVM:Java虚拟机,Java程序需要运行在虚拟机
- JRE:Java虚拟机+Java程序所需的核心类库
- JDK:Java虚拟机+Java程序所需的核心类库(JRE)+Java开发工具包
2.谈谈你对类生命周期的认识?
jvm(java虚拟机)中的几个比较重要的内存区域,这几个区域在java类的生命周 期中扮演着比较重要的角色:
- 方法区: 在java的虚拟机中有一块专门用来存放已经加载的类信息、常量、静态变量以及方法代码的内存区域,叫做方法区。
- 常量池: 常量池是方法区的一部分,主要用来存放常量和类中的符号引用等信息。
- 堆区: 用于存放类的对象实例。
- 栈区: 也叫java虚拟机栈,是由一个一个的栈帧组成的后进先出的栈式结构,栈桢中存放方法运行时产生的局部变量、方法出口等信息。当调用一个方法时,虚拟机栈中就会创建一个栈帧存放这些数据,当方法调用完成时,栈帧消失,如果方法中调用了其他方法,则继续在栈顶创建新的栈桢。
我们编写一个java的源文件后,经过编译会生成一个后缀名为class的文件,这种文件叫做字节码文件,只有这种字节码文件才能够在java虚拟机中运行,java类的生命周期就是指一个class文件从加载到卸载的全过程。一个java类的完整的生命周期会经历加载、连接、初始化、使用、和卸载五个阶段
3.谈谈你对面向对象和面向过程的理解
面向过程:分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现 面向对象:把构成问题事务分解成各个对象,然后描述对象的行为 举个例子:下五子棋:
- 面向过程的设计思路:1、开始游戏,2、黑子先走,3、绘制画面,4、判断输赢,5、轮到白子,6、绘制画面,7、判断输赢,8、返回步骤2,9、输出最后结果。
- 面向对象的设计思路:1、黑白双方,这两方的行为是一模一样的,2、棋盘系统,负责绘制画面,3、规则系统,负责判定诸如犯规、输赢等。第一类对象(玩家对象)负责接受用户输入,并告知第二类对象(棋盘对象)棋子布局的变化,棋盘对象接收到了棋子的变化就要负责在屏幕上面显示出这种变化,同时利用第三类对象(规则系统)来对棋局进行判定
4.面向过程和面向对象的优缺点
面向过程:
- 优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
- 缺点:没有面向对象易维护、易复用、易扩展
面向对象:
- 优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
- 缺点:没有面向对象易维护、易复用、易扩展
举个例子: 面向过程的程序是一份蛋炒饭,面向对象的程序是一份盖浇饭。 蛋炒饭的好处就是入味均匀,吃起来香。如果恰巧你不爱吃鸡蛋,只爱吃青菜的话,那么唯一的办法就是重新做一份青菜炒饭了。盖浇饭更换一份盖菜就可以了。盖浇饭的缺点是入味不均,可能没有蛋炒饭那么香。 到底是蛋炒饭好还是盖浇饭好呢?其实这类问题都很难回答,非要比个上下高低的话,就必须设定一个场景,否则只能说是各有所长。如果大家都不是美食家,没那么多讲究,那么从饭馆角度来讲的话,做盖浇饭显然比蛋炒饭更有优势,他可以组合出来任意多的组合,而且不会浪费。 盖浇饭的好处就是"菜"“饭"分离,从而提高了制作盖浇饭的灵活性。饭不满意就换饭,菜不满意换菜。用软件工程的专业术语就是"可维护性"比较好,“饭” 和"菜"的耦合度比较低。蛋炒饭将"蛋”“饭"搅和在一起,想换"蛋”"饭"中任何一种都很困难,耦合度很高,以至于"可维护性"比较差。软件工程追求的目标之一就是可维护性,可维护性主要表现在3个方面:可理解性、可测试性和可修改性。面向对象的好处之一就是显著的改善了软件系统的可维护性。
5.Java和C++的区别
- 都是面向对象的语言,都支持封装、继承和多态
- Java不提供指针来直接访问内存,程序内存更加安全
- Java的类是单继承的,接口可以多继承,C++支持多重继承
- Java有自动内存管理机制,不需要程序员手动释放无用内存
- Java可跨平台运行,C++要关注平台差异性
二、基础语法
6.&和&&的区别
运算符 | 功能描述 | 说明 |
---|
&& | 短路与 | 都为true才为true,从左到右依次判断,节省计算机资源提高逻辑运算的速度 | & | 无条件与 | 全部都要判断 | || | 短路或 | 全为false才为false,从左到右依次判断,节省计算机资源提高逻辑运算的速度 | | | 无条件或 | 全部都要判断 |
7. "= =“和equals方法究竟有什么区别?
== 对基本类型和引用类型作用效果是不同的,如下所示:
- 基本类型:比较的是值是否相同;
- 引用类型:比较的是引用是否相同;
equals 默认情况下是引用比较,只是很多类重新了 equals 方法,比如 String、Integer 等把它变成了值比较,所以一般情况下 equals 比较的是值是否相等
8.关键字final和static是怎么使用的?
static关键字主要有两种作用:
- 第一,为某特定数据类型或对象分配单一的存储空间,而与创建对象的个数无关。
- 第二,实现某个方法或属性与类而不是对象关联在一起
static修饰方法/变量:static方法一般称作静态方法,由于静态方法不依赖于任何对象就可以进行访问,因此对于静态方法来说,是没有this的;根据Java中定义变量位置的不同,变量有两大类:成员变量和局部变量,而成员变量里面根据有无static修饰又可分为类变量和实例变量
- 静态方法中不能直接访问非静态成员方法和非静态成员变量,非静态成员方法可直接可以访问所有成员方法/成员变量
- 同类可直接调用静态方法/类变量,不同类则是类.静态方法/类变量
- 同类非静态方法调用非静态方法/实例变量时,可直接调用方法名()/实例变量名;其它情况下调用非静态方法均要实例化对象通过对象调用非静态方法,类名 对象名 = new 类名(); 对象名.静态方法名()/实例变量名;
- 静态方法中,不能使用this关键字, this是相对于某个对象而言的,static修饰的方法是相对于类的,因此不能在静态方法中用this
static修饰类:
- 实例内部类:直接定义在类当中的一个类,在类前面没有任何一个修饰符。
- 静态内部类:在内部类前面加上一个static。
- 局部内部类:定义在方法当中的内部类,局部类当中不能使用static变量,不能使用 public、protected、private 修饰。
- 匿名内部类:属于局部的一种特殊情况。
final修饰方法/变量/类
- final有不可改变,最终的意思,可以用来修饰非抽象类、成员方法和变量
- 修饰类:该类不能再派生出新的子类,不能作为父类被继承。因此,一个类不能同时被声明为abstract 和 final,抽象类要被引用,所以不能用final修饰
- 修饰方法:该方法不能被子类重写。
- 修饰变量:该变量必须在声明时给定初值,而在以后只能读取,不可修改。 如果变量是对象,则指的是引用不可修改,但是对象的属性还是可以修改的。
9.switch语句后的控制表达式
- JDK1.0 - 1.4? ? 数据类型接受 byte short int char
- JDK1.5 ? ? 数据类型接受 byte short int char enum(枚举)
- JDK1.7? ? 数据类型接受 byte short int char enum(枚举), String,对应的包装类型
10.成员变量和局部变量的区别
不同点 | 局部变量 | 成员变量 |
---|
定义位置 | 方法内部 | 方法外部 | 作用范围 | 方法当中 | 整个类 | 默认值 | 手动赋值 | 有默认值 | 内存位置 | 栈内存 | 堆内存 | 生命周期 | 方法进栈而诞生,方法出栈而消失 | 对象创建而诞生,对象被垃圾回收而消失 |
11.lamda表达式了解过?
Lambda的前提条件
- Lambda只能用于替换有且仅有一个抽象方法的接口和匿名内部类对象,这种接口称为函数式接口
- Lambda具有上下文推断的功能,所以我们才会出现Lambda的省略格式
Lambda的使用场景
- 列表迭代:输出列表的每个元素
- 事件监听
- Predicate 接口
- Map 映射
- Reduce 聚合:对多个对象进行过滤并执行相同的处理逻辑
- 代替 Runnable:创建线程
12.正则表达式掌握过吗,在项目里面怎么用到的
正则表达式是一种用来匹配字符串的强有力的武器,用一种描述性的语言定义一个规则,凡是符合规则的字符串,我们就认为它“匹配”了 使用场景:
- 数据有效性验证:用户注册模块是应用正则表达式最集中的地方,主要是用于验证用户帐号、密码、EMAIL、电话号码、QQ号码、身份证号码、家庭地址等信息。如果填写的内容与正则表达式不匹配,可以断定填写的内容是不合乎要求或虚假的信息;
- 模糊查询,批量替换:可以在文档中使用一个正则表达式来查找匹配的特定文字,然后可以全部将其删除,或者替换为别的文字。
Android中正则表达式的用法:
- 核心类:
? ?Pattern :正则表达式的编译后的对象形式,即正则模式: 将一个字符串转成正则匹配模式对象 ? ?Matcher 是正则模式匹配给定字符串的匹配器,Pattern对象调用匹配器matcher()方法,查找符合匹配要求的匹配项
13.什么是拆箱 & 装箱,能给我举例子吗?
拆箱:包装类型转换为基本类型 装箱:基本类型转换为包装类型
Integer i1 = 40;Integer i2= 40; 进行比较时,基本类型int 被装箱为包装类型Integer 。在Java中,基本类型比较的是值,而封装类型比较的是对象的地址。但是这两个包装类对象是同一个对象。因为Integer类,里面涉及到缓存机制,如果给定的基本类型int值在-128到127之间的话,就会直接去cache数组里取,如果不在这个范围的话,那么就会创新的对象。
`平常我们在Java中使用的都是HashMap等来保存数据,但是有一个严重的问题就是HashMap里的key以及value都是泛型,那么就会不可避免的遇到装箱和拆箱的过程,而这个过程是很耗时的,所以为了规避装箱拆箱提高效率,于是诞生了SparseArray等集合类,但是SparceArray效率高也是有条件的,它适用于数据量比较小的场景,而在Android开发中,大部分场景都是数据量比较小的,在数据很大的情况下,当然还是hashmap效率较优。
三、实用类库
14.日期时间了解?谈谈做过那些业务?
切换时间格式,增加不同国家时设置功能,用到了TextClock、Calendar、SimpleDateFormat、AlarmManager类。 TextClock中setFormat24Hour方法设置24小时制度/12小时制 Calendar与SimpleDateFormat类,设置当前时间以及当前时间显示的格式 AlarmManager 中:setTimeZone(String timeZone)方法用来设置系统的默认时区。需要android.permission.SET_TIME_ZONE.权限:mAlarmManager = (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE);
15.字符串的更改
当切换系统语言时,引用到一些的string的写法会有不同,一般有以下三种方式选择:
.字符串的反转
- charAt():通过String类的charAt()的方法来获取字符串中的每一个字符,然后将其拼接为一个新的字符串
- toCharArray():通过String的toCharArray()方法可以获得字符串中的每一个字符并转换为字符数组,然后用一个空的字符串从后向前一个个的拼接成新的字符串。
- reverse():通过StringBuiler或StringBuffer的reverse()的方法
字符串替换
- replace():替换字符串中所有指定的字符,然后生成一个新的字符串,原来的字符串不发生改变
- replaceAll():字符串中某个指定的字符串替换为其它字符串
replaceFirst():替换第一个出现的指定字符串
ArrayMap的key-value改变字符串 保证key值相同,选择不同系统语言时,改变value值: if(true){ arrayMap.put(“A”, “GB”); }else { arrayMap.put(“A”, “GA”);
16.String、StringBuilder、StringBuffrer的区别
- String类是不可变类,任何对String的改变都会引发新的String对象的生成;
- StringBuffer是可变类,任何对它所指代的字符串的改变都不会产生新的对象,线程安全的。
- StringBuilder是可变类,线性不安全的,不支持并发操作,不适合多线程中使用,但其在单线程中的性能比StringBuffer高。
四、继承
17.谈一谈对值传递和引用传递的理解
引用类型传递是栈地址的传递,操作任何一个引用变量都会影响到在堆内存中实际对象的值 简单类型传递是具体值传递,如果在被传递函数中改变了这个传进来的值,不会改变原始的值
18.super 和 this 的异同
指代上的区别
- super:是对当前对象中父对象的引用。
- This:指当前对象的参考。
引用对象上的区别
- super:直接父类中引用当前对象的成员(当基本成员和派生类具有相同成员时,用于访问直接父类中隐藏父类中的成员数据或函数定义)。
- This:表示当前对象的名称(程序中容易出现歧义的地方,应该用来表示当前对象;如果函数的成员数据与该类中成员数据的名称相同,应用于表示成员变量名称)。
调用函数上的区别
- super:在基类中调用构造函数(是构造函数中的第一条语句)。
- This:在此类中调用另一个结构化的构造函数(是构造函数中的第一条语句)。
五、多态
19.重载(Overload)和重写(Override)的区别
方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。 重载:一个类中有多个同名的方法,但是具有有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)。 重写:发生在子类与父类之间,子类对父类的方法进行重写,参数都不能改变,返回值类型可以不相同,但是必须是父类返回值的派生类。即外壳不变,核心重写!重写的好处在于子类可以根据需要,定义特定于自己的行为。
| 重写 | 重载 |
---|
是否同类 | 不能同类 | 可同类也可不同类 | 方法名的参数形式是否相同 | 必须相同 | 必须不同 | 返回类型 | 必须相同 | 可同可不同 | 访问修饰符 | 子类不能比父类权限小 | 无要求 | 方法体异常 | 不能抛出新的异常或异常不能范围变大 | 无要求 | 构造方法 | 不能被重写 | 可以被重载 | 多态实现方式 | 实现运行时多态 | 实现编译时多态 |
20. 接口与抽象类的异同
相似点
- 两者都可包含抽象方法。实现抽象类和接口的非抽象类必须实现这些抽象方法
- 两者都不能用来实例化对象。可以声明抽象类和接口的变量,对抽象类来说,要用抽象类的非抽象子类来实例化该变量;对接口来说,要用实现接口的非抽象子类来实例化该变量
- 两者的子类如果都没有实现抽象类(接口)中声明的所有抽象方法,则该子类就是抽象类
- 两者都可以实现程序的多态性
不同点
- 一个类只能继承一个直接父类,但是可以实现多个接口
- 抽象类的子类使用 extends 来继承;接口必须使用 implements 来实现接口。
- 抽象类可以有构造函数;接口不能有。
- 抽象类可以有 main 方法,并且我们能运行它;接口不能有 main 方法。
- 接口中的方法默认使用 public 修饰;抽象类中的方法可以是任意访问修饰符。
- 抽象类不能在Java 8 的 lambda 表达式中使用
- 接口体现的是一种规范(打印机和相机都有打印的功能),与实现接口的子类中不存在父与子的关系;抽象类与其子类存在父与子的关系(圆形和方形都是一种形状)
六、 异常处理
21. Exception与Error的区别,RuntimeException
Error(错误):通常是灾难性的致命错误,不是程序(程序猿)可以控制的,如内存耗尽、JVM系统错误、堆栈溢出等。应用程序不应该去处理此类错误,且程序员不应该实现任何Error类的子类。 Exception(异常):用户可能捕获的异常情况,可以使用针对性的代码进行处理,如:空指针异常、网络连接中断、数组下标越界等。 RuntimeException类及其子类称为非检查型异常,Java编译器会自动按照异常产生的原因引发相应类型的异常,程序中可以选择捕获处理也可以不处理,虽然Java编译器不会检查运行时异常,但是也可以去进行捕获和抛出处理。RuntimeException类和子类以及Error类都是非受检异常。
22.几种异常类型
Java 的所有异常可以分为受检异常(checked exception)和非受检异常(unchecked exception)。
受检异常 编译器要求必须处理的异常。Exception 中除 RuntimeException 及其子类之外的异常都属于受检异常。编译器会检查此类异常,也就是说当编译器检查到应用中的某处可能会此类异常时,将会提示你处理本异常——要么使用try-catch捕获,要么使用方法签名中用 throws 关键字抛出,否则编译不通过。 非受检异常 编译器不会进行检查并且不要求必须处理的异常,也就说当程序中出现此类异常时,即使我们没有try-catch捕获它,也没有使用throws抛出该异常,编译也会正常通过。该类异常包括运行时异常(RuntimeException及其子类)和错误(Error)。 运行时异常 定义:RuntimeException 类及其子类。 特点:RuntimeException为Java虚拟机在运行时自动生成的异常,如被零除和非法索引、操作数超过数组范围、打开文件不存在等。此类异常的出现绝大数情况是代码本身有问题应该从逻辑上去解决并改进代码。 RuntimeException类及其子类称为非检查型异常,Java编译器会自动按照异常产生的原因引发相应类型的异常,程序中可以选择捕获处理也可以不处理,虽然Java编译器不会检查运行时异常,但是也可以去进行捕获和抛出处理。RuntimeException类和子类以及Error类都是非受检异常。 编译时异常 特点: Exception中除RuntimeException及其子类之外的异常,该异常必须手动在代码中添加捕获语句来处理该异常。编译时异常也称为受检异常,一般不进行自定义检查异常。。
23. Java语言如何进行异常处理,关键字:throws、throw、try、catch、finally分别如何使用?
try 语句中存放的是可能发生异常的语句。当异常抛出时,异常处理机制负责搜寻参数与异常类型相匹配的第一个处理程序,然后进入catch语句中执行,此时认为异常得到了处理。如果程序块里面的内容很多,前面的代码抛出了异常,则后面的正常程序将不会执行,系统直接catch捕获异常并且处理
catch 语句可以有多个,用来匹配多个异常,捕获异常的顺序与catch语句的顺序有关,当捕获到对应的异常对象时,剩下的catch语句不再进行匹配,因此在安排catch语句的顺序时,首先应该捕获最特殊的异常,然后一般化。catch的类型是Java语言定义的或者程序员自己定义的,表示抛出异常的类型。异常的变量名表示抛出异常的对象的引用,如果catch捕获并匹配了该异常,那么就可以直接用这个异常变量名来指向所匹配的异常,并且在catch语句中直接引用。部分系统生成的异常在Java运行时自动抛出,也可通过throws关键字声明该方法要抛出的异常,然后在方法内抛出异常对象。
final 语句为异常提供一个统一的出口,一般情况下程序始终都要执行final语句,final在程序中可选。final一般是用来关闭已打开的文件和释放其他系统资源。try-catch-final可以嵌套。
throws 关键字和 throw 关键字在使用上的几点区别如下:
- throw 关键字用在方法内部,只能用于抛出一种异常,用来抛出方法或代码块中的异常。
- throws 关键字用在方法声明上,可以抛出多个异常,用来标识该方法可能抛出的异常列表。调用该方法的方法必须包含可处理异常的代码,否则也要在方法声明中用 throws 关键字声明相应的异常。
有4 种特殊情况,finally块不会被执行:
- finally语句块中发生了异常
- 前面的代码中执行了System.exit()退出程序
- 程序中所在的线程死亡
- 关闭CPU
24. 关于return和finally的关系
try中有return 无论在什么位置添加return,finally子句都会被执行 catch和try中都有return 当try中抛出异常且catch中有return语句,finally中没有return语句,java先执行catch中非return语句,再执行finally语句,最后执行return语句。若try中没有抛出异常,则程序不会执行catch体里面的语句,java先执行try中非return语句,再执行finally语句,最后再执行try中的return语句。 finally 中有return finally中有return时,会覆盖掉try和catch中的return。 finally中没有return语句,但是改变了返回值 如果finally中定义的数据是基本数据类型或文本字符串,则在finally中对该基本数据的改变不起作用,try中的return语句依然会返回进入finally块中之前保存的值;如果finally中定义的数据是是引用类型,则finally中的语句会起作用,try中return语句的值就是在finally中改变后该属性的值。
七、输入与输出流
25.java 中 IO 流分为几种?
Java IO流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java IO流的40多个类大部分都是从如下4个抽象类基类中派生出来的。
- InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
- OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
- 按照流的流向分,可以分为输入流和输出流;
- 按照操作单元划分,可以分为字节流和字符流;
- 按照流的角色划分,可以分为节点流和处理流。
26.BIO,NIO,AIO 有什么区别,如何理解同步和异步,阻塞和非阻塞
同步与异步
- 同步: 同步就是发起一个调用后,被调用者未处理完请求之前,调用不返回。
- 异步: 异步就是发起一个调用后,立刻得到被调用者的回应表示已接收到请求,但是被调用者并没有返回结果,此时我们可以处理其他的请求,被调用者通常依靠事件,回调等机制来通知调用者其返回结果。
- 同步和异步的区别最大在于异步的话调用者不需要等待处理结果,被调用者会通过回调等机制来通知调用者其返回结果。
阻塞和非阻塞
- 阻塞: 阻塞就是发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。
- 非阻塞: 非阻塞就是发起一个请求,调用者不用一直等着结果返回,可以先去干其他事情。
- 阻塞和非阻塞的区别最大在于有没有一直在干其他的活动。
BIO是什么 同步阻塞:举个例子 : 我现在上厕所 现在厕所的坑已经满了 我什么事情都不做 我就一直等(主动观察)哪一个 坑没人了 ,我就立马去占坑 通过这个示例 可以理解这是同步阻塞的IO NIO是什么 NIO : 同步非阻塞 New IO Non-Block IO 举个例子: 我现在上厕所 现在厕所的坑已经满了 这时候我不会像之前一样 我会出去抽支烟 或者微信摇一摇 然后我会时不时回去厕所主动看看 看看有没有人走 然后再占有坑 AIO是什么 异步非阻塞IO 举个例子: 我没有在厕所里面等着 而是在厕所外面玩手机,如果有人上完厕所他告诉我: 我好了 你去吧, 这时候我再回去厕所做我自己的事情 异步阻塞IO 举个例子: 开发中非常少 我现在上厕所 现在厕所的坑已经满了 这时候比较懒 什么也不做 就在坑旁干等着 等上厕所的人上好了之后告诉我 我好了 你去吧 它们之间的区别 BIO: 发起请求–>一直阻塞–>处理完成 NIO: Selector主动轮询channel–>处理请求–>处理完成 AIO: 发起请求–>通知回调。
八、集合(容器)
27.谈谈Java集合中那些线程安全的集合 & 实现原理
首先要明白线程安全就是说多线程访问同一代码,不会产生不确定的结果。编写线程安全的代码是低依靠线程同步。 Vector、ArrayList、LinkedList
- Vector的方法都是同步的(Synchronized),是线程安全的(thread-safe),而ArrayList,LinkedList的方法不是,由于线程的同步必然要影响性能,因此,ArrayList的性能比Vector好
在一列数据的后面添加数据而不是在前面或中间,并且需要随机地访问其中的元素时,使用ArrayList会提供比较好的性能,而访问链表中的某个元素时,就必须从链表的一端开始沿着连接方向一个一个元素地去查找,直到找到所需的元素为止,所以,当你的操作是在一列数据的前面或中间添加或删除数据,并且按照顺序访问其中的元素时,就应该使用LinkedList
HashTable,HashMap,HashSet
- Hashtable:基于哈希表实现的,同样每个元素是一个key-value对,其内部也是通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长。
Hashtable也是JDK1.0引入的类,是线程安全的,能用于多线程环境中。 - HashMap,HashSet不是线程安全的
- HashMap: 实现了Map接口,Map接口对键值对进行映射。Map中不允许重复的键。Map接口有两个基本的实现,HashMap和TreeMap。TreeMap保存了对象的排列次序,而HashMap则不能。HashMap
允许键和值为null 。HashMap是非synchronized的,但是HashMap可以通过Collections 进行同步: Map m = Collections.synchronizeMap(hashMap); , 这样多个线程同时访问HashMap时,能保证只有一个线程更改Map。public Object put(Object Key,Object value)方法用来将元素添加到map中 - HashSet: 实现了Set接口,不允许集合中有重复的值,对象存储在HashSet之前,要先确保对象重写equals()和hashCode()方法,这样才能比较对象的值是否相等,以确保set中没有储存相等的对象。如果我们没有重写这两个方法,将会使用这个方法的默认实现。public boolean add(Object o)方法用来在Set中添加元素,当元素值重复时则会立即返回false,如果成功添加的话会返回true。
28.HashMap和Hashtable的区别
Hashtable继承自Dictionary类,而HashMap继承自AbstractMap类。但二者都实现了Map接口。但决定用哪一个之前先要弄清楚它们之间的分别。主要的区别有:线程安全性,同步(synchronization),以及速度。
- HashMap几乎可以等价于Hashtable,除了HashMap是非synchronized的,并可以接受null(HashMap可以接受为null的键值(key)和值(value),而Hashtable则不行)。
- HashMap是非synchronized,而Hashtable是synchronized,这意味着Hashtable是线程安全的,多个线程可以共享一个Hashtable;而如果没有正确的同步的话,多个线程是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。
- 另一个区别是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器,不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。这条同样也是Enumeration和Iterator的区别。
- 由于Hashtable是线程安全的也是synchronized,所以在单线程环境下它比HashMap要慢。如果不需要同步,只需要单一线程,那么使用HashMap性能要好过Hashtable。
- HashMap不能保证随着时间的推移Map中的元素次序是不变的。
29.HashSet和HashMap的区别
- HashMap实现了Map接口, HashSet实现了Set接口
- HashMap储存键值对, HashSet仅仅存储对象
- HashMap使用put()方法将元素放入map中, HashSet使用add()方法将元素放入set中
- HashMap中使用键对象来计算hashcode值, HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false
- HashMap比较快,因为是使用唯一的键来获取对象 HashSet较HashMap来说比较慢.
30.TreeMap和TreeSet的区别与联系
相同点:
- TreeMap和TreeSet都是有序的集合(非线程安全的),也就是说他们存储的值都是排好序
的。 - TreeMap和TreeSet都是非同步集合,因此他们不能在多线程之间共享,不过可以使用方法Collections.synchroinzedMap()来实现同步
- 运行速度都要比Hash集合慢,他们内部对元素的操作时间复杂度为O(logN),而
HashMap/HashSet则为O(1)。 - 要求存放的对象所属的类必须实现Comparable接口,该接口提供了比较元素的compareTo()方法,当插入元素时会回调该方法比较元素的大小。
不同点:
- 最主要的区别就是TreeSet和TreeMap分别实现Set和Map接口
- TreeSet只存储一个对象,而TreeMap存储两个对象Key和Value(仅仅key对象有序)
- TreeSet中不能有重复对象,而TreeMap中可以存在
- TreeMap的底层采用红黑树的实现,完成数据有序的插入,排序。因此它要求一定要有Key比较的方法,要么传入Comparator实现,要么key对象实现Comparable接口。
31. ArrayMap和HashMap的区别
HashMap: 内部是使用一个默认容量为16的数组来存储数据的,而数组中每一个元素却又是一个链表的头结点,所以,更准确的来说,HashMap内部存储结构是使用哈希表的拉链结构(数组+链表),HashMap获取数据是通过遍历Entry[]数组来得到对应的元素,在数据量很大时候会比较慢,所以在Android中,HashMap是比较费内存的。 ArrayMap: 是一个<key,value>映射的数据结构,它设计上更多的是考虑内存的优化,内部是使用两个数组进行数据存储,一个数组记录key的hash值,另外一个数组记录Value值,它和SparseArray一样,也会对key使用二分法进行从小到大排序,在添加、删除、查找数据的时候都是先使用二分查找法得到相应的index,然后通过index来进行添加、查找、删除等操作,所以,应用场景和SparseArray的一样,如果在数据量比较大的情况下,那么它的性能将退化至少50%。 HashMap和ArrayMap各自的优势
数据量比较小,并且需要频繁的使用Map存储数据的时候,推荐使用ArrayMap。 而数据量比较大的时候,则推荐使用HashMap。
- 查找效率
HashMap因为其根据hashcode的值直接算出index,所以其查找效率是随着数组长度增大而增加的。ArrayMap使用的是二分法查找,所以当数组长度每增加一倍时,就需要多进行一次判断,效率下降。所以对于数量比较大的情况下,推荐使用HashMap - 扩容数量
HashMap初始值16个长度,每次扩容的时候,直接申请双倍的数组空间。 ArrayMap每次扩容的时候,如果size长度大于8时申请size*1.5个长度,大于4小于8时申请8个,小于4时申请4个。这样比较ArrayMap其实是申请了更少的内存空间,但是扩容的频率会更高。因此,如果当数据量比较大的时候,还是使用HashMap更合适,因为其扩容的次数要比ArrayMap少很多。 - 扩容效率
HashMap每次扩容的时候时重新计算每个数组成员的位置,然后放到新的位置。 ArrayMap则是直接使用System.arraycopy。所以效率上肯定是ArrayMap更占优势。这里需要说明一下,网上有一种说因为ArrayMap使用System.arraycopy更省内存空间,这一点我真的没有看出来。arraycopy也是把老的数组的对象一个一个的赋给新的数组。当然效率上肯定arraycopy更高,因为是直接调用的c层的代码。 - 内存耗费
以ArrayMap采用了一种独特的方式,能够重复的利用因为数据扩容而遗留下来的数组空间,方便下一个ArrayMap的使用。而HashMap没有这种设计。由于ArrayMap只缓存了长度是4和8的时候,所以如果频繁的使用到Map,而且数据量都比较小的时候,ArrayMap无疑是相当的节省内存的。
32. Collection 和 Collections的区别?
Collection: 是集合类的上层接口。本身是一个Interface,里面包含了一些集合的基本操作。Collection接口时Set接口和List接口的父接口 Collections Collections是一个集合框架的帮助类,里面包含一些对集合的排序,搜索以及序列化的操作。最根本的是Collections是一个类,Collections 是一个包装类,Collection 表示一组对象,这些对象也称为 collection 的元素。一些collection 允许有重复的元素, 而另一些则不允许,一些 collection 是有序的,而另一些则是无序的。
33. Map的遍历方式有哪些?
在for-each循环中使用entries来遍历 在for-each循环中遍历keys或values。 使用Iterator遍历 通过键找值遍历(效率低)
九、Java并发
34. 什么是线程,什么是进程,两者区别?
进程 :进程是并发执行程序在执行过程中资源分配和管理的基本单位(资源分配的最小单位)。进程可以理解为一个应用程序的执行过程,应用程序一旦执行,就是一个进程。每个进程都有自己独立的地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段。通常来说,应用中的 Activity、Service 等四大组件默认都位于一个进程里面,并且这个进程名称的默认值就是我们给应用定义的包名。系统为每个进程分配的内存是有限的,比如在以前的低端手机上常见是 16M,现在的机器内存更大一些,32M、48M,甚至更高。但是,总是有限的,毕竟一个手机出厂之后RAM 的大小就定了,总是无法满足所有应用的需求。所以,一个明智的选择就是使用多进程,将一些看不见的服务、比较独立而又相当占用内存的功能运行在另外一个进程当中,主动分担主进程的内存消耗。常见如,应用中的推送服务,音乐类App 的后台播放器等等,单独运行在一个进程中。 线程: 程序执行的最小单位。每个进程都有自己的地址空间,即进程空间,在网络或多用户换机下,一个服务器通常需要接收大量不确定数量用户的并发请求,为每一个请求都创建一个进程显然行不通(系统开销大响应用户请求效率低),因此操作系统中线程概念被引进。引入目的是为了减少程序在并发执行过程中的开销,使OS的并发效率更高。 进程与线程的区别
- 调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位;
- 地址空间: 同一进程的所有线程共享本进程的地址空间,而不同的进程之间的地址空间是独立的。
- 资源拥有: 同一进程的所有线程共享本进程的资源,如内存,CPU,IO等。进程之间的资源是独立的,无法共享。
- 执行过程:每一个进程可以说就是一个可执行的应用程序,每一个独立的进程都有一个程序执行的入口,顺序执行序列。但是线程不能够独立执行,必须依存在应用程序中,由程序的多线程控制机制进行控制。
- 健壮性: 因为同一进程的所以线程共享此线程的资源,因此当一个线程发生崩溃时,此进程也会发生崩溃。 但是各个进程之间的资源是独立的,因此当一个进程崩溃时,不会影响其他进程。因此进程比线程健壮。线程执行开销小,但不利于资源的管理与保护。进程的执行开销大,但可以进行资源的管理与保护。进程可以跨机器前移。
进程与线程的联系:
- 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程;
- 资源分配给进程,同一进程的所有线程共享该进程的所有资源;
-处理机分给线程,即真正在处理机上运行的是线程; -线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。
举个例子更好理解: 假如我们把整条道路看成是一个“进程”的话,那由白色虚线分隔开来的各个车道就是进程中的各个“线程”了。这些线程(车道)共享了进程(道路)的公共资源(土地资源)。这些线程(车道)必须依赖于进程(道路),也就是说,线程不能脱离于进程而存在(就像离开了道路,车道也就没有意义了)。这些线程(车道)之间可以并发执行(各个车道你走你的,我走我的),也可以互相同步(某些车道在交通灯亮时禁止继续前行或转弯,必须等待其它车道的车辆通行完毕)。这些线程(车道)之间依靠代码逻辑(交通灯)来控制运行,一旦代码逻辑控制有误(死锁,多个线程同时竞争唯一资源),那么线程将陷入混乱,无序之中。这些线程(车道)之间谁先运行是未知的,只有在线程刚好被分配到CPU时间片(交通灯变化)的那一刻才能知道。
使用场景
- 在程序中,如果需要频繁创建和销毁的,使用线程。因为进程创建和销毁开销很大(需要不停的分配资源),但是线程频繁的调用只是改变CPU的执行,开销小
- 如果需要程序更加的稳定安全时,可以选择进程。如果追求速度,就选择线程。
35. 多线程的3种实现方式
1.继承Thread类创建线程类
- 定义Thread类的子类, 并重写该类的run方法, 该run方法的方法体就代表了
线程要完成的任务。 因此把run()方法称为执行体。 - 创建Thread子类的实例, 即创建了线程对象。
- 调用线程对象的start()方法来启动该线程
2.通过Runnable接口创建线程类
- 定义runnable接口的实现类, 并重写该接口的run()方法, 该run()方法的方法体同样是该线程的线程执行体。
- 创建 Runnable实现类的实例, 并依此实例作为Thread的target来创建Thread对象, 该Thread对象才是真正的线程对象。
- 调用线程对象的start()方法来启动该线程。
3.通过Callable和Future创建线程
- 创建Callable接口的实现类, 并实现call()方法, 该call()方法将作为线程执行体, 并且有返回值
- 创建Callable实现类的实例, 使用FutureTask类来包装Callable对象, 该FutureTask对象封装了该Callable对象的call()方法的返回值。
- 使用FutureTask对象作为Thread对象的target创建并启动新线程。
- 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值, 调用get()方法会阻塞线程
创建线程的三种方式的对比
- 采用实现Runnable、Callable接口的方式创建多线程时:
- 优势
线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。 - 劣势
编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。
- 优势
编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。 - 劣势
线程类已经继承了Thread类,所以不能再继承其他父类。
runnable 和 callable 的区别
- Callcble是可以有返回值的,具体的返回值就是在Callable的接口方法call返回的,并且这个返回值具体是通过实现Future接口的对象的get方法获取的,这个方法是会造成线程阻塞的;而Runnable是没有返回值的,因为Runnable接口中的run方法是没有返回值的;
- Callable里面的call方法是可以抛出异常的,我们可以捕获异常进行处理;但是Runnable里面的run方法是不可以抛出异常的,异常要在run方法内部必须得到处理,不能向外界抛出;
- callable和runnable都可以应用于executors。而thread类只支持runnable
36.并发编程的三大概念:原子性,有序性,可见性
Java 内存模型 (JMM)关键技术点都是围绕着多线程的原子性、可见性、有序来讨论的。JMM 解决了可见性和有序性的问题,而锁解决了原子性的问题。Java内存模型规定了所有的变量都存储在主内存中,每条线程中还有自己的工作内存,线程的工作内存中保存了被该线程所使用到的变量(这些变量是从主内存中拷贝而来)。线程对变量的所有操作(读取,赋值)都必须在工作内存中进行。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。 原子性:即一个操作或者多个操作,要么全部执行,并且执行的过程不会被任何因素打断,要么就都不执行。只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized 和Lock 能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。
可见性:是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。Java提供了volatile 关键字来保证可见性,当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。 有序性:即程序执行的顺序按照代码的先后顺序执行。一般来说,处理器为了提高程序运行效率,可能会进行指令重排序,也就是对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。可以通过volatile 关键字来保证一定的“有序性”,volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。另外可以通过synchronized 和Lock 来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
synchronized 关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile 关键字无法保证操作的原子性。
37.并行和并发有什么区别?
举个例子:你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,说明你不支持并发也不支持并行。 你吃饭吃到一半,电话来了,你停下来接了电话,然后继续吃饭,说明你支持并发。(不一定是同时) 你吃饭吃到一半,电话来了,你一边打电话一边继续吃饭,说明你支持并行。 并发的关键在于有处理多个事件的能力,不一定要同时。并行的关键在于有同时 处理多个事件的能力。
两个区别在于是否是同时 。并发是轮流处理多个事件,并行是同时处理多个事件。
38.守护线程是什么?
定义: 指在程序运行时 在后台提供一种通用服务的线程,这种线程并不属于程序中不可或缺的部分。通俗点讲,任何一个守护线程都是整个JVM中所有非守护线程的"保姆"。 特点: 守护线程拥有自动结束自己生命周期的特性,而非守护线程不具备这个特点。当 JVM 中不存在任何一个正在运行的非守护线程时,JVM 进程即会退出,也就是说只要有任何非守护线程还在运行,程序就不会终止,当JVM中只有守护线程运行时JVM会自动关闭。 JVM 中的垃圾回收线程就是典型的守护线程。 使用方法: 在Java语言中,守护线程一般具有较低的优先级,它并非只由JVM内部提供,用户在编写程序时也可以自己设置守护线程,例如:将一个线程设置为守护线程的方法就是在调用start()启动线程之前调用对象的setDaemon(true)方法,若将以上参数设置为false,则表示的是用户进程模式,需要注意的是,当在一个守护线程中产生了其他的线程,那么这些新产生的线程默认还是守护线程,用户线程也是如此。 应用场景:通常来说,守护线程经常被用来执行一些后台任务,但是呢,你又希望在程序退出时,或者说 JVM 退出时,线程能够自动关闭,此时,首选守护线程。
39.在 java 程序中怎么保证多线程的运行安全?
总体来说线程安全在三个方面体现: 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作, (atomic,synchronized)。 可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile)。 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before原则)。
导致原因:
- 缓存导致的可见性问题
- 线程切换带来的原子性问题
- 编译优化带来的有序性问题
解决办法: 确保线程安全作用是让程序按照我们预期的行为去执行
- JDK Atomic开头的原子类(AtomicInteger,AtomicLong,AtomicBoolean等等)、synchronized、LOCK,可以解决原子性问题
- synchronized、volatile、LOCK,可以解决可见性问题
- Happens-Before 规则可以解决有序性问题,synchronized和Lock来保证有序性
- 使用java提供的安全类:java.util.concurrent包下的类自身就是线程安全的,在保证安全的同时还能保证性能;
Happens-Before 规则如下:
- 程序次序规则:在一个线程内,按照程序控制流顺序,书写在前面的操作先行发生于书写在后面的操作
- 管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作
- volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作
- 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始
40.说一下 synchronized 底层实现原理?
基于对象的监视器(ObjectMonitor),在同步方法执行前后,有两个指令,进入同步方法前monitorenter,方法执行完成后monitorexit; 任意一个对象都拥有自己的监视器,当同步代码块或同步方法时,执行方法的线程必须先获得该对象的监视器才能进入同步块或同步方法,没有获取到监视器的线程将会被阻塞,并进入同步队列,状态变为 BLOCKED 。当成功获取监视器的线程释放了锁后,会唤醒阻塞在同步队列的线程,使其重新尝试对监视器的获取。 补充:一个synchronize锁会有两个monitorexit,这是保证synchronize能一定释放锁的机制,一个是方法正常执行完释放,一个是执行过程发生异常时虚拟机释放; synchronized可以用在如下地方
- 修饰实例方法,对当前实例对象this加锁
- 修饰静态方法,对当前类的Class对象加锁
- 修饰代码块,指定加锁对象,对给定对象加锁
41.synchronized 和 volatile 的区别是什么?
1、volatile 本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。 2、 volatile 仅能使用在变量级别;synchronized 则可以使用在变量、方法、和类级别的 3、volatile 仅能实现变量的修改可见性,不能保证原子性;而synchronized 则可以保证变量的修改可见性和原子性 4、 volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。 5、 volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化
42.synchronized 和 Lock 有什么区别?
1、synchronized不需要手动释放锁,lock需要在锁用完后进行unlock释放锁; 2、synchronized只能是默认的非公平锁,lock可以指定使用公平锁或者非公平锁; 3、lock提供的Condition(条件)可以指定唤醒哪些线程,而synchronized只能随机唤醒一个或者全部唤醒;
43.多线程锁的升级原理是什么?
JVM优化synchronized的运行机制,当JVM检测到不同的竞争状态时,就会根据需要自动切换到合适的锁,这种切换就是锁的升级。升级是不可逆的,也就是说只能从低到高,不能够降级。 锁的级别从低到高: 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁 锁分级别原因: 没有优化以前,synchronized是重量级锁(悲观锁),使用 wait 和 notify、notifyAll 来切换线程状态非常消耗系统资源;线程的挂起和唤醒间隔很短暂,这样很浪费资源,影响性能。所以 JVM 对 synchronized 关键字进行了优化,把锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。 无锁:没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其他修改失败的线程会不断重试直到修改成功。 偏向锁:HotSpot作者发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得。当一个线程访问同步块并获得锁时,会在对象头和栈帧中的锁记录里面存储偏向的线程ID,以后该线程再进入和退出同步块时不需要进行CAS操作来加锁和解锁。 轻量级锁:Java SE 1.6 为了减少获得/释放锁带来的性能消耗,引入了“轻量级锁”和“偏向锁”,“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的。但是,首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。在解释轻量级锁的执行过程之前,先明白一点,轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。 重量级锁:指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。重量级锁通过对象内部的监视器(monitor)实现,而其中 monitor 的本质是依赖于底层操作系统的 Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。
锁 | 优点 | 缺点 | 适用场景 |
---|
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块场景 | 轻量级锁 | 竞争的线程不会阻塞 ,提高了程序的响应速度 | 如果始终得不到锁竞争的线程使用自旋会消耗CPU | 追求响应时间,同步块执行速度非常快 | 重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,同步块执行速度较长 |
44.什么是死锁?
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。是操作系统层面的一个错误,是进程死锁的简称,系统发生死锁现象不仅浪费大量的系统资源,甚至导致整个系统崩溃,带来灾难性后果。
45.怎么防止死锁?
一般来说,要出现死锁问题需要满足以下4个条件:
- 互斥条件:一个资源每次只能被一个线程使用。
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
在JAVA编程中,有3种典型的死锁类型:
- 静态的锁顺序死锁:a和b两个方法都需要获得A锁和B锁。一个线程执行a方法且已经获得了A锁,在等待B锁;另一个线程执行了b方法且已经获得了B锁,在等待A锁。解决方法是将所有需要多个锁的线程,都以相同的对象顺序来获得锁。
- 动态的锁顺序死锁:指两个线程调用同一个方法时,传入的参数颠倒造成的死锁,解决方案是使用System.identifyHashCode来定义锁的顺序,确保所有的线程都以相同的顺序获得锁。
- 协作对象之间发生的死锁:一个线程调用了A对象的a方法,另一个线程调用了B对象的b方法。此时可能会发生,第一个线程持有A对象锁并等待B对象锁,另一个线程持有B对象锁并等待A对象锁。解决方案是需要调用某个外部方法时不需要持有锁,即避免在持有锁的情况下调用外部的方法。
在写代码时,要确保线程在获取多个锁时采用一致的顺序。同 时,要避免在持有锁的情况下调用外部方法。
46.ThreadLocal 是什么?
ThreadLocal是什么:
JDK1.2的版本中就提供java.lang.ThreadLocal类,每一个ThreadLocal能够放一个线程级别的变量, 它本身能够被多个线程共享使用,并且又能够达到线程安全的目的,且绝对线程安全。ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。
47.Synchronized 和 ReentrantLock 区别是什么?
相似点: 这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式 的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的(操作系统需要在用户态与内核态之间来回切换,代价很高,不过可以通过对锁优化进行改善)。 不同点:
Synchronized 是java语言的关键字 ,是原生语法层面的互斥,需要jvm实现,ReentrantLock 是JDK 1.5之后提供的API层面 的互斥锁,Synchronized 可以修饰实例方法,静态方法,代码块。自动释放锁。ReentrantLock 一般需要try catch finally语句,在try中获取锁,在finally释放锁。需要手动释放锁。Synchronized锁 的范围是整个方法或synchronized块部分,ReentrantLock 因为是方法调用,可以跨方法,灵活性更大
Synchronized 是重量级锁。重量级锁需要将线程从内核态和用户态来回切换。如:A线程切换到B线程,A线程需要保存当前现场,B线程切换也需要保存现场。这样做的缺点是耗费系统资源。这是一种被动阻塞悲观锁,状态是blockReentrantLock 是轻量级锁。采用cas+volatile管理线程,不需要线程切换切换获取锁线程,这是一种乐观的思想(可能失败),这是一种主动的阻塞乐观锁,状态是wait
- 公平和非公平
公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,
Synchronized 只有非公平锁。ReentrantLock 默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。公平锁通过构造函数传递true表示。
- 可中断的
持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待
Synchronized 是不可中断的。ReentrantLock 提供可中断和不可中断两种方式。其中lockInterruptibly 方法表示可中断,lock 方法表示不可中断。这相当于Synchronized来说可以避免出现死锁的情况。
- 条件队列
同步队列:多线程同时竞争一把锁失败被挂起的线程。 条件队列:正在执行的线程调用await/wait,从同步队列加入的线程会进入条件队列。正在执行线程调用signal/signalAll/notify/notifyAll,会将条件队列一个线程或多个线程加入到同步队列。 等待队列:和条件队列一个概念。
Synchronized 只有一个等待队列,要么随机唤醒一个线程要么唤醒全部线程。ReentrantLock 中一把锁可以对应多个条件队列。通过newCondition表示。一个ReentrantLock对象可以同时绑定对个对象。ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程
48.线程有哪些状态?
- 创建(NEW):新创建了一个线程对象。
- 就绪(RUNNABLE):线程对象创建后,其他线程(比如main线程)调用了该对象的
start() 方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。 - 运行(RUNNING):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。
- 阻塞(BLOCKED):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。阻塞的情况分三种:
- 等待阻塞:运行(running)的线程执行
o.wait() 方法,JVM会把该线程放入等待队列(waitting queue)中。 - 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
- 其他阻塞:运行(running)的线程执行
Thread.sleep(long ms) 或t.join() 方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
- 死亡(DEAD):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。
49.sleep() 和 wait() 有什么区别?
基本区别
- sleep是Thread类的方法,wait是Object类中定义的方法
- sleep方法可以在任何地方调用
- wait方法只能在synchronized方法或synchronized块中使用
本质区别
- Thread.sleep是static静态方法,不能改变对象的机锁,当一个synchronized块中调用了sleep() 方法,线程虽然进入休眠,但是对象的机锁没有被释放,其他线程依然无法访问这个对象。只会让出CPU,不会导致锁行为的改变
- Object.wait不仅让出CPU,还会释放已经占有的同步资源锁
区别 | wait() | sleep() |
---|
归属类 | Object类实例方法 | Thread类静态方法 | 是否释放锁 | 释放锁 | 不会释放锁 | 线程状态 | 等待 | 睡眠 | 使用时机 | 只能在同步块(Synchronized)中使用 | 在任何时候使用 | 唤醒条件 | 其他线程调用notify()或notifyAll()方法 | 超时或调用Interrupt()方法 | cpu占用 | 不占用cpu,程序等待n秒 | 占用cpu,程序等待n秒 |
50.notify()和 notifyAll()有什么区别?
- 如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
- 当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争
- 优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
- 如果wait被调用之前notify的唤醒通知就来了,那么这个线程并不能保证被唤醒,有可能会导致死锁问题。
51.线程的 run()和 start()有什么区别?
创建一个线程 Thread t1 = new Thread()
t1.run() :run是用主线程执行,只是调用了一个普通方法,并没有启动另一个线程,程序还是会按照顺序执行相应的代码,必须是这个方法执行完了代码才能往下走。t1.start() :start是new了一个线程执行的,表示重新开启一个线程,不必等待其他线程运行完,只要得到cup就可以运行该线程。有多个线程使用start方法开启时,并不需要等待其中一个完成,所以他们的执行顺序应该是并行的。
52.创建线程池有哪几种方式?
Executors类创建线程池,创建出来的线程池都实现了ExecutorService接口
newCachedThreadPool() 方法可缓存的线程池,如果线程池的容量超过了任务数,自动回收空闲线程,任务增加时可以自动添加新线程,线程池的容量不限制。比较适合处理执行时间比较小的任务。newFixedThreadPool() 创建固定数目线程的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程数量不再变化,当线程发生错误结束时,线程池会补充一个新的线程。可以用于已知并发压力的情况下,对线程数做限制。newScheduledThreadPool 创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。适用于需要多个后台线程执行周期任务的场景。newSingleThreadExecutor 创建一个单线程化的Executor,线程异常结束,会创建一个新的线程,能确保任务按指定顺序(FIFO,LIFO,优先级)执行。可以用于需要保证顺序执行的场景,并且只有一个线程在执行
53.线程池都有哪些状态?
ThreadPoolExecutor是Executor底下的实现类,开发中常用的线程池就是它 线程池的5种状态:Running、ShutDown、Stop、Tidying、Terminated。
- 状态说明:线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。
- 状态切换:线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0!
- 状态说明:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。
- 状态切换:调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。
- 状态说明:线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会
中断 正在处理的任务。 - 状态切换:调用线程池的
shutdownNow ()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。
- 状态说明:当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。
- 状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。
当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。
- 状态说明:线程池彻底终止,就变成TERMINATED状态。
- 状态切换:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。
54.线程池中 submit()和 execute()方法有什么区别?
两者都是执行任务的方法 execute() 适用于不需要关注返回值的场景,只需要将线程丢到线程池中去执行就可以了。 submit() 方法适用于需要关注返回值的场景
十、Java虚拟机
55.垃圾收集算法/收集器
一切都是为了JVM运行效率 标记-清除算法
- 机制: 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
- 缺点
- 效率问题:标记和清除两个过程的效率都不高
- 空间问题:标记清除之后产生大量不连续的内存碎片,可能会导致程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
复制算法
- 机制:将可用内存按容量大小划分为大小相等的两块,每次只使用其中的一块。当一块内存使用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况。现代的商业虚拟机都采用这种收集算法来回收新生代。
- 缺点: 将内存缩小为了原来的一半,在对象存活率较高时,就要进行较多的复制操作,效率就会变低。。
标记-整理算法
- 机制: 首先标记出所有需要回收的对象, 在标记完成后让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。
分代收集算法
- 机制:把Java堆分为新生代和老年代,根据年代特点采用适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法。
在老年代中,因为对象存活率高、没有额外空间对它进行分配担保,就必须采用“标记-清除”或“标记-整理”算法来进行回收。
56.判断对象是否存活
- 判断对象是否存活与“引用”有关
我们希望的垃圾回收器对它的回收时机的不同。对于一些比较重要的对象,我们希望垃圾回收器永远不去回收它,即使此时内存空间已经不足了,因为一旦它被回收,将导致严重的后果。而对于一些不那么重要的对象,比如在做图片缓存的时候生成的大量图片的缓存对象,我们希望垃圾回收器只在内存不足的情况下去对它进行回收以提升用户体验。 Java中实际上有四种强度不同的引用,从强到弱它们分别是,强引用(Strong Reference),软引用(Soft Reference),弱引用(Weak Reference)和虚引用(Phantom Reference)。
强引用 : 就是指在程序代码之中普遍存在的,类似“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
软引用 : 对象留在内存的能力不是那么强的引用。使用WeakReference,在系统将要发生内存溢出异常之前 ,将会把这些对象列进回收范围之中进行第二次回收。软引用非常适合于创建缓存。当系统内存不足的时候,缓存中的内容是可以被释放的。
弱引用 : 描述非必须对象的。被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够 ,都会回收掉只被弱引用关联的对象。弱引用最常见的用处在哈希表中。当一个键值对被放入到哈希表中之后,哈希表对象本身就有了对这些键和值对象的引用。如果这种引用是强引用的话,那么只要哈希表对象本身还存活,其中所包含的键和值对象是不会被回收的。如果某个存活时间很长的哈希表中包含的键值对很多,最终就有可能消耗掉JVM中全部的内存。
虚引用 : 一个对象无法通过虚引用来取得一个对象实例,就是形同虚设,与其他几种引用不同的是,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动 。虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列queue 中。 ReferenceQueue queue = new ReferenceQueue (); //虚引用对象 PhantomReference pr = new PhantomReference (object, queue); 程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。 - 引用计数法判断对象
给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用 失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能被再使用的。 主流的JVM里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解 决对象间的互循环引用的问题。 - 可达性分析算法
十一、 网络
57.给我介绍5层网络模型
分为应用层,传输层,网络层,数据链路层和物理层 物理层:定义物理设备如何传输数据。简单来说,物理层就是我们电脑的硬件,我们的网卡端口,网线,以及我们网线连出去之后要有条光缆来为我们把数据传输到互联网,没有物理,我们的软件是没有办法去传输的,所以物理层,就是这些硬件设备相关的东西。 数据链路层:在通信的物理层间建立数据链路链接,两台机器,也要有一个软件服务帮我们通过物理的设备去创建一个电路的链接,也就是说这两边可以传输数据,最基础的就是电脑传输数据。 网络层:为数据在结点之间传输创建逻辑链路。比如说从我的电脑访问百度的服务器,那么我们去寻找百度这台服务器它所在的地址,它就是一个逻辑关系,那么这个关系是在网络层为我们去创建的。 传输层:基于数据链路层、网络层在建立起了从电脑到百度到服务器之间的这么一个链接之后,对于不同数据的传输方式是由传输层来实现的。比如使用http协议要传输一个数据,我们只需要在浏览器里面输入一个url,他就会自动去发送相关的一个数据到服务器端,然后服务器端能够解析这些数据返回给我们的浏览器,然后把页面显示出来,那么我们输入url这个过程,其实涉及到了一系列的数据的拼装以及传输,但是我们不需要指导这些数据里面到底是怎么去分片,怎么去跟服务器创建一个链接的关系,因为传输层已经帮我们实现了。 应用层:为我们应用软件提供了很多服务。写网页的时候,我们使用http协议去发送请求,我们是非常方便的,只要去new一个request请求,然后就可以去把一些数据,比如post,get的方式去发送到服务端,这是应用层在http协议上面,它帮我们实现了http协议,然后我们只需要去使用http协议相关的一些工具,就可以帮我们去传输一些数据,它是构建于tcp协议之上的,所以它传输的方式,都是要落实于tcp,ip协议上面。
58.HTTP协议和HTTPS协议区别
- HTTP 是明文传输协议,HTTPS 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议,比 HTTP 协议安全。
- HTTPS比HTTP更加安全,对搜索引擎更友好,利于SEO,谷歌、百度优先索引HTTPS网页;
- HTTPS需要用到SSL证书,而HTTP不用;
- HTTPS标准端口443,HTTP标准端口80;
- HTTPS基于传输层,HTTP基于应用层;
- HTTPS在浏览器显示绿色安全锁,HTTP没有显示;
59.TCP和UDP协议工作在哪一层
传输层协议:TCP、UDP协议。传输层的协议+端口就可以标识一个应用层协议,TCP/IP协议中的端口范围是从0~65535。我们知道,一台拥有IP地址的主机可以提供许多服务,比如Web服务、FTP服务、SMTP服务等,这些服务完全可以通过1个IP地址来实现 网络层协议:IP协议 应用层协议:FTP、HTTP、SMTP
60.三次握手和四次挥手
TCP 和 UDP是网络协议的传输层上的两种不同的协议。TCP的特点是面向连接的、可靠的字节流服务。客户端需要和服务器之间建立一个TCP连接,之后才能传输数据。数据到达之前对方就一直在等待,除非对方直接关闭连接,数据有序,先发先到。UDP是一种无连接、不可靠的数据发送协议。发送方根据对方的ip地址发送数据包,但是不保证接收发接包的质量,数据无序还容易丢包。虽然UDP协议不稳定但是在即时通讯(QQ聊天、在线视频、网络语音电话)的场景下,可以允许偶尔的断续,但是这种协议速度快。
TCP连接建立的前提,是通信的双方都要知道本方和对方的发送和接收功能,都是正常的 。也就是在TCP连接建立之前,有以下八个待确认项:服务端和客户端都要知道服务端发送、服务端接收、客户端发送、客户端接收是正常的。
- 第一次握手:客户端发送连接请求,服务端接收,所以服务端可以确认客户端的发送功能,服务端的接收功能正常;
- 第二次握手:服务端收到连接请求报文段后,同意连接发送应答,客户端接收到第二次握手报文后,客户端能够确认服务端发送、服务端接收,客户端的接收功能和发送功能是正常的;但是此时服务端并不知道自己的发送功能,客户端的接收功能是否正常,于是需要第三次握手
- 第三次握手: 客户端收到连接同意的应答后,还要向服务端发送一个确认报文段,表示:服务端发来的连接同意应答已经成功收到。此时,服务端就可以确认自己的发送功能,客户端的接收功能是正常的。
- 为什么要三次握手?
client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达server。本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。于是就向client发出确认报文段,同意建立连接。假设不采用“三次握手”,那么只要server发出确认,新的连接就建立了。由于现在client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。但server却以为新的运输连接已经建立,并一直等待client发来数据。这样,server的很多资源就白白浪费掉了。
四次挥手是指终止TCP连接协议时,需要在客户端和服务器之间发送四个包,挥手的前提是双方中都知道对方没有数据可以发送给对方了:
- 第一次分手:主机1(可以使客户端,也可以是服务器端),设置SequenceNumber,向主机2发送一个FIN报文段;此时,主机1进入FIN_WAIT_1状态;这表示主机1没有数据要发送给主机2了;
- 第二次分手:主机2收到了主机1发送的FIN报文段,向主机1回一个ACK报文段,Acknowledgment Number为Sequence Number加1;主机1进入FIN_WAIT_2状态;主机2告诉主机1,我“同意”你的关闭请求;
- 第三次分手:主机2向主机1发送FIN报文段,请求关闭连接,同时主机2进入LAST_ACK状态;
- 第四次分手:主机1收到主机2发送的FIN报文段,向主机2发送ACK报文段,然后主机1进入TIME_WAIT状态;主机2收到主机1的ACK报文段以后,就关闭连接;此时,主机1等待2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,主机1也可以关闭连接了。
- 为什么要四次分手?
TCP协议是一种面向连接的、可靠的、基于字节流的运输层通信协议。TCP是全双工模式,这就意味着,当主机1发出FIN报文段时,只是表示主机1已经没有数据要发送了,主机1告诉主机2,它的数据已经全部发送完毕了;但是,这个时候主机1还是可以接受来自主机2的数据;当主机2返回ACK报文段时,表示它已经知道主机1没有数据发送了,但是主机2还是可以发送数据到主机1的;当主机2也发送了FIN报文段时,这个时候就表示主机2也没有数据要发送了,就会告诉主机1,我也没有数据要发送了,之后彼此就会愉快的中断这次TCP连接。
十二、、设计模式
61.请说说单例模式 & 你项目中常用的单例模式
顾名思义就是只有一个实例,并且她自己负责创建自己的对象,这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。核心代码:构造方法私有化,private。
五种写法
- 懒汉式:顾名思义就是实例在用到的时候才去创建,“比较懒”,用的时候才去检查有没有实例,如果有则返回,没有则新建。有线程安全和线程不安全两种写法,区别就是synchronized关键字。
- 饿汉式: 从名字上也很好理解,就是“比较勤”,实例在初始化的时候就已经建好了,不管你有没有用到,都先建好了再说。好处是没有线程安全的问题,坏处是浪费内存空间。
- 双检锁,又叫双重校验锁,综合了懒汉式和饿汉式两者的优缺点整合而成。特点是在synchronized关键字内外都加了一层 if 条件判断,这样既保证了线程安全,又比直接上锁提高了执行效率,还节省了内存空间。
- 静态内部类:静态内部类的方式效果类似双检锁,但实现更简单。但这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
- 枚举的方式是比较少见的一种实现方式,但是看上面的代码实现,却更简洁清晰。并且她还自动支持序列化机制,绝对防止多次实例化。
62.了解过的设计模式
创建型模式 对类的实例化过程进行了抽象,能够将软件模块中对象的创建和对象的使用分离。为了使软件的结构更加清晰,外界对于这些对象只需要知道它们共同的接口,而不清楚其具体的实现细节,使整个系统的设计更加符合单一职责原则。
- 简单工厂模式(Simple Factory)
- 工厂方法模式(Factory Method)
- 抽象工厂模式(Abstract Factory)
- 建造者模式(Builder)
- 原型模式(Prototype)
- 单例模式(Singleton)
结构型模式 描述如何将类或者对 象结合在一起形成更大的结构,就像搭积木,可以通过 简单积木的组合形成复杂的、功能更为强大的结构。
- 适配器模式(Adapter)
- 桥接模式(Bridge)
- 组合模式(Composite)
- 装饰模式(Decorator)
- 外观模式(Facade)
- 享元模式(Flyweight)
- 代理模式(Proxy)
行为型模式 是对在不同的对象之间划分责任和算法的抽象化。行为型模式不仅仅关注类和对象的结构,而且重点关注它们之间的相互作用。通过行为型模式,可以更加清晰地划分类与对象的职责,并研究系统在运行时实例对象 之间的交互。在系统运行时,对象并不是孤立的,它们可以通过相互通信与协作完成某些复杂功能,一个对象在运行时也将影响到其他对象的运行。
- 职责链模式(Chain of Responsibility)
- 命令模式(Command)
- 解释器模式(Interpreter)
- 迭代器模式(Iterator)
- 中介者模式(Mediator)
- 备忘录模式(Memento)
- 观察者模式(Observer)
- 状态模式(State)
- 策略模式(Strategy)
- 模板方法模式(Template Method)
- 访问者模式(Visitor)
|