Android面试之Java基础
写在前面的话
Android 基于Java语言开发,所以Java知识也是Android开发人员 必不可少的知识,经过这次面试总结了一些知识点,有点散。所以这篇记录一下总结的Java基础知识。
思维导图便于记忆
1. 基本数据类型
- 整形:byte(1个字节), short(2个字节) , int (4个字节), long(8个字节) 。
- 浮点型:float(4个字节),double(8个字节)。
- 浮点型:char(2个字节) 。
- 布尔型:boolean (1个字节)
2.引用类型String
String ,Stringbuffer ,Stringbulider的区别?
- String底层是一个final类型的字符数组,所以String的值是不可变的,每次对String的操作都会生成新的String对象,造成内存浪费。
- 在内存中专门有一个字符串缓冲池来存放字符串,当池中没有对应的字符串就会创建一个新的字符串对象
- newString():创建字符串是:当字符串常量池中没有会在池中创建一个字符串常量,在内存的堆中也开辟一个对象内存,当缓冲池有时,不会在被创建,只会在内存中创建一个新的字符串对象。
- StringBuffer和StringBuilder都继承AbstractStringBuilder抽象类,底层是可变的字符数组。
- StringBuffer是在jdk1.0出现的,是线程安全的,查看源码有synchronized关键字修饰,但是执行速度慢 ,StringBuilder是jdk1.5出现的,是非线程安全的,执行速度快。
String ,Stringbuffer ,Stringbulider效率谁快,为什么?
在串行的的情况下,String 的效率最低(因为底层是不可变数组,每次操作都是新建一个Sting对象),Stringbuffer因为是同步,有syncchronzed稀释,效率相对低,StringBulider效率最高。
3.int和Integer的区别?
- Integer是int的包装类,int则是java的一种基本数据类型
- Integer变量必须实例化后才能使用,而int变量不需要
- Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int则是直接存储数据值
- Integer的默认值是null,int的默认值是0
4. 面向对象
1. 定义
- 面向对象时基于面向过程的,面向过程强调的时步骤,注重的时过程,面向对象则注重于结果。
- 面向对象是一种思维方式,能简化程序员开发。
- 万物皆对象。
2. 三大特征
- 继承
一个类的属性和行为均于现有类相似,属于现有类的一种,可以定义为现有类的子类,如果由多个类有相同的属性和行为,可以抽取公共内容定义为父类 - 多态
表现多种形态,具有多种实现方式,比如一个人,可以是父亲,儿子,老板等,多种形态 - 封装
将对象的属性和行为封装起来,类就是面向对象的封装。
5.接口和抽象类的区别?
- 接口
- 只有抽象方法
- 变量必须用public static final 修饰。
- 不能含有静态代码块和静态方法。
- 可以实现多个接口。
- 抽象类
- 可以提供成员方法。
- 成员变量可任意是各种类型。
- 可以有静态代码块和静态方法。
- 只能单继承。
6.final ,finally ,fianlize的区别?
- final是一个关键字,用来修饰类,变量,方法.修饰的类不能被继承,但是可以继承其他的类,修饰的方法不能被子类冲重写,修饰的变量是常量,只能被赋值一次。
- finally是try-catch-finally语句的一个模块,正常情况下里面的代码永远会被执行,一般用来释放资源。
- finalize是Object类中的方法,当对象变成垃圾的时候,由GC来调用finalize()方法回收。
7. 静态变量和成员变量的不同?
① 所属范围不同。静态变量是属于类范围的;成员变量是属于对象范围的。
② 存活时间不同。类的一生有着静态变量的伴随;而成员变量只能陪类走一程,对象产生的时候它就产生,而且它会随着对象的消亡而消亡。
③ 存储位置不同。静态变量时存储在方法区里的静态区;成员变量存储在堆栈内存区。
④ 调用方式不同。静态变量可以通过类名调用,也可以通过对象来调用;成员变量只能通过对象名调用。
8. 集合
1. 集合框架,list,map,set都有哪些具体的实现类,区别都是什么?
- List:有序、可重复;索引查询速度快;插入、删除伴随数据移动,速度慢;
- Set:无序,不可重复;
- Map:键值对,键唯一,值多个;
- List,Set都是继承自Collection接口,Map则不是;
Java集合的类结构图如下所示:
2. 线程安全集合类与非线程安全集合类
- LinkedList、ArrayList、HashSet是非线程安全的,Vector是线程安全的;
- HashMap是非线程安全的,HashTable是线程安全的;
- StringBuilder是非线程安全的,StringBuffer是线程安的。
3. HashMap
- jdk1.7,底层采用了数组+链表,头插法;
- 负载因子:默认容量为16,负载因子为0.75,当超过16*0.75=12时,就要仅从hashmap的扩容,扩容涉及到rehash和复制数据等,会非常消耗性能。
- 真正存储数据的是entry<key,value>[] table,entry是hashmap的一个静态内部类,有key,value,next,hash(key的hashcode)成员变量。
- put 过程
- 判断当前数组是否要初始化。
- 如果key为空,则put一个空值进去。
- 根据key计算出hashcode。
- 根据hsahcode定位出在桶内的位置。
- 如果桶是链表,则需要遍历判断hashcode ,如果key和原来的key是否相等,相等则进行覆盖,返回原来的值。
- 如果桶是空的,说明当前位置没有数据存入,新增一个 Entry 对象写入当前位置.当调用 addEntry 写入 Entry 时需要判断是否需要扩容。如果需要就进行两倍扩充,并将当前的 key 重新 hash 并定位。而在 createEntry中会将当前位置的桶传入到新建的桶中,如果当前桶有值就会在位置形成链表。)。
- get过程
- 根据key计算出hashcode,并定位到桶内的位置。
- 判断是不是链表,如果是,则需要根据遍历直到 key 及 hashcode 相等时候就返回值,如果不是就根据 key、key 的 hashcode 是否相等来返回值。
- 如果啥也没取到就返回null。
- 1.8,采用是的底层+链表+红黑树,尾插法,增加一个阈值进行判断是否将链表转红黑树,HashEntry 修改为 Node.解决hash冲突,造成链表越来越长,查询慢的问题。
- put 过程
- 判断当前桶是不是空,空就需要初始化;
- 根据key,计算出hashcode,根据hashcode,定位到具体的桶中,并判断当前桶是不是为空,为空表明没有hsah冲突创建一个新桶即可;
- 如果有hash冲突,那么就要比较当前桶中的 key、key 的 hashcode 与写入的 key 是否相等,相等就赋值给 e,在第 8 步的时候会统一进行赋值及返回;
- 如果当前位置是红黑树,就按照红黑树的方式写入数据;
- 如果当前位置是链表,则需要把key,value封装一个新的节点,添加到当前的桶后面(尾插法),形成链表;
- 接着判断当前链表的大小是否大于预设的阈值,大于时就要转换为红黑树;
- 如果在遍历过程中找到 key 相同时直接退出遍历;
- 如果 e != null 就相当于存在相同的 key,那就需要将值覆盖;
- 最后判断是否需要进行扩容;
- get过程
- 首先将 key hash 之后取得所定位的桶。
- 如果桶为空则直接返回 null 。
- 否则判断桶的第一个位置(有可能是链表、红黑树)的 key 是否为查询的 key,是就直接返回 value。
- 如果第一个不匹配,则判断它的下一个是红黑树还是链表。
- 红黑树就按照树的查找方式返回值。
- 不然就按照链表的方式遍历匹配返回值。
通过indexFor算法计算真实索引,确定在桶内的位置key
static int indexFor(int h, int length) {
return h & (length-1);
}
- 使用了位运算来得到索引
- HashMap的初始容量和扩容都是以2的次方来进行的,如果不满足则会出现key冲突的情况
在并发过程中还是会出现死循环
在 HashMap 扩容的时候会调用 resize() 方法,就是这里的并发操作容易在一个桶上形成环形链表;这样当获取一个不存在的 key 时,计算出的 index 正好是环形链表的下标就会出现死循环:在 1.7 中 hash 冲突采用的头插法形成的链表,在并发条件下会形成循环链表,一旦有查询落到了这个链表上,当获取不到值时就会死循环。
ConcurrentHashMap代替HashMap
ConcurrentHashMap是线程安全的。
原理
- jdk1.7:使用锁分段技术,初始16个segment,采用ReentrantLock,乐观锁,尝试获取锁失败,会不断尝试,当达到阈值时,会阻塞等待,真正存放数据的地方时hashEntry;
- jdk1.8:采用CAS和synchronized来保证线程同步,get的时候不加锁,使用volitaile来保证变量的原子性,其他和hashmap一样;
9. 注解
注解相当于一种标记,在程序中加了注解就等于为程序打上了某种标记。程序可以利用ava的反射机制来了解你的类及各种元素上有无何种标记,针对不同的标记,就去做相应的事件。标记可以加在包,类,字段,方法,方法的参数以及局部变量上。
10. 反射
Java 中的反射首先是能够获取到Java中要反射类的字节码, 获取字节码有三种方法:
- Class.forName(className)
- 类名.class
- this.getClass()。
然后将字节码中的方法,变量,构造函数等映射成相应的Method、Filed、Constructor等类,这些类提供了丰富的方法可以被我们所使用。
11. 泛型
泛型是泛指某种特定的数据类型,是jdk1.5的新特性。
好处
- 提高了安全性(不会出现类型转换异常);
- 将运行时期会可能出现的异常转移至编译期;
- 优化程序设计(定义为泛型后我们的程序可以放多种数据类型的一种) ;
- 避免来强制类型转换的麻烦;
12. 内存区域
运行时内存区域如图所示:
- 程序计数器
- 当线程切换时,用来标记当前线程所执行的字节码的行号指示器;
- 每一个线程都有一个独立的程序计数器,用来线程切换时能恢复到正确的位置;
- JAVA虚拟机栈
- 线程私有,生命周期和线程一样,用来存储:局部变量表,操作数栈,动态链接,方法出口等;
- 局部变量表所占用的内存在编译器就完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小;
- 本地方法栈
- 为虚拟机执行native方法;
- JAVA堆
- 存放对象实例;
- java堆是垃圾收集器管理的主要区域;
- 被所有线程共享的一块内存区域;
- 方法区
- 各个线程所共享的内存区域;
- 用于存储class二进制文件,包含了虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据;
- 线程安全的;
- 运行时常量池存在方法区中;
- 常量池
- 常量是指被final修饰的变量,值一旦确定就无法改变;
- 常量池在编译期间就将一部分数据存放于该区域,包含基本数据类型如int、long等以final声明的常量值,和String字符串、特别注意的是对于方法运行期位于栈中的局部变量String常量的值可以通过 String.intern()方法将该值置入到常量池中。
- 分类:
- Class文件常量池;
- 运行时常量池;
- 全局字符串常量池;
- 基本类型包装类对象常量池;
13. 描述一下GC的原理和回收策略
- 对象存活判定算法
- 引用计数法
- 用到就+1,不用就-1,直到为0 ,就证明类不可能被调用
- 但是在java中没有使用,很难解决对象相互引用
- 可达性分析法
思想:以“GC roots”作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链(即GC Roots到对象不可达)时,则证明此对象是不可用的。
- 引用计数法
- GC roots 来源
- 虚拟机栈中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI引用的对象
- 四种引用对象
强引用:不进行垃圾回收
软引用:在内存溢出前进行垃圾回收
弱引用:只要有垃圾回收就会回收掉
虚引用:无用对象,垃圾收集时会接收到通知
-
对于可达性分析算法而言,未到达的对象并非是“非死不可”的,若要宣判一个对象死亡,至少需要经历两次标记阶段;
- 第一次标记:如果没有和gcroots 相连,就进行第一次标记筛选,筛选条件为是否有必要执行该对象的finalize方法,判断对象有没有覆盖finalize 方法,或者有没有执行过,执行过则代表要进行回收,如果覆盖了并没有执行过,则会放到F-Queue的队列中,进行第二次标记;
- 第二次标记对:F-Queue中对象进行第二次标记,如果在finalize方法中关联上了gcroots的引用连,则不会被回收,从即将回收的集合里移除;
-
方法区的垃圾回收
- 废弃常量
- 无用的类
- 该类的所有实例都已经被回收,即Java堆中不存在该类的任何实例。
- 加载该类的ClassLoader已经被回收。
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
-
回收算法
- 复制算法:
- 复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这种算法适用于对象存活率低的场景,比如新生代。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况。
- 标记-清除:
- 标记-清除算法采用从根集合进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收。标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。
- 标记-整理:
- 标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。该垃圾回收算法适用于对象存活率高的场景(老年代)。
- 分代收集算法:
- 不同的对象的生命周期(存活情况)是不一样的,而不同生命周期的对象位于堆中不同的区域,因此对堆内存不同区域采用不同的策略进行回收可以提高 JVM 的执行效率。当代商用虚拟机使用的都是分代收集算法:新生代对象存活率低,就采用复制算法;老年代存活率高,就用标记清除算法或者标记整理算法
- 新生代:
- 老年代:
- 复制算法:
-
垃圾收集器
- Serial收集器(复制算法):
新生代单线程收集器,标记和清理都是单线程,优点是简单高效; - **ParNew收集器 (复制算法): **
新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现; - Parallel Scavenge收集器 (复制算法):
新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景 - **Serial Old收集器 (标记-整理算法): **
老年代单线程收集器,Serial收集器的老年代版本 - CMS(Concurrent Mark Sweep)收集器(标记-清除算法):
老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。 - Parallel Old收集器 (标记-整理算法):
老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本; - G1(Garbage First)收集器 (标记-整理算法):
Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。
- Serial收集器(复制算法):
-
内存分配与回收策略
JAVA自动内存管理:给对象分配内存 以及 回收分配给对象的内存。 -
对象优先在Eden分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次MinorGC。
-
大对象直接进入老年代。如很长的字符串以及数组。很长的字符串以及数组。
-
长期存活的对象将进入老年代。当对象在新生代中经历过一定次数(默认为15)的Minor GC后,就会被晋升到老年代中。
-
动态对象年龄判定。为了更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
14. 类加载器
1. java类加载过程
-
加载
1. 通过一个类的全限定名来获取定义此类的二进制字节流
2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3. 将类的class文件读入内存,并为之创建一个java.lang.Class对象
4. 当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象- 二进制数据的来源 :
从本地文件系统加载class文件;
从JAR包加载class文件;
通过网络加载class文件
把一个Java源文件动态编译,并执行加载
- 二进制数据的来源 :
-
连接
-
验证:
验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致 -
验证方式:
文件格式验证:主要验证字节流是否符合Class文件格式规范,并且能被当前的虚拟机加载处理
元数据验证:对字节码描述的信息进行语义的分析,分析是否符合java的语言语法的规范。
字节码验证:最重要的验证环节,分析数据流和控制,确定语义是合法的,符合逻辑的。主要的针对元数据验证后对方法体的验证。保证类方法在运行时不会有危害出现
符号引用验证:主要是针对符号引用转换为直接引用的时候,是会延伸到第三解析阶段,主要去确定访问类型等涉及到引用的情况,主要是要保证引用一定会被访问到,不会出现类等无法访问的问题。
-
准备:
类准备阶段负责为类的静态变量分配内存,并设置默认初始值; -
解析:
将类的二进制数据中的符号引用替换成直接引用;
-
-
初始化
1. 主要是对类变量进行初始化;
2. 声明类变量时指定初始值;
3. 使用静态初始化块为类变量指定初始值; -
类加载器
- 启动类加载器
- 扩展类加载器
- 应用类加载器
-
类加载机制
- 全盘负责
所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。 - 双亲委派
工作原理
先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。
优势
1. 采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载。
2. 其次是考虑到安全因素,java核心api中定义类型不会被随意替换。 - 缓存机制
缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。
- 全盘负责
15. 线程和并发
1. Synchronized、volatile、Lock(ReentrantLock)相关
volatile
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
指令重排序:一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
- volatile没办法保证对变量的操作的原子性
1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
- volatile的原理和实现机制
加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也称内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
- 使用volatile必须具备以下2个条件【保证原子性】:
1)对变量的写操作不依赖于当前值
2)该变量没有包含在具有其他变量的不变式中
在使用单例的时候用过,保证单例对象在多线程可见
synchronized
- synchronized可作用于一段代码或方法,既可以保证可见性,又能够保证原子性。
(1)可见性体现在:通过synchronized或者Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存中。
(2)原子性表现在:要么不执行,要么执行到底。
(3)性能方面,synchronized关键字是防止多个线程同时执行一段代码,就会影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized。
(*)但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。 - synchronized的缺陷
1)如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:
1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
2)线程执行发生异常,此时JVM会让线程自动释放锁。
3)如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能等待,影响程序执行效率。
(2)当多个线程都是要读一个数据的时候,synchronized效率很低。
- synchronized的三种应用方式
- 修饰普通方法
一个对象中的加锁方法只允许一个线程访问。但要注意这种情况下锁的是访问该方法的实例对象, 如果多个线程不同对象访问该方法,则无法保证同步。 - 修饰静态方法
由于静态方法是类方法, 所以这种情况下锁的是包含这个方法的类,也就是类对象;这样如果多个线程不同对象访问该静态方法,也是可以保证同步的。 - 修饰代码块
其中普通代码块 如Synchronized(obj) 这里的obj 可以为类中的一个属性、也可以是当前的对象,它的同步效果和修饰普通方法一样;Synchronized方法 (obj.class)静态代码块它的同步效果和修饰静态方法类似。
Lock
- Lock是一个接口,可以实现同步访问。
- ReentrantLock,意思是“可重入锁”,是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。
- 如果锁具备可重入性,则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁,可重入性实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。
举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。
- volatile 与 synchronized对比
1)volatile本质是在告诉jvm当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住.
(2)volatile仅能使用在变量级别,synchronized则可以使用在变量,方法.
(3)volatile仅能实现变量的修改可见性,而synchronized则可以保证变量的修改可见性和原子性.
(4)volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞.
(5)当一个域的值依赖于它之前的值时,volatile就无法工作了,如n=n+1,n++等。如果某个域的值受到其他域的值的限制,那么volatile也无法工作,如Range类的lower和upper边界,必须遵循lower<=upper的限制。
(6)使用volatile而不是synchronized的唯一安全的情况是类中只有一个可变的域。
- synchronized和Lock区别
(1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
(2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
(3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
(4)synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
(5)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。 (6)Lock可以提高多个线程进行读操作的效率。
(7)在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
2. 锁
- 自旋锁
占着CPU不妨,等待获取锁的机会 - 偏向锁
偏向锁就是一旦线程第一次获得了监视对象,之后让监视对象“偏向”这个线程,之后的多次调用则可以避免
CAS操作 - 轻量级锁
偏向锁升级而来,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁竞争用的时候,偏向锁就会升级为轻量级锁 - 重量级锁
- 对象监视器
- 既要做到互斥,又要做到线程同步
3. 线程
创建方式
- 继承Thread类
- 创建方法
1. 定义threade的子类,重写run方法
2. 创建Thread子类的实例,即创建了线程对象
3. 调用线程对象的start()方法来启动该线程 - 优缺点
- 优点:编写简单,如果需要访问当前线程,无需使用Thread.currentThread()方法,直接使用this,即可获得当前线程
- 缺点:因为线程类已经继承了Thread类,所以不能再继承其他的父类
- 实现Runnable接口
- 创建方法
- 定义runnable接口的实现类,并重写该接口的run()方法
- 创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
- 调用线程对象的start()方法来启动该线程
实现代码如图:
- 优缺点
- 优点:线程类只是实现了Runable接口,还可以继承其他的类
- 缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法
- 实现Callable接口
- 创建方式
- 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值
- 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
- 使用FutureTask对象作为Thread对象的target创建并启动新线程
- 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
实现代码如图所示:
- Runnable和Callable的区别:
- Runnable实现是run方法,callable 重写的是call(),并有返回值。
- call方法可以抛异常,run方法不行。
- 运行Callable任务可以拿到一个Future对象,表示异步计算的结果.它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
生命周期
- 新建
就是刚使用new方法,new出来的线程;
就绪:
就是调用的线程的start()方法后,这时候线程处于等待CPU分配资源阶段,谁先抢的CPU资源,谁开始执行 - 运行:
当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能; - 阻塞:
在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态,比如sleep()、wait()之后线程就处于了阻塞状态,这个时候需要其他机制将处于阻塞状态的线程唤醒,比如调用notify或者notifyAll()方法。唤醒的线程不会立刻执行run方法,它们要再次等待CPU分配资源进入运行状态; - 死亡:
如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源; - notify、notifyAll()的区别?
notify() 方法随机唤醒对象的等待池中的一个线程,进入锁池;notifyAll() 唤醒对象的等待池中的所有线程,进入锁池。
- sleep()、wait()的区别?
sleep()
方法是线程类(Thread)的静态方法,让调用线程进入睡眠状态,让出执行机会给其他线程,等到休眠时间结束后,线程进入就绪状态和其他线程一起竞争cpu的执行时间。因为sleep() 是static静态的方法,他不能改变对象的机锁,当一个synchronized块中调用了sleep()方法,线程虽然进入休眠,但是对象的机锁没有被释放,其他线程依然无法访问这个对象。
wait()
是Object类的方法,当一个线程执行到wait方法时,它就进入到一个和该对象相关的等待池,同时释放对象的机锁,使得其他线程能够访问,可以通过notify,notifyAll方法来唤醒等待的线程。
sleep() 和 wait() 的区别就是: 调用sleep方法的线程不会释放对象锁,而调用wait() 方法会释放对象锁 线程池
4. 线程池
- 使用线程池的好处
1.重用,减少创建和销毁所产生的性能开销
2.避免过多的线程抢占CPU而产生阻塞现象
3.统一管理 - 常见的线程池
1. CacheThreadPool(),可缓存的线程池。
1、线程数无限制。
2、有空闲线程则复用空闲线程,若无空闲线程则新建线程。
3、一定程序减少频繁创建/销毁线程,减少系统开销。
- FixedThreadPool(n)定长线程池
1、可控制线程最大并发数(同时执行的线程数)。
2、超出的线程会在队列中等待
3.SingleThreadExecutor()单线程化的线程池
1、有且仅有一个工作线程执行任务。
2、所有任务按照指定顺序执行,即遵循队列的入队出队规则。串行
4.ScheduledThreadPool(int n):创建一个定长线程池,支持定时及周期性任务执行
写在最后的话
革命尚未成功,同志仍需努力。