最近依旧是每天在复习着,暂时还是0面试,但自己还蛮喜欢复习的感觉的,把以前学过的知识短时间内都串起来,脑子里的知识框架也相对来说更加清晰了,有些知识点确实前前后后学很多遍了,细节也越来越清晰。我先把最近复习中记录的Java基础和部分高级篇分享给大家吧,发现看多了关于同一知识点不同的描述之后,自己也更清楚哪些是重点了。
相信不少小伙伴在面试或者笔试的时候会遇到手写单例模式的情况,对于不了解的同学来说可能很有难度,还记得刚开始跟着尚硅谷宋红康老师学懒汉式和饿汉式的时候当时不理解,最后背着背着会默写了,过了几天还是会忘记,这东西确实得反复 ,等自己理解水平跟上来之后这些都是小问题。这些我们暂时只说单例,有其他设计模式大家也可以自行查阅资料进行深一步学习。
23种设计模式 总体分成三类:
工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式
适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式
策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式
另外Java创建对象的四种方式:new、反射、反序列化、克隆(要实现Cloneable接口)
单例模式确保某个类只有一个实例
,而且自行实例化并向整个系统提供这个实例,选择单例模式就是为了避免不一致状态,大致思路是:防止new对象 构造私有 写一个公共的方法 返回对象
接下来就结合代码给大家详细讲解这些单例模式
一上来就创建对象,占用空间,线程安全
public class Item{
private static final Item i = new Item();
private Item (){}
public static Item getItem(){ return i;}
}
//static关键字:随着类的加载而加载,优先于对象存在
// 修饰成员、方法、静态代码块、内部类
//final 类(不能继承)、成员(不能改变)、方法(不能重写)
//方法重写和重载: 重写:有子父类关系,剩下的一模一样。权限修饰大于等于
// 重载:一个类中,方法相同,参数列表不同,与返回值无关
延迟加载,内存空间占用小。但是加锁了,效率有问题
public class Item{
private static Item i = null;
private Item (){}
public static synchronized Item getItem(){
if(i == null) i = new Item();
return i;
}
}
//synchronized:同步方法、同步代码块 synchronized(锁){需要加锁的范围i}
//对象锁和类锁:对象锁可以有多个
基于懒汉式进行了优化,如果面试中面试官让我们写一种单例的话,强烈推荐这种,可以往多线程和锁上面说说
public class Item{
//加volatile防止下面赋值操作指令重排
private static volatile Item i = null;
private Item(){
if (i != null) {//这里面的判断是为了解决反射创建单例的问题
throw new IllegalStateException("不能反射创建 已创建对象");
}
}
public static Item getItem(){
if(i == null){
synchronized(Item.class){
//下面赋值实际有三步:堆内存开辟空间、对象初始化、属性赋值初始化
if(i == null) i = new Item();
}
}
return i;
}
//锁的作用范围
//volatile关键字:JMM三大特性,也是产生安全问题的原因
// 原子性(volatile不保证)、可见性、有序性 线程安全 是指令级别的
}
程序员自己能想出来的 最牛单例
public class Item{
private Item(){}
public static Item getItem(){
return InnerItem.i;
}
static class InnerItem{
private static Item i = new Item();
}
}//延迟加载的目的 内部类不适应不加载
//没加锁 线程安全 纯靠静态内部类属性
解决线程安全、反射、反序列化。前面那些不全能满足(要自己增加)
public enum Item{
ITEM;
public static Item getItem(){ return ITEM; }
}//反序列化问题 也是JDK开发人员在底层源码就给解决了
下面附上我写的小demo 可以深入理解反射的一些知识
public class Test {
public static void main(String[] args) throws Exception {
Item i = Item.getItem();//两个地址不一样 反射破坏单例
// ItemEnum i = ItemEnum.getItem();
Class clazz = Item.class;
// Class clazz = ItemEnum.class;
Constructor constructor = clazz.getDeclaredConstructor(null);
// Constructor constructor = clazz.getDeclaredConstructor(String.class, int.class);//IllegalArgumentException
constructor.setAccessible(true);//强制访问
Item item = (Item)constructor.newInstance();//不能反射创建 已创建对象
// ItemEnum item = (ItemEnum)constructor.newInstance();//ClassCastException
System.out.println("反射创建"+item);
System.out.println("单例创建"+i);
}
}
代理模式在我们平时开发和框架中用的也挺多的,代理类本身并不真正实现服务,而是同过调用委托类的相关方法,来提供特定的服务。真正的业务功能还是由委托类来实现,但是可以在业务功能执行的前后加入一些公共的服务
。例如我们想给项目加入缓存、日志这些功能,我们就可以使用代理类来完成,而没必要修改已经封装好的委托类
代理模式又分为静态代理和动态代理
静态:只为单一目标服务 动态:服务目标不单一
目标对象(被代理对象)、代理对象(帮别人干活的那个)
JDK proxy类:
必须实现接口,基于接口实现
代理对象把被代理对象的所有实现的接口,都给实现了一遍,然后可以重写接口方法,就知道
被代理对象想做什么事了
CGlib:第三方的(功能更强)
类不能被final修饰,基于继承,代理对象把被代理对象当作父类了。重写父类的方法
AOP底层实现:默认使用JDK的。如果满足不了。自动切换成CGlib
这些我相信有很多博客文章都写得很详细,我这里只是一个纲要和我在这轮复习中觉得比较重要的知识点。
一种事物,有多种表现形式。父类引用指向子类对象 接口和接口的实现
- 重载发生在本类,重写发生在父类和字类之间
- 重载的方法名必须相同,重写的方法名相同且返回值类型必须相同
- 重载的参数列表不同,重写的参数列表相同
- 重写的访问权限不能比父类中被重写的访问权限更低
- 构造方法不能被重写
接口:默认public,方法都是抽象的。1.8默认方法,只有一个。多实现
抽象类:只要有一个方法是抽象的,这个类就是抽象。可以有非抽象方法。单继承
深:地址不同,多个对象
浅:引用有多个,地址相同
sleep:thread类,有时间参数,不释放锁,释放线程执行权。时间到了就醒
wait:object类,释放锁,释放,必须通过notify或notifyAll唤醒
==
:基本类型比较值,引用类型比较地址
equals:object方法,默认和==一样,如果重写 按照重写的规则比较
String先比较地址相同 不同比较类型,类型相同比较长度 长度相同比较内容
native关键字:调用系统本地资源的
String类是最常用的类之一,为了效率
,禁止被继承和重写
为了安全,String类中有native
关键字修饰的调用系统级别的本地方法,调用了操作系统的API,如果方法可以重写,可能被植入恶意代码,破坏程序。Java的安全性也体现在这里
finally:必须和try结合使用,不能单独和catch使用,最终要执行的代码 放到finally块中
finalize:一个object的方法,调用可以进行垃圾回收
clone equals finalize getClass notify notifyAll toString wait
分为Collection(Set、List)和Map(HashMap、TreeMap),千万注意Map和Set、List并不是同一层级的
数据结构:1.8 Node
1.初始化长度:16 负载因子:0.75 可以自己指定 长度*负载因子=扩容点
2.put方法
3.扩容(这两个结合文档详细去看看)
4.hash碰撞:形成链表的条件
5.什么时候变成树:数组长度大于64 并且 链表长度大于8
6.树转链表:树的高度小于6转链表
7.为什么要转树?当链表长度大于8 查询效率低于红黑树了
HashMap:线程不安全,效率高,可以存唯一为null的key,多个null值;没有contains方法 有containsKey和containsValue方法;初始化长度默认16,扩容 2的整数倍 2倍扩容
HashTable:线程安全,效率低,不可以存null的key,null的值;有contains,containsKey,containsValue方法;默认为11,没要求2的整数倍,2倍+1
继承Thread类;实现Runnable(没有返回值)接口;
使用Callable(有Object返回值)和Future;使用线程池
并发修改异常(查的时候不能改)
组成部分:方法区、堆、虚拟机栈、本地方法栈、程序计数器
方法区:(所有线程共享) 已被虚拟机加载的类信息、常量、静态变量和即时编译器编译后的代码;还有一个运行时常量池
虚拟机栈:局部变量表、操作数栈、动态链接和方法出口等信息
本地方法栈:与上面类似,不过为Native方法服务
堆(所有线程共享) (新生代(eden、s0、s1,默认占比8:1:1)、老年代):几乎所有对象实例,经常GC
程序计数器:实现代码流程控制和记录线程执行位置 不会OOM
复制:需要内存容量大,耗内存,使用在占空间较小,刷新次数较多的新生区
标记-清除:效率较差,会产生碎片 老年代一般是标记清除和标记整理的混合实现
标记-整理:效率低速度慢,需要移动对象,但不会产生碎片
垃圾收集器:7种 新生代3种 老年代3种(标记清除CMS) 1种全局的G1
xms:最小堆内存 xmx:最大堆内存
对象优先在堆的Eden区分配、大对象直接进入老年代、长期存活的对象将直接进入老年代
当Eden区没有足够的空间进行分配时,虚拟机会执行一次MinorGC(局部GC),Eden区
的对象生存期短,往往发生GC的频率较高,回收速度比较快
,FullGC/MajorGC
发生在老年代
,一般情况下,触发老年代GC的时候不会触发MinorGC,但是通过配置,可以在FullGC之前进行一次MinorGC可以加快老年代的回收速度
引用计数法
:当一个对象的引用计数器为0的时候,就是死对象(无法解决循环依赖问题)
可达性分析(GCRoots引用链法)
:通过一些被称为引用链(GC Roots)的对象作为起点,
从这些节点开始向下搜索,走过的路径被称为(Reference Chain),当一个对象到GC Roots
没有任何引用链相连时,该对象就不可用
在Java中可以作为GC Roots的对象
:虚拟机栈中引用的对象、方法区类静态属性引用的对象、本地方法栈JNI引用的对象
栈溢出:
无限递归循环调用(最常见)、执行大量方法、方法内部声明海量局部变量
堆溢出:
内存中加载数据过多,如一次从数据库读取过多数据,代码中存在死循环或循环产生过多重复的对象实体 启动参数内存值设定的过小
排查
:jvisualvm 进行快照分析 jconsole(还能检测死锁)
juc.Executors中提供了生成多种线程池的静态方法(基本不用,用的是ThreadPoolExecutor)
newCachedThreadPool、newFixedThreadPool newScheduledThreadPool、newSingleThreadExecutor
线程池的7个核心参数:
corePoolSize、maximumPoolSize、keepAliveTime、 unit、workQueue、threadFactory、rejectedExecutionHandler
线程池大小设置:CPU密集型(最大线程数=CPU核心数+1)
IO密集型(CPU核心数*2)
降低资源消耗
,通过重复利用已创建的线程降低线程创建和销毁造成的消耗
提高响应速度
,当任务到达时,任务可以不需要等到线程创建就能立即执行
提高线程的可管理性
,线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配、调优和监控
CopyOnWrite(ArrayList、ArraySet) 采用写时复制
ConcurrentHashMap 采用分段锁
基本类型AtomicInteger、数组类型AtomicIntegerArray、引用类型AtomicReference等,
其中AtomicStampedReference
是源自更新带有版本号
的引用类型。该类将整数值与引用关联起来, 可用于解决使用CAS进行原子更新时可能出现的ABA问题
AtomicInteger类
利用CAS(Compare and Swap)+volatile+native方法
来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升
CAS的原理,是拿期望值和原本的值作比较,如果相同,则更新成新的值
UnSafe类的objectFieldOffset()方法
是个本地方法,这个方法是用作拿"原值"的内存地址,返回值是valueOffset;另外,value是一个volatile变量因此JVM总是可以保证任意时刻的任何线程总能拿到该变量的最新值
(同步代码块:monitorenter、monitorexit;方法:ACC_SYNCHRONIZED标识符)
synchronized方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法的调用和操作之中。
JVM从方法常量池中的方法表结构(method_info Structure)中的ACC_SYNCHRONIZED
访问标志区分
一个方法是否同步方法。当方法调用时,调用指令将会检查方法的那个访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词),然后再执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放monitor代码块的同步是利用monitorenter和monitorexit这两个字节码指令。
它们分别位于同步代码块的开始和结束位置。当jvm执行到monitorenter指令时,当前线程试图获取monitor对象的所有权,如果未加锁或者已经被当前线程所持有,就把锁的计数器+1,当执行monitorexit指令时,锁计数器-1;当锁计数器为0时该锁就释放了。如果获取monitor对象失败,该线程则会进入阻塞状态,直到其他线程释放锁
javap-v
就能看到具体的字节码指令
Lock的存储结构:一个int类型是状态值(用于锁的状态变更),一个双向链表(用于存储等待中的线程)
Lock获取锁的过程:本质上是通过CAS来获取状态值修改,如果当场没获取到,会将该线程等待链表中
Lock释放锁的过程:修改状态值,调整等待链表
Lock大量使用CAS+自旋。因此根据CAS特性,lock建议使用在低锁冲突的情况下
1.Lock的加锁和解锁都是由
java代码配合native方法
(调用操作系统的相关方法)实现的,而synchronized的加锁和解锁的过程是由JVM
管理的
2.当一个线程使用synchronized获取锁时,若锁被其他线程占用着,那么当前只能被阻塞
,直到成功获取锁。而Lock则提供超时锁和可中断
等更加灵活的方式,在未能获取锁的条件下提供一种退出的机制
3.一个锁内部可以有多个Condition实例
,即有多路条件队列,而synchronized只有一路条件队列
;同样Condition也提供灵活的阻塞方式,在未获得通知之前可以通过中断线程以及设置等待时限等方式退出队列
4.synchronized对线程的同步仅提供独占模式
,而Lock既可以提供独占模式
,又能够共享
Con是一个线程安全的Map容器,JDK8之前,Con使用锁分段技术,将数据分成一段段存储,每个数据段配置一把锁,即segment类,这个类继承ReentrantLock来保证线程安全,JDK8的版本取消Segment这个分段锁数据结构,底层也是
使用Node数组+链表+红黑树
,从而实现每一段数据
就行加锁,也减少了并发冲突的概率
hashTable类基本上所有的方法都是采用synchronized进行线程安全控制,效率低.con分段锁,锁粒度更细化
ConcurrentHashMap的put方法
先判断键值是否为null 延迟加载 new的时候不会马上初始化
,得第一次put才会
然后判断当前Hash槽是否为空,如果为空则说明没有Hash冲突,则通过CAS进行插入元素
如果设置成功就跳出循环,不成功就说明有并发问题 进行下一次循环
判断是否在进行数据迁移 拿到表头元素的锁标记进行操作 判断当前元素是否改变,说明当前槽为链表,遍历链表
判断元素是否重复,重复就用新值替换旧值 到表尾就直接将元素插入
volatile本质是告诉JVM当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是可以锁定变量 操作,只有当前线程可以访问操作该变量,其他线程被阻塞住
volatile只能用在变量
级别,只能实现可见性,不能保证原子性,不会阻塞线程,不会被编译器优化
synchronized可以用在方法、类
级别,会阻塞线程,会被编译器优化
加载-验证-解析-初始化
加载:一个类的全限定名获取该类的二进制流 —> 静态存储结构转化为方法区行时数据结构 —> 在内存中生成Class对象,作为该类的数据访问入口
验证:为了确保Class文件的字节流中的信息不会危害到JVM
解析:符号引用到直接引用的转换动作
初始化:真正开始执行类中定义的Java代码
四种类加载器:启动类(Bootstrap)、扩展类(extension)、用户自定义类
系统类(system):也叫应用类加载器,它根据Java应用的类路径(classPath
)来加载Java类
一般来说,Java应用类都是由它来完成加载的。可以通过ClassLoader.getSystemClassLoader()来获取它
Java死锁可以采用
jconsole、jstack
等等
SQL死锁:
查询是否锁表:show OPEN TABLES where In_use>0
查询进程:show processlist
查看正在锁的事务:select * from information_schema.innodb_locks
查看等待锁的事务:select * from information_schema.innodb_lock_waits
造成死锁的几个原因:一个资源每次只能被一个线程使用、一个线程在阻塞等待某个资源时,不释放已经占有的资源、一个线程已经获得的资源,在未使用完之前,不能被强行剥夺、若干线程形成头尾相接的循环等待资源关系
打破循环等待资源关系即可:注意加锁顺序、加锁时限,可以针对锁设置一个超时时间
注意死锁检查
,这是一种预防机制,确保在第一时间发现死锁并进行解决
这些也只是Java那庞大体系中的冰山一角,关于Java高级部分还有好多需要进行深入学习,这部分也确实很难啃,记得年初的时候开始看那些jvm的时候一头雾水,之前看尚硅谷周洋老师讲的juc举步维艰,发现这些需要有操作系统的很多知识储备才能较好的理解,虽然这些每次看着都觉得难,好在每多看一次,下一次再看下来就更轻松了,也是一个慢慢积累的过程吧,有一定的知识体系和实战经验我相信大家再来看这部分的知识也一定是会有更深的感触,差不多总结完了,先休息一下,再继续好好复习了。