目录
- 1、
- 1.0、什么是面向对象
- 1.1、JDK、JRE、JVM之间的区别
- 1.2、什么是字节码
- 1.3、hashCode()与equals()之间的联系
- 1.4、String、StringBuffer、StringBuilder的区别
- 1.5、==和equals方法的区别
- 1.6、重载和重写的区别
- 1.7、List和Set的区别
- 1.8、ArrayList和LinkedList的区别
- 1.9、ArrayList的优缺点?
- 1.10、Array和ArrayList有何区别?什么时候更适合用Array?
- 1.11、遍历一个List有哪些不同的方式?
- 1.12、ArrayList的扩容机制
- 1.13、ConcurrentHashMap的实现原理是什么?
- 1.14、JDK1.8 中为什么使用内置锁 synchronized替换 可重入锁 ReentrantLock?
- 1.15、ConcurrentHashMap的扩容机制
- 1.16、HashMap 与 ConcurrentHashMap 的区别是什么?
- 1.17、JDK1.7和JDK1.8的ConcurrentHashMap实现有什么不同
- 1.18、ConcurrentHashMap的put方法执行逻辑是什么
- 1.19、ConcurrentHashMap的get方法执行逻辑是什么
- 1.20、ConcurrentHashMap 的 get 方法是否要加锁,为什么?
- 1.21、get 方法不需要加锁与 volatile 修饰的哈希桶数组有关吗?
- 1.22、ConcurrentHashMap 不支持 key 或者 value 为 null 的原因?
- 1.23、ConcurrentHashMap 和 Hashtable 的效率哪个更高?为什么?
- 1.24、具体说一下Hashtable的锁机制
- 1.25、jdk1.7到jdk1.8HashMap发生了什么变化
- 1.26、解决hash冲突的办法有哪些?HashMap用的哪种?
- 1.27、为什么在解决hash冲突的时候,不直接用红黑树?而选择先用链表,再转为红黑树?
- 1.28、HashMap默认加载因子是多少?为什么是0.75
- 1.29、HashMap的Put方法
- 1.30、HashMap为什么线程不安全
- 1.31、深拷贝和浅拷贝
- 1.32、HashMap的扩容机制
- 1.33、Java中的异常体系是怎样的
- 1.34、什么时候应该抛出异常,什么时候捕获异常
- 1.35、了解ReentrantLock吗
- 1.36、ReentrantLock中tryLock()和lock()方法的区别
- 1.37、ReentrantLock中的公平锁和非公平锁的底层实现
- 1.38、ReadWriteLock是什么
- 1.39、Sychronized的偏向锁、轻量级锁、重量级锁
- 1.40、synchronized为什么是非公平锁?非公平体现在哪些地方
- 1.41、Sychronized和ReentrantLock的区别
- 1.42、谈谈你对AQS的理解,AQS如何实现可重入锁
- 1.43、什么是泛型?有什么作用
- 1.44、泛型的使用方式有哪几种
- 1.45、Java泛型的原理是什么?什么是类型擦除
- 1.46、什么是泛型中的限定通配符和非限定通配符
- 1.47、Array中可以用泛型吗
- 1.48、项目中哪里用到了泛型
- 1.49、反射
- 1.50、如何获取反射中的Class对象
- 1.51、Java反射API有几类?
- 1.52、反射机制的应用有哪些?
- 1.53、什么是注解?
- 1.54、注解的解析方法有哪几种
- 1.55、序列化和反序列化
- 1.56、如果有些字段不想进行序列化怎么办
- 1.57、为什么不推荐使用JDK自带的序列化
- 1.58、静态变量会被序列化吗
- 1.56、如果有些字段不想进行序列化怎么办
- 1.57、为什么不推荐使用JDK自带的序列化
- 1.58、静态变量会被序列化吗
对比面向过程,面向对象是不同的处理问题的角度。面向过程更注重事情的每一个步骤及顺序,面向对象更注重事情有哪些参与者,及各自需要做什么。
比如:洗衣机洗衣服。面向过程就会将任务拆解成一系列的函数:1.打开洗衣机 2.放衣服 3.放洗衣粉 4.清洗
面向对象会拆出人和洗衣机两个对象,人:打开洗衣机、放衣服、放洗衣粉,洗衣机只需要清洗即可。
面向过程比较直接高效,面向对象更易于维护、扩展和复用。
面向对象有三大特性:封装、继承和多态
封装的意义在于明确标识出允许外部使用的所有成员函数和数据项,内部细节对外部调用透明,外部调用无需关心内部的实现,Java中有两个比较经典的场景
Javabean 的属性私有,对外提供 get、set 方法访问
private String name;
public void setName(String name){
this.name = name;
}
orm 框架:操作数据库,我们不需要关心链接是如何建立的、sql是如何执行的,只需要引入 mybatis,调相应的方法即可
继承:继承父类的方法,并作出自己的改变和扩展
多态:比如都是动物类型的对象,执行 run 方法,cat 猫类和 dog 狗类会表现出不同的行为特征。
我们写出来的 Java 代码,想要运行,需要先编译成字节码,那就需要编译器,而 JDK 中就包含了编译器 javac,编译之后的字节码,想要运行,就需要一个可以执行字节码的程序,这个程序就是 JVM Java虚拟机,专门用来执行 Java 字节码的。
JDK包含JRE,JRE包含JVM。
Java之所以可以“一次编译,到处运行”,一是因为JVM针对各种操作系统、平台都进行了定制,二是因为无论在什么平台,都可以编译生成固定格式的字节码(.class文件)供JVM使用。因此,也可以看出字节码对于Java生态的重要性。
Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行。
在Java中,每个对象都可以调用自己的hashCode()方法得到自己的哈希值(hashCode),我们可以利用hashCode来做一些提前的判断,比如:
在Java的集合类的实现中,在比较两个对象是否相等时,会先调用对象的hashCode()方法得到哈希值进行比较,如果hashCode不相同,就可以直接认为这两个对象不相同。如果hashCode相同,那么就会进一步调用equals()方法进行比较,可以用来最终确定两个对象是不是相等的。所以如果我们重写了equals()方法,一般也要重写 hashCode() 方法。
String是不可变的,如果尝试去修改,会新生成一个字符串对象,StringBuffer和StringBuilder是可变的【补充:String 不是基本数据类型,是引用类型,底层用 char 数组实现的】,String类利用了 final 修饰的 char 类型数组存储字符,它里面的对象是不可变的,也就可以理解为常量,显然线程安全。
private final char value[];
StringBuffer是线程安全的,StringBuffer 属于可变类,对方法加了同步锁,线程安全【说明:StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的】
StringBuilder是线程不安全的
执行效率:StringBuilder > StringBuffer > String
线程安全:当多个线程访问某一个类(对象或方法)时,对象对应的公共数据区始终都能表现正确,那么这个类(对象或方法)就是线程安全的
对于三者使用的总结:
String
StringBuilder
StringBuffer
String 为什么要设计为不可变类?不可变对象的好处是什么?
主要的原因主要有以下三点:
不可变对象指对象一旦被创建,状态就不能再改变,任何修改都会创建一个新的对象,如 String、Integer及其它包装类.不可变对象最大的好处是线程安全.
String 字符串修改实现的原理?
当用 String 类型来对字符串进行修改时,其实现方法是首先创建一个 StringBuilder,其次调用 StringBuilder 的 append() 方法,最后调用 StringBuilder 的 toString() 方法把结果返回。
String str = “i” 和 String str = new String(“i”) 一样吗?
不一样,因为内存的分配方式不一样。String str = “i” 的方式,Java 虚拟机会将其分配到常量池中。
String str = new String(“i”) 则会被分到堆内存中。
public class StringTest {
public static void main(String[] args) {
String str1 = "abc";
String str2 = "abc";
String str3 = new String("abc");
String str4 = new String("abc");
System.out.println(str1 == str2); // true
System.out.println(str1 == str3); // false
System.out.println(str3 == str4); // false
System.out.println(str3.equals(str4)); // true
}
}
在执行 String str1 = “abc” 的时候,JVM 会首先检查字符串常量池中是否已经存在该字符串对象,如果已经存在,那么就不会再创建了,直接返回该字符串在字符串常量池中的内存地址;如果该字符串还不存在字符串常量池中,那么就会在字符串常量池中创建该字符串对象,然后再返回。所以在执行 String str2 = “abc” 的时候,因为字符串常量池中已经存在“abc”字符串对象了,就不会在字符串常量池中再次创建了,所以栈内存中 str1 和 str2 的内存地址都是指向 “abc” 在字符串常量池中的位置,所以 str1 = str2 的运行结果为 true
而在执行 String str3 = new String(“abc”) 的时候,JVM 会首先检查字符串常量池中是否已经存在“abc”字符串,如果已经存在,则不会在字符串常量池中再创建了;如果不存在,则就会在字符串常量池中创建 “abc” 字符串对象,然后再到堆内存中再创建一份字符串对象
String类的常用方法都有哪些?
String 有哪些特性?
在使用HashMap的时候,用 String 做 key 有什么好处?
HashMap 内部实现是通过 key 的 hashcode 来确定 value 的存储位置,因为字符串是不可变的,所以当创建字符串时,它的 hashcode 被缓存下来,不需要再次计算,所以相比于其他对象更快。
public int add(int a,String b)
public String add(int a,String b)
// 上方不是重载,重载与返回值和访问修饰符无关,只看方法名称和参数
构造器是否可被重写?
构造器不能被继承,因此不能被重写,但可以被重载。每一个类必须有自己的构造函数,负责构造自己这部分的构造。子类不会覆盖父类的构造函数,相反必须一开始调用父类的构造函数。
LinkedList
作为链表就最适合元素增删的场景,LinkedList
仅仅在头尾插入或者删除元素的时候时间复杂度近似 O(1),其他情况增删元素的时间复杂度都是 O(n) )LinkedList
的,需要用到 LinkedList
的场景几乎都可以使用 ArrayList
来代替,并且,性能通常会更好!就连 LinkedList
的作者都说他自己几乎从来不会使用 LinkedList
ArrayList的优点如下:
ArrayList 的缺点如下:
ArrayList 比较适合顺序添加、随机访问的场景。
遍历方式有以下几种:
Java Collections 框架中提供了一个 RandomAccess 接口,用来标记 List 实现是否支持 Random Access。
ArrayList扩容的本质就是计算出新的扩容数组的size后实例化,并将原有数组内容复制到新数组中去。默认情况下,新的容量会是原容量的1.5倍。
先来看下JDK1.7:
JDK1.7 中的 ConcurrentHashMap 是由 Segment
数组结构和 HashEntry
数组结构组成,即 ConcurrentHashMap 把哈希桶数组切分成小数组(Segment ),每个小数组有 n 个 HashEntry 组成。
如下图所示,首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一段数据时,其他段的数据也能被其他线程访问,实现了真正的并发访问。
Segment 继承了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。Segment 默认为 16,也就是并发度为 16。
再来看下JDK1.8:
在数据结构上, JDK1.8 中的ConcurrentHashMap 选择了与 HashMap 相同的Node数组+链表+红黑树结构;在锁的实现上,抛弃了原有的 Segment 分段锁,采用CAS + synchronized
实现更加细粒度的锁。
将锁的级别控制在了更细粒度的哈希桶数组元素级别,也就是说只需要锁住这个链表头节点(红黑树的根节点),就不会影响其他的哈希桶数组元素的读写,大大提高了并发度。
1.7版本
1.8版本
Segment
分段锁来保证安全, Segment
是继承自 ReentrantLock
。JDK1.8 放弃了 Segment
分段锁的设计,采用 Node + CAS + synchronized
保证线程安全,锁粒度更细,synchronized
只锁定当前链表或红黑二叉树的首节点先来看JDK1.7:
首先,会尝试获取锁,如果获取失败,利用自旋获取锁;如果自旋重试的次数超过 64 次,则改为阻塞获取锁
获取到锁后:
再来看JDK1.8:
f.hash = MOVED = -1
,说明其他线程在扩容,参与一起扩容。先来看JDK1.7:
首先,根据 key 计算出 hash 值定位到具体的 Segment ,再根据 hash 值获取定位 HashEntry 对象,并对 HashEntry 对象进行链表遍历,找到对应元素。
由于 HashEntry 涉及到的共享变量都使用 volatile 修饰,volatile 可以保证内存可见性,所以每次获取时都是最新值。
再来看JDK1.8:
get 方法不需要加锁。因为 Node 的元素 value 和指针 next 是用 volatile 修饰的,在多线程环境下线程A修改节点的 value 或者新增节点的时候是对线程B可见的。这也是它比其他并发集合比如 Hashtable、HashMap效率高的原因。
没有关系。哈希桶数组table
用 volatile 修饰主要是保证在数组扩容的时候保证可见性
我们先来说value 为什么不能为 null。因为 ConcurrentHashMap 是用于多线程的 ,如果ConcurrentHashMap.get(key)
得到了 null ,这就无法判断,是映射的value是 null ,还是没有找到对应的key而为 null ,就有了二义性。
而用于单线程状态的 HashMap 却可以用containsKey(key)
去判断到底是否包含了这个 null 。
ConcurrentHashMap 的效率要高于 Hashtable,因为 Hashtable 给整个哈希表加了一把大锁从而实现线程安全。而ConcurrentHashMap 的锁粒度更低,在 JDK1.7 中采用分段锁实现线程安全,在 JDK1.8 中采用CAS+synchronized
实现线程安全。
Hashtable 是使用 synchronized来实现线程安全的,给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞等待需要的锁被释放,在竞争激烈的多线程场景中性能就会非常差!
解决Hash冲突方法有:开放定址法、再哈希法、链地址法(拉链法)。HashMap中采用的是 链地址法 :将哈希值相同的元素构成一个同义词的单链表,并将单链表的头指针存放在哈希表的第i个单元中,查找、插入和删除主要在同义词链表中进行。链表法适用于经常进行插入和删除的情况。
因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡,而单链表不需要。当元素小于 8 个的时候,此时做查询操作,链表结构已经能保证查询性能。当元素大于 8 个的时候, 红黑树搜索时间复杂度是 O(logn),而链表是 O(n),此时需要红黑树来加快查询速度,但是新增节点的效率变慢了。
因此,如果一开始就用红黑树结构,元素太少,新增效率又比较慢,无疑这是浪费性能的。
不用红黑树,用二叉查找树可以吗?
可以。但是二叉查找树在特殊情况下会变成一条线性结构(这就跟原来使用链表结构一样了,造成很深的问题),遍历查找会非常慢。
0.75是对空间和时间效率的一个平衡选择,一般不要修改,除非在时间和空间比较特殊的情况下 :
HashMap的Put方法的流程:
jdk1.7是先判断需要需要扩容,再进行插入。jdk1.8是先插入,然后再判断是否需要扩容
java当中的克隆跟生物上所说的克隆类似,就是复制出一个一模一样的个体,当然迁移到java当中,那就是复制出一个一模一样对象。
克隆的作用:对象克隆主要是为了解决引用类型在进行等号赋值时使得两个引用同时指向同一个对象实例,从而导致通过两个引用去操作对象时,会直接更改实例中的属性破坏对象的相互独立性
//例如一下代码段
public class Test {
public static void main(String[] args) {
// TODO
Student s1 = new Student("Tom", 12);
Student s2 = s1;//s2的引用指向s1
System.out.println(s1); // name = Tom,age = 12
System.out.println(s2); // name = Tom,age = 12
s2.setName("Jerry");//修改s2的值时s1的属性
System.out.println(s1); // name = Jerry,age = 12
System.out.println(s2); // name = Jerry,age = 12
}
由上述运行结果可知,在引用类型当中,由于都是指向同一个对象实例,当我们用引用类型去修改对象实例的值时,原来对象的属性也会跟着改变,从而导致了数据的不一致性。对象的克隆就能解决上述问题,防止发生此类情况。
如何实现对象的克隆?
浅克隆:实现 Cloneable 接口并重写 clone() 方法(浅克隆)
//实现Cloneable接口
public class Product implements Cloneable{
private String name;
private Integer price;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getPrice() {
return price;
}
public void setPrice(Integer price) {
this.price = price;
}
public Product(String name, Integer price) {
super();
this.name = name;
this.price = price;
}
@Override
public String toString() {
return "Product [name=" + name + ", price=" + price + "]";
}
//重写clone的方法
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
测试:
public class Test {
public static void main(String[] args) {
// TODO 对象的克隆
Product product1 = new Product("篮球", 189);
// 1.在Product实现CoCloneable接口
// 2.重写clone方法
Product product2 = product1.clone();
System.out.println(product2);
product2.setPrice(200);//篮球涨价了
System.out.println(product1);//此时修改product2不会影响product1的值
System.out.println(product2);
}
}
深克隆:实现 Serializable 接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深克隆
HashMap 在容量超过负载因子所定义的容量之后,就会扩容。Java 里的数组是无法自动扩容的,方法是将 HashMap 的大小扩大为原来数组的两倍,并将原来的对象放入新的数组中。
jdk1.7版本:
jdk1.8版本:
Java中的所有异常都来自顶级父类Throwable。Throwable下有两个子类Exception和Error。
Error表示非常严重的错误,比如java.lang.StackOverFlowError 栈溢出和Java.lang.OutOfMemoryError 内存溢出,通常这些错误出现时,仅仅想靠程序自己是解决不了的,可能是虚拟机、磁盘、操作系统层面出现的问题了,所以通常也不建议在代码中去捕获这些Error,因为捕获的意义不大,因为程序可能已经根本运行不了了。
Exception表示异常,表示程序出现Exception时,是可以靠程序自己来解决的,比如NullPointerException空指针异常IllegalAccessException违法访问异常等,我们可以捕获这些异常来做特殊处理。
Exception的子类通常又可以分为RuntimeException运行时异常和非RuntimeException非运行时异常两类
throw 和 throws 的区别
异常相当于一种提示,如果我们抛出异常,就相当于告诉上层方法,我抛了一个异常,我处理不了这个异常,交给你来处理,而对于上层方法来说,它也需要决定自己能不能处理这个异常,是否也需要交给它的上层。
所以我们在写一个方法时,我们需要考虑的就是,本方法能否合理的处理该异常,如果处理不了就继续向上抛出异常,包括本方法中在调用另外一个方法时,发现出现了异常,如果这个异常应该由自己来处理,那就捕获该异常并进行处理。
本方法能处理异常则捕获异常,不能处理异常则向上抛出。
常见的异常类有哪些?
主线程可以捕获到子线程的异常吗?
线程设计的理念:“线程的问题应该线程自己本身来解决,而不要委托到外部”。
正常情况下,如果不做特殊的处理,在主线程中是不能够捕获到子线程中的异常的。如果想要在主线程中捕获子线程的异常,我们可以用如下的方式进行处理,使用 Thread 的静态方法。
Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandle());
ReetrantLock是一个可重入的独占锁,主要有两个特性,一个是支持公平锁和非公平锁,一个是可重入。
ReetrantLock实现依赖于AQS,ReetrantLock主要依靠AQS维护一个阻塞队列,多个线程对加锁时,失败则会进入阻塞队列。等待唤醒,重新尝试加锁。
reentrantLock.lock()
这行代码,后续代码不会执行。直到这个线程获得锁之后,才会解阻塞public class Hello {
static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) {
reentrantLock.lock(); // 阻塞加锁
// 若没有加锁后续代码不会执行
boolean result = reentrantLock.tryLock(); //尝试加锁,非阻塞加锁
// 自旋锁
while(!reentrantLock.tryLock()){
// 其他事情
}
}
}
首先不管是公平锁还是非公平锁,它们的底层实现都会使用 AQS 来进行排队,它们的区别在于:线程在使用 lock() 方法加锁时:
不管是公平锁还是非公平锁,一旦没竞争到锁,都会进行排队,当锁释放时,都是唤醒排在最前面的线程,所以非公平锁只是体现在了线程加锁阶段,而没有体现在线程被唤醒阶段。
另外, ReentrantLock 是可重入锁(同一个线程不停的加锁都可以),不管是公平锁还是非公平锁都是可重入的。默认情况下我们 new ReentrantLock()
底层是非公平锁,因为非公平性能会更高。但是解锁时候也得不停解锁,加锁两次,解锁也得两次。
public class Hello {
static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) {
reentrantLock.lock(); // 阻塞加锁
reentrantLock.lock(); // 阻塞加锁
reentrantLock.unlock();
reentrantLock.unlock();
}
}
首先ReentrantLock某些时候有局限,如果使用ReentrantLock,可能本身是为了防止线程A在写数据、线程B在读数据造成的数据不一致,但这样,如果线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。
因为这个,才诞生了读写锁ReadWriteLock。ReadWriteLock是一个读写锁接口,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能
偏向锁:在锁对象的对象头中记录一下当前获取到该锁的线程ID,该线程下次如果又来获取该锁就可以直接获取到了。也就是这把锁偏向于这个线程,所以称为 偏向锁
轻量级锁:由偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁,此时如果有第二个线程来竞争这把锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分,轻量级锁底层是通过自旋来实现的,并不会阻塞线程。如果自旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞。
自旋锁:自旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就无所谓唤醒线程,阻塞和唤醒这两个步骤都需要操作系统去进行的,比较消耗时间,自旋锁就是一直循环获取锁。
说一说自己对于 synchronized 关键字的了解
synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized 关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。另外,在 Java 早期版本中,synchronized 属于重量级锁,效率低下,JDK6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
说说 jdk1.6 之后的 synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗?
说明:这道题答案有点长,但是回答的详细面试会很加分。
JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,它们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
重量级锁
我们知道,我们要进入一个同步、线程安全的方法时,是需要先获得这个方法的锁的,退出这个方法时,则会释放锁。如果获取不到这个锁的话,意味着有别的线程在执行这个方法,这时我们就会马上进入阻塞的状态,等待那个持有锁的线程释放锁,然后再把我们从阻塞的状态唤醒,我们再去获取这个方法的锁。这种获取不到锁就马上进入阻塞状态的锁,我们称之为重量级锁。
自旋锁和自适应自旋锁
我们知道,线程从运行态进入阻塞态这个过程,是非常耗时的,因为不仅需要保存线程此时的执行状态,上下文等数据,还涉及到用户态到内核态的转换。当然,把线程从阻塞态唤醒也是一样,也是非常消耗时间的。
刚才我说线程拿不到锁,就会马上进入阻塞状态,然而现实是,它虽然这一刻拿不到锁,可能在下 0.0001 秒,就有其他线程把这个锁释放了。如果它慢0.0001秒来拿这个锁的话,可能就可以顺利拿到了,不需要经历阻塞/唤醒这个花时间的过程了。
然而重量级锁就是这么坑,它就是不肯等待一下,一拿不到就是要马上进入阻塞状态。为了解决这个问题,我们引入了另外一种愿意等待一段时间的锁 — 自旋锁。
自旋锁就是,如果此时拿不到锁,它不马上进入阻塞状态,而是等待一段时间,看看这段时间有没其他人把这锁给释放了。怎么等呢?这个就类似于线程在那里做空循环,如果循环一定的次数还拿不到锁,那么它才会进入阻塞的状态
至于是循环等待几次,这个是可以人为指定一个数字的。
上面我们说的自旋锁,每个线程循环等待的次数都是一样的,例如我设置为 100次的话,那么线程在空循环 100 次之后还没拿到锁,就会进入阻塞状态了。而自适应自旋锁就牛逼了,它不需要我们人为指定循环几次,它自己本身会进行判断要循环几次,而且每个线程可能循环的次数也是不一样的。而之所以这样做,主要是我们觉得,如果一个线程在不久前拿到过这个锁,或者它之前经常拿到过这个锁,那么我们认为它再次拿到锁的几率非常大,所以循环的次数会多一些。
而如果有些线程从来就没有拿到过这个锁,或者说,平时很少拿到,那么我们认为,它再次拿到的概率是比较小的,所以我们就让它循环的次数少一些。因为你在那里做空循环是很消耗 CPU 的。
所以这种能够根据线程最近获得锁的状态来调整循环次数的自旋锁,我们称之为自适应自旋锁。
轻量级锁
上面我们介绍的三种锁:重量级、自旋锁和自适应自旋锁,他们都有一个特点,就是进入一个方法的时候,就会加上锁,退出一个方法的时候,也就释放对应的锁。
轻量级锁认为,当你在方法里面执行的时候,其实是很少刚好有人也来执行这个方法的,所以,当我们进入一个方法的时候根本就不用加锁,我们只需要做一个标记就可以了,也就是说,我们可以用一个变量来记录此时该方法是否有人在执行。也就是说,如果这个方法没人在执行,当我们进入这个方法的时候,采用CAS机制,把这个方法的状态标记为已经有人在执行,退出这个方法时,在把这个状态改为了没有人在执行了。
倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(JDK1.6 之后加入的)。轻量级锁不是为了代替重量级锁,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗,因为使用轻量级锁时,不需要申请互斥量。另外,轻量级锁的加锁和解锁都用到了 CAS 操作。
如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥操作的开销。但如果存在锁竞争,除了互斥量开销外,还会额外发生 CAS 操作,因此在有锁竞争的情况下,轻量级锁比传统的重量级锁更慢!如果锁竞争激烈,那么轻量级将很快膨胀为重量级锁!所以轻量级锁适合用在那种,很少出现多个线程竞争一个锁的情况,也就是说,适合那种多个线程总是错开时间来获取锁的情况。
偏向锁:
偏向锁就更加牛逼了,我们已经觉得轻量级锁已经够轻,然而偏向锁更加省事,偏向锁认为,你轻量级锁每次进入一个方法都需要用CAS来改变状态,退出也需要改变,多麻烦。
偏向锁进入一个方法的时候是这样处理的:如果这个方法没有人进来过,那么一个线程首次进入这个方法的时候,会采用CAS机制,把这个方法标记为有人在执行了,和轻量级锁加锁有点类似,并且也会把该线程的 ID 也记录进去,相当于记录了哪个线程在执行。
然后,但这个线程退出这个方法的时候,它不会改变这个方法的状态,而是直接退出来,懒的去改,因为它认为除了自己这个线程之外,其他线程并不会来执行这个方法。
然后当这个线程想要再次进入这个方法的时候,会判断一下这个方法的状态,如果这个方法已经被标记为有人在执行了,并且线程的ID是自己,那么它就直接进入这个方法执行,啥也不用做
你看,多方便,第一次进入需要CAS机制来设置,以后进出就啥也不用干了,直接进入退出。
然而,现实总是残酷的,毕竟实际情况还是多线程,所以万一有其他线程来进入这个方法呢?如果真的出现这种情况,其他线程一看这个方法的ID不是自己,这个时候说明,至少有两个线程要来执行这个方法论,这意味着偏向锁已经不适用了,这个时候就会从偏向锁升级为轻量级锁。所以呢,偏向锁适用于那种,始终只有一个线程在执行一个方法的情况哦。
但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。
好文推荐:https://mp.weixin.qq.com/s/9zRmjH5Bgzo-EDIzZ5C7Hg
最开始我们说的三种锁,重量级锁、自旋锁和自适应自旋锁,进入方法之前,就一定要先加一个锁,这种我们为称之为悲观锁。悲观锁总认为,如果不事先加锁的话,就会出事,这种想法确实悲观了点,这估计就是悲观锁的来源了。
而乐观锁却相反,认为不加锁也没事,我们可以先不加锁,如果出现了冲突,我们在想办法解决,例如 CAS 机制,上面说的轻量级锁,就是乐观锁的。不会马上加锁,而是等待真的出现了冲突,在想办法解决。
synchronized 的非公平其实在源码中应该有不少地方,因为设计者就没按公平锁来设计,核心有以下几个点:
当持有锁的线程释放锁时,该线程会执行以下两个重要操作
当线程尝试获取锁失败,进入阻塞时,放入链表的顺序,和最终被唤醒的顺序是不一致的,也就是说你先进入链表,不代表你就会先被唤醒
**泛型就是将类型参数化,其在编译时才确定具体的参数。**这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。使用泛型参数,可以增强代码的可读性以及稳定性。
ArrayList<Persion> persons = new ArrayList<Persion>();
// 这行代码就指明了该 ArrayList对象只能传入 Persion 对象
使用泛型的好处有以下几点:
泛型一般有三种使用方式:泛型类、泛型接口、泛型方法。
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey(){
return key;
}
}
如何实例化泛型类:
Generic<Integer> genericInteger = new Generic<Integer>(123456);
public interface Generator<T> {
public T method();
}
实现泛型接口,不指定类型:
class GeneratorImpl<T> implements Generator<T>{
@Override
public T method() {
return null;
}
}
实现泛型接口,指定类型:
class GeneratorImpl<T> implements Generator<String>{
@Override
public String method() {
return "hello";
}
}
public static < E > void printArray( E[] inputArray )
{
for ( E element : inputArray ){
System.out.printf( "%s ", element );
}
System.out.println();
}
使用:
// 创建不同类型数组: Integer, Double 和 Character
Integer[] intArray = { 1, 2, 3 };
String[] stringArray = { "Hello", "World" };
printArray(intArray);
printArray(stringArray);
注意: public static < E > void printArray( E[] inputArray )
一般被称为静态泛型方法,在 java 中泛型只是一个占位符,必须在传递类型后才能使用。
类在实例化时才能真正的传递类型参数,由于静态方法的加载先于类的实例化,就是说类中的泛型还没有传递真正的类型参数,静态的方法的加载就已经完成了,所以静态泛型方法是没有办法使用类上声明的泛型的。只能使用自己声明的
泛型是一种语法糖,泛型这种语法糖的基本原理是类型擦除。也就是说:**泛型只存在于编译阶段,而不存在于运行阶段。**在编译后的 class 文件中,是没有泛型这个概念的。
类型擦除:使用泛型的时候加上的类型参数,编译器在编译的时候去掉类型参数。
public class Caculate<T> {
private T num;
}
// 反编译一下上面的 Caculate 类
public class Caculate{
public Caculate(){}
private Object num;
}
发现编译器擦除 Caculate 类后面的两个尖括号,并且将 num 的类型定义为 Object 类型。大部分情况下,泛型类型都会以 Object 进行替换,而有一种情况则不是。那就是使用到了extends和super语法的有界类型
public class Caculate<T extends String> {
private T num;
}
这种情况的泛型类型,num 会被替换为 String 而不再是 Object。它限定 T 是 String 或者 String 的子类
有两种限定通配符:
public class People<E extends Number> {
}
? 表示非限定通配符,因为 ? 可以用任意类型来替代
例如 List< ? extends Number > 可以接受 List< Integer > 或 List< Float > 。
不可以。因为 List 可以提供编译期的类型安全保证,而 Array 却不能。
CommonResult
通过参数 T
可根据具体的返回类型动态指定结果的数据类型通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。反射的核心思想和关键就是得到:编译后的class文件对象。
像是使用 Spring 的时候,一个@Component
注解就声明了一个类为 Spring Bean ,通过一个 @Value
注解就读取到配置文件中的值,这些都是因为你可以基于反射分析类,然后获取到类/属性/方法/方法的参数上的注解。你获取到注解之后,就可以做进一步的处理。
应用举例:工厂模式,使用反射机制,根据全限定类名获得某个类的 Class 实例
反射的优缺点:
Class.forName(“类的路径”);当你知道该类的全路径名时,你可以使用该方法获取 Class 类对象
Class clz = Class.forName("java.lang.String");
类名.class。这种方法只适合在编译前就知道操作的 Class。
Class clz = String.class;
对象名.getClass()
String str = new String("Hello");
Class clz = str.getClass();
如果是基本类型的包装类,可以调用包装类的Type属性来获得该包装类的Class对象
反射 API 用来生成 JVM 中的类、接口或则对象的信息
反射的使用步骤?
Oracle 希望开发者将反射作为一个工具,用来帮助程序员实现本不可能实现的功能。举两个最常见使用反射的例子,来说明反射机制的强大之处:
第一种:JDBC 的数据库的连接:在JDBC 的操作中,如果要想进行数据库的连接,则必须按照以下的几步完成
第二种:Spring 框架的使用,最经典的就是xml的配置模式
Spring 通过 XML 配置模式装载 Bean 的过程:
Spring这样做的好处是:
注解 Annotation
可以看作是一种特殊的注释,主要用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。注解本质是一个继承了Annotation
的特殊接口,JDK 提供了很多内置的注解(比如 @Override
、@Deprecated
),同时,我们还可以自定义注解。
注解只有被解析之后才会生效,常见的解析方法有两种:
@Override
注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。@Value
、@Component
)都是通过反射来进行处理的如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。
对于 Java 这种面向对象编程语言来说,我们序列化的都是对象(Object)也就是实例化后的类(Class),但是在 C++这种半面向对象的语言中,struct(结构体)定义的是数据结构类型,而 class 对应的是对象类型。
下面是序列化和反序列化常见应用场景:
对象在进行网络传输之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化
将对象存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化
将对象存储到数据库(如 Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化
将对象存储到内存之前需要进行序列化,从内存中读取出来之后需要进行反序列化
序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。
OSI 七层协议模型中,表示层做的事情主要就是对应用层的用户数据进行处理转换为二进制流。反过来的话,就是将二进制流转换成应用层的用户数据。所以序列化协议对应于 TCP/IP 模型的表示层。
序列化的实现:需要被序列化的类实现 Serializable 接口,用于标注该对象是可被序列化的,然后使用一个输出流(如:FileOutputStream)来构造一个 ObjectOutputStream 对象,接着使用 ObjectOutputStream 对象的 writeObject(Object obj) 方法可以将参数为 obj 的对象写出,要恢复的话则使用输入流构造一个 ObjectInputStream 对象,接着使用 ObjectInputStream 对象的 readObject () 方法读取对象。
对于不想进行序列化的变量,使用 transient
关键字修饰。
transient
关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient
修饰的变量值不会被持久化和恢复。
注意:
transient
只能修饰变量,不能修饰类和方法。transient
修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰 int
类型,那么反序列后结果就是 0
static
变量因为不属于任何对象(Object),所以无论有没有 transient
关键字修饰,均不会被序列化不会。因为序列化是针对对象而言的, 而静态变量优先于对象存在, 随着类的加载而加载, 所以不会被序列化.
的对象之后需要再进行反序列化
将对象存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化
将对象存储到数据库(如 Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化
将对象存储到内存之前需要进行序列化,从内存中读取出来之后需要进行反序列化
序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。
OSI 七层协议模型中,表示层做的事情主要就是对应用层的用户数据进行处理转换为二进制流。反过来的话,就是将二进制流转换成应用层的用户数据。所以序列化协议对应于 TCP/IP 模型的表示层。
序列化的实现:需要被序列化的类实现 Serializable 接口,用于标注该对象是可被序列化的,然后使用一个输出流(如:FileOutputStream)来构造一个 ObjectOutputStream 对象,接着使用 ObjectOutputStream 对象的 writeObject(Object obj) 方法可以将参数为 obj 的对象写出,要恢复的话则使用输入流构造一个 ObjectInputStream 对象,接着使用 ObjectInputStream 对象的 readObject () 方法读取对象。
对于不想进行序列化的变量,使用 transient
关键字修饰。
transient
关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient
修饰的变量值不会被持久化和恢复。
注意:
transient
只能修饰变量,不能修饰类和方法。transient
修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰 int
类型,那么反序列后结果就是 0
static
变量因为不属于任何对象(Object),所以无论有没有 transient
关键字修饰,均不会被序列化不会。因为序列化是针对对象而言的, 而静态变量优先于对象存在, 随着类的加载而加载, 所以不会被序列化.