哈尔滨模板建站源码,光聚济南网站建设,昆明网站制作内容,百度收录官网Java开发面试高频考点学习笔记#xff08;每日更新#xff09;
1.深拷贝和浅拷贝2.接口和抽象类的区别3.java的内存是怎么分配的4.java中的泛型是什么#xff1f;类型擦除是什么#xff1f;5.Java中的反射是什么6.序列化与反序列化7.Object有哪些方法#xff1f;8.JVM内存…Java开发面试高频考点学习笔记每日更新
1.深拷贝和浅拷贝2.接口和抽象类的区别3.java的内存是怎么分配的4.java中的泛型是什么类型擦除是什么5.Java中的反射是什么6.序列化与反序列化7.Object有哪些方法8.JVM内存模型9.类加载机制10.对象的创建和对象的布局11.Java的四种引用强引用、软引用、弱引用和虚引用)12.内存泄露和内存溢出13.List、Set和Map三者的区别和其底层数据结构14.创建线程的四种方式15.NIO、AIO和BIO16.重写和重载17.final/finally/finalize与static18.String、StringBuffer和StringBuilder的区别19.如果判断一个对象是否该被回收20.垃圾收集算法21.Double与Float22.垃圾收集器23.线程池24.线程同步和线程通讯25.中断线程26.Synchronized的用法27.Synchronized的原理28.Synchronized的四种状态29.Synchronized与重入锁ReentrantLock的区别30.锁优化31.Java设计模式
Java
1.深拷贝和浅拷贝
内存中有栈区和堆区基本类型数据直接存在栈中而引用类型new出来的是在堆中存储在栈中保存堆中的地址。也就是说引用类型中在栈中存的不是数据而是地址。赋值其实就是拷贝。
在基本类型数据赋值的时候没有深浅拷贝的区别因为直接赋予的是数据。
但在引用类型数据赋值的时候实际上是把原来的地址复制给了新的并没有实际复制其中的数据所以这是一个浅拷贝拷贝的深度不够当使用新的变量操作地址中的值的时候旧变量对应的值也会发生改变。Java中Object的clone方法默认是浅拷贝。
深拷贝会创造另外一个一模一样的对象新对象和原来的对象不共享内存修改新对象不会影响旧对象。
2.接口和抽象类的区别 抽象类被abstract关键字修饰。抽象方法也被abstract修饰只有方法声明没有方法体。 抽象类不能被实例化只能被继承 抽象类可以有属性、方法和构造方法但是构造方法不能用于实例化主要用于被子类调用 子类继承抽象类必须实现抽象类抽象方法否则子类必须也是抽象类 抽象类中的抽象方法只能是public或protected 接口被interface关键字修饰。 接口可以包含变量和方法变量隐式设定为public static final方法被隐式设定为public abstract 接口支持多继承一个接口可以extends多个接口 一个类可以实现多个接口 jdk1.8中增加了默认方法和静态方法default/static 接口只能是功能的定义而抽象类既可以为功能的定义也可以为功能的实现。 接口和抽象类都不能被实例化接口的实现类和抽象类的子类只有实现了接口中/抽象类中的方法才能实例化。 实现接口的关键字是implements继承抽象类的关键字是extends。一个类可以实现多个接口但一个类只能继承一个抽象类。 接口强调特定功能的实现而抽象类强调所属关系。
3.java的内存是怎么分配的 内存分配分为在栈上分配和在堆上分配大多数都是引用类型所以堆空间用的较多。 对象根据存活时间分为年轻代、年老代、永久代方法区 年轻代对象被创建时首先分配在年轻代。年轻代有三个区域Eden区survivor 0区和survive 1区Eden区大多数对象消亡速度很快Eden是连续的内存空间分配内存很快。Eden区满的时候执行Minor GC清理消亡对象将存活的对象放在survivor 0区中每次执行Minor GC的时候将剩余存活对象都放在非空的survivor区中survivor区满之后就会清理并转移到另一个survivor区也就是说总有一个survivor区是空的。HotSpot虚拟机中默认切换15次之后仍然存活的对象放在年老代中。 年老代年老代的空间一般比年轻代大存放更多的对象年老代内存不足的时候执行Major GCFull GC如果对象比较大的情况可能直接放在老年代上。有可能出现老年代引用新生代对象的情况java维护一个512 byte的块“card table”记录引用映射进行Minor GC的时候直接查card table就可以了。
4.java中的泛型是什么类型擦除是什么
java源代码要运行首先要经过编译器编译出字节码字节码存储着能被JVM解释运行的指令。java的泛型在运行时无法获得类型参数的真正类型因为编译器编译生成的字节码不包括类型参数的具体类型。 泛型是java 1.5之后引入的其本质是参数化类型也就是说变量的类型是一个参数在使用的时候再指定为具体类型泛型可以用于类、接口和方法。
public class UserT {private T name;
}//泛型实际上就是把类型当作参数传入了
而类型擦除机制使得Java的泛型实际上是伪泛型类型参数只存在于编译期运行时JVM并不知道泛型的存在。public class ErasedTypeEquivalence {public static void main(String[] args) {Class c1 new ArrayListString().getClass();Class c2 new ArrayListInteger().getClass();System.out.println(c1 c2); //代码输出是true}
}在C、C#这些支持真泛型的语言中它们代表着不同的类但在JVM看来他们是同一个类。无论何时定义一个泛型相应的原始类型都会被自动提供类型变量擦除并使用其限定类型无限定的变量用 Object替换。Java 编译器是通过先检查代码中泛型的类型然后在进行类型擦除再进行编译。当具体的类型确定后泛型提供了一种类型检测的机制只有相匹配的数据才能正常的赋值否则编译器就不通过。
5.Java中的反射是什么
java反射就是把类中的各个成分映射成一个个java对象在运行期间对于任意一个类都能够知道这个类的属性和方法是一种动态获取信息、动态调用对象的方法。
优点动态加载类提高代码灵活度缺点降低性能可能引起安全问题
我们使用的Spring/hibernate中使用了反射机制在使用JDBC连接数据库使用class.forName()通过反射加载数据库的驱动程序。 Spring框架的IOC动态加载管理bean创建对象AOP动态代理都和反射有关系。
6.序列化与反序列化
序列化将Java对象转换成字节序列的过程。反序列化将字节序列转换成java对象。
serializable接口是可以进行序列化的标志性接口仅仅是告诉JVM该类对象可以进行序列化。 先让需要序列化的类实现serializable接口序列化对象创建输出流ObjectOutputStream然后调用writeObject()方法反序列化对象创建输入流Obje ctInputStream然后调用readObject方法,得到一个object对象。最后关闭流。
7.Object有哪些方法
equals比较对象是否相等这里实质是比较地址是否相等。 wait:调用wait方法会导致线程阻塞释放该对象的锁 notify调用对象的notify方法会随机解除该对象阻塞的线程该线程重新获取该对象的锁 notifyAll唤醒所有正在等待对象的线程全部进入锁池竞争获取锁 wait,notify,notifyAll必须在synchronized方法块中使用。 toString转换为字符串表示 getClass返回对象运行时类即反射机制。 hashCode: 对象在内存中的地址转换为int值。
8.JVM内存模型
程序计数器PC register线程执行的字节码行号指示器线程私有唯一一个没有内存超出错误的区域。
Java虚拟机栈每个线程创建时都会创建一个虚拟机栈内部保存一个个栈帧对应每一次方法调用。生命周期与线程相同。保存方法的局部变量和部分结果参与方法的调用和返回。如果线程请求的栈深度大于虚拟机所允许的深度将抛出StackOverflow异常如果虚拟机栈可以动态扩展当扩展到无法申请足够内存时抛出OutOfMemoryError异常。本地方法栈与虚拟机栈类似但只为native方法服务。Java堆线程共享内存用来存放对象实例是垃圾回收的主要区域。java堆可以处于物理上不连续的内存空间中只要逻辑上连续就可以了就类似于磁盘空间。如果在堆中没有内存完成实例分配而且堆也无法再拓展的时候将会抛出OutOfMemoryError的异常。方法区是线程共享内存它用于存储已被虚拟机加载的类信息等数据。它可以叫做永久代也可以是元空间在jdk1.8之后永久代的数据被分配到堆和元空间中元空间存储类信息字符串常量和运行时常量池放入堆中。方法区无法满足内存分配需求时抛出OutOfMemoryError异常。
JVM调优参数
(1) -Xms初始化堆内存。默认为物理内存的六十四分之一 (2) -Xmx: 最大堆内存。默认为物理内存的四分之一 (3) -Xss单个线程栈的大小 (4) -Xmn设置新生代的大小 (5) -XXMetaspaceSize设置元空间大小 (6) -XXSurvivorRatio调节新生代eden和S0、S1的空间比例 默认为811
JVM性能监控工具
(1)jps -l查看进程号 (2)jstackjava堆栈跟踪工具 查看死锁和cpu占用过高的代码 (3)jinfo -flag查看运行的java程序参数属性的详情
9.类加载机制
类加载就是将类的数据从class文件加载到内存并且进行校验解析和初始化形成可以让虚拟机使用的java类型。
类的生命周期加载链接初始化使用卸载。
加载通过类名获取二进制字节流通过类加载器把静态数据结构放在方法区内存中生成对应class对象作为访问入口。链接确保当前字节流包含的信息符合虚拟机要求。正式分配内存设置初始值仅分配静态变量虚拟机将常量池内的符号引用替换成直接引用。初始化按照代码逻辑赋予属性真正的初始值初始化阶段就是执行类构造器方法的过程。类加载器包括启动类加载器、扩展类加载器和应用程序类加载器。
10.对象的创建和对象的布局
对象创建的方法
用new语句创建
调用clone方法需要实现cloneable接口
反射class的newInstance
反序列化从文件中获取一个对象的二进制流使用ObjectInputStream的readObject方法。
对象创建的过程
类加载检查判断这个类是不是已经被加载链接初始化了。
为对象分配内存如果内存规整虚拟机使用碰撞指针法指针向空闲区前移对象大小的距离如果不规整则使用空闲列表法。并发安全虚拟机维护一个列表记录哪些内存块可用再分配的时候从列表中找到一块足够大的空间划分给对象实例并更新列表内容。
初始化分配的空间所有属性初始化为零保证对象实例字段在不赋值的时候可以直接用
设置对象头信息
执行构造方法初始化
逃逸方法体内创建的对象方法体外被其他变量引用过。这样在方法执行完毕之后该方法中创建的对象不能被GC回收。开启逃逸分析之后如果对象的作用域仅在方法内那对象可以创建在虚拟机栈上随方法入栈创建出栈销毁减少GC回收压力。
对象的内存布局包含三部分对象头实例数据和对齐填充。
对象头运行时数据和类型指针。标记字段包含hashcode、GC分代年龄、锁状态标志、线程持有锁等信息类元数据的指针可以知道这个对象是哪个类的实例。
实例数据存储对象真正的数据也包含父类的数据。
对齐填充保证对象大小是8字节的整数倍。 11.Java的四种引用强引用、软引用、弱引用和虚引用)
在jdk1.2之前Java对引用的定义很传统如果reference类型的数据中存储的数值是另一块内存的起始地址就称这块内存代表一个引用。
强引用Java中默认声明的引用为强引用只要强引用存在垃圾回收器永远不会回收被引用的对象哪怕内存不足JVM也只会抛出OOM错误不会去回收。 Object obj new Object();软引用用于描述一些非必需但仍有用的对象。内存足够的时候软引用对象不会被回收只有在内存不足的时候系统会回收软引用对象如果内存还是不够才会抛出OOM异常。这种特性使他往往用于实现缓存技术。在 JDK1.2 之后用java.lang.ref.SoftReference类来表示软引用。弱引用弱引用的强度比软引用更弱。无论内存是否足够只要JVM开始垃圾回收那些被弱引用关联的对象都会被回收。在 JDK1.2 之后用java.lang.ref.WeakReference来表示弱引用。虚引用最弱的引用关系。与其他几种引用不同虚引用不会决定对象的生命周期如果一个对象仅持有虚引用那么它就和没有任何引用一样任何时期都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动且必须与引用队列联合使用。当垃圾回收器准备回收一个对象的时候如果发现它还有虚引用就会在回收对象的内存之前把这个虚引用加入到与之关联的引用队列中。
12.内存泄露和内存溢出
内存泄漏一个不再被线程所使用的对象或变量还在内存中占用空间。内存溢出程序无法申请到足够的内存。
内存泄漏的原因
1.长生命周期的对象持有短生命周期对象的引用。
2.连接未正常关闭。
3.变量作用域设置过大
避免内存泄漏
1.避免在循环中创建对象
2.没有用的对象尽早释放
3.慎用静态变量
4.字符串的拼接使用Stringbuffer/StringBuilder
5.增大xmx和xms的值
内存溢出的原因
1.加载数据过大
2.死循环或过多循环
3.启动参数中内存值设定过小
栈溢出
原因递归深度过大、局部变量过大
解决递归不要太深局部变量改为静态变量
如果排查内存问题 1.JConsole能看到内存用量的趋势确定是否有问题 2.GC日志能看到年轻代和老年代等区域配置是否合理 3.代码中打印内存使用量 4.分析dump文件针对性的看到发生OOM时候的内存使用量和线程情况
13.List、Set和Map三者的区别和其底层数据结构
List有序的对象
1ArrayList:数组 2Vector数组 3LinkedList双向链表
Set不允许重复的集合
1HashSet无序且唯一基于HashMap 2LinkedHashSet基于HashMap 3TreeSet有序且唯一基于红黑树
Map使用键值对存储
1HashMapJdk1.8之前HashMap由数组链表组成之后再链表长度大于阈值默认8时将链表转换为红黑树以减少搜索时间。 2LinkedHashMap继承自 HashMap所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外LinkedHashMap 在上面结构的基础上增加了一条双向链表使得上面的结构可以保持键值对的插入顺序。 3HashTable数组链表组成数组是HashMap的主体链表为了解决哈希冲突 4TreeMap红黑树 ArrayList、LinkedList、Vector的区别
存储结构ArrayList和Vector是基于数组实现的而LinkedList是基于双向链表实现的。线程安全性ArrayList不具有线程安全性ArrayList添加元素的操作不是原子操作可能会出现一个线程的值覆盖另一个线程添加的值的问题在单线程的环境中LinkedList也是不安全的。Vector实现了线程安全它大部分的关键字都包含synchronized但效率低。扩容机制ArrayList和Vector都是用数组来存储容量不足的时候可以扩容ArrayList扩容后的容量是之前的1.5倍Vector默认是2倍。Vector可以设置扩容增量capacityIncrement。可变长度数组的原理是当元素个数超过数组长度时产生一个新的数组将原数组的数据复制到新数组再将新元素添加到新数组中。增删改查效率ArrayList和Vector中从指定的位置检索一个对象或在末尾插入删除一个元素时间复杂度都是O1但是在其他位置增加和删除对象的时间是OnLinkedList插入删除任何位置的时间都是O1但是检索一个元素的时间是On。
14.创建线程的四种方式
继承Thread类重写run方法继承Thread类的线程类不能再继承其他父类。
实现Runnable接口重写run方法
通过Callable接口和Future接口创建线程执行call方法有返回值可以抛异常
线程池。前三种的线程如果创建关闭频繁的话会消耗系统资源影响性能而使用线程池可以不用线程的时候放回线程池用的时候再从线程池取。
15.NIO、AIO和BIO
BIO传统的网络通讯模型同步阻塞IO。服务器实现是一个连接一个线程客户端有连接请求的时候服务端就要启动一个线程去处理。线程数量可能会爆炸导致崩溃。适用于连接数目小且固定的架构。 NIO同步非阻塞。服务器实现是一个请求一个线程客户端发送的连接请求都会注册到多路复用器上复用器轮询到连接有IO请求才启动线程。适用于连接数目多且连接比较短的架构比如聊天服务器。 AIO异步非阻塞。用户进程只需要发起一个IO操作然后立即返回等IO操作真正完成之后应用程序会得到IO操作完成的通知。适用于连接数目多且连接长的架构。
16.重写和重载
重写Override重写是子类对父类允许访问的方法实现过程进行重新编写返回值和形参都不能改变。重写的好处是子类可以根据特定需要定义特定行为。异常范围可以减少但是不能抛出新的或更广的异常。
class Animal{public void move(){System.out.println(动物可以移动);}
}
//加入Java开发交流君样756584822一起吹水聊天
class Dog extends Animal{public void move(){System.out.println(狗可以跑和走);}
}public class TestDog{public static void main(String args[]){Animal a new Animal(); // Animal 对象Animal b new Dog(); // Dog 对象a.move();// 执行 Animal 类的方法b.move();//执行 Dog 类的方法}
}虽然b属于Animal类型但是它运行的是Dog类的move方法。因为在编译阶段只是检查参数的引用类型运行时JVM指定对象的类型并运行该对象的方法。 方法重写规则 1参数列表和被重写方法的参数列表必须完全相同。 2访问权限不能比父类中被重写的方法访问权限更低。 3父类的成员方法只能被它的子类重写。 4声明为final的方法不能被重写声明为static的方法不能被重写但是能被再次声明。 5构造方法不能被重写。 6子类和父类在同一个包中那么子类可以重写父类中没有声明为private和final的方法如果不在同一个包中子类只能重写父类声明为public和protected的非final方法。
当需要在子类中调用父类的被重写方法时使用super关键字。
重载Overload是在一个类里面方法名字相同参数不同的两个方法。返回类型可以相同也可以不同。每个重载的方法或者构造函数必须有一个独一无二的参数类型列表。常用于构造器重载。
重载规则
1被重载的方法必须改变参数列表。 2被重载的方法可以改变返回类型可以改变访问修饰符可以声明新的或更广的异常检查。 3方法能够在同一个类中或者在一个子类中被重载。
public class Overloading {public int test(){System.out.println(test1);return 1;}public void test(int a){System.out.println(test2);} //以下两个参数类型顺序不同public String test(int a,String s){System.out.println(test3);return returntest3;} public String test(String s,int a){System.out.println(test4);return returntest4;} public static void main(String[] args){Overloading o new Overloading();System.out.println(o.test());o.test(1);System.out.println(o.test(1,test3));System.out.println(o.test(test4,1));}
}方法重载和方法重写是java多态的不同表现。 参考文章
17.final/finally/finalize与static
final:java中的关键字修饰符。如果一个类被声明为final就意味着它不能再派生出新的子类不能作为父类被继承。一个类不能被同时声明final和abstract抽象类。如果变量或方法被声明为final就能保证它们在使用中不被改变变量必须在声明时赋值以后的引用中只读被声明final的方法只能使用不能重载。finallyjava的一种异常处理机制。java异常处理模型的最佳补充finally结构使代码总会执行而不管有无异常发生。使用finally可以维护对象的内部状态清理非内存资源。在关闭数据库连接时如果把数据库连接的close()方法放到finally中就会减少出错的可能。finalizeJava中的一个方法名该方法是在垃圾收集器将对象从内存中清除出去前做必要的清理工作。这个方法是由垃圾收集器确定这个对象没被引用的时候调用的。它在Object类中定义因此所有类都继承了它。子类可以覆盖该方法来整理资源和清理。staticstatic修饰的属性在编译器初始化初始化之后能改变final修饰的属性可以在编译器也可以在运行期初始化但是不能被改变static不能修饰局部变量但是final可以。
18.String、StringBuffer和StringBuilder的区别
String是java编程中广泛使用的但它的底层实现实际是一个final类型的字符数组其中的值不可变每次对String进行操作就会生成一个新对象造成内存浪费。
private final char value[];StringBuffer/StringBuilder它们的底层是可变的字符数组都继承AbstractStringBuilder抽象类所以在进行频繁的字符串操作的时候尽量使用这两个类它们的区别是StringBuilder是线程不安全的但执行速度较快StringBuffer线程安全但执行速度慢。StringBuffer使用synchronized关键字进行同步锁。 另外String类型的比较“”是比较两个内存地址是否一样而“equals”是比较两个字符串的值是不是一样的。
19.如果判断一个对象是否该被回收
引用计数算法为对象增加一个引用计数器当对象增加一个引用的时候1引用失效-1引用计数为0的对象可以被回收。但是当两个对象循环引用的情况下计数器永远不为0因此JVM不使用引用计数算法。 可达性分析算法以GC Roots为起点开始搜索可达的对象都是存活的不可达的对象可以被回收JVM使用该算法进行判断。GC Roots中包含虚拟机栈中引用的对象、本地方法栈中引用的对象方法区中静态成员或常量引用的对象。 20.垃圾收集算法
标记-清除算法Mark-Sweep
标记阶段标记的过程实际上就是可达性分析算法过程遍历GC Roots对象可达的对象都做好标记在对象的header中将其记录为可达。
清除阶段对堆进行遍历如果发现有某个对象没有可达对象标记则回收。
缺点两次遍历效率低GC运行时需要停止整个程序产生大量的碎片需要维护一个空闲列表。
复制算法Copying 对象在Survivor区每经历一次Minor GC就将对象年龄1当对象年龄达到某个值时对象复制到老年代默认为15。JVM中Eden和Survivor区的默认比例为8:1:1保证内存利用率为90%如果每次回收有多于10%的对象存活Survivor空间可能就不够用了此时借用老年代空间。
缺点复制收集算法在对象存活率高的时候需要进行很多的复制操作效率会变低老年代一般不会用该算法。
标记-整理算法
第一阶段和标记-清楚算法一样第二阶段将所有存活的对象压缩到内存的另一端按顺序排放。之后清理边界外所有的空间。 缺点效率不高不仅要标记存活对象还要整理所有存活对象的引用地址移动过程中要全程暂停用户应用程序。 分代收集算法
新生代使用复制算法因为大量对象需要回收。 老年代回收的对象很少所以采用标记清除或者标记整理算法。
21.Double与Float
java语言支持两种基本的浮点类型float和double。32位浮点数float用1位表示符号8位表示指数用23位表示尾数64位浮点数double用一位表示符号11位表示指数52位表示尾数。在表示超过23位的时候float就会自动四舍五入这就是float的精度限制所以会出现double可以表示而float会不精确的情况如果要将这两个浮点数进行转型java提供了Float.doubleValue()和Double.floatValue()方法。使用这个方法在单精度转双精度的时候会出现偏差。 浮点运算很少是精确的只要超过精度表示范围就会产生误差。
解决方法可以通过String结合BigDecimal或者通过使用long类型来转换。
22.垃圾收集器
查看默认垃圾收集器:-XX:PrintCommandLineFlags Serial串行收集器单线程收集器只使用一个线程回收垃圾需要停掉其他所有线程Client模式下默认新生代垃圾收集器新生代使用复制算法老年代使用标记整理算法Serial Old也作为CMS收集器的后备垃圾收集方案。JVM参数-XX:UseSerialGC ParNew收集器Serial的多线程版本对应的JVM参数-XX:UseParNewGC。开启参数之后会使用ParNew新生代复制算法Serial Old老年代标记整理算法的组合Java8之后不再推荐使用这种组合。 Parallel scavenge收集器新生代和老年代都使用并行Parallel scavenge收集器可以使用自适应调节策略把基本的内存数据设置好然后设定是更关注最大停顿时间或者更关注吞吐量给虚拟机设立一个优化目标。JVM参数是-XX:UseParallelGC。新生代使用复制算法老年代使用标记-整理算法。 CMS收集器一种以获取最短回收停顿时间为目标的收集器。JVM参数-XX:UseConcMarkSweepGC。使用ParNew新生代CMS老年代Serial Old后备的收集器组合。优点是并发收集停顿少。缺点是并发会造成CPU的压力而且标记清除算法会产生大量空间碎片。
1初始标记标记GC Roots能直接关联到的对象速度很快需要停顿。 2并发标记进行GC Roots Trancing的过程不需要停顿。 3重新标记修正并发标记期间因为用户程序继续运作而导致变动的那一部分对象重新进行标记需要停顿。 4并发清除不需要停顿。 G1垃圾收集器它使得Eden、Survivor和Tenured等内存区域不再连续而变成一个个大小一样的region每个region从1M到32M不等。它不再采用CMS的标记清理算法G1整体上使用标记整理算法局部上看是基于复制算法。JVM参数:-XX:UseG1GC。 降低停顿时间是G1和CMS共同的关注点但G1除了追求低停顿外还能建立可以预测的停顿时间模型能让使用者明确指定在一个长度为M毫秒的时间片内。是因为G1收集器在后台维护了一个优先列表每次根据允许的收集时间优先选择回收价值最大的region。 另JVM设置参数的方法win10环境变量中新建变量JAVA_OPTS,在里面设置。
23.线程池 我们使用线程的时候去创建一个线程这种方法非常简便但是会导致一个问题如果并发的线程数量很多并且每个线程都是执行一个时间很短的任务就结束了这样频繁的创建线程会大大降低系统效率。 Java中引入了线程池来使得线程可以复用执行完一个任务不会被立刻销毁而是可以继续执行其他任务。 ThreadPoolExecutor类是线程池技术最核心的类
其构造器中的参数意义 corePoolSize:核心池大小。在创建线程池之后默认线程池中是没有线程的除非调用prestartAllCoreThreads()或者prestartCoreThread()方法来预创建线程就是没有任务到来之前先创建corePoolSize个线程。当线程池中的线程数目到达corePoolSize个之后就会把到达的任务放到缓存序列中。 maximumPoolSize非常重要的参数表示线程池中最多能创建多少个线程。 keepAliveTime表示线程没有任务执行时最多保持多久会终止。 unit参数keepAliveTime的时间单位。 workQueue阻塞队列用来存储等待执行的任务会对线程池的运行过程产生重大影响。有三个选择ArrayBlockingQueue、LinkedBlockingQueue和SynchronousQueue一般使用后两者。 threadFactory线程工厂主要用来创建线程。handler:表示拒绝处理任务的策略有四种取值
1ThreadPoolExecutor.AbortPolicy丢弃任务抛出RejectedExecutionException异常 2ThreadPoolExecutor.DiscardPolicy:丢弃任务不抛异常 3ThreadPoolExecutor.DiscardOldestPolicy丢弃队列最前面的任务然后重新尝试执行任务重复该过程 4ThreadPoolExecutor.CallRunsPolicy由调用线程处理该任务
ThreadPoolExecutor类的方法
execute()和submit()都是提交任务execute方法用于提交不需要返回值的任务无法判断任务是不是被线程池执行成功submit提交需要返回值的任务线程池返回future类型的对象以判断是否执行成功future对象具有的get()方法可以获取返回值。
shutdown()和shutdownNow()都是关闭线程池他们的原理是遍历线程池中的工作线程然后逐个调用线程的interrupt方法来中断线程所以无法响应中断的任务可能永远无法终止。shutdownNow首先将线程池的状态设置成STOP然后尝试停止所有正在执行或者暂停的线程并返回等待执行任务的列表shutdown只是将线程池的状态设置为SHUTDOWN然后中断所有没有执行任务的线程。
如何合理分配线程池的大小CPU密集型任务一般公式为最大线程数 CPU核数1IO密集型的最大线程数 CPU核数 * 2
实现一个线程池
public class Test {public static void main(String[] args) { ThreadPoolExecutor executor new ThreadPoolExecutor(5, 10, 200, TimeUnit.MILLISECONDS,new ArrayBlockingQueueRunnable(5));for(int i0;i15;i){MyTask myTask new MyTask(i);executor.execute(myTask);System.out.println(线程池中线程数目executor.getPoolSize()队列中等待执行的任务数目executor.getQueue().size()已执行完别的任务数目executor.getCompletedTaskCount());}executor.shutdown();}
}线程池不允许使用Executors的静态方法创建必须通过ThreadPoolExecutor。
线程池的处理流程 当线程池提交一个任务的时候 1线程池判断核心线程池中的线程是不是都在执行任务如果不是则创建一个新的工作线程执行任务否则进入流程2 2线程池判断工作队列是否已满如果没有满则将新提交的任务存储在这个任务队列中如果工作队列满了则进入流程3 3线程池判断池中的线程是否都处在工作状态如果没有则创建一个新的工作线程来执行任务如果已经满了就交给拒绝策略handler来处理任务。
四种线程池 1newCachedThreadPool 创建一个可以缓存的线程池。 2newFixedThreadPool 创建一个定长线程池可以控制线程最大并发数。 3newScheduledThreadPool 创建一个定长线程池支持定时和周期性任务执行。 4newSingleThreadExecutor 创建一个单线程化的线程池他只会用唯一的工作线程来执行任务保证所有任务按照指定顺序执行。
//可以缓存的线程池
ExecutorService cachedThreadPool Executors.newCachedThreadPool(); //需要指定长度
ExecutorService fixedThreadPool Executors.newFixedThreadPool(3);24.线程同步和线程通讯
线程同步的五种方式synchronized的关键字修饰方法、静态资源或者代码块Lock必须放在try-catch-finally中执行finally释放锁以防止死锁wait和notify必须在synchronized范围内被synchronized锁住的对象就是wait和notify的调用对象CAS信号量Semaphore。
线程通讯的方式
1wait()、notify()、nofityAll()等待/通知机制。线程A调用了对象O的wait方法进入等待状态另一个线程B调用了对象O的notify或notifyAll方法线程A收到通知之后从对象O的wait方法中返回执行后续操作。调用对象的wait方法会导致线程阻塞释放该对象的锁调用对象的notify方法会随机解除该对象阻塞的线程该线程重新尝试获取该对象的锁从wait方法返回的前提是获得了调用对象的锁必须在synchronized块或方法中使用。2conditionCondition用await(),signalsingalAll方法代替wait和notify。notify只能随机唤醒一个线程但是用condition可以唤醒指定线程。3管道4volatile5Thread.join如果一个线程执行了Thread.join()意味着当前线程A等待thread线程中止之后才从thread.join()返回。
25.中断线程
调用一个线程的interrupt()方法来中断线程如果该线程处于阻塞、限期等待或者无限期等待状态那么就会抛出InterruptedException从而提前结束该线程。
如果线程的run()执行一个死循环并且没有执行sleep()等会抛出InterruptedException的操作那么调用interrupt()方法无法使线程提前结束。但是调用interrupt方法会设置线程的中断标记此时调用Thread.interrupted()或Thread.currentThread().isInterrupted()方法会返回true。因此可以在循环体中使用interrupted()方法判断线程是否处于中断状态从而提前结束线程。
26.Synchronized的用法 线程安全是Java并发编程中的重点造成线程安全问题主要有两个原因一是存在共享数据二是存在多条线程共同操作共享数据。因此当存在多个线程操作共享数据的时候需要保证同一时刻有且只有线程在操作共享数据其他线程必须等到该线程处理完才能进行这种方式叫做互斥锁。Java中关键字synchronized可以保证在同一时刻只有一个线程可以执行某个方法或者某个代码块同时它还可以保证一个线程共享数据的变化被其他线程所看到可见性保证完全可以替代Volatile功能 synchronized是Java的关键字是一种同步锁。
Java的内置锁synchronized每个java对象都可以用做一个实现同步的锁这些锁称为内置锁。线程进入同步代码块或方法的时候会自动获得该锁退出同步代码块的时候会释放该锁。获得内置锁的唯一途径就是进入锁保护的同步代码块/方法。
Java的对象锁和类锁在锁的概念上与内置锁一致但对象锁是用于对象实例方法或对象实例上的类锁是用于类的静态方法或者一个类的class对象上的。
Java中每个对象都有一把锁和两个队列一个队列用于挂起未获得锁的线程一个队列用于挂起条件不满足而等待的线程。synchronized实际上是一个加锁和释放锁的集成。JVM负责跟踪对象被加锁的次数。如果一个对象被解锁计数归零。线程第一次给对象加锁的时候计数变成1。每当这个相同的线程在此对象上获得锁的时候计数就会递增。每当任务离开一个synchronized方法计数就会递减为0的时候锁被完全释放。
Synchronized有三种应用方式
修饰一个实例方法被修饰的方法称为实例同步方法其作用范围是整个方法锁定的事该方法所属的对象调用该方法的对象。所有需要获得该对象锁的操作都会对该对象加锁。 public synchronized void method(){}//等同于public void method(){synchronized(this){}}如果一个对象有多个synchronized方法只要一个线程访问了其中的一个synchronized方法其他线程不能同时访问这个对象中任何一个synchronized方法。 当一个对象O1在不同的线程中执行这个同步方法的时候会形成互斥。但是O1对象所属类的另一对象O2是可以调用这个被加了synchronized关键字的方法的。其他线程调用O2中的相同方法时不会造成同步阻塞。程序可能在这种情况下摆脱同步机制的控制造成数据混乱。注意
1synchronized关键字不会被继承子类覆盖父类带synchronized方法的时候必须也要给子类的这个方法显式的增加synchronized关键字。2定义接口的时候不能使用synchronized关键字。3构造方法不能使用synchronized关键字但可以使用synchronized代码块完成同步。
修饰一个静态方法被修饰的方法被称为静态同步方法其作用域是整个静态方法锁是静态方法所属的类。 public synchronized static void method(){}
修饰代码块被修饰的代码块被称为同步语句块。synchronized的括号中必须传入一个对象作为锁作用范围是大括号中的代码锁是synchronized括号中的内容可以分为类锁和对象锁//锁对象为实例对象public void method(Object o){synchronized(o){...}}//加入Java开发交流君样756584822一起吹水聊天
//锁对象为类的Class对象 public class Demo{public static void method(){synchronized(Demo.class){...}}}27.Synchronized的原理
实际上是通过monitor监视器。Java中的同步代码块是使用monitorenter和monitorexit指令实现的其中monitorenter指令插入到同步代码块的开始位置monitorexit指令插入同步代码块的结束位置。
JVM保证这两个指令成对出现。 当执行monitorenter指令的时候线程试图获取锁也就是获取monitor对象的所有权当计数器为0的时候就可以成功获取获取后将计数器加一。在执行monitorexit指令之后将锁计数器减一表明锁被释放。 synchronized修饰方法的时候没有monitorenter和monitorexit指令取而代之的是ACC_SYNCHRONIZED标识这个标识指明这个方法是一个同步方法。
28.Synchronized的四种状态
无锁–偏向锁–轻量级锁–重量级锁过程不可逆
偏向锁大多数情况下锁不存在多线程竞争总是由同一线程多次获得如果一个线程获得了锁锁进入偏向模式此时对象头的Mark Word结构也变为偏向锁结构。
对象头在第十章节中提到过另外这篇文章讲的更详细。 当该线程再次请求锁的时候只需要检查Mark Word锁标记为是否为偏向锁以及当前线程ID是不是等于Mark Word的Thread Id即可省去了大量有关锁申请的操作。 偏向锁只适用于只有一个线程访问同步块的场景。 轻量级锁当锁是偏向锁的时候被另外的线程所访问偏向锁就会升级为轻量级锁其他线程会通过自旋的形式尝试获取锁不会阻塞从而提高性能。适用于追求响应时间同步快执行速度非常快的情况。
代码在进入同步块的时候如果同步对象锁状态是无锁虚拟机首先在当前线程的栈帧中创建锁记录Lock Record空间拷贝对象头的Mark Word复制到锁记录中。
之后虚拟机使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针并将Lock Record的owner指针指向对象的Mark Word。如果这个动作成功了那么这个线程就有了该对象的锁对象的锁标记为设置为“00”说明处于轻量级锁定状态。
如果这个动作失败了JVM检查对象的Mark Word是否指向当前线程的栈帧是则说明当前线程已经拥有了这个对象的锁否则说明多个线程竞争锁。
如果有两个以上的线程竞争同一个锁轻量级锁不再有效膨胀为重量级锁。 重量级锁多线程情况线程阻塞响应时间缓慢频繁的释放获取锁会带来巨大的性能损耗。适用于追求吞吐量同步快执行速度较长的情景。
29.Synchronized与重入锁ReentrantLock的区别
相对与ReentrantLock而言synchronized锁是重量级的而且是内置锁意味着JVM可以对synchronized锁做优化。 在synchronized锁上阻塞的线程是不可中断的而ReentrantLock锁实现了可中断的阻塞。
synchronized锁释放是自动的而ReentrantLock需要显式释放在try-finally块中释放\
线程在竞争synchronized锁的时候是非公平的如果synchronized锁被线程A占有线程B请求失败被放入队列中线程C此时来请求锁恰好A在此时释放了线程C会跳过队列中等待的线程B直接获得这个锁。但是ReentrantLock可以实现锁的公平性。
synchronized锁是读写和读读都互斥ReentrankWriteLock分为读锁和写锁读锁可以同时被多个线程持有适合于读多写少的并发场景。
ReentrantLock只能锁代码块但是synchronized可以锁方法和类。ReentrantLock可以知道线程有没有拿到锁但是synchronized不行。
30.锁优化
在28章节中我们提到过重量级锁在重量级锁中JVM会阻塞未获取到锁的线程在锁被释放的时候唤醒这些线程阻塞和唤醒依赖于操作系统需要从用户态切换到内核态开销很大。monitor调用了OS底层的互斥量mutex切换成本很高。因此JVM引入了自旋的概念。
自旋锁与自适应自旋锁CAS实现 自旋锁很多情况下共享数据的锁定状态持续时间短切换线程不值得通过让线程执行忙循环等待锁的释放不让出CPU缺点是如果锁被其他线程长时间占用带来很多开销。 自适应自旋锁自旋的次数不固定由前一次在同一个锁上的自旋时间和锁的拥有者状态来决定。 优点自旋锁不会使线程状态发生改变一直处于用户态不会使线程阻塞执行速度快。 CASCompare And Swap 乐观锁与悲观锁synchronized操作就是悲观锁这种情况线程一旦得到锁其他需要锁的线程就挂起的情况是悲观锁CAS操作实际上是乐观锁每次不加锁而是假设没有冲突而去完成某项操作如果失败了就重试直到成功为止。悲观在认为程序中的并发情况严重乐观在于并发情况不那么严重可以多次尝试。 锁消除虚拟机在即时编译器运行时对一些代码上要求同步而被检测到实际不可能存在共享数据竞争的锁进行消除。依据是JVM会判断一段程序中的同步明显不会逃逸出去从而被其他线程访问JVM就把它们当作栈上的数据对待认为这些数据是线程独有的。 锁粗化在加同步锁的时候我们尽量的把同步块的作用范围限制到尽量小的范围。但是如果存在一连串的操作都对同一个对象反复加锁解锁甚至加锁出现在循环体内即使没有线程竞争频繁的进行互斥同步也会导致消耗。
public static String test04(String s1, String s2, String s3) {StringBuffer sb new StringBuffer();sb.append(s1);sb.append(s2);sb.append(s3);return sb.toString();}上述连续的append操作就属于这类情况jvm检测到一连串操作都是对同一个对象加锁就会把锁同步范围扩展粗化到整个一系列操作的外部使得一连串append操作只需要加一次锁就可以了。
31.Java设计模式
设计模式是一套被反复使用多数人知晓的经过分类编目的代码设计经验的总结。使用设计模式是为了可重用代码让代码更容易被他人理解。实际上就是在某些场景下针对某类问题的某种通用的解决方案。 设计模式分为三类 1创建型模式对象实例化的模式创建型模式用于解耦对象的实例化过程。包括单例模式、简单工厂、抽象工厂等。 2结构型模式把类和对象结合在一起形成一个更大的结构。包括适配器模式、组合模式、装饰模式等。 3行为型模式类和对象如何交互、及划分责任和算法。包括模板模式、解释器模式、观察者模式等。
单例模式属于创建型模式主要有三种写法懒汉式、饿汉式和登记式。
单例模式的特点
1单例类只能有一个实例2单例类必须自己创建自己的唯一实例3单例类必须给所有其他对象提供这一实例
懒汉式在第一次调用的时候就实例化自己。 public class Singleton{private Singleton(){}private static Singleton single null;//静态工厂方法private static Singleton getInstance(){if(single null) single new Singleton();}return single;}懒汉式并不考虑线程安全问题所以他是线程不安全的并发情况下很可能出现多个Singleton实例要实现线程安全有以下三个方式
在getInstance方法上加同步关键字在并发环境下多个一起进入getInstance里因为还没有实例化单例模式single都是null就会创建多个Singleton实例化对象破坏了单例模式想要的结果。我们可以在getInstance方法上加synchronized锁。 public static synchronized Singleton getInstance(){if(single null) single new Singleton();return single;}双重校验锁定 public static Singleton getInstance(){if(singleton null){synchronized (Singleton.class){if(singleton null) singleton new Singleton();}}return singleton;}双重校验锁定的单例仍然需要再加上volatile确保线程安全。
静态同步类即实现了线程安全又避免了同步带来的性能影响。 public class Singleton{private static class LazyHolder{private static final Singleton INSTANCE new Singleton();}private Singleton(){}public static final Singleton getInstance(){return LazyHolder.INSTANCE;}}饿汉式饿汉式在类创建的同时就已经创建好了一个静态的对象供系统使用以后不再改变所以天生是线程安全的。 public class Singleton1{private Singleton1(){}private static final Singleton1 single new Singleton1();//静态工厂方法public static Singleton1 getInstance(){return single;}}饿汉就是类一旦加载就把单例初始化完成保证getInstance的时候单例已经存在了而懒汉比较懒只有用户调用getInstance的时候才会初始化这个实例。
最后祝大家早日学有所成拿到满意offer