集合
Collection接口
Collection接口:单列集合,用来存储一个一个的对象
----List接口:存储有序的、可重复的数据。 --->“动态”数组 ----ArrayList、LinkedList、Vector
----Set接口:存储无序的、不可重复的数据 ----HashSet、LinkedHashSet、TreeSet
Map接口:双列集合,用来存储一对(key - value)一对的数据 --->y=f(x) ----HashMap、LinkedMap、TreeMap、Hashtable、Properties
1、ArrayList和LinkedList的区别
- ArrayList、LinkedList、Vector三者异同
- 同:三个类都是实现了List接口,存储数据特点相同:有序可重复
- 异:
- ----ArrayList:作为List接口的主要实现类;线程不安全,效率高;底层使用Object[] elementData存储
- Array(数组)是基于索引(index)的数据结构,它使用索引在数组中搜索和读取数据是很快的。 Array获取数据的时间复杂度是O(1),但是要删除数据却是开销很大, 因为这需要重排数组中的所有数据,(因为删除数据以后, 需要把后面所有的数据前移)
- 因为其本身是一个单链表,更新数据需要统一向前后移动数据
- ----LinkedList:对于频繁的插入、删除操作,使用此类效率比ArrayList高,底层使用双向链表存储(数据结构)
- LinkedList是一个双链表,在添加和删除元素时具有比ArrayList更好的性能,数据更新时只需解开链条,修改数据,将新数据连接上去,或者删除数据之后,前后两个数据连接上即可
- ----Vector:作为List接口的古早实现类:线程安全,效率低;底层使用Object[] elementData存储
2、HashMap和HashTable的区别
----Map:双列数据,存储key-value对的数据 --类似于:y=f(x)
----HashMap:作为Map的主要实现类:线程不安全,效率高;可以存储null的key和value
-
HashMap:key可以为null,但是这样的key只能有一个,
-
但是它的效率远远高于Hashtable,这样设计是合理的,因为大部分的使用场景都是单线程。 当需要多线程操作的时候可以使用线程安全的ConcurrentHashMap。
-
ConcurrentHashMap虽然也是线程安全的,但是它的效率比Hashtable要高好多倍。 因为ConcurrentHashMap使用了分段锁,并不对整个数据进行锁定。
----LinkedHashMap:保证在遍历map元素时,可以按照添加的顺序实现遍历
原理:在原有的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素
对于频繁的遍历操作,此类执行效率高于HashMap
----TreeMap:保证添加的key-value对进行排序,实现排序遍历,此时考虑key的自然排序或定制排序
底层使用红黑树
----Hashtable:古早实现类;线程安全的,效率低,不能存储null的key和value
----properties:常用来处理配置文件(数据库链接配置文件)。key和value都是String类型
HashMap的底层:数组+链表 (jdk7) 数组+链表+红黑树 (jdk8)
3、Collection包结构,与Collections的区别
Collection是集合类的上级接口,子接口有 Set、List、LinkedList、ArrayList、Vector、Stack、 Set; Collections是集合类的一个帮助类, 它包含有各种有关集合操作的静态多态方法,用于实现对各 种集 合的搜索、排序、线程安全化等操作。此类不能实例化,就像一个工具类,服务于Java的 Collection框架。
4、泛型常用特点
“泛型” 意味着编写的代码可以被不同类型的对象所重用。
“泛型”,顾名思义,“泛指的类型”。
我们提供了 泛型的概念,但具体执行的时候却可以有具体的规则来约束,比如我们用的非常多的ArrayList就是个泛型类,ArrayList作为集合可以存放各种元素,如 Integer String,自定义的各种类型等,但在我们使用的时候通过具体的规则来约束,如我们可以约束集合中只存放某一种数据类型,约定好这种约束之后,可以在编译阶段检测编写的规范性是否符合约束
是我们可以通过规则按照自己的想法控制存储的数据类型。
5、说说List,Set,Map三者区别
说说ArrayList(数组)
ArrayList 是最常用的 List 实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。 数 组的缺点是每个元素之间不能有间隔, 当数组大小不满足时需要增加存储能力,就要将已经有数组的 数 据复制到新 的存储空间中。 当从 ArrayList 的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。 因此,它适合随机查找和遍历,不适合插入和删除。
## 说说LinkedList(链表)
LinkedList 是用链表结构存储数据的,很适合数据的动态插入和删除,随机访问和遍历速度比较慢。另外,他还提供了 List 接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用
什么是TreeSet(二叉树)
TreeSet()是使用二叉树的原理对新 add()的对象按照指定的顺序排序(升序、降序),每增加一个对象都会进行排序,将对象插入的二叉树指定的位置。
Integer 和 String 对象都可以进行默认的 TreeSet 排序,而自定义类的对象是不可以的, 自己定 义的 类必须实现 Comparable 接口,并且覆写相应的 compareTo()函数,才可以正常使用。
在覆写 compare()函数时,要返回相应的值才能使 TreeSet 按照一定的规则来排序
比较此对象与指定对象的顺序。如果该对象小于、等于或大于指定对象,则分别返回负整数、零或正整数
/**
1.向TreeSet中添加的数据,要求是相同类的对象。
2.两种排序方式:自然排序(实现Comparable借口)和定制排序(在Person类中重写CompareTo(Object o)方法)
3.自然排序中,比较两个对象是否相同的标准为:compareTo()返回0 不再是equals()
*/
@Test
public void test3(){
TreeSet<Object> set = new TreeSet<>();
//不能添加不同类型的对象
set.add(123);
set.add(124);
//set.add("zmj");
set.add(12);
set.add(-14);
System.out.println(set);
}
@Test
public void test4(){
//定制排序:按照年龄从小到大排序
Comparator com=new Comparator(){
@Override
public int compare(Object o1, Object o2) {
if(o1 instanceof Person && o2 instanceof Person){
Person p1 = (Person)o1;
Person p2 = (Person)o2;
return Integer.compare(p1.getAge(),p2.getAge());
}else {
throw new RuntimeException("输入数据不匹配");
}
}
};
TreeSet<Object> set = new TreeSet<>(com);
set.add(new Person("zmj",22));
set.add(new Person("lnx",21));
set.add(new Person("sbh",22));
set.add(new Person("sbh",23));
/**
* 自然排序:通过在Person类里重写compareTo(Object o)方法实现
@Override
public int compareTo(Object o) {
if(o instanceof Person){
Person person=(Person)o;
int compare = -this.name.compareTo(person.name);
if(compare != 0){
return compare;
}else {
return Integer.compare(this.age,person.age);
}
}else {
throw new RuntimeException("类型不匹配");
}
}
*/
for(Object obj : set){
System.out.println(obj);
}
}
说说ConcurrentHashMap
ConcurrentHashMap 和 HashMap 思路是差不多的,但是因为它支持并发操作,所以要复杂一 些。
整个 ConcurrentHashMap 由一个个 Segment 组成, Segment 代表”部分“或”一段“的意思, 所以很多地方都会将其描述为分段锁。 注意,行文中,我很多地方用了“槽”来代表一个segment。
线程安全(Segment继承ReentrantLock加锁)
简单理解就是, ConcurrentHashMap 是一个 Segment 数组, Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全
并行度(默认16)
默认是 16,也 就是 说 ConcurrentHashMap 有 16 个 Segments,所以理论上, 这个时候,最多可以同时支持 16 个 线程并发写,只要它们的操作分别分布在不同的 Segment 上
这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩容的。 再具体到每个 Segment 内部,其实每个 Segment 很像之前介绍的 HashMap,
泛型类
- 静态方法中不能使用类的泛型
- 异常类不能使用泛型
/**
* Created by KingsLanding on 2022/6/29 15:03
*/
public class Order<T> {
String orderName;
int orderId;
//类的内部结构就可以使用类的泛型
T orderT;
public Order() {
}
public Order(String orderName, int orderId, T orderT) {
this.orderName = orderName;
this.orderId = orderId;
this.orderT = orderT;
}
public T getOrderT() {
return orderT;
}
public void setOrderT(T orderT) {
this.orderT = orderT;
}
@Override
public String toString() {
return "Order{" +
"orderName='" + orderName + '\'' +
", orderId=" + orderId +
", orderT=" + orderT +
'}';
}
}
泛型类的实例化
-
如果定义了泛型类,实例化没有指明类的泛型,则默认此泛型类型为Object类型
-
要求:如果定义了类是带泛型的,建议在实例化时要指明类的泛型
-
一旦指定了泛型,这里就不能设置为除泛型规定之外的数据了
public void test(){
//如果定义了泛型类,实例化没有指明类的泛型,则默认此泛型类型为Object类型
//要求:如果定义了类是带泛型的,建议在实例化时要指明类的泛型
Order order = new Order();
order.setOrderT(12);
order.setOrderT("zmj");
//指明实例化时类的泛型
Order<String> order1 = new Order<String>("zmj",12,"ZMJ");
// Order<String> order1 = new Order<String>("zmj",12,20);//指定了泛型,这里就不能设置为除String之外的数据了
}
泛型方法
- 泛型方法:在方法中出现的泛型结构,泛型参数和类的泛型参数没有任何关系
- 泛型方法所属的类是不是泛型类都无所谓
- 泛型方法可以声明为静态的。原因:泛型参数是在调用方法是才确定的,并非在实例化时确定
public <E> List<E> copyFromArrayToList(E[] arr){
ArrayList<E> list = new ArrayList<>();
for(E e : arr){
list.add(e);
}
return list;
}
方法调用
@Test
public void test2(){
Order<String> order = new Order<>();
Integer[] arr = new Integer[]{1,2,3,4};
//泛型方法在调用是,指明泛型参数的类型
List<Integer> list = order.copyFromArrayToList(arr);
System.out.println(list);
}
类型擦除
使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个过程就称为类型擦除。**
Java 中的泛型基本上都是在编译器这个层次来实现的。在生成的 Java 字节代码中是不包含泛型中的类型信息的。
- 由泛型附加的类型信息对 JVM来说是不可见的。
泛型在继承中的体现
非泛型类继承泛型类
public class SubOrder extends Order<Integer>{//SubOrder不是泛型类
}
泛型类之间的继承
/**
* Created by KingsLanding on 2022/6/29 15:14
*/
public class SubOrder1<T> extends Order<T> {//SubOrder1<T>仍然是泛型类
}
实例化
public void test1(){
SubOrder subOrder = new SubOrder();
//由于子类在继承带泛型的父类时,指明了泛型类型,则实例化子类对象时,不需要指明泛型,只能遵循父类的要求
subOrder.setOrderT(12);
//自定义子类自己的泛型约束,不需要遵循父类的泛型约束
SubOrder1<String> stringSubOrder1 = new SubOrder1<>();
stringSubOrder1.setOrderT("zmj");
}
- 虽然类A是B的父类,但是G
<A>和G<B>二者不具备子父类关系,二者是并列关系 - 补充:类A是类B父类,A
<G>是B<G>的父类
List<Object> list=null;
List<String> list1 =null;
//此时的list和list1的类型不具有子父类关系,编译不通过
// list= list1;
List<String> list1=null;
ArrayList<String> list2=null;
//此时的list1和list1的类型具有子父类关系,编译通过
list1 = list2;
通配符
类型通配符一般是使用?代替具体的类型参数。
例如List在逻辑上是List,List 等所有List<具体类型 实参>的父类。
Java异常面试题
1、运行时异常
不需要程序员去手动处理,处理工作是交给jvm
比如的运行时异常有:
- ClassCastException(类转换异常)
- ClassNotFoundException
- IndexOutOfBoundsException(数组越界异常)
- NullPointerException(空指针异常)
- ArrayStoreException(数组存储异常,即数组存储类型不一致) 6. 还有IO操作的BufferOverflowException异常
2.非运行时异常
在编译期间显示的异常,需要程序员手动处理,因为java默认能显示或者检查出来的异常都是可以处理的异常
常见的异常有: 1. IOException 2. SqlException
异常的处理机制
1.try...catch..finally,捕获异常,处理后抛出,最终finally善后
2.throws抛出异常
什么是java异常
java提供的一种识别和响应错误的一致性机制,将异常的处理跟业务代码分离,提供啊程序的健壮性。异常回答了“什么异常”“哪里的异常”“为什么产生这个异常”。
Throwable
Throwable 是 Java 语言中所有错误与异常的超类。
- Throwable 包含两个子类:Error(错误)和 Exception(异常),它们通常用于指示发生了异常 情况。
- Throwable 包含了其线程创建时线程执行堆栈的快照,它提供了 printStackTrace() 等接口用于获 取堆栈跟踪数据等信息。
Error(错误)
定义:Error 类及其子类。程序中无法处理的错误,表示运行应用程序中出现了严重的错误。
特点:此类错误一般表示代码运行时 JVM 出现问题。通常有 Virtual MachineError(虚拟机运行错 误)、NoClassDefFoundError(类定义错误)等。比如 OutOfMemoryError:内存不足错误; StackOverflflowError:栈溢出错误。此类错误发生时,JVM 将终止线程。这些错误是不受检异 常,非代码性错误。因此,当此类错误发生时,应用程序不应该去处理此类错误。按照Java惯例, 我们是不应该实现任何新的Error子类的!
Exception(异常)
程序本身可以捕获并且可以处理的异常。Exception 这种异常又分为两类:运行时异常和编译时异 常。
运行时的异常:
可以通过编译器,但是在运行时会被jvm发现并抛出,一般是程序的逻辑问题导致的,需要在编写代码时极力避免的
编译时异常:
定义: Exception 中除 RuntimeException 及其子类之外的异常。
特点: Java 编译器会检查它。如果程序中出现此类异常,比如 ClassNotFoundException(没有找 到指定的类异常),IOException(IO流异常),要么通过throws进行声明抛出,要么通过 trycatch进行捕获处理,否则不能通过编译。在程序中,通常不会自定义该类异常,而是直接使用系统提供的异常类。该异常我们必须手动在代码里添加捕获语句来处理该异常。
受检异常
编译器要求必须处理的异常
正确的程序在运行过程中,经常容易出现的、符合预期的异常情况。 一旦发生此类异常,就必须采用某种方式进行处理。除 RuntimeException 及其子类外,其他的 Exception 异常都属于受检异常。编译器会检查此类异常,也就是说当编译器检查到应用中的某 处 可能会此类异常时,将会提示你处理本异常——要么使用try-catch捕获,要么使用方法签名中用 throws 关键字抛出,否则编译不通过。
(比如在处理流的时候,那么就需要去提前的将可能出现的异常进行处理)
非受检异常
编译器不会进行检查并且不要求必须处理的异常,也就说当程序中出现此类异常时,即使我们没有 try-catch捕获它,也没有使用throws抛出该异常,编译也会正常通过。该类异常包括运行时异常 (RuntimeException极其子类)和错误(Error)。
Java异常关键字
try:异常监听,将可能出现异常,或者是想要监听异常的代码放入try内
catch:捕获异常:
finally:异常后续的处理,无论有无异常都会执行
throw:直接抛出异常:throw关键字作用是在方法内部抛出一个 Throwable 类型的异常。任何Java代码都可以通过throw 语句抛出异常。;throw 关键字用在方法内部,只能用于抛出一种异常,用来抛出方法或代码块中的异常,受查异常 和非受查异常都可以被抛出。
throws:在方法体上抛出异常
异常捕获
程序通常在运行之前不报错,但是运行后可能会出现某些未知的错误,但是还不想直接抛出到上一级, 那么就需要通过try…catch…的形式进行异常捕获,之后根据不同的异常情况来进行相应的处理。
JVM 是如何处理异常的?
在一个方法中如果发生异常,这个方法会创建一个异常对象,并转交给 JVM,该异常对象包含异常名称,异常描述以及异常发生时应用程序的状态。
创建异常对象并转交给 JVM 的过程称为抛出异 常。可能有一系列的方法调用,最终才进入抛出异常的方法,这一系列方法调用的有序列表叫做调用栈。 JVM 会顺着调用栈去查找看是否有可以处理异常的代码,如果有,则调用异常处理代码。当 JVM发 现可以处理异常的代时,会把发生的异常传递给它。如果 JVM 没有找到可以处理该异常的代码块, JVM 就会将该异常转交给默认的异常处理器(默认处理器为 JVM 的一部分),默认异常处理器打印 出异常信息并终止应用程序。
final、finally、finalize 有什么区别?
fifinal可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、 修饰变量表示该变量是一个常量不能被重新赋值。
fifinally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法 fifinally代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代 码。
fifinalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,Java 中允许使用 fifinalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。
NoClassDefFoundError 和 ClassNotFoundException 区别?
1、NoClassDefFoundError是一个Error类型的异常,由jvm引起的
引起该异常的原因是 JVM 或 ClassLoader 尝试加载某类时在内存中找不到该类的定义,该动作发 生在运行期间,即编译时该类存在,但是在运行时却找不到了,可能是变异后被删除了等原因导 致;
2、ClassNotFoundException 是一个受检异常,需要显式地使用 try-catch 对其进行捕获和处理,或 在方法签名中用 throws 关键字进行声明。当使用 Class.forName, ClassLoader.loadClass 或 ClassLoader.fifindSystemClass 动态加载类到内存的时候,通过传入的类路径参数没有找到该 类,就会抛出该异常;另一种抛出该异常的可能原因是某个类已经由一个类加载器加载至内存中, 另一个加载器又尝试去加载它。
如果 catch 中 return 了,finally 还会执行吗?
通常情况会。
(1)finally的作用就是,无论出现什么状况,finally里的代码一定会被执行。
(2)如果在catch中return了,也会在return之前,先执行finally代码块。
(3)而且如果finally代码块中含有return语句,会覆盖其他地方的return。
(4)对于基本数据类型的数据,在finally块中改变return的值对返回值没有影响,而对引用数据类型的 数据会有影响。
注: finally也不是一定会被执行。
什么情形下,finally代码块不会执行?
1、没有进入到try代码块中
2、程序强制退出-System.exit();
System.exit()的作用是中止当前虚拟机,虚拟机都被中止了,finally代码块自然不会执行。
3、守护线程被中止
守护(daemon)线程被中止时 java线程分为两类,守护线程和非守护线程。当所有的非守护线程中止时,不论存不存在守护线程,虚拟机都会kill掉守护线程从而中止程序。 虚拟机中,执行main方法的线程就是一个非守护线程,垃圾回收则是另一个守护线程,main执行完,程序就中止了,而不管垃圾回收线程是否中止。 所以,如果守护线程中存在finally代码块,那么当所有的非守护线程中止时,守护线程被kill掉,其finally代码块是不会 执行的。
finally的含义 finally的真正含义是指从try代码块出来才一定会执行相应的finally代码块
IO
BIO、NIO、AIO 有什么区别?
BIO:同步阻塞式,传统的io方式,模式简单使用方便,并发处理能力低
NIO:同步非阻塞,IO的升级,客户端和服务器端通过Channel(通道)通讯,实现了多路复用。
AIO:是 NIO 的升级,实现了异步非堵塞,异步 IO 的操作基于事件和回调机制。
NIO
1.NIO的使用说明: Java NIO (New I0, Non-Blocking I0) 是从Java 1. 4版本开始引入的一套新的I0 API, 可以替代标准的Java IO API。 NIO与原来的IO同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的(IO是面向流的)、基于通道的IO操作。 NIO将以更加高效的方式进行文件的读写操作。
随着JDK 7的发布,Java对NIO进行了极大的扩展,增强了对文件处理和文件系统特性的支持,以至于我们称他们为NIO2 。
- NIO 和传统 IO 之间第一个 最大的区别是,IO 是面向流的, NIO 是面向缓冲区的
阻塞IO模型
最传统的一种 IO 模型,即在读写数据过程中会发生阻塞现象。
当用户线程发出 IO 请求之后,内核 会去 查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交 出 CPU。
当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除 block 状 态。 典型的阻塞 IO 模型的例子为: data = socket.read();
如果数据没有就绪,就会一直阻塞在 read 方法
非阻塞IO模型
当用户线程发起一个 read 操作后,并不需要等待,而是马上就得到了一个结果。
如果结果是一个 error 时,它就知道数据还没有准备好,于是它可以再次发送 read 操作。一旦内核中的数据准备好了, 并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。
所以事实上, 在非阻塞 IO 模型 中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞 IO不会交出 CPU,而会一直占用 CPU。
while(true){
data = socket.read();
if(data!= error){
//处理数据
break;
}
}
但是对于非阻塞 IO 就有一个非常严重的问题, 在 while 循环中需要不断地去询问内核数据是否就 绪, 这样会导致 CPU占用率非常高,因此一般情况下很少使用 while 循环这种方式来读取数据。
多路复用IO模型
多路复用 IO 模型是目前使用得比较多的模型。 Java NIO 实际上就是多路复用 IO。在多路复用 IO 模型 中,会有一个线程不断去轮询多个socket 的状态,只有当 socket 真正有读写事件时,才真正调用 实际 的 IO
所 以它大大减少了资源占用。在 Java NIO 中,是通过 selector.select()去查询每个通道是否有到达事件, 如果没有事件,则一直阻塞在那里,因此这种方式会导致用户线程的阻塞。
而多路复用 IO 模式,通过一个线程就可以管理多个 socket,只有当socket 真正有读写事件发生才会占用资源来 进行实际的读写操作。因此,多路复 用 IO 比较适合连接数比较多的情况。
另外多路复用 IO 为何比非阻塞 IO 模型的效率高是因为在非阻塞 IO 中,不断地询问 socket 状态时 通过 用户线程去进行的,而在多路复用IO 中,轮询每个 socket 状态是内核在进行的,这个效率要比用 户线 程要 高的多。 不过要注意的是,多路复用 IO 模型是通过轮询的方式来检测是否有事件到达,并且对到达的事件 逐一 进行响应。因此对于多路复用 IO 模型来说, 一旦事件响应体很大,那么就会导致后续的事件迟迟 得不 到处 理,并且会影响新的事件轮询。
NIO的非阻塞
IO 的各种流是阻塞的。这意味着,当一个线程调用 read() 或 write()时,该线程被阻塞,直到有一 些数 据被读取,或数据完全写入。 该线程在此期间不能再干任何事情了。
NIO 的非阻塞模式,使一个 线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据, 如果目前没有数据可用时,就什么 都不会获取。 而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。
java反射面试题
反射的几种方式
.getClass()
Person person = new Person();
Class cla=person.getClass()
.class
Class clazz=Person.class;
Class.forName("Person");
Class clazz = Class.forName("xx.xx.Person");
java反射创建对象效率高还是通过new创建对象效率高?
通过new创建对象的效率比较高。
通过反射时,先找查找类资源,使用类加载器创建,过程比较繁琐,所以效率较低
java反射的作用
通过反射机制来获得类的所有信 息。 这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制。
- 1.类的加载过程: :
程序经过
javac. exe命令以后,会生成一个或多个字节码文件(.class结尾)。接着我们使用java.exe命令对某个字节码文件进行解释运行。相当于将某个字节码文件加载到内存中。此过程就称为类的加载。加载到内存中的类,我们就称为运行时类,此运行时类,就作为Class的一 个实例。 - 2.换句话说,Class的实例就对应着一个运行时类。
- 3.加载到内存中的运行时类会缓存一定的时间。在此时间之内,我们可以通过不同的方式、来获取此运行时类
反射机制的优缺点
优点:
1、能够运行时动态获取类的实例,提高灵活性;
2、与动态编译结合
缺点:
1、使用反射性能较低,需要解析字节码,将内存中的对象进行解析。
- 通过
setAccessible(true)关闭JDK的安全检查来提升反射速度; - 多次创建一个类的实例时,有缓存会快很多
2、相对不安全,破坏了封装性(因为通过反射可以获得私有方法和属性)
反射机制与面向对象中的封装性是不是矛盾的?如何看待两个技术?
-
不矛盾
封装相当于是限制被封装的属性的使用,而不是完全不能使用,这是一种约定的编码规范,而在使用反射的时候并不是在破坏这种规范,反射就像一个工具,这个工具能破坏规范,但是它产生的目的不是为了破坏规范。
-
反射是站在我们不知道对象的内部结构但是又不得已非得用到他的时候,比如在各大框架的编写中我们能发现,运用到了很多了反射知识。
-
目的:为了能动态的产生用户需要的对象,也正是反射才为我们提供了这么多便捷的框架来供我们开发。这也是反射的真实用处和意义所在。
利用反射动态创建对象实例
使用 Class 对象的 newInstance()方法来创建该 Class 对象对应类的实例,
newInstance():调用此方法,创建对应的运行时类的对象,内部调用了运行时类的空参构造器
- 此方法能正常创建运行时类的对象的前提 1.运行类必须提供空参构造器 2.空参构造器的访问权限必须是可访问的,一般设置为public
@Test
public void test() throws Exception{
Class<Person> clazz = Person.class;
/*
newInstance():调用此方法,创建对应的运行时类的对象,内部调用了运行时类的空参构造器
此方法能正常创建运行时类的对象的前提
1.运行类必须提供空参构造器
2.空参构造器的访问权限必须是可访问的,一般设置为public
javabean中要求提供public的空参构造器的原因:
1.便于用过反射,创建运行时类的对象
2.便于子类继承此运行时类时,默认调用super()时,保证父类有此构造器
*/
Person person = clazz.newInstance();
System.out.println(person);
}
//////////////////////////////////////////////////
//未涉及反射之前
public void test(){
//1.创建Person类对象
Person p1 = new Person("zmj", 22);
//2.通过对象,调用其内部的属性、方法
p1.age=20;
p1.show();
//在Person类的外部,不可以通过Person类的对象调用其内部私有结构:name 、test()、私有构造器
}
//反射之后,对于Person的操作
Class clazz = Person.class;
//1.通过反射,创建Person类的对象
Constructor cons = clazz.getConstructor(String.class, int.class);
Object obj = cons.newInstance("zmj1", 22);
Person p = (Person)obj;
System.out.println(p.toString());
java序列化
对象流
作用:用于存储和读取基本数据类型数据或对象的处理流。他的强大之处就是可以把java中的对象写入到数据源中,也能把对象从数据源中还原回来
ObjectInputStream
ObjectOutputStream
保存(持久化)对象及其状态到内存或者磁盘
-
这些对象的生命周期不会比 JVM的生命周期更长
-
对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。
-
当其它程序获取了这种二进制流,就可以恢复成原来的Java对象。
对象要想实现序列化,需要满足哪几个条件。
1.实现接口: Serializable\标识接口+ 2.对象所在的类提供常量:序列版本号 3.要求对象的属性也必须是可序列化的。(基本数据类型、String: 本身就默认已经是可序列化)如果一个内中存在内部类,那么这个内部类也必须是可序列化的
4.如果这个类继承于某父类,那么要想将父类对象序列化,那么父类也必须实现Serializable接口
Transient 关键字阻止该变量被序列化到文件中
-
在变量声明前加上 Transient 关键字,可以阻止该变量被序列化到文件中,在被反序列化后, transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。
-
服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。
多线程篇
1.发挥多核cpu的优势
随着工业的进步,现在的笔记本、台式机乃至商用的应用服务器至少也都是双核的,4核、8核甚至16核 的也都不少见,如果是单线程的程序,那么在双核CPU上就浪费了50%,在4核CPU上就浪费了75%。单核CPU上所谓的"多线程"那是假的多线程,同一时间处理器只会处理一段逻辑,只不过线程之间切换得比较快,看着像多个线程"同时"运行罢了。多核CPU上的多线程才是真正的多线程,它能让你的多段逻辑同时工作,多线程,可以真正发挥出多核CPU的优势来,达到充分利用CPU的目的。
2.防止阻塞
-
单核CPU不但不会发挥出多线程的优势,反而会因为在单核CPU上运行多 线程导致线程上下文的切换,反而降低程序整体的效率
比方说远程读取某个数据吧, 对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了
-
但是多核CPU中多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。
便于建模
利于大的任务A分解成几个小任务,分别建立程序模型,并通过多线程分别运行这几个任务
基本方法
线程相关的基本方法有 wait,sleep,yield,interrupt,join, notify 等。
线程等待 wait
阻塞线程,必须在同步代码块或同步方法中使用(synchronized()),一旦执行此方法,当前线程就进入阻塞状态,只有等待另外线程的通知或被中断才会返回,并释放同步监视器
线程睡眠 sleep
sleep(long millitime)导致当前线程休眠指定的时间
sleep()不会释放锁,wait()会释放锁
**sleep(long millitime)**会导致线程 进入 TIMED-WATING状态,而 **wait()**方法会导致当前线程进入 WATING 状态
sleep()和wait()异同
-
同:
一旦执行方法,线程都将进入阻塞状态
-
异:
1.两个方法声明的位置不同;Thread类中声明sleep(),Object类中声明wait()
2.调用的要求的不同:sleep()可以在任何需要的场景下调用。wait()必须在同步代码块或同步方法中使用
3.关于是否调用同步监视器:如果两个方法都使用在同步代码块或同步方法中sleep()不会释放锁,wait()会释放锁
线程让步(yield)
-
yield():释放当前CPU的执行权
yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争 CPU 时间片
join()
掐断原本的线程,切换到其他线程,执行完后再返回调用
为什么要用 join()方法?
比如主线程生成并启动了子线程,需要用到子线程返回的结果,也就是需要主线程需要在子线 程结 束后再结束,这时候就要用到 join() 方法 。
线程唤醒(notify)
作用:唤醒被wait()阻塞的线程,必须在同步代码块或同步方法中使用(synchronized())
一旦执行此方法,就会唤醒被wait()阻塞的线程。如果有多个线程被wait();唤醒优先级别高的
notifyAll();
作用:唤醒所有被阻塞的线程,必须在同步代码块或同步方法中使用(synchronized())
线程中断(interrupt)
中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。 这 个线程本身并不会因此而改变状态(如阻塞,终止等)。
- 调用 interrupt()方法并不会中断一个正在运行的线程。也就是说处于 Running 状态的线程并不会因为被中断而被终止,仅仅改变了内部维护的中断标识位而已。
- 若调用 sleep()而使线程处于 TIMED-WATING 状态,这时调用 interrupt()方法,会抛出
InterruptedException,从而使线程提前结束 TIMED-WATING状态。 - 许多声明抛出 InterruptedException 的方法(如 Thread.sleep(long mills 方法)),抛出异常前,都会清除中断标识位,所以抛出异常后,调用 isInterrupted()方法将会返回 false。
- 中断状态是线程固有的一个标识位,可以通过此标识位安全的终止线程。比如,你想终止一个线程 thread 的时候,可以调用
thread.interrupt()方法,在线程的 run 方法内部可以根据thread.isInterrupted()的值来优雅的终止线程。
多线程和单线程的区别和联系
-
1、在单核 CPU 中,将 CPU 分为很小的时间片,在每一时刻只能有一个线程在执行,是一种微观上 轮流 占用 CPU 的机制。
-
2、多线程会存在线程上下文切换,会导致程序执行速度变慢,即采用一个拥有两个线程的进程执行所需要的时间比一个线程的进程执行两次所需要的时间要多一些。
-
结论:即采用多线程不会提高程序的执行速度,反而会降低速度,但是对于用户来说,可以减少用户的响应时间。
简述线程、程序、进程之间的联系和关系
线程
与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程**。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源**,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程
程序
是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。
### 进程
-
是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。
-
系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如 CPU 时间,内存空间,文件,输入输出 设备的使用权等等。
-
线程是进程划分成的更 小的运行单位。
-
线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程 中的线程极有可能会相互影响
## 创建线程的几种方式
1、继承Thread的方式
class Mythread extends Thread{
@Override
public void run() {
for(int i=0;i<=100;i++){
if(i%2 != 0){
//sleep阻塞线程,单位毫秒
try {//快捷键:ctrl+alt+T
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
}
System.out.println(Thread.currentThread().getName()+"/*****/"+i);
}
if(i%10==0){
yield();//当i可以除以10 线程主动停止一下,但仍然会再次调用线程
}
}
}
2、实现Rannable接口的方式
/*
* 1.创建一个实现了Runnable接口的类(该接口已定义了)
* 2.实现类去实现Runnable类中的run()方法
* 3.创建实现类的对象
* 4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
* 5.通过Thread类的对象调用start()方法
*/
public class ThreadTest05 {
public static void main(String[] args) {
MyThread05 M1 = new MyThread05();//将这100封装到M1对象中
Thread T1 = new Thread(M1);//将M1封装(传递)到Thread对象T1中;
Thread T2 = new Thread(M1);
T1.start();
T2.start();
}
}
class MyThread05 implements Runnable{
private int ticket=100;
@Override
public void run() {
while (true){
if(ticket>0){
System.out.println(Thread.currentThread().getName()+"票号:"+ticket);
ticket--;
}else {
break;
}
}
}
}
3、实现Callable接口的方式
/**
* 创建线程的方式三,实现Callable接口。---JDK5.0新增
*
* Created by KingsLanding on 2022/3/20 22:01
*/
public class ThreadTest01 {
public static void main(String[] args) {
//3.创建Callable实现类的对象
MyThread m1 = new MyThread();
//4.将Callable接口实现类对象最为参数传递到FutureTask构造器中,创建FutureTask对象
FutureTask F1 = new FutureTask(m1);
//5.将FutureTask的对象作为阐述传递到Thread构造器中,创建Thread对象
Thread T1 = new Thread(F1);
T1.start();
try {
//获取Callable中call()方法的返回值
//get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值
Object sum = F1.get();
System.out.println(sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} finally {
}
}
}
//1.创建实现Callable接口的实现类
class MyThread implements Callable {
//实现call()方法,将需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
int sum=0;
for(int i=1;i<=100;i++){
if(i%2==0){
System.out.println(i);
sum+=i;
}
}
return sum;
}
}
4、线程池
/**
* 创建多线程的方式四:线程池
*
* ExecutorService:真正的线程池接口,常见子类ThreadPoolExecutor
*
* Executor:工具类、线程池工厂类,创建并返回不同类型的线程池
* Executor.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
* Executor.newFixedThreadPool(n):创建一个可重用固定线程数的线程池
* Executor.newSingleThreadExecutor():创建一个只有一个线程的线程池
* Executor.newScheduledThreadPool(n):创建一个线程池,可安排在给定延迟后
* 运行命令或定期执行
*
*
* Created by KingsLanding on 2022/3/25 17:10
*/
public class ThreadTest02 {
public static void main(String[] args) {
//1.提供指定线程数量的线程池(声明线程池信息)
ExecutorService service = Executors.newFixedThreadPool(10);
System.out.println(service.getClass());//查看对象是哪个类造的
MyThread02 m1 = new MyThread02();
MyThread03 m2 = new MyThread03();
//2.执行指定的线程的操作,需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(m1);//适合使用于Runnable
service.execute(m2);
service.shutdown();//关闭线程池
// service.submit();//适合适用于Callable
}
}
class MyThread02 implements Runnable{
@Override
public void run() {
for(int i=0;i<=100;i++){
if(i%2==0){
System.out.println(Thread.currentThread().getName()+"偶数"+i);
}}}}
class MyThread03 implements Runnable{
@Override
public void run() {
for(int i=0;i<=100;i++){
if(i%2 != 0){
System.out.println(Thread.currentThread().getName()+"奇数"+i);
}}}}
Java线程池中submit() 和 execute()方法有什么区别?
execute()方法的返回类型是void,它定义在Executor接口中,适合使用于Runnablesubmit()方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,适合适用于Callable
## 简述一下你对线程池的理解
入手方向:线程 池如何用、线程池的好处、线程池的启动策略
第一:降低资源消耗。 避免频繁创建销毁、实现重复利用
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源, 还会降 低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
线程有哪些基本状态(生命周期)
1.创建(初始化)、就绪、运行(runing)、阻塞(wait(),sleep())、销毁(终止)
如何停止一个正在运行的线程
-
1、使用退出标志,使线程正常退出,也就是当run方法完成后线程终止
-
2、使用interrupt方法中断线程。
-
3、stop(); 已过时,执行此方法时,强制结束当前线程
start()方法和run方法的区别
-
.start();启动当前线程,调用当前线程的方法体run() -
只有调用了start()方法,才会表现出多线程的特性,不同线程的run()方法里面的代码交替执行。
-
如果只是调用run()方法,那么代码还是同步执行的,必须等待一个线程的run()方法里面的代码全部执行 完毕之后,另外一个线程才可以执行其run()方法里面的代码。
## 为什么wait, notify 和 notifyAll这些方法不在thread类里面?
-
wait()方法强制当前线程释放对象锁。这意味着在调用某对象的wait()方法之前,当前线程必须已经获得该对象的锁。
-
因此,线程必须在某个对象的同步方法或同步代码块中才能调用该对象的wait()方法。
-
在调用对象的notify()和notifyAll()方法之前,调用线程必须已经得到该对象的锁。
-
因此,必须在某个对象的同步方法或同步代码块中才能调用该对象的notify()或notifyAll()方法
synchronized 与 Lock 的异同
-
同:两种方法都可以通过加锁方式解决线程安全问题
- 而且都是阻塞式的同步,也就是说当如果一 个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的。
- lock对象也要保证唯一性,用继承Thread的方式创建的多线程应当使用static
-
异:
-
synchronized机制在执行完相应的代码后自动的释放同步监视器
-
Lock需要手动的启动同步(调用lock()),手动的结束同步(调用unlock()方法
-
对于Synchronized来说,它是java语言的关键字,是原生语法层面的互斥, 需要jvm实现
-
而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock() 方法配合
-
-
相比Synchronized;ReentrantLock类提供了一些高级功能
- 1.等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待这相当于Synchronized来说可以避免出现死锁的情况。
- .公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁
## SynchronizedMap和ConcurrentHashMap有什么区别?
-
SynchronizedMap
一次锁住整张表来保证线程安全,所以每次只能有一个线程来访问 map。
-
ConcurrentHashMap
- 使用分段锁来保证在多线程下的性能。
ConcurrentHashMap中则是一次锁住一个桶。ConcurrentHashMap默认将 hash 表分为 16 个桶,诸如 get,put,remove 等常用操作只锁当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有 16 个写线程执行,并发性能显著提升ConcurrentHashMap使用了一种不同的迭代方式。在这种迭代方式中,当 iterator 被创建后集合再发生改变就不再是抛出 ConcurrentModificationException,取而代之的是在改变时 new 新的数据从而不影响原有的数据 ,iterator 完成后再将头指针替换为新的数据 ,这样 iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变。
-
显而易见
- ConcurrentHashMap在性能以及安全性方面,明显比Collections.synchronizedMap()更加有优势。
- 同步操作精确控制到桶,这样,即使在遍历map时,如果其他线程试图对map进行数据修 改, 也不会抛出ConcurrentModifificationException 。
Runnable接口和Callable接口的区别
为什么说Callable接口方法比Runnable方法更强大
- 1.call()方法可以有返回值
- 2.call()可以抛出异常,被外面的操作捕获,获取异常的信息
- 3.call()是支持泛型的
1、Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;
2、Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取 异步执行的结果。
这其实是很有用的一个特性,因为多线程相比单线程更难、更复杂的一个重要原因就是因为多线程充满着未知性,某条线程是否执行了?某条线程执行了多久?某条线程执行的时候我们期望的数据是否已经赋值完毕?无法得知,我们能做的只是等待这条多线程的任务执行完毕而已。
而 Callable+Future/FutureTask 却可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务,真的是非常有用。
锁
乐观锁
一种乐观思想,即认为读多写少,并发的可能性低,每次去拿数据的时候都认为别人不会更改数据,因此不会上锁。而是在这个数据上额外增加一个uid版本号标识,当有请求对数据进行修改就会改变这个uid,其他请求不能在对这个已经更新过的数据进行更改操作;(比较跟上一次的版本号,如果一样则更新), 如果失败则要 重复读-比较-写的操作
java 中的乐观锁基本都是通过 CAS操作实现的, CAS是一种更新的原子操作, 比较当前值跟传入值是否一样,一样则更新,否则失败。
悲观锁
java中 的悲观锁就是Synchronized
顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁;传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
自旋锁
如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
线程自旋是需要消耗 cup 的,说白了就是让 cup 在做无用功,如果一直获取不到锁,那线程也不能一 直占用 cup 自旋做无用功
所以需要设定一个自旋等待的最大时间。 如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。
自旋锁的优缺点
-
优点
-
自旋锁可以尽可能的减少线程的阻塞,这对于锁竞争不太激烈,且对锁的占用时间较短的情况能够对性能大幅提升,避免用户线程和内核切换的消耗。
-
因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生 两次上下文切换!
-
-
缺点
- 如果竞争激烈的情况,或者说对锁的占用时间较长的情况,自旋锁反而会加大CPU的消耗和浪费
- 因为自旋锁在获取到锁之前,一直在CPU中做无用功,反复等待锁的释放,线程自旋的消耗大于线程阻塞挂起再唤醒的消耗,导致其他需要CPU的线程无法执行
自旋锁时间阈值
避免自旋锁长时间占用cpu资源,
JVM对于自旋周期的选择, jdk1.5 这个限度是一定的写死的, 在 1.6 引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间
-
同时JVM还针对当前 CPU 的负荷情况做了较多的优化
-
如果平均负载小于 CPUs 则一直自旋,如果有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞
-
如果正在自旋的线程发现 Owner 发生了变化则延迟自旋时间(自旋计数)或进入阻塞,如果 CPU 处于节电模式则停止自旋,自旋时间的最坏情况是 CPU的存储延迟(CPU A 存储了一个数据,到 CPU B 得知这个数据直接的时间差), 自旋时会适当放弃线程优先级之间的差异。
-
ReentantLock 继承接口
Lock 并实现了接口中定义的方法, 他是一种可重入锁, 除了能完成 synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多 线程死锁的方法。
Lock 接口的主要方法
1、boolean tryLock()
如果锁可用, 则获取锁, 并立即返回 true, 否则返回 false. 该方法和lock()的区别在于
1、tryLock()
只是"试图"获取锁, 如果锁不可用, 不会导致当前线程被禁用,当前线程仍然继续往 下执行代码.
2、lock()
是一定要获取到锁, 如果锁不可用, 就一直等待, 在未获得锁之前,当前线程并不继 续向下执行.
2、void unlock():
执行此方法时, 当前线程将释放持有的锁. 锁只能由持有者释放, 如果线程并不持有 锁,却执行该方法,
可能导致异常的发生
3、getHoldCount()
查询当前线程保持此锁的次数,也就是执行此线程执行 lock 方法的次数。
4、getQueueLength()
返回正等待获取此锁的线程估计数,比如启动 10 个线程, 1 个线程获得锁,此时返回的是 9
微信号:mahukang联系我们获取更多资源 公众号:码出宇宙 xiaomage0086
5、getWaitQueueLength(Condition condition)
返回等待与此锁相关的给定条件的线程估计 数。比如 10 个线程,
用同一个 condition 对象,并且此时这 10 个线程都执行了condition 对象的 await 方法,那么此时
执行此方法返回 10
6、hasWaiters(Condition condition)
查询是否有线程等待与此锁有关的给定条件(condition),对于 指定contidion 对象,有多少线程执
行了 condition.await 方法
7、Condition newCondition()
才能调用该组件的 await()方法,而调用后,当前线程将缩放锁。
8、hasQueuedThread(Thread thread)
查询给定线程是否等待获取此锁
9、hasQueuedThreads()
是否有线程等待此锁
10、isFair()
该锁是否公平锁
11、isHeldByCurrentThread()
当前线程是否保持锁锁定,线程的执行 lock 方法的前后分别是 false 和true
12、isLock()
此锁是否有任意线程占用
13、lockInterruptibly()
如果当前线程未被中断,获取锁
14、tryLock()
尝试获得锁,仅在调用时锁未被线程占用,获得锁
15、tryLock(long timeout TimeUnit unit)
如果锁在给定等待时间内没有被另一个线程保持,则获取该锁 。
公平锁
- 公平锁指的是锁的分配机制是公平的,通常先对锁提出获取请求的线程会先被分配到锁
- ReentrantLock 在构造函数中提供了是否公平锁的初始化方式来定义公平锁
Condition 类和 Object 类锁方法区别区别
- 调用Condition的
await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用
- Condition 类的 awiat 方法和 Object 类的 wait 方法等效
- Condition 类的 signal 方法和 Object 类的 notify 方法等效
- Condition 类的 signalAll 方法和 Object 类的 notifyAll 方法等效
- ReentrantLock 类可以唤醒指定条件的线程,而 object 的唤醒是随机的
非公平锁
- JVM按随机、就近原则分配锁的机制则称为不公平锁
- ReentrantLock 在构造函数中提供了是否公平锁的初始化方式,默认为非公平锁。非公平锁实际执行的效率要远远超出公平锁,除非程序有特殊 需要,否则最常用非公平锁的分配机制。
tryLock 和 lock 和 lockInterruptibly 的区别
- tryLock 能获得锁就返回 true,不能就立即返回 false
- tryLock(long timeout,TimeUnit unit),可以增加时间限制,如果超过该时间段还没获得锁,返回 false
- lock 能获得锁就返回 true,不能的话一直等待获得锁
- lock 和 lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程,lock 不会抛出异常,而 lockInterruptibly 会抛出异常。
可重入锁(递归锁)
可重入锁,也叫做递归锁,指的是同一线程外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
在 JAVA 环境下 ReentrantLock 和 synchronized 都是可重入锁。
ReadWriteLock 读写锁为了提高性能
-
在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下, 读是无阻塞的,在一定程度上提高了程序的执行效率
-
读写锁分为读锁和写锁,多个读锁 不互斥,读锁与写锁互斥,这是由 jvm 自己控制的,你只要上好相应的锁即可。
读锁
如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁
写锁
如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。
总之,读的时候上读锁,写的时候上写锁!
共享锁和独占锁
独占锁:独占锁是一种悲观保守的加锁策略
独占锁模式下,每次只能有一个线程能持有锁, ReentrantLock 就是以独占方式实现的互斥锁。
共享锁:共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源
比如java 的并发包中提供了 ReadWriteLock,读-写锁。它允许一个资源可以被多个读操作访问, 或者被一个写操作访问,但两者不能同时进行。
锁的状态
无锁
无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。 无锁的特点就是修改操作在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并 退出,否则就会继续循环尝试
偏向锁
在JDK1.6中为了提高一个对象在一段很长的时间内都只被一个线程用做锁对象场景下的性能,引入了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只会执行几个简单的命令,而不是开销相对较大的CAS命令。
引入偏向锁的目的
-
在没有多线程竞争的情况下,尽量减少不必要的轻量级锁的执行。轻量级锁的获取及释放依赖多次CAS 原子指令,而偏向锁只依赖一次CAS原子指令
-
但在多线程竞争时,需要进行偏向锁撤销步骤,因此其撤销的开销必须小于节省下来的CAS开销,否则偏向锁并不能带来收益。JDK 1.6中默认开启偏向锁,可 以通过-XX:-UseBiasedLocking来禁用偏向锁。
-
优点
加锁解锁无需额外的消耗,和非同步方法时间相差纳秒级别。
-
缺点
如果竞争的线程多,那么会带来额外的锁撤销的消耗(撤销时会暂停原所有者线程)。
CAS 的优点
非阻塞的轻量级乐观锁, 通过CPU指令实现, 在资源竞争不激烈的情况下性能高, 相比synchronize重量级悲观锁, synchronize有复杂的加锁, 解锁和唤醒线程操作。
锁优化
减少锁持有时间
- 只用在有线程安全要求的程序上加锁
减小锁粒度
- 将大对象(这个对象可能会被很多线程访问),拆成小对象,大大增加并行度,降低锁竞争。降低了锁的竞争,偏向锁,轻量级锁成功率才会提高。最最典型的减小锁粒度的案例就是ConcurrentHashMap。
锁分离
- 最常见的锁分离就是读写锁 ReadWriteLock,根据功能进行分离成读锁和写锁,这样读读不互斥, 读写 互斥,写写互斥,即保证了线程安全,又提高了性能。读写分离思想可以延伸,只要操作互不影响,锁 就可以分离
锁粗化
- 通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。但是, 如果对同一个锁不停的进行请求、同步和释放, 其本身也会消耗系统宝贵的资源,反而不利于性能的优化 。
锁消除
在不可能被共享的对象,则可以消除这些对象的锁操作
死锁
何为死锁,就是多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放
死锁的原因
互斥使用:当资源被一个线程使用或者占用时,别的线程不能使用该资源
不可抢占:获取资源的一方,不能从正在使用资源的一方抢占掠夺资源,资源只能被使用者主动释放
请求和保持:资源请求者在请求别的资源时,同时保持对已有资源的占有
循环等待:即p1占有p2的资源,p2占有p3的资源,p3占有p1的资源,这样形成了一个等待环路
上述这四个条件满足即造成的结果就是死锁
如何避免
死锁的产生必须满足互斥使用,不可抢占,请求和保持,循环等待这四个条件,但是只要破坏其中任意一个条件即可破坏死锁,其中最容易破坏的就是循环等待这个条件,那么如何破坏循环等待这个条件呢?
多个线程约定好一定的顺序,按照这个顺序加锁释放锁
MySQL
说一说数据库的三大范式
第一范式:列不可再拆分
第二范式:非主键列完全依赖于主键列,不能是只依赖一部分
第三范式:非主键列只能依赖于主键列,不能依赖于其他非主键列
- 在设计数据库结构的时候,要尽量遵守三范式,如果不遵守,必须有足够的理由.比如性能. 事实上我们经常会 为了性能而妥协数据库的设计。
MySQL有哪些部分组成以及作用
1、server
-
连接器: 管理连接, 权限验证.
-
分析器: 词法分析, 语法分析.
-
优化器: 执行计划生成, 索引的选择.
-
执行器: 操作存储引擎, 返回执行结果
2、储存引擎
- 存储数据, 提供读写接口.
ACID是什么?可以详细说一下嘛
A=Atomicity
- 原子性:就是上面说的,要么全部成功,要么全部失败.不可能只执行一部分操作。
C=Consistency
- 一致性:系统(数据库)总是从一个一致性的状态转移到另一个一致性的状态,不会存在中间状态
I=Isolation
- 隔离性:通常来说:一个事务在完全提交之前,对其他事务是不可见的.注意前面的通常来说加了红色,意味着 有例外情况.
D=Durability
- 持久性:一旦事务提交,那么就永远是这样子了,哪怕系统崩溃也不会影响到这个事务的结果.
同时有多个事务正在进行提交,会怎么样呢?
事务(transaction)是作为一个单元的一组有序的数据库操作。如果组中的所有操作都成功, 则认为事务成功, 即使只有一个操作失败, 事务也不成功。如果所有操作完成, 事务则提交, 其修改将作用于所有其他数据库进程。如果一个操作失败, 则事务将回滚, 该事务所有操作的影响都将取消。
事务特性
-
原子性:即不可分割性, 事务要么全部被执行, 要么就全部不被执行。
-
一致性:或可串性事务的执行使得数据库从一种正确状态转换成另一种正确状态
-
隔离性:在事务正确提交之前,不允许把该事务对数据的任何改变提供给任何其他事务
-
持久性:事务正确提交后, 其结果将永久保存在数据库中, 即使在事务提交后有了其他故障, 事务的处理结果也会得到保存。
或者这样理解:事务就是被绑定在一起作为一个逻辑工作单元的 SQL 语句分组,如果任何一个语句操作失败那么整个操作就被失败,以后操作就会回滚到操作前状态, 或者是上有个节点。为了确保要么执行,要么不执行,就可以使用事务。
- 要将有组语句作为事务考虑, 就需要通过 ACID 测试, 即原子性,一致性, 隔离性和持久性。
Mysql索引分哪些?
从逻辑上分
1、主键索引:主键索引是一种特殊的唯一索引,不允许有空值
2、普通索引或者单列索引
3、多列索引(复合索引):复合索引指多个字段上创建的索引,只有在查询条件中使用了创建索引时的 第一个字段,索引才会被使用。使用复合索引时遵循最左前缀集合
CREATE INDEX PIndex
ON Persons (LastName, FirstName)
4、唯一索引或者非唯一索引
5、空间索引:空间索引是对空间数据类型的字段建立的索引,MYSQL中的空间数据类型有4种,分别是 GEOMETRY、POINT、LINESTRING、POLYGON。MYSQL使用SPATIAL关键字进行扩展,使得能够用于 创建正规索引类型的语法创建空间索引。创建空间索引的列,必须将其声明为NOT NULL,空间索引只 能在存储引擎为MYISAM的表中创建
建立了主键索引还可以建立唯一索引吗?
- 主键是一种约束,目的是对这个表的某一列进行限制;
- 唯一索引是一种索引,索引是数据库表的一个冗余结构,目的是为了更好的查询;
- 主键列不允许为空值,而唯一性索引列允许空值;
- 一个表最多只能一个主键,但是可以包含多个唯一索引;
注:可以多列组合成一个唯一索引或者一个主键,即组合索引或组合主键
主键与索引的区别如下:
- 主键是一种约束,唯一索引是一种索引,两者在本质上是不同的。
- 主键创建后一定包含一个唯一性索引,唯一性索引并不一定就是主键。
- 唯一性索引列允许空值,而主键列不允许为空值。
- 主键列在创建时,已经默认不为空值 + 且创建了唯一索引。
- 主键可以被其他表引用为外键,而唯一索引不能。
- 一个表最多只能创建一个主键,但可以创建多个唯一索引。
- 主键更适合那些不容易更改的唯一标识,如自动递增列、身份证号等。
为什需要对数据加索引,为什么需要索引
当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度。
1、索引是数据库本身在执行的时候调用的,而不是我们去程序中使用
2、在常常需要进行查询的才需要建立索引,需要提高查询效率的时候
3、并不是建立索引了就一定会提高数据库的查询效率,在查询数据超过30%的情况就完全没必要使用了
索引怎么去使用?
在查询sql中where条件中使用索引列
怎么查看索引被调用了?
执行计划中可以体现用到了的索引有那些,在Navicat for sql中查询执行计划是点‘解释’查看 possible_keys列用到的索引,pl/sql可以直接查看 那建立索引的作用以及优缺点?
作用:
- 快速查询数据
- 保证数据的唯一性
- 实现表与表之间的参照完整性
- 在使用order by、group by子句进行数据检索时,利用索引可以减少排序和分组的时间。
- 加速表之间的连接
缺点:
创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。 索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大。
数据库中的数据结构
计算机网络
OSI七层模型
1、应用层:进程通信与交互、计算机程序
2、表示层:进行数据格式处理,
3、会话层:当前主机进程和目标主机进程的会话建立,管理和终止
4、传输层:数据传输,定义了序列号,差错校验、流量控制
5、网络层:网络IP地址查询
6、数据链路层:建立逻辑地址的连接,逻辑地址寻址
7、物理层:最底层数据(比特流0、1)传输、管理、关闭
TCP/IP四层结构
1、应用层
2、运输层
3、网际层、
4、网络接口层
五层协议体系结构
1、应用层
2、运输层
3、网络层
4、数据链路层
5、物理层
TCP是什么?
传输控制协议:运输层协议,处理应用层的数据、特点:面向连接、单播、全双工、面向字节流、可靠支付、头部开销大
1、面向连接:应用程序之间通信必须建立连接才能进行数据传输、传输完成后必须关闭TCP连接
2、单播:一对一的数据传输
3、全双工:三次握手,四次挥手;发送到发送缓存,接收缓存就可以去做其他事情了
UDP是什么?
传输控制协议:运输层协议,处理应用层数据;特点:无连接、最大努力支付、头部开销小、多对一、一对多、多对多、没有拥塞控制
1、无连接:不需要像TCP那样建立连接;无三次握手四次挥手
2、最大努力支付:数据传输不可靠,可能会丢失数据
3、无拥塞控制;不会在网络差的情况下降低数据传输效率,允许丢失一些数据,但不能有太大的延迟;适用于音视频
TCP三次握手、四次挥手过程
1、客户端发送一个连接请求给服务端
2、服务端收到后,如果同意则向客户端发送应答
3、客户端向服务端发送确认报文,连接建立
为什么:如果客户端第一次发送请求,由于意外原因导致服务器端没收到、那么客户端再次发送时,服务端就会收到两次连接请求,服务端就会确认两次,如果没有客户端最后的确认,那么就会连接两次,造成多余的开销
四次挥手
1、客户端发送连接关闭请求到服务端、
2、服务端接收到请求,并进行应答,此时进入半连接状态,服务端仍然可以向客户端发送数据
3、服务端要进行确认是否已经没有数据要发送,发送连接释放应答,进入最终确认状态
4、客户端接收到服务端的释放请求之后,向服务端发出确认,此时连接还没有释放,等到2倍的MSL(最长报文段寿命)连接释放
TCP怎么保证可靠传输的
1、序列号:包编号,接收方在对包进行排序,保证数据的有序不重复
2、数据校验:检验数据在传输过程中的变化
3、确认应答:检验通过、没有差错,确认收到。如果有差错,将会丢弃该报文段
3、流量控制:接收端和发送端都有缓冲区,如果缓冲区满了,TCP使用流量控制协议限制发送方发送速率、防止包丢失
4、阻塞控制:网络不好的时候,减少数据的发送
6、停止等待协议:每发完一组就停止,等待收到确认之后在进行下一次的发送
7、超时重传:长时间没有收到确认,就会重发
如何让UDP变可靠
1、向TCP靠拢:加入序列号机制
2、进行数据校验
3、进行流量控制、阻塞机制、应答机制
HTTP超文本协议是什么
1、应用层协议:基于请求\响应、简单快速,无连接无状态
2、请求过程:建立一个TCP连接,发送HTTP请求,接收响应报文后关闭TCP连接
3、通过HTTP超文本传输协议、URL连接地址可以将web服务器上的目标资源提取出来