平台无关性,GC垃圾回收机制,面向对象,类库,异常处理
平台无关性: 源码编译成字节码,再由不同平台的JVM进行解析,java语言在不同的平台上运行时不需要重新编译,JVM在执行字节码的时候,把字节码转换成平台具体上的机器指令
为什么JVM不直接不将源码解析成机器码去执行?
准备工作:每次执行会有各种检查(语法,语义,重新编译,分析),性能下降,形成字节码能保证多次执行程序,不用
检查
JDK: Java Development Kit
的简称,java开发工具包,提供了Java的开发环境和运行时环境
JRE:Java Runtime Environment
的简称,Java运行环境,为java的运行提供了所需环境
根据特定格式,加载class文件到内存
主要在类装载的加载阶段,作用是从系统外部获得类二进制数据流
它通过将类文件里的二进制数据流装载进系统,然后交给Java虚拟机进行连接,初始化等操作
种类:
BootStrapClassLoader
: C++编写,加载核心库 java>*
ExtClassLoader
: Java编写,加载扩展库 javax.*
AppClassLoader
: Java编写,加载程序所在目录
自定义ClassLoader
: Java编写,定制化加载
对命令进行解析
融合不同开发语言的原生库为Java所用
JVM内存结构模型
一块小的内存区域,线程私有,是唯一不会发生OutOfMemoryError(内存溢出)
的区域,可理解为
方法进栈后,每一行代码都有一个标识,程序按标识往下执行
java.lang.StackOverFlowError
,栈内存溢出错误,常见于递归调用,递归过深,栈帧数超过虚拟机深度 本地方法栈为native
方法服务,java虚拟机栈为java方法服务
Eden
,Survivor1
,Survivor2
三个区域,垃圾收集器主要管理的区域,Eden
区回收效率很高OutOfMemoryError
错误,内存溢出堆和栈的区别
管理方式: 栈自动释放,堆需要GC
空间大小: 栈比堆小
碎片相关: 栈产生的碎片远小于堆
分配方式: 栈支持静态和动态分配,而堆只支持动态分配
效率: 栈的效率比堆高
元空间(MetaSpace)和永久代(PermGen)的区别?
元空间和永久代都是方法区的实现,方法区只是一种JVM的规范
JDK8后使用了元空间替代了永久代
元空间使用本地内存,永久代使用的是JVM内存
元空间比永久代的优势:
字符串常量池存在永久代中,容易出现性能问题和内存溢出
类和方法的信息大小难确定,给永久代的大小指定带来困难
永久代会给GC带来不必要的复杂性
静态存储: 编译时确定每个数据目标在运行时的存储空间需求 不允许有可变数据,嵌套,递归,会导致计算
动态存储(栈式存储): 编译时未知,运行时确定 先知道数据区的大小,才能分配内存
堆式存储: 编译和运行时都无法确定,动态分配 如对象实例
通过类加载器将符合格式要求的class文件加载进内存,并通过执行引擎去解析里面的字节码,并提交给操作系统去执行
JVM三大性能调优参数
-Xss: 规定了每个线程虚拟机栈(堆栈)的大小 一般256K足够。此配置将会影响此进程中并发线程数的大小
-Xms: 堆的初始值 初始java堆的大小,即该进程创建出来时堆的大小
-Xmx: 堆能达到的最大值 一旦对象容量唱过-Xms大小,则将java堆大小扩容至该参数。为防止堆扩容导致内存抖动,影响程序进行稳定性,一般设置成与Xms一样大
加载: 通过类加载器加载class文件字节码,生成class对象
链接:
初始化: 执行类变量赋值和静态代码块
问:比如父类静态数据,构造函数,字段,子类静态数据,构造函数,字 段,当new的时候,他们的执行顺序。
答: 类加载器实例化时进行的操作步骤(加载–>连接->初始化)。
父类静态变量、 父类静态代码块、 子类静态变量、 子类静态代码块、 父类非静态变量(父类实例成员变量)、 父类构造函数、 子类非静态变量(子类实例成员变量)、 子类构造函数。
1.堆内存用来存放由new创建的对象和数组。
2.栈内存用来存放方法或者局部变量等
3.堆是先进先出,后进后出
4.栈是后进先出,先进后出
用于初始化对象状态的特殊方法类型。在实例化类时调用,并为对象分配内存。构造方法的名称必须和类名相同。构造方法没有显式返回类型 隐式返回类的当前实例。
根据构造方法中传递的参数,分为默认构造方法和参数化构造(有参无参)
无参构造(默认构造):
不用接受任何值,主要用于使用默认值初始化实例变量,如果类中没有定义构造方法,编译器会隐式创建无参构造
有参构造(参数化构造):
可以接收参数的构造方法
JVM实例化字符串常量池时
String str1 = "hello";
String str2 = "hello";
System.out.println("str1 == str2" : str1 == str2 ) //true
通过new
创建的 字符串对象 不指向字符串池 的任何对象,但是可通过 使用字符串的intern()
方法来指向其中的一个。
java.lang.String.intern()
返回一个 保留池字符串,就是在 全局字符串池中有了一个入口, 如果以前没有在 全局字符串池中,那么它就会被 添加 到里面
不同JDK版本间initern()方法的区别
JDK6:
调用时,如果字符串常量池已创建该字符串对象,则返回池中的该字符串的引用。
否则,将此字符串对象添加到字符串常量池中,并且返回该字符串对象的引用
JDK6+:
调用时,如果字符串常量池已创建该字符串对象,则返回池中的该字符串的引用。
否则,如果字符串对象已经存在Java堆中,则将堆中对此对象的引用添加到字符串常量池中,并且返回该引用;
如果堆中不存在,则在池中创建该字符串并返回其引用
String s1 = "Hello";
String s2 = new String("Hello");
String s3 = s2.intern();
System.out.println("s1 == s3? " + (s1 == s3));// true
length();//计算字符串的长度
charAt();//返回指定索引处的字符
getChars();//截取多个字符
equals();//比较两个字符串
equalsIgnoreCase();//比较两个字符串,忽略大小写
startsWith();//startsWith()方法决定是否以特定字符串开始
endWith();//方法决定是否以特定字符串结束
indexOf();//返回指定字符的索引
getBytes();//返回字符串的 byte 类型数组
lastIndexOf();//查找字符或者子串是后一次出现的地方。
substring();//截取字符串
split();//分割字符串,返回一个分割后的字符串数组
concat();//连接两个字符串
replace();//替换
trim();//去掉起始和结尾的空格
valueOf();//转换为字符串
toLowerCase();//转换为小写
toUpperCase();// 转换为大写
使用 StringBuilder 或者 stringBuffer 的 reverse()
方法。
示例代码:
// StringBuffer reverse
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("abcdefg");
System.out.println(stringBuffer.reverse()); // gfedcba
// StringBuilder reverse
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("abcdefg");
System.out.println(stringBuilder.reverse()); // gfedcba
重载:发生在同一类中,方法名相同,参数类型,返回值,个数,访问权限修饰符可以不同,发生在编译时
重写:发生在父类中,方法名,参数列表必须相同,返回值范围与抛出的异常小于等于父类,访问权限修饰符大于等于父类,如果父类方法为private则子类不能重写该方法
String类
中使用final关键字字符数组来保存字符串,所以String对象或常量是不可变的,且线程安全
而StringBuffer和StringBuilder
都继承自AbstractStringBuilder类
,AbstractStringBuilder中也是使用字符数组来保存,但没有用final关键字
修饰,所以可变
StringBuffer对方法或调用的方法加了同步锁,所以线程安全
StringBuild没有对方法加同步锁,所以非线程安全
使用:
操作少量数据使用String
单线程操作大量数据使用 StringBuilder 线程不安全,不同步,效率高
多线程操作大量数据使用 StringBuffer 线程安全,同步,效率低
装箱: 将基本数据类型用引用类型包装起来
拆箱: 将包装类型转为基本数据类型
==:基本类型比较的是值,引用类型比较的是地址
equals:重写前比较的是地址,重写后比较的是值
this关键字是引用当前对象的引用变量。 它可以用于引用当前类属性,如实例方法,变量,构造函数等。它也可以作为参数传递给方法或构造函数。 也可以作为当前类实例从方法返回。
this
可以用来引用当前的类实例变量。this
可以用来调用当前的类方法(隐式)this()
可用于调用当前的类构造函数。this
可以作为方法调用中的参数传递。this
可以作为构造方法调用中的参数传递。this
可以用于从方法返回当前类实例。super关键字是引用变量,用于引用父类对象。 无论何时创建子类的实例,都会隐式创建父类的实例,该实例由父类引用变量引用。 如没有super
或this
这俩关键字,编译器则隐式地在类构造函数中调用super()
super
可用于引用直接父类实例变量super
可用于调用直接父类方法super()
可用于调用直接父类构造函数this
和super
的区别:
super
始终指向父类上下文,而this
始终指向当前类上下文。
super
主要用于初始化子类构造函数中的基类变量,而this
主要用于在类构造函数中传递时区分本地变量和实例变量。
super
和this
必须是构造函数中的第一个语句,否则编译器将抛出错误。
作用于:变量,方法,类
final变量: 表示常量,只能被赋值一次,赋值后值不再改变
基本数据类型的变量,初始化后就不能更改;引用类型则初始化后不会再指向另外对象,但内容是可变的
final方法: 1. 无法被重写,将方法锁定,以防继承的类修改其含义
2. 效率,以前需要,现在则不用final来优化,类中所有private方法都隐式指定为final
final类: 无法被继承,所有成员方法都会被隐式为final方法
空白final
变量
未在声明时初始化的最终变量称为final空白变量。final空白变量可以直接赋值初始化。 也可以使用类构造函数初始化它。 当用户具有一些不得被其他人更改的数据时,例如:身份号码
public final native Class<?> getClass()
//native方法,用于返回当前运行时对象的Class对象,使用了 final关键字修饰,故不允许子类重写。
public native int hashCode()
//native方法,用于返回对象的哈希码,主要使用在哈希表中,比如JDK中的 HashMap。
public boolean equals(Object obj)
//用于比较2个对象的内存地址是否相等,String类对该方法进行了重写用户 比较字符串的值是否相等。
protected native Object clone() throws CloneNotSupportedException
//naitive方法,用于创建并返回 当前对象的一份拷贝。
//一般情况下,对于任何对象 x,表达式 x.clone() != x 为true,x.clone().getClass() == x.getClass() 为true。
//Object本身没有实现Cloneable接口,所以不重写clone方法并且进行调用的话会发生 CloneNotSupportedException异常。
public String toString()
//返回类的名字@实例的哈希码的16进制的字符串。建议Object所有的子类都重写这个方 法。
public final native void notify()
//native方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视 器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
public final native void notifyAll()
//native方法,并且不能重写。跟notify一样,唯一的区别就是会唤醒 在此对象监视器上等待的所有线程,而不是一个线程。
public final native void wait(long timeout) throws InterruptedException
//native方法,并且不能 重写。暂停线程的执行。注意:sleep方法没有释放锁,而wait方法释放了锁 。timeout是等待时间。
public final void wait(long timeout, int nanos) throws InterruptedException
//多了nanos参数, 这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上nanos毫秒。
public final void wait() throws InterruptedException
//跟之前的2个wait方法一样,只不过该方法一直等 待,没有超时时间这个概念
protected void finalize() throws Throwable { }
//实例被垃圾回收器回收的时候触发的操作
主要标示修饰块的作用域,方便隔离防护
public
: 被其修饰的类、属性以及方法不仅可以跨类访问,而且允许跨包(package)访问。
protected
: 只能被类本身的方法及子类访问,即使子类在不同的包中也可以访问。
default
:“默认访问模式“。只允许在同一个包中进行访问。
private
: 只能被该类的对象访问,其子类不能访问,更不能允许跨包访问。
浅拷贝
复制了对象的引用地址,两个对象指向同一个内存地址,所以修改其中任意的值,另一个值都会随之变化,
传递地址指向,新的对象并没有对引用数据类型创建内存空间
深拷贝
将对象及值复制过来,两个对象修改其中任意的值另一个值不会改变
对引用数据类型的成员变量的所有的对象都开辟了内存空间
Throwable类有两个子类Exception(异常)和Error(错误)
Exception(异常): 程序可以处理的异常
Erroe(错误): 程序无法处理
Throeable类常用方法
public String getMessage():返回异常发生时的详细信息
public String toString():返回异常发生时的简要描述
public String getLocalizedMessage():返回异常对象的本地化信息。使用Throwable的子类覆盖这个方法,可
以声称本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与getMessage()返回的结果相同
public void printStackTrace():在控制台上打印Throwable对象封装的异常信息
try块: 用于发现异常,后面可接多个catch块,没有catch块则必须跟finally块
cache块: 用于处理try发现的异常
finally块: 无论如何都会执行,当try或cache中有return
时,finally
会在方法返回之前执行
finally不会执行的情况:
system.exit(0)
退出程序或system.exit(1)
结束虚拟机throw 和 throws 的区别?
throw 声明一个方法可能抛出的所有异常信息,
throws 声明但是不处理,而是将异常往上传,谁调用就交给谁处理。而throw则是指抛出的一个具体的异常类型
常见的异常类有哪些?
NullPointerException:空指针,当应用程序试图访问空对象时,则抛出该异常。
ClassCastException:类型转换异常,当试图将对象强制转换为不是实例的子类时,抛出该异常。
IllegalArgumentException:传递非法参数异常,抛出的异常表明向方法传递了一个不合法或不正确的参数。
IndexOutOfBoundsException:索引越界,指示某排序索引(例如对数组、字符串的排序)超出范围时抛出。
NumberFormatException:数字格式异常,试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时抛出该异常
SQLException:提供关于数据库访问错误或其他错误信息的异常。
FileNotFoundException:当试图打开指定路径名表示的文件失败时,抛出此异常。
IOException:当发生某种I/O异常时,抛出此异常。此类是失败或中断的I/O操作生成的异常的通用类。
ArrayStoreException:试图将错误类型的对象存储到一个对象数组时抛出的异常。
ArithmeticException:当出现异常的运算条件时,抛出此异常。例如,一个整数“除以零”时,抛出此类的一个实例。
NegativeArraySizeException:如果应用程序试图创建大小为负的数组,则抛出该异常。
NoSuchMethodException:无法找到某一特定方法时,抛出该异常。
SecurityException:由安全管理器抛出的异常,指示存在安全侵犯。
UnsupportedOperationException:当不支持请求的操作时,抛出该异常。
RuntimeExceptionRuntimeException:是那些可能在Java虚拟机正常运行期间抛出的异常的超类。
常见Error
StackOverflowError: 深递归导致栈被耗尽而抛出的异常
OutOfMemoryError: 内存溢出异常
NoClassDefFoundError: 找不到class定义的异常 ,类找不到依赖的class或jia;依赖存在,但在不同域;大小写问题
final、finally、finalize 有什么区别?
1.通过Scanner
Scanner input = new Scanner(System.in);
String s = input.nextLine():
input.close;
2.通过BufferedReader
BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
String s = input.readLine();
1.接口的方法默认是public abstract
,java8
前方法在接口不能有实现,8开始有普通方法,但必须是加static
静态或default
修饰表示新拓展,抽象类的方法可以是任意访问修饰符
2.接口中的实例变量 默认是public static final
修饰,抽象类中则不一定
3.一个类可以实现多个接口,而抽象类只能实现一个
4.实现接口要实现接口中的所有方法,抽象类则不一定
5.接口不能new
实例化,但可以声明,但必须引用一个实现该接口的对象
6.构造函数:抽象类可以有构造函数;接口不能有
7.main 方法:抽象类可以有 main 方法,并且我们能运行它;接口不能有 main 方法
8.接口只能做方法声明,抽象类中可以做方法声明,也可以做方法实现
面向对象的转型只会发生在具有继承关系的父子类中(接口也是继承的一种)
向上转型:其核心目的在于参数的统一上,根本不需要强制类型转换。
向下转型:是为了操作子类定义的特殊功能,需要强制类型转换,
可是现在存在的问题是:向下转型其实是一种非常不安全的操作,以为编译的时候,程序不会报错,而在运行的时候会报错,这就是传说中的—迷之报错。
在JDK1.5之后,新增加了泛型的技术,这就将上述向下转型的问题消灭在了萌芽之中。
泛型的核心意义在于:
类在进行定义的时候可以使用一个标记,此标记就表示类中属性或者方法以及参数的类型,标记在使用的时候,才会去动态的设置类型
、List 的三者的区别以及 与 的区别List
:完全没有类型限制和赋值限定List
:看似用法与List一样,但是在接受其他泛型赋值时会出现编译错误List
:是一个泛型,在没有赋值前,表示可以接受任何类型的集合赋值,但赋值之后不能往里面随便添加元素,但可以remove和clear
,并非immutable(不可变)
集合。List
一般作为参数来接收外部集合,或者返回一个具体元素类型的集合,也称为通配符集合List 最大的问题是只能放置一种类型,如果随意转变类型的话,就是破窗理论,泛型就失去了意义。为了放置多种受泛型约束的类型,出现了与
两种语法。简单来说, 是Get First,适用于,消费集合元素的场景;是Put First,适用于,生产集合元素为主的场景。
:可以赋值给任意T及T的子类集合,上界为T,取出来的类型带有泛型限制,向上强制转型为T。null 可以表示任何类型,所以null除外,任何元素都不得添加进集合内
: 可以复制T及任何T的父类集合,下界为T。再生活中,投票选举类似于的操作。选举投票时,你只能往里投票,取数据时,根本不知道时是谁的票,相当于泛型丢失的场景是put功能受限,而
的场景是get功能受限
//使用通配符
public static void test(List<?> list) {
}
//使用泛型方法
public <T> void test2(List<T> t) {
}
如果参数之间的类型有依赖关系,或者返回值是与参数之间有依赖关系的。那么就使用泛型方法
如果没有依赖关系的,就使用通配符,通配符会灵活一些.
等于-1,因为在数轴上取值时,中间值(0.5)向右取整,所以正0.5是向上取整,负0.5是直接舍去
把对象的属性私有化,再提供可被外界访问的方法
从已有的类基础上建立新类,新类保存已有类的属性和行为,并能拓展新的功能
子类拥有父类所以属性和方法,但私有属性和方法无法访问,只是拥有
同一种事务,由于环境/条件等因素的不同,呈现的状态不同
方法调用在编程时并不确定,而是在程序运行期间才确定
方法三要素: 返回值类型 参数列表 方法名
多态的两种实现方式
使用父类作为方法形参
实现多态
使用父类作为方法返回值
实现多态
多态作为形参
形式参数
基本类型
引用类型
普通类 当一个形参希望我们传入的是普通类时,我们实际传入的是该类的对象/匿名对象
抽象类 当一个形参希望我们传入的是抽象类时,我们实际传入的是该类的子类对象/子类匿名对象
接口 当一个形参希望我们传入的是接口时,我们实际传入的是该类的实现类对象/实现类匿名对象
注意:当一个方法的形参是引用类型是,建议养成一个好习惯:做非空判断
多态作为返回值
返回值类型
基本类型
引用类型
普通类
当一个方法的返回值是一个普通的类时,实际上返回的是该类的对象,我们可以使用该类的对象接收
抽象类
当一个方法的返回值是一个抽象类时,实际上返回的是该抽象类的子类对象,我们可以使用该抽象类接收
接口
当一个方法的返回值是一个接口时,实际上返回的是该接口的实现类对象,我们可以使用接口接收
当方法的返回值类型是引用类型的时候,可以使用链式调用
Collection
java.util.Collection
是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection
接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List与Set。Collections
是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作HashMap
实现,底层采用HashMap
来保存元素LinkedHashSet
继承于HashSet
,且内部是通过LinkedHashMap
来实现HashMap
由数组+链表
组成,数组是HashMap
的主体,链表则是主要为了解决 哈希冲突而存在的(“拉链法解决冲突”), JDK1.8以后在 解决哈希冲突 时有了变化,当链表长度 大于阀值(默认为8)时,将链表转为红黑树,以减少搜索时间,线程不安全LinkedHashMap
继承自HashMap
,所以底层是基于 拉链式散列结构
,即由 数组+链表/红黑树
组成,LinkedHashMap
在前面结构的基础上,另外增加了一条 双向链表
,使上面的结构可以 保持键值对 的 插入顺序。同时通过对链表进行对应的操作,实现了访问顺序相关逻辑,线程不安全数组+链表
组成,数组使HashMap
的主体,链表则是主要为了解决哈希冲突而存在的,线程安全ArrayList
的toArray()
方法Arrays
的asList()
方法, 不能对List增删,只能查改一.最常见方式(未必最佳)
通过 Arrays.asList(strArray)
方式,将数组转换List后,不能对List增删,只能查改,否则抛异常。
关键代码:List list = Arrays.asList(strArray);
List list = Arrays.asList(1, 2, 3);
list.remove(2);
System.out.println(list);
结果
Exception in thread "main" java.lang.UnsupportedOperationException
at java.util.AbstractList.remove(AbstractList.java:161)
原因解析:
Arrays.asList(strArray)
返回值是java.util.Arrays
类中一个私有静态内部类java.util.Arrays.ArrayList
,它并非java.util.ArrayList
类。ava.util.Arrays.ArrayList
类具有 set(),get(),contains()
等方法,但是不具有添加add()或删除remove()方法,所以调用add()方法会报错。
使用场景:Arrays.asList(strArray)方式仅能用在将数组转换为List后,不需要增删其中的值,仅作为数据源读取使用。
二.数组转为List后,支持增删改查的方式
通过ArrayList的构造器,将Arrays.asList(strArray)
的返回值由java.util.Arrays.ArrayList
转为java.util.ArrayList
。
关键代码:ArrayList
String[] strArray = new String[2];
ArrayList<String> list = new ArrayList<String>(Arrays.asList(strArray)) ;
list.add("1");
System.out.println(list);
使用场景:需要在将数组转换为List后,对List进行增删改查操作,在List的数据量不大的情况下,可以使用。
三.通过集合工具类Collections.addAll()方法(最高效)
通过Collections.addAll(arrayList, strArray)
方式转换,根据数组的长度创建一个长度相同的List,然后通过Collections.addAll()
方法,将数组中的元素转为二进制,然后添加到List中,这是最高效的方法。
关键代码:
ArrayList< String> arrayList = new ArrayList
String[] strArray = new String[2];
ArrayList< String> arrayList = new ArrayList<String>(strArray.length);
Collections.addAll(arrayList, strArray);
arrayList.add("1");
System.out.println(arrayList);
使用场景:需要在将数组转换为List后,对List进行增删改查操作,在List的数据量巨大的情况下,优先使用,可以提高操作速度。
注:附上Collections.addAll()方法源码:
public static <T> boolean addAll(Collection<? super T> c, T... elements) {
boolean result = false;
for (T element : elements)
result |= c.add(element);//result和c.add(element)按位或运算,然后赋值给result
return result;
}
1.是否保证线程安全: ArrayList
和LinkedLis
t都是不同步的,不保证线程安全
2.底层数据结构: ArrayList
底层使用的是Object数组
,LinkedList
底层使用的是双向链表数据结构
3.插入和删除是否受元素位置的影响:
ArrayList
采用数组存储,所以插入删除受元素位置影响
如:执行 add(E e) 方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是O(1)。
但是如果要在指定位置 i 插入和删除元素的话( add(int index, E element) )时间复杂度就为 O(n-i)。
因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。
LinkedList
采用链表存储,插入删除不受元素位置的影响,都是类似o(1)
,而数组类似o(n)
4.是否支持快速随机访问: ArrayList
支持,而LinkedList
而不支持高效的随机元素访问
快速随机访问就是通过元素的序号快速获取元素对象(对应get(index x)方法)
5.内存空间占用: ArrayList
的空间浪费主要体现在list
列表的结尾会预留一定的容量空间,而LinkedList
的空间花费则体现 在它的每个元素都需要消耗比ArrayList
更多的资源(因为要存放直接后继和直接前驱以及数据)
list 的遍历方式选择:
RandomAccess
接口的List
,优先选择普通for循环
,其次foreach
RandomAccess
接口的List
,优先选择iterator
遍历(foreach
遍历底层也是通过iterator
实现的)补充:数据结构基础之双向链表
双向链表也称双链表,是链表的一种,它的每个数据结点中都有 两个指针,分别指向 直接前驱和 直接后继。
从双向链表的任意一个结点开始,都可以很方便地访问它的 前驱结点和后继结点。
一般构建双向循环链表,也是LinkedList
底层使用的是双向循环链表数据结构,如下:
ArrayList
不是同步的,所以在不需要保证线程安全时使用
Vector
类的所有方法都是同步的。可以由两个线程安全地访问一个Vector
对象,但是一个线程访问Vector
的话代码要在同步操作上耗费大量的时间
相同: 都是返回第一个元素,并在队列中删除返回的对象
不同点: 如果没有元素poll()
会返回null
,而remove()
会直接抛出NoSuchElementException
异常
第一次添加元素时,为容器赋初始容量为16,
再把 key 根据 扰动函数 得出哈希值,和容量减一按位与运算,得到要存到数组中的 索引,存到数组中,
哈希值重了的话会出现哈希冲突,会在数组对应位置形成链表,链表超过了 8个 时会形成红黑树
形成红黑树的俩条件
链表长度>=8,HashMap数组到64
如果长度>8但数组长度未到64则会扩容,不会树化没;超过8就不会扩容
if (binCount >= TREEIFY_THRESHOLD - 1) // 如果链表长度大于等于8
treeifyBin(tab, hash); //将链表转为红黑树
if (++size > threshold) //如果元素数量大于临界值,则进行扩容
resize();
如果一个桶采用了树形结构存储,其他桶是不是也采用树形结构存储?
结论是,如果其他桶中bin的数量没有超过TREEIFY_THRESHOLD
,则用链表存储,如果超过TREEIFY_THRESHOLD
,则用树形存储。
由链表变成红黑树也只是当前桶挂载的bin会进行转换,不会影响其它桶的数据结构
JDK1.8前
jdk1.8前HashMap
底层是 数组和链表
结合使用,就是链表散列
。
HashMap
通过key
的hashCode
经过 扰动函数
处理过后的到hash值
,然后通过(n - 1) & hash
判断当前元素存放的位置(n指数组的长度),如果当前位置 存在元素的话,就 判断该元素与 要存入的元素的 hash值
以及key
是否相同, 相同的话, 直接覆盖,不相同就通过 拉链法
解决冲突
扰动函数:HashMap
的hash
方法。使用扰动函数后可以减少碰撞,是为了防止一些实现比较差的hashCode
方法
JDK 1.8 HashMap 的 hash 方法源码:
JDK 1.8 的 hash方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变
static final int hash(Object key) {
int h;
// key.hashCode():返回散列值也就是hashcode
// ^ :按位异或
// >>>:无符号右移,忽略符号位,空位都以0补齐
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
对比一下 JDK1.7的 HashMap 的 hash 方法源码
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
两个方法相比较,JDK1.7的性能会差点,因为扰动了4次
拉链法 :将链表数组与数组相结合。 也就是创建个链表数组,数组中的每一格就是一个链表,若遇到哈希冲突,则将冲突的值加到链表中即可
JDK1.8后
JDK1.8后在解决哈希冲突时有了变化,当链表长度大于阀值(默认为8)时,将链表转为红黑树,以减少搜索时间
ThreeMap
,ThreeSet
以及JDK1.8之后的HashMap
底层都用到了红黑树。
红黑树就是为了解决 二叉查找树的缺陷,因为二叉树在某些情况下会退化成一个线性结构
TREEIFY_THRESHOLD=8,MIN_TREEIFY_CAPACITY=64
,如果链表长度超过8,但整个HashMap的数组没到64,那么只会扩容,不会树化resize
(扩容两倍后重排)1.线程是否安全:HashMap
是非线程安全的,Hashtable
是线程安全的。
HashTable
内部的方法基本都经过synchronized
修饰。(如要保证线程安全还是使用ConcurrentHashMap
吧)
2.效率: 因为线程安全的问题,HashMap
要比HashTable
效率要高点。HashTable
基本被淘汰,不要用
3. 对Null key和Null value的支持: HashMap
中,null
可作为键, 但只有一个,可以有 一个或多个 键对应的值为null
。HashTable
中put进去的键值有一个为null
,会抛出NullPointerExecption
4. 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
5.初始容量大小和每次扩充容量大小的不同 :
①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。
HashMap 默认的初始化大小为16。
之后每次扩充,容量变为原来的2倍。
②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充
为2的幂次方大小(HashMap 中的 tableSizeFor() 方法保证,下面给出了源代码)。
也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。
为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。
Hash 值的范围值-2147483648到2147483647,前后加起来大概40亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。
但问题是一个40亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。
用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数下标。
这个数组下标的计算方法是“ (n - 1) & hash ”。(n代表数组长度)。这也就解释了 HashMap 的长度为什么是2的幂次方。
这个算法应该如何设计呢?
我们首先可能会想到采用%取余的操作来实现。但是,重点是:
“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作
(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。”
并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。
对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择。
然而,假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选择。
基于你的collection的大小,也许向HashMap中添加元素会更快,将map换为TreeMap进行有序key的遍历
HashSet 底层就是基于 HashMap 实现的。HashSet的值存放于HashMap的key上
(HashSet 的源码非常非常少,因为除了 clone() 方法、writeObject()方法、readObject()方法是 HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。
HashMap | HashSet |
---|---|
实现了Map接口 | 实现了Set接口 |
存储键值对 | 仅存储对象 |
调用put() 向map中添加元素 |
调用add() 方法向Set中添加元素 |
HashMap使用键(key) 计算Hashcode |
HashSet使用成员对象计算Hashcode值 ,对两个对象来说 Hashcode 可能相同,所以 equals() 用来判断对象的相等性,如果两个对象不同的话,返回 false |
HashMap比HashSet快,因为使用唯一的键获取对象 | HashSet比HashMap慢 |
HashMap线程不安全,数组+链表+红黑树
HashTable线程安全,锁住整个对象,数组+链表
ConcurrentHashMap线程安全,CAS+同步锁,数组+链表+红黑树
HashMap的key,value均可为null,其他两个则不支持
ConcurrentHashMap
和 Hashtable
的区别主要体现在实现线程安全的方式上不同。
底层数据结构:
jdk1.7的ConcurrentHashMap
底层采用分段的数组+链表实现,
jdk1.8采用的数据结构和HashMap1.8
一样,数组+链表/红黑二叉树
。
HashTable
和JDK1.8之前的HashMap
的底层数据结构类似,都是数组+链表
,
数组是HashMap
的主体,链表则是为了解决哈希冲突而存在的
实现线程安全的方式(重要):
1.jdk1.7时,ConcurrentHashMap(分段锁)
对整个桶数组进行了分割分段(Segment),每把锁只锁容器其中一部分数据,多线程访问容器里不同的数据段的数据,就不会出现锁竞争,提高并发访问率
jdk1.8时已经放弃了Segment
的概念,直接使用Node数组 + 链表 + 红黑树
的数据结构来实现,并发控制使用synchronized
和CAS
来操作。
2. HashTable(同一把锁):
使用synchronized
来保证线程状态,效率低下。
当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用put()
添加元素,另一个线程不能使用put()
添加元素,也不能使用get()
,竞争会越来越激烈效率越低
JDK1.7:
首先将数据分为一段一段的存储,然后给 每一段数据配一把锁,当一个线程占用锁访问其中一个数据时,其他段数据也能被其他线程访问
ConcurrentHashMap是由Segment数据结构和HashEntry数据结构组成
Segment
实现了ReentrantLock
,所以Segment
是一种可重入锁,扮演锁的角色。HashEntry
用于存储键值对数据
static class Segment<K,V> extends ReentrantLock implements Serializable { }
一个ConcurrentHashMap
里包含一个Segment
数组。Segment
的结构和HashMap
类似,是一种数组和链表结构,一个Segment
包含一个HashEntry
数组,每个HashEntry
是一个链表结构的元素,每个Segment
守护着一个HashEntry
数组里的元素,当对HashEntry
数组的数据进行修改时,必须首先获得对应的Segment
的锁
JDK1.8:
ConcurrentHashMap
取消了Segment
分段锁,采用CAS
和synchronized
来保证并发安全。
数据结构跟HashMap
的结构类似,数组+链表/红黑二叉树
synchronized
只锁定当前链表或红黑二叉树的首结点,这样只要 hash不冲突,就不会产生并发,效率又提升了
hash
定位数组的索引坐标,是否有Node节点,没有则使用CAS进行添加(链表的头节点),添加失败则进行下次循环f!=null
,则使用synchronized
锁住f元素(链表/红黑二叉树的投元素)Node(链表结构)
则执行链表的添加操作TreeNode(树形结构)
则执行树添加操作ArrayList
多了同步化机制(线程安全),因为效率较低,不建议使用HashMap
多了个线程安全JDK1.5后出现了 Java.util.concurrent
并发包的出现,很多集合有了自己对应的线程安全类,如HashMap
对应的线程安全类就是ConcurrentHashMap
迭代器是一种设计模式,它是一个对象,它可以遍历并选择序列中的对象,而开发人员不需要了解该序列的底层结构。
迭代器通常被称为“轻量级”对象,因为创建它的代价小
Java中的Iterator功能比较简单,并且只能单向移动:
(1) 使用方法iterator()
要求容器返回一个Iterator
。第一次调用Iterator
的next()
方法时,它返回序列的第一个元素。注意:iterator()
方法是java.lang.Iterable
接口,被Collection
继承。
(2) 使用next()
获得序列中的下一个元素
(3) 使用hasNext()
检查序列中是否还有元素
(4) 使用remove()
将迭代器新返回的元素删除
Iterator
是Java迭代器最简单的实现,为List
设计的ListIterator
具有更多的功能,它可以从两个方向遍历List
,也可以从List中插入和删除元素
数据结构
LinkedList底层的数据结构是基于双向循环链表的,每个节点分为头部、尾部以及业务数据,
前一个节点尾部指向后一个节点的头部,后一节点的头部指向前一个节点的尾部
但对于头结点和尾节点比较特殊,头结点的头部没有上一个结点,从上图可知并没有指向,尾节点的尾部也没有指向下一个结点。
所以,如可以知道链表是由一个一个节点构成,可以定义一个结点类Node,如下:
//用来表示一个节点
public class Node {
Node previous;//头部,用来指向上一个节点
Object obj;
Node next;//尾部,用来指向下一个节点
get()..set()...有参无参
添加数据add
public class MyLinkedList {
private Node first;//定义一个头结点
private Node last;//定义一个尾节点
private int size;
//添加业务数据
public void add(Object obj){
//new一个结点出来
Node n = new Node();
//如果是一个新的链表,没有任何数据
if(first==null){
//从图示可知,这个时候新增的结点既是头结点也是尾节点,头结点的头部没有任何指向所以设置为null,
//尾节点的尾部没有任何指向,所以也为null,真正的业务数据放在obj属性里面
n.setPrevious(null);
n.setObj(obj);
n.setNext(null);
first = n;
last = n;
}else{
//这个时候我们添加下一个结点,直接往last节点后增加新的节点
n.setPrevious(last);
n.setObj(obj);
n.setNext(null);
//当前结点尾部要指向新添加进来的结点
last.setNext(n);
//此时,新加进来的结点就变成了尾节点
last = n;
}
size++;
}
public int size(){
return size;
}
}
查询数据get
public Object get(int index){ //2
// 0 1 2 3 4
Node temp = node(index);
if(temp!=null){
return temp.obj;
}
return null;
}
public Node node(int index){
Node temp = null;
if(first!=null){
temp = first;
for(int i=0;i<index;i++){
temp = temp.next;
}
}
return temp;
}
删除数据remove
只需要打断原有的指向关系,重新连接指向,就可以删除指定位置处的数据,非常高效。
如果是ArrayList删除数据,上面2节点,将会被后面的3节点取代,它要移位,后面的所有节点都要跟着移位,所以ArrayList效率比较低。
public void remove(int index){
Node temp = node(index);
if(temp!=null){
Node up = temp.previous;
Node down = temp.next;
up.next = down;
down.previous = up;
size--;
}
}
按功能来分:输入流(input)、输出流(output)。
按类型来分:字节流和字符流。
字节流处理单位为 1 个字节,字符流处理单位为 2 个字节。
BIO:Block IO
同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。
NIO:New IO
同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。
AIO:Asynchronous IO
是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机制
Files.exists():检测文件路径是否存在。
Files.createFile():创建文件。
Files.createDirectory():创建文件夹。
Files.delete():删除一个文件或目录。
Files.copy():复制文件。
Files.move():移动文件。
Files.size():查看文件个数。
Files.read():读取文件。
Files.write():写入文件
//方式一:对象.getClass(),获取类中的字节码文件
Class class1 = p1.getClass();
//方式二:类.class:需要输入一个明确的类,任意一个类型都有一个静态的class属性
Class class3 = Person.class;
//方式三:Class.forName(String className):className必须是全路径名称
Class class4 = Class.forName("cn.xbmchina.Person");
反射指程序可以访问、检测和修改它本身状态或行为的一种能力
通过反射可以获取字节码文件对象,使用字节码文件对象可以获取到一个类的所有信息,包括私有
可以动态创建,动态赋值,动态调用
为了保存在内存中的各种对象的状态(也就是实例变量,不是方法),并且可以把保存的对象状态再读出来。
Java提供一种比你自己写要好的保存对象状态的机制,就是序列化
序列化: 把Java对象转化为字节流的过程
反系列化: 把字节流转为Java对象的过程
什么情况下需要序列化:
如何实现序列化? 实现Serializable
接口即可,一标记接口,不包含任何方法或字段,仅用于标识可序列化的语义
注意事项: transient 修饰的属性,是不会被序列化的 静态static的属性,他不序列化。 实现这个Serializable 接口的时候,一定要给这个 serialVersionUID 赋值
关于 serialVersionUID 的描述: 序列化运行时使用一个称为 serialVersionUID 的版本号与每个可序列化类相关联,该序列号在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。如果接收者加载的该对象的类的 serialVersionUID 与对应的发送者的类的版本号不同,则反序列化将会导致 InvalidClassException。可序列化类可以通过声明名为 “serialVersionUID” 的字段(该字段必须是静态 (static)、最终 (final) 的 long 型字段)显式声明其自己的 serialVersionUID
如果试图序列化一个不可序列化的对象怎么办?
将得到一个 RuntimeException
异常:主线程中出现异常 java.io.NotSerializableException
。
当某些变量不想被序列化,又不适合使用static关键字
声明,那么此时就需要用transient关键字
来声明该变量。
transient
关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;
当对象被反序列化时,被transient
修饰的变量值不会被持久化和恢复。transient只能修饰变量,不能修饰类和方法
注: 对于某些类型的属性,其状态是瞬时的,这样的属性是无法保存其状态的。例如一个线程属性或需要访问IO、本地资源、网络资源等的属性,对于这些字段,我们必须用transient关键字标明,否则编译器将报措
引用文章
在不动源码的情况下,动态地加入其他代码,运行时动态生成代理类
原理区别:
动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler
来处理。
而cglib动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。
1、如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP
2、如果目标对象实现了接口,可以强制使用CGLIB实现AOP
3、如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换
如何强制使用CGLIB实现AOP?
(1)添加CGLIB库,SPRING_HOME/cglib/*.jar
(2)在spring配置文件中加入
JDK动态代理和CGLIB字节码生成的区别?
(1)JDK动态代理只能对实现了接口的类生成代理,而不能针对类
(2)CGLIB是指定的类生成一个子类,覆盖其中的方法 因为是继承,所以该类或方法最好不要声明成final
多线程是一种机制,它允许在程序中并发的执行多个线程,且每个线程间相互独立
并行: 在同一个时间刻度上同时执行
并发: 在同一个时间段同时执行
进程: 正在执行的程序
线程: 具有完成独立任务的一条执行路径
一个程序下至少有一个进程,一个进程下至少有一个线程,一个线程下也可以有多个线程来增加程序的执行速度
守护线程是 运行在后台的一种特殊进程
它独立于 控制终端 并且 周期性地执行 某种任务或者等待 处理某些发生的事件
在Java中 垃圾回收线程 就是 特殊的守护线程
Thread
重写run()
方法,创建对象,调用start()
启动线程Runnable
接口,重写run()
,创建Runnable
对象,创建Thread
实现对象,把Runnable
对象包装成Thread
,调用start()
启动Callable
接口 ,会返回结果,且可以抛出经过检查的异常线程一定不能直接调用run()
方法执行,否则线程会被当成是普通类和普通方法执行,不会出现随机性
优缺点:
继承Thread
类,简单,这个线程就不能再继承其他类了,功能受到约束
实现Runnable
接口,可以继续实现其他接口,继承其他类。功能可以拓展,Runnable
对象适合做线程池
实现Callable
接口,可以继续实现其他接口,继承其他类。功能可以拓展,可以获得线性的执行结果
1.synchronized
修饰同步代码块或同步方法
synchronized (同一个数据){}
同一个数据:就是N条线程同时访问同一个数据
public synchronized 返回值 方法名(){}
3. wait
和notify
等待唤醒
Runnable
没有返回值,Callable
可以拿到返回值,返回结果,可以看作是Runnable
的补充
新建,就绪,运行,阻塞,死亡
新建:就是刚使用new方法,new出来的线程;
就绪:就是调用的线程的start()
方法后,这时候线程处于等待CPU分配资源阶段,谁先抢的CPU资源,谁开始执行;
运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能;
阻塞:在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态,比如sleep()
、wait()
之后线程就处于了阻塞状态,这个时候需要其他机制将处于阻塞状态的线程唤醒,比如调用notify
或者notifyAll()
方法。唤醒的线程不会立刻执行run方法,它们要再次等待CPU分配资源进入运行状态;
销毁:如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源;
NEW
初始化,尚未启动RUNNABLE
可运行/运行状态BLOCKED
阻塞(被同步锁或者IO锁唤醒)WAITING
永久等待状态TIMED_WAITING
等待指定的时间重新被唤醒状态TERMINATED
执行完成,终止状态wait
不用抛异常, 只能在同步方法和同步代码块使用 ,会释放锁
sleep
要抛异常, 可以在任何地方使用,没有释放锁,让出线程,监控,状态依然保持
用法不同:sleep
时间到会自动恢复,wait
可以使用notify()/notifyAll()
直接唤醒
notify
: 只会唤醒一个线程,可能会导致死锁
notifyAll
: 将全部线程由等待池移到锁池
start()
方法用于启动线程,run()
方法用于执行线程的运行时代码
run()
可以重复调用,start()
只能调用一次
修饰变量,不会发生阻塞,解决变量在多个线程中的可见性
synchronized
关键字解决的是多个线程之间访问资源的同步性,
synchroized
关键字可以保证被它修饰的方法或代码块在某一时刻只能有一个线程执行
另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。
如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized 效率低的原因。
庆幸的是在 Java 6 之后Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。
JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
synchronized关键字最主要的三种使用方式:
static
表明这是该类的一个 静态资源, 不管new
了多少个对象, 只有一份, 所以对 该类的 所有对象 都加了锁)。所以一个 线程A调用 一个实例对象的 非静态synchronized
方法,而线程B 需要调用这个实例对象所属类的静态synchronized
方法,是允许的,不会出现互斥现象,因为访问静态synchronized
方法占用的锁,是当前类锁,而访问非静态synchronized
方法占用的锁是当前实例对象的锁synchronized(this)
代码块也是锁定当前对象的。synchronized
关键字加到static
静态方法和synchronized(class)
代码块上都是给Class
类上锁。synchronized
关键字加到非static
静态方法上是给对象实例加锁。synchronized(String s)
因为JVM
中,字符串常量池具有缓冲功能synchronized 关键字的具体使用:
单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单例模式的原理
呗!
双重校验锁实现对象单例(线程安全)
public class Singleton {
private volatile static Singleton user;
private Singleton() { }
public static Singleton getUser() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
if (user== null) {
//类对象加锁
synchronized (Singleton.class) {
if (user== null) {
user= new Singleton();
}
}
}
return user;
}
}
user采用 volatile
关键字修饰也是很有必要的, user= new Singleton();
这段代码其实是分为三步执行:
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。
指令重排在单线程环境下不会出先问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例
例如,线程 T1 执行了 1 和 3,此时 T2 调用getUniqueInstance()
后发现uniqueInstance
不为空,因此返回 uniqueInstance
,但此时 uniqueInstance
还未被初始化。
使用 volatile
可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
volatile
是变量修饰符;synchorized
是修饰类,方法,代码块volatile
仅能实现变量的可见性,不能保证原子性;而synchorized
能保证变量的可见性和原子性volatile
不会造成线程阻塞;synchorized
可能会synchorized
可以给类,方法,代码块加锁;而Lock
只能给代码块加锁Lock
是接口,会造成死锁, 可以知道有没有成功获取锁synchronized
是关键字,不会造成死锁,线程会一直等待,不能响应中断synchronized
不需要手动获取锁和释放锁,发生异常会自动释放锁,不会造成死锁;Lock
需要自己加锁和释放锁,如果使用不当没有unLock()
去释放锁就会造成死锁都是可重入锁
ReentrantLock
依赖于JDK
实现
synchronized
依赖于JVM
实现,多三项功能
通过start()
调用run()
达到多线程
单独调用run(),同步执行
通过start()
调用run()
,异步执行
启动一个线程是调用start()
,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM调度并执行,但不意味着线程就会立马执行
run()
可以产生必须退出的标志来停止一个线程
线程池提供了一种限制和管理资源,每个线程池还维护一些基本统计信息,例如已完成任务的数量
newCachedThreadPool
创建可缓存的线程池
newFixedThreadPool
创建固定大小的线程池
newSingleThreadExecutor
创建单线程的线程池
newScheduledThreadPool
创建大小无限的线程池
新建: 创建线程
就绪: 当start()
方法返回后 线程处于 就绪状态
运行: 获得CPU时间,进入运行状态
阻塞: sleep
方法, 获取被 其他线程持有的锁, 等待某个触发时间
死亡: run
方法执行完后,或者因为某些异常产生退出了 run 方法,该线程的生命周期结束
execute
: 只能执行Runnable
类型的任务,无法判断任务是否成功完成,无返回值
submit
: 可以执行Callable
和Runnable
类型的任务,但Runnable
执行无返回值,
返回个future
对象,可以用这个future
来判断任务是否完成
两个以上线程,互相持有对方所需要的资源,导致线程处于相互等待状态,无法执行
产生原因:1.系统资源不足 2.进程运行推进的顺序不合适 3.资源分配不当
产生死锁的四个必要条件:
避免:
1. 加锁顺序相同
2. 加锁时限,设置超时时间
3. 死锁检测
4. 尽量减少同步的代码块
CAS是英文单词CompareAndSwap
的缩写,中文意思是:比较并替换。CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。
CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作
锁分类与使用
线程可以随意抢占CPU的资源,谁也不知道下一刻谁会抢到资源,这导致线程访问共享数据时出现了问题
锁机制是将可能出现问题的代码用锁对象锁起来,被锁起来的代码就叫同步代码块,同一时间只能有一个线程来访问这个同步代码块.这类似于数据库中事务这个概念
不是特指两种类型,指看待并发同步的角度
乐观锁:
每次拿数据时都认为别人不会修改,所以不会上锁,但在更新时会判断在此期间有没有人更新这个数据,但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般使用数据版本机制或CAS操作
实现
适用于读操作多的应用类型,可以提高吞吐量,在Java中java.util.concurrent.atomic
包下的原子变量类就是使用了乐观锁的一种实现方式CAS(Compare and Swap 比较并交换)
实现的
数据版本机制: 实现有两种,版本号和时间戳
版本号方式:
在数据表上加上个数据版本号version
字段,表示数据被修改的次数,当数据被修改时,version
值会加一。 当线程A要更新数据值时,在读取数据的同时也会读取version
值,在提交更新时,若刚才读取到的version
值相等时才更新,否则重试更新操作,直到更新成功
核心SQL代码:
update table set xxx=#{xxx}, version=version+1 where id=#{id} and version=#{version};
CAS操作:
(Compare and Swap比较并交换) ,当多个线程尝试使用CAS同时更新一个变量时,只有其中一个线程能更新变量的值,而其他线程都失败,失败的线程并不会被挂起,而是这次竞争失败,可以再次尝试
CAS操作包含三个操作数 需要读写的内存位置(V),进行比较的预期原值(A)和拟如的新值(B)
如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B,否则处理器不做任何操作
悲观锁:
总是假设最坏的情况,对于同一个数据的并发操作,一定会发生修改,哪怕没改也认为改了,所以每次拿都上锁,这样别人想拿到这个数据就会阻塞直到拿到锁,synchronized
就是悲观锁,悲观的认为不加锁并发操作一定会出问题
在对任意记录进行修改前,先尝试为该记录加上排他锁(exclusive locking)
如果加锁失败,说明该记录正在被修改,那当前查询要等待或抛出异常,具体响应方式由开发者根据实际决定
如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁
期间如果有其他对该记录做修改或排他锁的操作,都会等待我们解锁或直接抛出异常
悲观所适合写操作多的情况,乐观锁适合读操作多的情况,不加锁会带来大量的性能提升
悲观锁在Java中的使用,就是利用各种锁。
乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新
独享锁: 该锁一次只能被一个线程所持有
共享锁: 该锁可以被多个线程所持有
Java ReentrantLock
是独享锁。但Lock的另一个实现类ReadWriteLock
,其读锁是共享锁,其写锁是独享锁。
读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的
独享锁与共享锁通过AQS来实现的,通过实现不同的方法,来实现独享或者共享
synchronized是独享锁
独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现
互斥锁在java中具体实现是ReentrantLock
读写锁在java中具体实现是ReadWriteLock
又名递归锁,指在同一个线程在外层方法获取锁时,在进入内层方法会自动获取锁
synchronized也是个可重入锁,可重入锁可一定程度避免死锁
synchronized void setA() throws Exception{
Thread.sleep(1000);
setB();
}
synchronized void setB() throws Exception{
Thread.sleep(1000);
}
如果不是可重入锁的话,setB可能不会被当前线程执行,可能造成死锁
公平锁: 多个线程按照申请锁的顺序来获取锁
非公平锁: 多个线程获取锁的顺序并不是按照申请锁的顺序,可能申请后的线程比先申请的线程优先获取锁,可能会造成优先级反转或饥饿现象
ReentrantLock
通过构造方法指定该锁是否公平锁,默认非公平锁,非公平锁的优点在于吞吐量比公平锁大
synchronized
也是种非公平锁,但不像ReentrantLock
是通过AQS来实现线程调度,所以并没有办法使其变成公平锁
一种锁的设计,不是具体的锁,ConcurrentHashMap
其并发的实现就是通过分段锁的形式来实现高效的并发操作
以ConcurrentHashMap
来说一下分段锁的含义以及设计思想,ConcurrentHashMap
中的分段锁称为Segment,它即类似于HashMap
(JDK7和JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock
(Segment继承了ReentrantLock)。
当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在哪一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。
分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作
指synchronized
锁的三种状态,通过对象监控器在对象头的字段来表明
偏向锁: 一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价
轻量级锁: 当锁时偏向锁时,被另一个线程所访问,偏向锁会升级为轻量级锁,其他线程会通过自旋的方式尝试获取锁,不会阻塞,提高性能
重量级锁: 当锁为轻量级锁时,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数时还没获取到锁,该锁变为重量级锁, 重量级锁会让它申请的线程进入阻塞,性能降低
自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁
好处: 减少上下文切换的消耗
坏处: 循环会消耗CPU
面向连接协议,文件传输量没有限制,数据安全,速度慢
三次握手?
目的是建立可靠的通讯信道,就是数据的发送和接收,主要目的是双方确认自己和对方的发送和接收是正常的
第一次握手:Client 什么都不能确认;Server 确认了对方发送正常,自己接收正常。
第二次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己接收正常,对方发送正常
第三次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己发送、接收正常,对方发送接收正常
四次挥手?
任一方都可在 数据传输结束后发出连接释放的通知,待对方确认后进入半关闭状态。
当另一方也没有数据再发送时,则发出连接释放通知,对方确认后完全关闭TCP连接
客户端-发送一个 FIN,用来关闭客户端到服务器的数据传送
服务器-收到这个 FIN,它发回一 个 ACK,确认序号为收到的序号加1 。和 SYN 一样,一个 FIN 将占用一个序号
服务器-关闭与客户端的连接,发送一个FIN给客户端
客户端-发回 ACK 报文确认,并将确认序号设置为收到序号加1
超文本传输协议,定义了客户端和服务器之间文件传输的沟通方式
HTTP是一种通信协议,它允许将超文本标记语言HTML文档从web服务器传送到客户端的浏览器面前,无连接,无状态
为了实现可靠数据传输, TCP 协议的通信双方, 都必须维护一个序列号, 以标识发送出去的数据包中, 哪些是已经被对方收到的。
三次握手的过程即是通信双方相互告知序列号起始值, 并确认对方已经收到了序列号起始值的必经步骤。
如果只是两次握手, 至多只有连接发起方的起始序列号能被确认, 另一方选择的序列号则得不到确认
1.物理层(网卡): 定义的硬件设备标准,用于计算机之间的数据传输,传输bit流。
2.数据链路层(交换机):数据帧,对bit数据格式化,校验。目的是保障数据传输可靠性
3.网络层(路由选择,点到点):IP寻址,通过IP连接网络上的计算机。
4.传输层(端到端):建立了主机端到端的链接。tcp、udp。ipv6传输效率高就和这层有关。
5.会话层(会话控制):管理不同设备之间通信
6.表示层(数据格式转化):对应用层数据编码和数据格式转换,保障不同设备之间通信(windows和linux)。
7.应用层:提供应用接口,为用户直接提供各种网络服务。
@CrossOrigin
运行跨域jsonp
跨域JSON with Padding
,利用script
标签的src
属性 连接可以访问不同源的特性,加载远程返回的 JS函数 来执行
一、PO(persistant object)持久化对象,可以看成是与数据库中的表映射的java对象。
最简单的PO就是对应数据库中某个表的一条记录,多个记录可以用PO的集合。
PO中不应该包含任何数据库的操作。基本上持久对象生命周期和数据库密切相关。
二、VO(Value Object)值对象。通常用于业务层之间的数据传递,和PO一样也仅仅包含数据而已。
但应是抽象出的业务对象,可以和表对象,这根据业务的需求,通常同DTO(数据传输对象),在web上传递。
三、DAO(data access object)数据库访问对象,此对象用于访问数据库。
通常和PO结合使用,DAO中包含了各种数据库的操作方法。通过它的方法,结合PO对数据库进行相关的操作。
四、BO(business object)业务对象,封装业务逻辑的java对象,通过调用DAO方法,结合PO,VO进行业务操作。
五、POJO(plain old java object)简单无规则java对象,顾名思义POJO类的作用是方便程序员使用数据库中的数据表,对于广大的程序员,可以很方便的将POJO类当做对象来进行使用,当然也是可以方便的调用其get、set方法。POJO类也给我们在struts框架中的配置带来很大的方便。
简介理解:
PO:
persistant object 持久对象
最形象的理解就是一个PO就是数据库中的一条记录。
好处是可以把一条记录作为一个对象处理,可以方便的转为其他对象。
BO:
business object业务对象
主要作用是把业务逻辑封装为一个对象。这个对象可以包括一个或多个其它的对象。
举例说明PO和BO的关系:
比如一个简历,有教育经历、工作经历、关系等等
我们可以把教育经历对应一个PO,工作经历对应一个PO,关系对应一个PO。
建立一个对应简历的BO对象处理简历,每个BO包含这些PO。
这样处理业务逻辑时,我们就可以针对BO去处理。
VO:
value object值对象。
ViewObject表现出对象
主要对应界面显示的数据对象。对应一个WEB页面,或者SWT、SWING的一个界面,用一个VO对象对应整个界面的值。
DTO:
Date Transfer object 数据传输对象
主要用于远程调用等需要大量传输对象的地方。
举例说明:
比如我们一张表有100个字段,那么对应的PO就有100个属性。
但是我们界面上只要显示10个字段,客户端用WEB service来获取数据,没有必要把整个PO对象传递到客户端,
这时我们就可以用只有这10个属性的DTO来传递结果到客户端,这样也不会暴露服务端表结构,到达客户端以后,如果用这个对象来对应界面显示,那此时他的身份就转为VO。
POJO:
plain old java object 简单java对象
POJO是最常见最多变得对象,是一个中间对象,也是我们最常打交道的对象。
一个POJO持久化以后就是PO
直接用它传递,传递过程中就是DTO
直接来对应表示层就是VO
DAO:
data access object 数据访问对象
数据访问对象是第一个面向对象的数据库接口,是一个数据访问接口。它可以把POJO持久化为PO,用PO组装出来VO、DTO。
DAO模式是标准的j2EE设计模式之一,开发人员使用这个模式把底层的数据访问操作和上层的商务逻辑分开,一个典型的DAO实现有下列几个组件:
1.一个DAO工厂类
2.一个DAO接口
3.一个实现DAO接口的具体类
4.数据传递对象(有时候叫做值对象)
Forward和Redirect代表了两种请求转发方式:直接转发和间接转发。
直接转发方式(Forward),客户端和浏览器只发出一次请求,Servlet、HTML、JSP或其它信息资源,由第二个信息资源响应该请求,在请求对象request中,保存的对象对于每个信息资源是共享的。
间接转发方式(Redirect)实际是两次HTTP请求,服务器端在响应第一次请求的时候,让浏览器再向另外一个URL发出请求,从而达到转发的目的
HttpServletRequest
对象,HttpServletResponse
对象以及HttpServlet
对象得到Servlet是独立于平台和协议的服务器端的Java应用程序,可以动态生成Web页面,并采用响应
服务器启动通过init()方法
初始化servlet,再根据不同的请求调用doGet()或doPost()方法
,最后通过dostroy()方法
销毁
servlet
是单例的,可以提高性能
四个作用域,两个输出,三个酱油
Get
和Post
请求的参数Cookie,session,application
Cookie
是http对象,客户端和服务端都可以操纵
cookie
是在客户端保存状态,session
是服务端保存状态,
由于cookie
是保存在客户端本地,所以数据容易被窃取,当访问量很多时使用session
则会降低服务器的性能
application
的作用域是整个工程只有一个,可以在不同浏览器之间共享数据,所有人都可以共享,所有也是不安全的
Request
是客户端向服务端发送请求
Response
是服务端对客户端请求做出响应
Session
在servlet中不能直接使用,需要通过getSession()
创建,如果没有设定它的生命周期,或者通过invildate()
方法销毁,关闭浏览器session
就会消失
Application
不能直接创建,存在于服务器的内存中,由服务器创建和销毁
session是一个存在服务器上的类似于一个散列表格的文件。
里面存有我们需要的信息,在我们需要用的时候可以从里面取出来。
类似于一个大号的map,里面的键存储的是用户的sessionid,用户向服务器发送请求的时候会带上这个sessionid。这时就可以从中取出对应的值了。
因为session
底层是由cookie
实现的,当客户端的cookie
被禁用后,session
也会失效
且应尽量少向session
中保存信息,session
的数据保存在服务器端,当有大量session
时,会降低服务器的性能
Request
可以通过getAttribute()
方法直接取值,也可通过getParameter()
取值
Session
需要通过request.getSession().getAttribute()
才能取值
Request
是针对一次请求,Session
是针对整个会话
乱码: 在页面通过contentType,pageEncoding,content
设置编码格式,必须要一致
通过session
的isNew()
可以判断是否是新用户
可以用cookie
来保存信息到客户端,可以连接上一次操作
getParameter()
获取的是客户端
设置的数据, 永远返回字符串
getAttribute()
获取的是服务器
设置的数据, 返回值是任意类型
既然parameter和attribute都是传递参数,为什么不直接使用parameter
呢?
①服务器端不能通过setParameter(key, value)
来添加参数,因为没有这个函数
所以如果需要在服务器端进行跳转,并需要想下个页面发送新的参数时,则没法实现。
但是Attribute
可以,可以通过setAttribute()
,将值放入到request对象
,然后在其他页面使用getAttribute
获取对应的值,这样就达到一次请求可以在多个页面共享一些对象信息
②parameter
返回值是字符串,意味着不能传递其他的对象,如Map,List
,但是attribute则可以存放任意类型的Java对象
可以使用pageContext
对象来设定属性,并指定属性的作用范围,提供了对JSP页面内所有的对象及名字空间的访问
首先初始化过滤器,然后服务器组织过滤器链,所有的请求都必须需要先通过过滤器链,
过滤器链是一个栈,遵循先进后出的原则 ,所有的请求需要经过一个一个的过滤器,执行顺序要根据web.xml
里配置的
的位置前后执行,每个过滤器之间通过chain.doFilter连接, 最后抵达真正请求的资源,执行完后再从过滤器链退出
Servlet:用来处理客户端发送的请求,然后生成响应传给服务器,最后服务器将响应返回给客户端
Filter: 用于对servlet容器调用servlet的过程进行拦截,可以在serlet进行响应处理前后做一些特殊的处理,如日志,权限,编码等
设计模式总结
单例模式的核心是保证一个类只有一个实例,并提供一个访问实例的全局访问点
只生成一个实例,减少了系统性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,可通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决
使用场景:
实现方式 | 优缺点 |
---|---|
饿汉式 | 线程安全,调用效率高 ,但是不能延迟加载 |
懒汉式 | 线程安全,调用效率不高,能延迟加载 |
双重检测锁式 | 由于JVM底层内部模型原因,偶尔会出问题。不建议使用 |
静态内部类式 | 线程安全,资源利用率高,可以延时加载 |
枚举单例 | 线程安全,调用效率高,但是不能延迟加载 |
饿汉式
public class User{
//私有化构造方法,限制直接构造,只能调用 getInstance() 方法获取单例对象
private User(){}
// 私有化静态 final成员,类加载直接生成单例对象,比较占用内存
private static final User user=new User();
//提供对外的公共方法获取单例对象
public static User(){
return User;
}
}
饿汉式在 类创建的同时就 实例化一个 静态对象出来,不管之后用不用这个单例,都会占据一定的内存,
但是相应的,在第一次调用时速度也会更快,因为其资源已经初始化完成
懒汉式
public class User{
//私有化构造方法,限制直接构造,只能调用 getInstance() 方法获取单例对象
private User(){}
//静态域初始化为null,为的是需要时再创建,避免像饿汉式那样占用内存
private static User user=null;
//提供对外的公共api获取单例对象
public static User getInstance(){
if(user == null){
synchronized (User.class){
if(user == null){
user = new User();
}
}
}
return lazySingleton;
}
}
在getInstance()
中做了两次null
检查,确保了只有第一次调用单例的时候才会做同步,这样也是线程安全的,同时避免了每次都同步的性能损耗
缺点:有同步锁的性能消耗
静态内部类实现
public class User{
//私有化构造方法,限制直接构造,只能调用 getInstance() 方法获取单例对象
private User(){}
//提供对外的公共方法获取单例对象
public static User getInstance(){
//当getInstance方法第一次被调用的时候,它第一次读取HolderClass.user,内部类HolderClass类得到初始化;
//而这个类在装载并被初始化的时候,会初始化它的静态域,从而创user的实例,由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,并由虚拟机来保证它的线程安全性。
return HolderClass.user;
}
// 静态内部类
private static class HolderClass{
// 声明外部类型的静态常量
private static User user= new User();
}
// 防止反序列化获取多个对象的漏洞
private Object readResolve() throws ObjectStreamException {
return HolderClass.user;
}
}
1.外部类没有static
属性,则不会像饿汉式那样立即加载对象。
2.只有真正调用getInstance()
,才会加载静态内部类。加载类时是线程 安全的instance
是static final
类型,保证了内存中只有这样一个实例存在,而且只能被赋值一次,从而保证了线程安全性.
3.兼备了并发高效调用和延迟加载的优势
getInstance
方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本
优势:兼顾了懒汉模式的内存优化(使用时才初始化)以及饿汉模式的安全性(不会被反射入侵)
劣势:需要两个类去做到这一点,虽然不会创建静态内部类的对象,但是其 Class 对象还是会被创建,而且是属于永久带的对象。
双重检测锁
public class SingletonInstance3 {
// 声明此类型的变量,但没有实例化
private static SingletonInstance3 instance = null;
// 私有化所有的构造方法,防止直接通过new关键字实例化
private SingletonInstance3(){}
// 对外提供一个获取实例的静态方法,
public static SingletonInstance3 getInstance(){
if(instance == null){
SingletonInstance3 s3 = null;
synchronized(SingletonInstance3.class){
s3 = instance;
if(s3 == null){
synchronized(SingletonInstance3.class){
if(s3 == null){
s3 = new SingletonInstance3();
}
}
}
instance = s3;
}
}
return instance;
}
}
该模式将同步内容下方到if内部,提高执行效率不必每次获取对象时都进行同步,只有第一次才同步创建了以后就没必要了。
问题:由于编译器优化原因和JVM底层内部模型原因,偶尔会出问题。不建议使用
枚举
public enum SingletonInstance5 {
// 定义一个枚举元素,则这个元素就代表了SingletonInstance5的实例
INSTANCE;
public void singletonOperation(){
// 功能处理
}
}
枚举本身就是单例模式。由JVM从根本上提供保障!避免通过反射和反序列化的漏洞
缺点:没延迟加载
单例模式的漏洞
1.通过反射的方式我们依然可用获取多个实例(除了枚举的方式)
public static void main(String[] args) throws Exception, IllegalAccessException {
SingletonInstance1 s1 = SingletonInstance1.getInstance();
// 反射方式获取实例
Class c1 = SingletonInstance1.class;
Constructor constructor = c1.getDeclaredConstructor(null);
constructor.setAccessible(true);
SingletonInstance1 s2 = (SingletonInstance1)constructor.newInstance(null);
}
解决方式:在无参构造方法中手动抛出异常控制
// 私有化所有的构造方法,防止直接通过new关键字实例化
private SingletonInstance2(){
if(instance != null){
// 只能有一个实例存在,如果再次调用该构造方法就抛出异常,防止反射方式实例化
throw new RuntimeException("单例模式只能创建一个对象");
}
}
2.通过反序列化的方式也可以破解上面几种方式(除了枚举的方式)
public static void main(String[] args) throws Exception, IllegalAccessException {
SingletonInstance s1 = SingletonInstance.getInstance();
// 将实例对象序列化到文件中
ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("c:/tools/a.txt"));
oos.writeObject(s1);
oos.flush();
oos.close();
// 将实例从文件中反序列化出来
ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("c:/tools/a.txt"));
SingletonInstance s2 = (SingletonInstance) ois.readObject();
ois.close();
}
解决:在单例类中重写readResolve方法并在该方法中返回单例对象
public class SingletonInstance implements Serializable{
// 声明此类型的变量,但没有实例化
private static SingletonInstance instance = null;
// 私有化所有的构造方法,防止直接通过new关键字实例化
private SingletonInstance(){
if(instance != null){
// 只能有一个实例存在,如果再次调用该构造方法就抛出异常,防止反射方式实例化
throw new RuntimeException("单例模式只能创建一个对象");
}
}
// 对外提供一个获取实例的静态方法,为了数据安全添加synchronized关键字
public static synchronized SingletonInstance getInstance(){
if(instance == null){
// 当instance不为空的时候才实例化
instance = new SingletonInstance();
}
return instance;
}
// 重写该方法,防止序列化和反序列化获取实例
private Object readResolve() throws ObjectStreamException{
return instance;
}
}
总结:
1、单例对象占用资源少,不需要延时加载:
枚举式 好于 饿汉式
2、单例对象占用资源大,需要延时加载:
静态内部类式 好于 懒汉式
定义一个创建对象的接口,让其子类自己决定实例化那个工厂类,工厂模式使其创建过程延迟到子类进行
对象间一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新
静态代理
类代理对象与目标对象实现相同的接口或继承相同的父类,需要创建一个代理类
只能为一个被代理类服务,如果需要代理的类比较多,那么会产生过多的代理类。
静态代理在编译时产生class文件,运行时无需产生,可直接使用,效率好。
jdk动态代理
必须实现接口,通过反射来动态代理方法,消耗系统性能。
但是无需产生过多的代理类,避免了重复代码的产生,系统更加灵活。
调用Proxy.newProxyInstance
(3个参数:类加载器,目标对象实现的所有接口,事务处理器对象invocationHandler
,并重写invoke
);生成一个代理实例,通过该代理实例调用方法
cglib动态代理
无需实现接口,通过生成子类字节码来实现,比反射快一点,没有性能问题。
但是由于cglib会继承被代理类,需要重写被代理方法,所以被代理类不能是final类,被代理方法不能是final。
因此,cglib的应用更加广泛一点