Java&JVM知识点最详细总结

自己整理的一些Java相关的知识点,内容有些比较散乱,后续会再调整
对于每个知识点,建议按照 为什么需要这个技术?这个技术的原理是什么?在实践中具体是怎么用的?的思路来进行学习。

类的实例化顺序

1. 父类静态成员和静态初始化块 ,按在代码中出现的顺序依次执行
2. 子类静态成员和静态初始化块 ,按在代码中出现的顺序依次执行
3. 父类实例成员和实例初始化块 ,按在代码中出现的顺序依次执行
4. 父类构造方法
5. 子类实例成员和实例初始化块 ,按在代码中出现的顺序依次执行
6. 子类构造方法

Java中创建对象的几种方式

(1)new关键字
(2)Class类的newInstance方法
(3)Constructor的newInstance方法
(4)Clone方法
(5)使用反序列化

序列化和反序列化 RPC

序列化将对象转换为字节流保存在磁盘或者通过网络进行传输,反序列化是相反过程。需要实现serializable接口。Transient关键字修饰变量表示该变量不序列化。静态变量不是对象状态的一部分不参与序列化。Final直接通过值参与序列化。
序列化:
Employee emp = new Employee(“sky”, 50);需要序列化的对象
FileOutputStream fos = new FileOutputStream(“EMP.ser”); 文件输出流输出到文件
ObjectOutputStream oos = new ObjectOutputStream(fos);序列化
Oos.writeObject(emp);

反序列化:
FileInputStream fis = new FileInputStream(“EMP.ser”)读取文件
ObjectInputStream ois = new ObjectInputStream(fis);
Empin = (Employee)ois.readObject();

SeriversionUID是一个版本号,只有相同才能进行序列化和反序列化。

RPC:包括序列化层、传输层、函数调用层、服务框架层,一个RPC过程需要有如下元素:
(1)协议接口定义(需要调用的函数接口)
(2)协议实现(服务端对函数的实现)
(3)服务端(进行请求的监听,读取,反序列化,处理,序列化和传输)
(4)客户端(调用函数)
(5)客户端代理(负责进行远程调用,其中包括请求的封装、序列化、传输、反序列化等)

JVM内存分配及JVM执行过程

(1)程序计数器:当前线程执行的字节码的位置指示器;
(2)VM栈:JAVA指令由操作码(方法本身)和操作数(方法内部变量)组成,方法本身保存在栈中,方法内部变量保存在栈中(简单类型),对象类型在栈中保存地址,在堆中保存值。VM栈在线程创建时创建生命周期随线程的生命周期,对于栈来说不存在垃圾回收。栈帧:内存区块,关于方法和运行期数据的数据集,包括方法索引,输入输出参数,本地变量,父帧,子帧,类文件,局部变量表,操作数栈,动态连接,方法出口信息。
(3)堆,内存数据区,被所有线程共享内存区域在JVM创建时启动,专门用来保存对象的实例。实际上只保存对象的属性值,属性的类型和对象本身的类型标记并不保存对象的方法(以栈帧的方式保存在栈中)。对象实例在heap中分配好之后需要在栈中保存一个四字节的堆内存地址便于找到实例对象。堆是垃圾回收的主要场所。
(4)方法区,加载类的类定义数据,常量,静态变量,JIT编译后的代码都放在方法区。这个区域的内存回收主要针对常量池的回收和类的卸载。
(5)运行时常量池,字面量和符号引用(类和接口的全限定名,字段的名称和描述符,方法和名称的描述符)等被类加载后都会存储在方法区的RCP中,运行时产生的新常量也可以放入其中,例如string的intern()方法。
(6)本地方法栈。为JVM提供使用native方法的服务。
(7)直接内存区。不是JVM管理的内存区域的一部分,而是其之外。
对象实例都是分配在堆上。
OOM原因;
堆内存不足是常见的OOM原因之一。

JVM程序处理过程:

public class JVMShowcase {  
 2 //静态类常量,  
 3 public final static String ClASS_CONST = "I'm a Const";  
 4 //私有实例变量  
 5 private int instanceVar=15;  
 6 public static void main(String[] args) {  
 7 //调用静态方法  
 8 runStaticMethod();  
 9 //调用非静态方法  
10 JVMShowcase showcase=new JVMShowcase();  
11 showcase.runNonStaticMethod(100);  
12 }  
13 //常规静态方法  
14 public static String runStaticMethod(){  
15 return ClASS_CONST;  
16 }  
17 //非静态方法  
18 public int runNonStaticMethod(int parameter){  
19 int methodVar=this.instanceVar * parameter;  
20 return methodVar;  
21 }  
22 }  

(1)向操作系统申请空闲的空间。操作系统找到合适的内存段将起始地址返回给JVM
(2)JVM分配内存,首先给heap分内存,然后给栈分内存;
(3)文件检查和分析class文件,如果发现错误直接返回错误;
(4)加载类。JVM默认使用bootstrap加载器,把rt.jar中的所有类加载到了方法区,同时类中的静态方法也加载到里面。此时heap和stack为空因为没有对象的新建和线程的执行。
(5)执行方法。执行main方法,启动一个线程,将静态常量加入到方法区中。将Object和showcase对象放入堆内存中。在栈中有三个栈帧:自上而下为runNonStaticMethod frame, runStaticMethod frame, main frame。同时创建了一个程序计数器指向下一条要执行的语句。
(6)释放内存
注意:作为局部变量时,基本数据类型直接放在栈中,实例类型放在堆中同时栈中有一个指向该位置的指针。实例存放在栈中是一个指针,对象存放在堆中是一个具体的实体。

对象创建过程:
(1)类加载检查,查看常量池中的符号引用代表的类是否被加载过。
(2)分配内存,指针碰撞(规整)和空闲列表(不规整)两种分配方式,内存分配并发问题(CAS+失败重试,TLAB)。
(3)初始化零值。
(4)设置对象头,对象是哪个类的实例、类的元数据信息、对象的哈希码、对象的GC分代年龄。
(5)执行init方法

对象内存布局:
对象头(对象自身的运行时数据,类型指针),实例数据(对象真正存储的有效信息),对齐填充。
对象的访问定位:
句柄:维护一个句柄池,里面包含对象实例数据与类型数据各自的具体地址信息;引用中存储的是稳定的句柄地址,对象改变,引用不用改变;
直接指针:速度快,但是随对象的改变需要改变。

Java内存模型(java memory model jmm)目标是定义程序中各个变量的访问规则。

GC相关

jvm分为三个代:年轻代,年老代和持久代(java8将永久代取代为metaspace,将类元数据放到本地内存中,另外,将常量池和静态变量放到 Java 堆里,这样就从一定程度上解决了原来在运行时生成大量类的造成经常 Full GC 问题,如运行时使用反射、代理等)
年轻代:存放刚生成的对象(Eden和survival8:1.会使用90% )
Survivor有两个,分别为To Survivor、 From Survivor,这个两个区域的空间大小是一样的。执行垃圾回收的时候Eden区域不能被回收的对象被放入到空的survivor(也就是To Survivor,同时Eden区域的内存会在垃圾回收的过程中全部释放),另一个survivor(即From Survivor)里不能被回收的对象也会被放入这个survivor(即To Survivor),然后To Survivor 和 From Survivor的标记会互换,始终保证一个survivor是空的。
年老代:在年轻代中经历了N次垃圾回收仍然存活的方法,或者年轻代中内存放不下的对象
持久代:用户存放静态文件,如java类、方法等

判断是否需要回收:
(1).引用计数
两方面的问题:(1)无法准确区分强引用,软引用,弱引用和虚引用 (2)存在循环引用的情况,引用数永远为1
(2).对象引用遍历(是否可达)
从 GC Root(一组必须活跃的引用)作为根出发,通过引用关系遍历对象图,能被遍历到的(可到达的)对象就被判定为存活,其余对象(也就是没有被遍历到的)就自然被判定为死亡

造成内存泄漏的几种情况:
1.静态集合类引起的内存泄漏
2.各种连接,数据库连接,网络连接
3.外部模块的引用,类A调用了类B的方法。
3.单例模式,如果单例模式持有外部引用,那么这个对象将不能被jvm正常回收,导致内存泄漏
栈开启过多;加载对象过大;数据库或者资源没有关闭;非静态内部类持有外部类的引用,使用非静态内部类创建静态变量;递归次数过多;大量小的对象被频繁的创建;

GC root包含以下几种对象:
1、虚拟机栈(栈中的本地变量表)中引用的对象;
2、方法区中类静态属性引用的对象;
3、方法区中常量引用的对象;
4、本地方法栈中JNI(即一般说的Native方法)引用的对象。

强引用:new出来的实例
软引用:非必须引用,内存溢出之前进行回收(用于实现类似缓存的功能,可以选择与referenceQueue配合使用)
弱引用:下一次垃圾回收时回收
虚引用:每次垃圾回收时都会回收,但是无法通过引用取到对象值(主要用来追踪对象被垃圾回收期回收的活动)
(虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,
如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。
程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。)

触发GC的两个条件:
1)GC在优先级最低的线程中运行,一般在应用程序空闲即没有应用线程在运行时被调用。
2)Java堆内存不足时,GC会被调用。
其中minor GC触发条件:当Eden区满时
Full GC:
(1)调用System.gc系统建议执行Full GC但是不必然执行
(2)老年代空间不足
(3)方法区空间不足(永久代空间满)
(4)统计得到的minGC晋升到老年代的平均大小大于老年代的剩余空间

垃圾收集器的种类:
(1)串行收集器 暂停所有应用的线程来工作; 单线程
(2)并行收集器 默认的垃圾收集器。暂停所有引用; 多线程
(3)G1收集器 用于大堆区域。堆内存分隔,并发回收 以到达一定的吞吐量为目标,使用于科学技术和后台处理。过程:初始标记;并发标记;最终标记;筛选回收。整体上看是“标记-整理”,局部看是“复制”,不会产生内存碎片。 (不分新生代和老年代,而是划分成大小相等的独立区域region,但是也分为E,S,O,H(巨大对象,大于region的一半)G1收集器跟踪垃圾堆积的价值大小,后台维护一个优先队列)
(4)CMS收集器 多线程扫描,标记需要回收的实例,清除。一款以获取最短回收停顿时间为目标的收集器,是基于“标记-清除”算法实现的,分为4个步骤:初始标记、并发标记、重新标记、并发清除。 适用于应用服务器、电信领域
初始标记->并发标记->重新标记->并发清除

7种垃圾收集器:
(1)新生代:1.Serial收集器(单线程,stop the world,复制算法) 2.ParNew(Serial的多线程版本) 3.Parallel Scavenge (Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。
所谓吞吐量就是CPU用于运行用户代码的时间和CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 / (运行用户代码时间+垃圾收集时间),虚拟机总共运行了100min,
其中垃圾收集花费了1min,那吞吐量就是99%.)
(2)老年代:1.Serial Old(Serial收集器的老年代版本,标记整理算法) 2.Parallel Old(Parallel Scavenge的老年代版本,标记整理算法) 3.CMS收集器(并发收集器,一款以获得最短回收停顿时间为目标的收集器,标记-清除算法)
(3)不区分新生代和老年代:G1收集器

停顿时间短适合需要与用户交互的程序,良好的响应速度能提升用户体验;高吞吐量则可以高效率利用CPU时间,尽快完成运算任务,主要适合在后台运算而不需要太多交互的任务。

JVM参数调优的目的:

  • GC的时间足够的小
  • GC的次数足够的少
  • 发生Full GC的周期足够的长
  • 更大的年轻代必然导致更小的年老代,大的年轻代会延长普通GC的周期,但会增加每次GC的时间;小的年老代会导致更频繁的Full GC
  • 更小的年轻代必然导致更大年老代,小的年轻代会导致普通GC很频繁,但每次的GC时间会更短;大的年老代会减少Full GC的频率
  • 如何选择应该依赖应用程序对象生命周期的分布情况:如果应用存在大量的临时对象,应该选择更大的年轻代;如果存在相对较多的持久对象,年老代应该适当增大。

Java中的拷贝

拷贝:引用拷贝 和 对象拷贝
对象拷贝又分为浅拷贝和深拷贝(浅拷贝:基本数据类型的变量会重新复制一份,对于引用类型变量只是对引用进行拷贝。深拷贝:同时会对引用指向的对象进行拷贝,重写clone函数或者使用序列化)
Clone(){
Stu = (student) super.cleon();
Stu.addr = (address)addr.clone();
}

类的加载过程

加载:由类加载器完成(双亲委派模型,保证加载一次,保证API不被修改),找到对应的字节码,创建一个Class对象
链接:验证类中的字节码,为静态域分配空间,解析(符号引用替换为直接引用)(验证,准备和解析)
初始化:在连接的准备阶段,类变量已赋过一次系统要求的初始值,而在初始化阶段则是根据程序员自己写的逻辑去初始化类变量和其他资源。

问:能否在加载类的时候对类的字节码进行修改
可以,使用java探针技术

java中class.forName()和classLoader都可用来对类进行加载。
区别:class.forName()前者除了将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块。
而classLoader只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。

(1)bootstrap loader 加载jvm运行需要的类
(2)extclassloader jre/lib/ext中的jar
(3)appClassloader 加载classPath指定的类以及上面没有包括的类

双亲委派模型的优点:
Java类伴随其类加载器具备了带有优先级的层次关系,确保了在各种加载环境的加载顺序。 保证了运行的安全性,防止不可信类扮演可信任的类。
2.8 Array.sort实现原理
Arrays.sort实现原理和Collections实现原理
JDK8中array.sort:小于47采用插入排序,48-286采用双轴快排,大于286如果逆序度大于67则快排,小于67则归并排序。

JSE 7对对象进行排序,没有采用快速排序,是因为快速排序是不稳定的,而Timsort是稳定的。
Collections.sort底层使用了arrays.sort。array.sort底层使用了timsort(二分插入排序+归并排序):根据已有序列划分run,调整run的长度,合并小run,合并时采用二分插入。
(0)数组长度小于某个值,直接用二分插入排序算法
(1)找到各个run,并入栈
(2)按规则合并run

Arrays.sort(a2, (o1, o2) -> o2 - o1);倒序排序
Hashmap对值进行排序
//将map.entrySet()转换成list
List> list = new ArrayList>(map.entrySet());
Collections.sort(list, new Comparator>() {
//降序排序
@Override
public int compare(Entry o1, Entry o2) {
//return o1.getValue().compareTo(o2.getValue());
return o2.getValue().compareTo(o1.getValue());
}
});

hashmap相关

为什么hashmap长度需要是2的幂,及保证方法。
Tablesizefor(int cap){
Int n = cap -1;
N |= n >>>1;
N |= n >>>2;
N |= n >>>4;
N |= n >>>8;
N |= n >>>16;
Return (n<0) ?1:(n>=MAXIMUM_CAPACITY)?Maximum_capacity:n+1;
}
在put的时候需要indexFor寻找数组下标,需要进行取余操作,而位运算效率远远高于map。当容量一定是2^n时,h & (length - 1) == h % length

hashfunc(String key){
int sum = 0;
for(int i = 0; i < key.length(); i++){
sum = sum*31 + (int)(key.charAt(i));
sum = sum % HashTable_Size;
}
return sum;
}
31是经验值从计算速度和冲突率.apache中是33

扰动函数:int的范围是32,40亿的映射空间,太大,之后需要h&(length-1),扰乱函数可以让低16位和高16位异或,保留高16位的信息
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

hashmap的bucket和链上的都是entry,重要属性:capacity,loadFactor,threshold
put: 先初始化数组大小,根据key生成hash,然后如果有重复key进行覆盖,然后添加到bucket中(需要先判断是否需要rehash),如果有冲突就加入到链表末尾
get:根据key获得hash,IndexFor获得下表,遍历链表找到key值相同,equals(两个值可能hashcode相同但是实际值不同)
jdk1.8之前是头插法(多线程操作可能会导致死循环),jdk1.8是尾插法(jdk1.8每当节点>8时就会变为红黑树,而树的遍历会更加快速,7个值的链表遍历起来也很快)
hashmap出现死循环的原因所在:主要是多线程同时put时,如果同时触发了rehash操作,会导致HashMap中的链表中出现循环节点
http://www.importnew.com/22011.html
负载因子容量=门限值 负载因子容量>元素数量
负载因子没有特别要求不要修改,不要设置超过0.75的数,缩小负载因子需要对预设容量进行调整。

解决hash冲突的方法:开发定址法,再哈希法,链地址法,建立公共溢出区。

Hashmap和hashtable:
Hashmap非线程安全,hashtable线程安全;hashmap效率比hashtable效率高,hashmap初始16每次变为原来2倍。Hashtable初始11,每次扩充变为2n+1.
Linkedhashmap可以理解为是hashmap+一个双向链表,可以保持键值对的插入顺序。

concurrentHashMap结构
分段数组。一个segment数组,默认16不可扩容,分段锁。每个segment对应之前的一个hashMap。JDK1.8中使用Node数组+链表+红黑树,并发控制通过synchronized和cas操作(循环时间开销大,只能保证一个共享变量的原子操作,ABA问题,初次读取和再次读取都为A但是中间该值可能发生变化)。
对于get操作不需要加锁,因为val和next都用volatile修饰。数组用volatile修饰主要是保证数组在扩容时的可见性。

hashcode和equals都是Object类中定义的。

jdk1.8中改变:
改进一:取消segments字段,直接采用transient volatile HashEntry table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率。
改进二:将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构。

cas操作:
unsafe:对应的是Unsafe类,Java无法直接访问底层操作系统,而是通过本地(native)方法来访问。JDK中有一个类Unsafe,它提供了硬件级别的原子操作。
transient是Java语言的关键字,用来表示一个域不是该对象串行化的一部分

工业级水平的散列表
(1)散列函数设计不能太复杂,随机均匀分布。直接取后几位取模,将字符以某种方式计算后取模。
(2)哈希表要能够进行动态的扩容和缩容,通过装载因子实现。
(3)在动态扩容时不要一次性完成,可以分批完成,当达到阈值后申请新的空间,后续的插入都放在新表中。
(4)数据量小装载因子小时用开放寻址法。大对象大数据量用链表法。

Java动态代理

动态代理:在不改变目标对象方法的情况下对方法进行增强
组成要素:抽象类接口;被代理类;动态代理类;
代理这种设计模式是通过不直接访问被代理对象的方式,而访问被代理对象的方法。(开闭原则)
在代码编译时就确定了被代理的类是哪一个,那么就可以直接使用静态代理;如果不能确定,那么可以使用类的动态加载机制,在代码运行期间加载被代理的类这就是动态代理,比如RPC框架和Spring AOP机制。
主要用来做方法的增强,让你可以在不修改源码的情况下,增强一些方法,在方法执行前后做任何你想做的事情(甚至根本不去执行这个方法),
因为在InvocationHandler的invoke方法中,你可以直接获取正在调用方法对应的Method对象,具体应用的话,比如可以添加调用日志,做事务控制等。

public class MyDynamicProxy {
    public static  void main (String[] args) {
        HelloImpl hello = new HelloImpl();
        MyInvocationHandler handler = new MyInvocationHandler(hello);
        // 构造代码实例
        Hello proxyHello = (Hello) Proxy.newProxyInstance(HelloImpl.class.getClassLoader(), HelloImpl.class.getInterfaces(), handler);
        // 调用代理方法
        proxyHello.sayHello();
    }
}
interface Hello {
    void sayHello();
}
class HelloImpl implements  Hello {
    @Override
    public void sayHello() {
        System.out.println("Hello World");
    }
}
 class MyInvocationHandler implements InvocationHandler {
    private Object target;
    public MyInvocationHandler(Object target) {
        this.target = target;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        System.out.println("Invoking sayHello");
        Object result = method.invoke(target, args);
        return result;
    }
}

Proxy 类是用于创建代理对象,而 InvocationHandler 接口主要是来处理执行逻辑。
其中newProxyInstance包含三个参数:
(1)loader:  一个ClassLoader对象,定义了由哪个ClassLoader对象来对生成的代理对象进行加载
(2)interfaces:  一个Interface对象的数组,表示的是我将要给我需要代理的对象提供一组什么接口,如果我提供了一组接口给它,那么这个代理对象就宣称实现了该接口(多态),这样我就能调用这组接口中的方法了
(3)handler:一个InvocationHandler对象,表示的是当我这个动态代理对象在调用方法的时候,会关联到哪一个InvocationHandler对象上

spring的两种代理jdk和cglib
java动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。
而cglib动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。
1、如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP
2、如果目标对象实现了接口,可以强制使用CGLIB实现AOP
3、如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换
JDK动态代理和CGLIB字节码生成的区别?
(1)JDK动态代理只能对实现了接口的类生成代理,而不能针对类
(2)CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法。因为是继承,所以该类或方法最好不要声明成final

动态代理实例:
(1)定义一个接口
(2)定义一个类实现这个接口
(3)动态代理类实现invocationHandler这个接口
(4)定义一个handler并将需要代理的对象传进去
(5)定义一个代理类
(6)使用代理类进行调用

volatile synchronized与锁

Synchronize解决执行控制问题,保护代码块无法被其他线程访问,创建内存屏障,保证所有操作结果直接刷到主存中,先获得锁的线程的所有操作都happens-before随后获得这个锁的线程操作。
volatile 关键字只能保证可见性,顺序性,不能保证原子性。对volatile变量的读写都会直接刷到主存。(对变量的写入操作不依赖变量的当前值,变量没有包含在具有其他变量的不变式中)
(1)保证可见性、不保证原子性
(2)禁止指令重排序
如果要保证原子性需要满足:运算结果不依赖于变量的当前值,或者能够确保只有一个线程修改变量的值;变量不需要与其他的状态变量共同参与不变的约束。

锁机制:
Synchronized互斥锁属于悲观锁,它有一个明显的缺点,它不管数据存不存在竞争都加锁,随着并发量增加,且如果锁的时间比较长,其性能开销将会变得很大。基于冲突检测的乐观锁可以解决这个问题。这种模式下,已经没有所谓的锁概念了,每条线程都直接先去执行操作,计算完成后检测是否与其他线程存在共享数据竞争,如果没有则让此操作成功,如果存在共享数据竞争则可能不断地重新执行操作和检测,直到成功为止,可叫CAS自旋。
乐观锁的核心算法是CAS(Compareand Swap,比较并交换),它涉及到三个操作数:内存值、预期值、新值。
当且仅当预期值和内存值相等时才将内存值修改为新值。这样处理的逻辑是,首先检查某块内存的值是否跟之前我读取时的一样,如不一样则表示期间此内存值已经被别的线程更改过,舍弃本次操作,否则说明期间没有其他线程对此内存值操作,可以把新值设置给此块内存。如图2-5-4-1,有两个线程可能会差不多同时对某内存操作,线程二先读取某内存值作为预期值,
执行到某处时线程二决定将新值设置到内存块中,如果线程一在此期间修改了内存块,则通过CAS即可以检测出来,
假如检测没问题则线程二将新值赋予内存块。(CPU硬件指令保证了这个compare and swap的原子性)

扩展到happen-before
happen-before原则保证了程序的“有序性”,通过定义一定的规则,在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度(编译器和处理器通过调整执行顺序进行优化)。

DCL简单来说就是check-lock-check-act,先检查再锁,锁之后再检查一次,最后才执行操作。这样做的目的是尽可能的推迟锁的时间
比较经典的使用(单例模式中的懒汉加载,要注意volatile保证sinngleton实例化的原子性):

public class Singleton {
    //通过volatile关键字来确保安全
    private volatile static Singleton singleton;

    private Singleton(){}

    public static Singleton getInstance(){
        if(singleton == null){
            synchronized (Singleton.class){
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

如何破坏单例模式:1.反射 2.序列化
如何防止被破坏:1.构造函数中对实例化次数进行统计,第二次实例化的时候,抛出异常。

悲观锁:
每次在拿数据的时候都会上锁(行锁,表锁等)
synchronized是悲观锁

  1. 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
  2. 一个线程持有锁会导致其它所有需要此锁的线程挂起。
  3. 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。

乐观锁:
所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。
具体实现细节:(1)冲突检测(2)数据更新
典型实现:compare and swap(CAS)
需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B

threadLocal不是解决资源共享的问题,而是用来提供线程内的局部变量
teadLocal内部有一个静态类ThreadLocalMap,使用到ThreadLocal的线程会与ThreadLocalMap绑定,维护着这个Map对象,而这个ThreadLocalMap的作用是映射当前ThreadLocal对应的值,它key为当前ThreadLocal的弱引用:WeakReference
!!!注意threadLocal引用手动remove否则会出现内存泄漏情况
所以避免内存泄露的方法,是对于ThreadLocal要设为static静态的
每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向threadlocal. 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收.
  所以得出一个结论就是只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。其实这是一个对概念理解的不一致,也没什么好争论的。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的。就可能出现内存泄露。

lock用于synchronized相同的并发性和内存语义,此外还多了 锁投票,定时锁等候和中断锁等候(优点:可定时的、可轮询的与可中断的锁获取操作,提供了读写锁、公平锁和非公平锁)
lock:在jvm层,在finally中必须释放锁,可以判断锁状态,可重入可判断可公平;
synchronized:java关键字,获取锁的线程执行完同步代码释放锁,无法判断状态;可重入,不可中断,非公平。

可重入锁(ReenTrantLock)和synchronized锁的区别:
便利性:很明显Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。
锁的细粒度和灵活度:很明显ReenTrantLock优于Synchronized
synchronized的语义底层是通过一个monitor对象来完成,wait/notify等方法也依赖于monitor对象。重入的话monitorenter+1.执行monitorexit的线程必须是object所对应的monitor的所有者,执行执行时,monitor的进入数减1,如果减1后进入数为0拿线程退出monitor.ReenTrantLock通过条件变量的signal/await组合实现。

轻量级锁是通过CAS来避免进入开销较大的互斥操作(线程挂起需要用户态和进程态的切换),而偏向锁是在无竞争场景下完全消除同步,连CAS也不执行(CAS本身仍旧是一种操作系统同步原语,始终要在JVM与OS之间来回,有一定的开销

CLH队列:我们了解了AQS的CLH队列相比原始的CLH队列锁,它采用了一种变形操作,将自旋机制改为阻塞机制。当前线程将首先检测是否为头结点且尝试获取锁,如果当前节点为头结点并成功获取锁则直接返回,当前线程不进入阻塞,否则将当前线程阻塞

根据前面的线程阻塞与唤醒小节知道,目前在Java语言层面能实现阻塞唤醒的方式一共有三种:suspend与resume组合、wait与notify组合、park与unpark组合。其中suspend与resume因为存在无法解决的竟态问题而被Java废弃,同样,wait与notify也存在竟态条件,wait必须在notify之前执行,假如一个线程先执行notify再执行wait将可能导致一个线程永远阻塞,如此一来,必须要提出另外一种解决方案,就是park与unpark组合,它位于juc包下,应该也是因为当时编写juc时发现java现有方式无法解决问题而引入的新阻塞唤醒方式,由于park与unpark使用的是许可机制,许可最大为1,所以unpark与park操作不会累加,而且unpark可以在park之前执行,如unpark先执行,后面park将不阻塞。

ReenTrantLock独有的能力:
(1).ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。
(2).ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
(3).ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。

Condition的强大之处在于:它能够更加精细地控制多线程的休眠与唤醒。Condition中的await()方法相当于Object的wait()方法。signal()相当于Object的notify()方法,signalAll()相当于Obejct的notifyAll()方法。

Reentrantlock的底层实现 AQS(AbstractQueuedSynchronized)抽象的队列式的同步器
AQS维护了一个CLH队列(多线程争用资源时被阻塞会进入此队列,双向FIFO队列),AQS的核心方法compareAndSetState就是使用CAS设置当前状态
private transient volatile Node head;
private transient volatile Node tail;
private volatile int state;(同步状态)

死锁的必要条件:
1.互斥 资源是独占的且排他使用
2.请求并保持
3.非抢占
4.循环等待
解决死锁,第一个是死锁预防,就是不让上面的四个条件同时成立。二是,合理分配资源。
三是使用银行家算法,如果该进程请求的资源操作系统剩余量可以满足,那么就分配。

Synchronized的缺点:
(1)不可控制,无法做到随心的加锁和释放锁;
(2)效率低下,例如并发读两个文件,读与读之间互不影响;
(3)无法知道线程是否获取到了锁。
(4)如果获得锁的线程阻塞,则等待锁的线程也需要等待。
Lock:
Reentrantlock是lock的唯一实现类。是一个上层的接口,特性:尝试非阻塞的获取锁、被中断的获取锁,超时获取锁,包含lock()(获取锁),lockInterruptibly(可响应中断,避免死等锁),tryLock()(尝试获取锁),tryLock(time,unit)(在一定时间内尝试获取锁),unlock()(释放锁),newCondition()(实现线程通信,await等待,signalall唤醒其他线程)。
读写锁:
读锁:是一种自旋锁,允许多个线程同时访,可用在存在大量读的操作中,或实现一个数据缓存的功能;写锁:写操作对读操作和写操作阻塞;提供锁接口ReadWriteLock。Reentrantreadwritelock是其实现类。如果一个线程申请写锁需要等待释放读锁。
非公平锁虽然会导致饥饿但是会有更少的线程切换,保证更大的吞吐量;公平锁为了保证获取锁的顺序代价是进行大量的线程切换。

java.util.concurrent Java集合

list->CopyOnWriteArrayList(线程安全是基于锁实现的,是JUC下面的包使用了ReentrantLock)
set->CopyOnWriteArraySet,ConcurrentSkipListSet(基于 ConcurrentSkipListMap 的可缩放并发 NavigableSet 实现,
其扩展了 SortedSet,具有了为给定搜索目标报告最接近匹配项的导航方法。方法 lower、floor、ceiling 和 higher 分别返回小于、小于等于、大于等于、大于给定元素的元素)
map->ConcurrentHashMap,ConcurrentSkipListMap
concurrenthashmap:hashtable将所有方法都用synchronized修饰,开销大;
get操作,因为value和next都用volatile修饰,保证可见性,不需要同步方法;
1.7
put加锁
通过分段加锁segment,一个hashmap里有若干个segment,每个segment里有若干个桶,桶里存放K-V形式的链表,put数据时通过key哈希得到该元素要添加到的segment,然后对segment进行加锁,然后在哈希,计算得到给元素要添加到的桶,然后遍历桶中的链表,替换或新增节点到桶中
size
分段计算两次,两次结果相同则返回,否则对所以段加锁重新计算

1.8
put CAS 加锁
1.8中不依赖与segment加锁,segment数量与桶数量一致;
首先判断容器是否为空,为空则进行初始化利用volatile的sizeCtl作为互斥手段,如果发现竞争性的初始化,就暂停在那里,等待条件恢复,否则利用CAS设置排他标志(U.compareAndSwapInt(this, SIZECTL, sc, -1));否则重试
对key hash计算得到该key存放的桶位置,判断该桶是否为空,为空则利用CAS设置新节点
否则使用synchronize加锁,遍历桶中数据,替换或新增加点到桶中
最后判断是否需要转为红黑树,转换之前判断是否需要扩容
size
利用LongAdd累加计算

queue->
ArrayBlockingQueue:一个由数组支持的有界阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序;
LinkedBlockingQueue:一个基于已链接节点的、范围任意的 blocking queue。此队列按 FIFO(先进先出)排序元素;
LinkedBlockingDeque:一个基于已链接节点的、任选范围的阻塞双端队列;
ConcurrentLinkedQueue:一个基于链接节点的无界线程安全队列。此队列按照 FIFO(先进先出)原则对元素进行排序;
ConcurrentLinkedDeque:是双向链表实现的无界队列,该队列同时支持FIFO和FILO两种操作方式。

这种方式是使用 AbstractQueuedSynchronizer 的标准化方式,大致分为两步:
1、内部持有继承自 AbstractQueuedSynchronizer 的对象 Sync;
2、并在 Sync 内重写 AbstractQueuedSynchronizer protected 的部分或全部方法,这些方法包括如下4个:
(1).独占模式:tryAcquire,tryRelease
(2).共享模式:tryAcquireShared,tryReleaseShared
isHeldExclusively

1、AQS 分为独占模式和共享模式,CountDownLatch 使用了它的共享模式。
2、AQS 当第一个等待线程(被包装为 Node)要入队的时候,要保证存在一个 head 节点,这个 head 节点不关联线程,也就是一个虚节点。
3、当队列中的等待节点(关联线程的,非 head 节点)抢到锁,将这个节点设置为 head 节点。
4、第一次自旋抢锁失败后,waitStatus 会被设置为 -1(SIGNAL),第二次再失败,就会被 LockSupport 阻塞挂起。
5、如果一个节点的前置节点为 SIGNAL 状态,则这个节点可以尝试抢占锁。

AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock

tips:公平性讲的是线程获取锁的方式是粗暴的直接抢占,还是排队获取。独占性讲的是某lock已经被其他线程占据后当前线程是否还可以去占有

如上面的分析CopyOnWriteArrayList表达的一些思想:
1、读写分离,读和写分开
2、最终一致性
3、使用另外开辟空间的思路,来解决并发冲突(String类也是写时复制)
2.13 arrayList & vector&linkedlist
ArrayList和vector:
1)vector是线程同步的(很早,jdk1中存在),所以它也是线程安全的,而arraylist是线程异步的,是不安全的。如果不考虑到线程的安全因素,一般用arraylist效率比较高。Linkedlist也是线程不安全的。
2)如果集合中的元素的数目大于目前集合数组的长度时,vector增长率为目前数组长度的100%,而arraylist增长率为目前数组长度的50%.如过在集合中使用数据量比较大的数据,用vector有一定的优势。
3)arraylist底层是object数据,linkedlist底层是双向链表。Arraylist实现了randomaccess接口。Arraylist会有空余空间,而linkedlist单个元素所占空间更大。
ArrayList默认构造的容量为10。切记一定不要使用普通for循环去遍历LinkedList。使用迭代器或者foreach循环(foreach循环的原理就是迭代器)去遍历LinkedList即可,这种方式是直接按照地址去找数据的,将会大大提升遍历LinkedList的效率。
List.of(),Set.of()保证不可变性。

comparable & comparator

comparable和comcomparator区别比较
Comparable是排序接口,若一个类实现了Comparable接口,就意味着“该类支持排序”。
而Comparator是比较器,我们若需要控制某个类的次序,可以建立一个“该类的比较器”来进行排序。
Comparable相当于“内部比较器”,而Comparator相当于“外部比较器”。
两种方法各有优劣, 用Comparable 简单, 只要实现Comparable 接口的对象直接就成为一个可以比较的对象,但是需要修改源代码。
用Comparator 的好处是不需要修改源代码, 而是另外实现一个比较器,当某个自定义的对象需要作比较的时候,把比较器和对象一起传递过去就可以比大小了,并且在Comparator里面用户可以自己实现复杂的可以通用的逻辑,使其可以匹配一些比较简单的对象,那样就可以节省很多重复劳动了。

java IO & NIO

同步和异步:同步是一种可靠的有序运行机制,当前任务返回后后续任务才会继续。异步不需要等待前面任务返回,依靠事件、回调机制来实现。
阻塞和非阻塞:阻塞无法从事其他任务;非阻塞直接返回相应操作在后台继续处理。
NIO步骤:
(1)创建一个selector,类似调度员的角色;
(2)创建一个serversockerchannel向selector注册;
(3)当有channel发生接入请求时,selector就会被唤醒。
java IO和NIO
(1)面向流和面向缓冲
Java NIO和IO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。
Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
(2)阻塞与非阻塞IO
Java IO的各种流是阻塞的。这意味着,当一个线程调用read()或write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。
Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。
非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
(3)inpustream/outputstream用于读取或写入字节,reader/writer用于操作字符。
(4)文件拷贝方式:IO库中的fileinputstrean,fileoutputstream;NIO库中的transferto,transferfrom;file.copy。
读取数据:BIO内核态将数据从磁盘读取至内核缓存,切换到用户态将数据从内核缓存读取至用户缓存。NIO采用零拷贝技术,在内核态完成拷贝。

java进程和线程

Notify()唤醒一个线程,具体唤醒谁由线程调度器确定;
notifyAll()唤醒在该监视器上等待的所有线程;
Entryset(处于线程的blocked状态,也想获得对象锁)和waitset(waiting状态)

线程状态包括:新建、就绪、阻塞、等待、计时等待、终止

threadpoolExecutor和newFixedCachedThreadPool选择:
使用前者,可以看到在newFixedThreadPool中将最大线程数设为Integer.MAX_VALUE,会造成OOM
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, Integer.MAX_VALUE,
0L, TimeUnit. MILLISECONDS,
new LinkedBlockingQueue());
}
参数含义:1.核心线程数 2.最大线程数 3.keepAliveTime 4.unit为keepAliveTime的单位 5.线程池中的任务队列
常用的有三种:SynchronousQueue,LinkedBlockingDeque,ArrayBlockingQueue
keepAliveTime是指当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间

线程间通信:
1.synchronized 相同对象 这种方式,本质上就是“共享内存”式的通信。多个线程需要访问同一个共享变量,谁拿到了锁(获得了访问权限),谁就可以执行。
e.g.
MyObject object = new MyObject();(object中方法synchronized)
//线程A与线程B 持有的是同一个对象:object
ThreadA a = new ThreadA(object);
ThreadB b = new ThreadB(object);这样a和b就是顺序执行object的方法
2.while轮询 判断共享变量(浪费CPU资源)
3.wait/notify机制 条件不满足时不会轮询而是进入阻塞状态,cpu利用率提高了

进程间通信:
进程间通信主要包括管道, 系统IPC(包括消息队列,信号量,共享存储), SOCKET.
1.socket(基于流Tcp或数据报文UDP)创建socket客户端和Socket服务端(serverSocket)
2.管道(匿名管道(父子进程)和命名管道(无亲缘关系进程))
IPC(inter-process-communication包括两部分LPC(local)和RPC(remote))

虚拟地址:跟逻辑地址类似,它也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址(虚拟地址)则对应了硬件页式内存的转换前地址。

进程同步的几种方式:
1.信号量,通过PV操作
2.管程。一个进程通过调用管程的一个过程进入管程,其他进程会阻塞
3.消息传递。

进程调度的方式:
进程调度有以下两种基本方式:
非剥夺方式
分派程序一旦把处理机分配给某进程后便让它一直运行下去,直到进程完成或发生某事件而阻塞时,才把处理机分配给另一个进程。
剥夺方式
当一个进程正在运行时,系统可以基于某种原则,剥夺已分配给它的处理机,将之分配给其它进程。剥夺原则有:优先权原则、短进程优先原则、时间片原则。

CountDownLatch(实现也是基于CAS):
final CountDownLatch latch = new CountDownLatch(1000);
latch.countDown();
latch.await();(在当前计数到达零之前,await方法会一直)

如何停止一个线程
stop方法已经停用。最佳实践是使用共享变量(该变量最好加上volatile修饰符),但是对于处于阻塞状态的进程可以使用interrupt使进程抛出中断异常从而退出阻塞状态。

一个线程运行时发生异常:
Java中throwable分为exception和error:
出现error时,程序会停止运行
exception分为运行时异常和非运行时异常,
非运行时异常必须处理,比如thread中sleep()时,必须处理InterruptedException异常,才能通过编译。
而RuntimeException可以处理也可以不处理,因为编译并不能检测该类异常,比如NullPointerException
在运行时如果发生异常属于运行时异常,存在两种情形:
① 如果该异常被捕获或抛出,则程序继续运行。
② 如果异常没有被捕获该线程将会停止执行。

java.lang.Thread 中有一个方法叫 holdsLock (),可以用来检查一个线程是否拥有一个锁

创建线程的方式:
1.继承Thread类
2.实现Runnable接口

Thread方法存在的问题:
1.当前线程重写run方法定义该线程要完成的工作,这就导致了任务是定义在线程内部的,于是线程与任务有一个强耦合关系,不利于线程的重用。
2.由于java是单继承的,这就导致了若继承了线程就无法继承其他类,在实际开发中经常会出现继承冲突的问题(单继承极限)。

生产者消费者模型

public class ProducerConsumerPattern{
 
	public static final int MAX_CAPACITY = 5;      //缓冲区最大容量
	static LinkedList<Object> list = new LinkedList<Object>();   //用一个Linked作为缓冲区
	static class Producer implements Runnable {         //生产者
		public void run() {
			while(true) {                    //循环生产产品
				synchronized(list) {    //获取缓冲区的锁
					while(list.size()==MAX_CAPACITY) {   // 如果缓冲区已满,
						try {
							System.out.println("当前产品个数为" + list.size() + "已达到最大容量,等待消费者消费。。。");
							list.wait();                  //则调用wait进行阻塞,并释放缓冲区的锁
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					}
					System.out.println("当前产品个数为" + list.size());
					list.add(new Object());               //缓冲区未满,则生产产品
					System.out.println("生产了一个产品,当前产品个数为" + list.size());
					list.notifyAll();             //生产完一个产品之后,通知所有阻塞在wait调用中的线程
				}
				try {
					Thread.sleep(1000);   //模拟花费一段时间进行生产
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}
	
	static class Consumer implements Runnable {    //消费者
		public void run() {
			while(true) {         //不断消费产品
				synchronized(list) {    //获取缓冲区的锁
					while(list.size()==0) {       //如果缓冲区为空,
						try {
							System.out.println("当前产品个数为" + list.size() + ",等待生产者生产。。。");
							list.wait();     //则进行阻塞,并释放缓冲区的锁
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					}
					System.out.println("当前产品个数为" + list.size());
					list.remove();     //缓冲区不空,则消费一个产品
					System.out.println("消费了一个产品,当前产品个数为" + list.size());
					list.notifyAll();    //消费一个产品之后,通知所有阻塞在wait调用上的线程
				}
				try {
					Thread.sleep(2000);   //模拟花费一段时间进行消费
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}
	public static void main(String[] args) throws InterruptedException {
		for(int i=0; i<3; i++) {     //创建3个生产者进行生产
			new Thread(new Producer()).start();
		}
		for(int i=0; i<5; i++) {    //创建5个消费者进行消费
			new Thread(new Consumer()).start();
		}
		
	}
}

程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程:轻量级进程就是我们通常意义上讲的线程。而每个轻量级线程都由一个内核线程支持。
jdk1.3之后java调用操作系统的内核级线程,因为这样才能利用多核。

用户态和内核态的切换:
所有用户程序都是运行在用户态的, 但是有时候程序确实需要做一些内核态的事情, 例如从硬盘读取数据, 或者从键盘获取输入等. 而唯一可以做这些事情的就是操作系统, 所以此时程序就需要先操作系统请求以程序的名义来执行这些操作.这时需要一个这样的机制: 用户态程序切换到内核态。之所以分开,是因为内核态可以操作更多的硬件资源,而且不用用户去关心,如果让用户自己操作,可能会产生许多错误。用户态切内核态有三种情况:1.系统调用,2.异常,3.外围设备中断。

Java多线程和多进程
进程是操作系统进行资源分配和调度的一个独立单位。
线程是操作系统能够进行运算调度的最小单位。
多线程优势:进程之间不能共享内存但是线程之间可以共享内存;系统创建进程需要为该进程重新分配系统资源但是创建线程代价小效率高;资源利用率好;程序设计简单;程序响应快;

Java实现线程池:
重要变量:存放所有线程的集合;任务队列;线程池的限定大小;当前运行的工作线程数据;
重要方法:线程池执行任务,查看线程池是否为空,不为空则放入线程池;分配任务给空闲线程,运行中的线程一旦没有执行任务时就从队列中取任务来执行,这样需要重写run方法;

Java泛型

java中泛型的作用 该机制允许程序员在编译时检测到非法的类型,同时参数化类型,减少了重复代码的编写
分类:泛型类、泛型接口、泛型方法
(1).创建泛型对象的时候,一定要指出类型变量T的具体类型。争取让编译器检查出错误,而不是留给JVM运行的时候抛出异常
(2).JVM并不知道泛型,所有的泛型在编译阶段就已经被处理成了普通类和方法=>类型擦除
如果泛型类型的类型变量没有限定(),那么我们就用Object作为原始类型;
如果有限定(),我们就XClass作为原始类型;
如果有多个限定(),我们就用第一个边界的类型变量XClass1类作为原始类型;
泛型的使用:
(1).定义泛型:Point
首先,大家可以看到Point,即在类名后面加一个尖括号,括号里是一个大写字母。这里写的是T,其实这个字母可以是任何大写字母,大家这里先记着,可以是任何大写字母,意义是相同的。
(2).类中使用泛型
//定义变量
private T x ;
//作为返回值
public T getX(){
return x ;
}
//作为参数
public void setX(T x){
this.x = x ;
}
(3).使用泛型类
Point p = new Point() ;
p.setX(new Integer(100)) ;
System.out.println(p.getX());
2.18 Java反射
反射,提供程序在运行时自省的能力,直接操作类或对象。可以在运行时判断一个对象所属的类、构造任意一个类对象,判断任意一个类具有的成员变量和方法。
java反射:
获取类对象的三种方式:
1.class.forName(类的完整对象)
2.new实例化对象t , t.getClass
3.类名.class获取类对象newInstance

class对象的常用方法介绍:
1.getName()
2.getDeclaredField()获得类中的所有属性
3.getDeclaredMethods()获得类中所有的方法
4.getConstructors()获得类构造方法
5.newInstance()实例化类对象,注意需要是无参数的类实例化

Class c=Class.forName(“java.util.HashSet”);
Object o=c.newInstance();
Method[] methods=c.getDeclaredMethods();
for(Method method:methods){
System.out.println(method);
}
Method m1=c.getMethod(“add”, Object.class);
m1.invoke(o, “cyq”);
m1.invoke(o, “hello”);
m1.invoke(o, “java”);

要正确使用Java反射机制就得使用java.lang.Class这个类。它是Java反射机制的起源。当一个类被加载以后,Java虚拟机就会自动产生一个Class对象。
通过这个Class对象我们就能获得加载到虚拟机当中这个Class对象对应的方法、成员以及构造方法的声明和定义等信息。

获取类的 Class 对象实例
Class clz = Class.forName(“com.zhenai.api.Apple”);
根据 Class 对象实例获取 Constructor 对象
Constructor appleConstructor = clz.getConstructor();
使用 Constructor 对象的 newInstance 方法获取反射类对象
Object appleObj = appleConstructor.newInstance();
而如果要调用某一个方法,则需要经过下面的步骤:
获取方法的 Method 对象
Method setPriceMethod = clz.getMethod(“setPrice”, int.class);
利用 invoke 方法调用方法
setPriceMethod.invoke(appleObj, 14);

java反射举例:
1.一个是加载jdbc连接的驱动类
2.就是eclipse中的alt+/调出函数的方法的属性
3.一个水果工厂需要根据传入的参数生成对应实例。
2.19 接口与抽象类
接口( interface)可以说成是抽象类的一种特例,接口中的所有方法都必须是抽象的(jdk8中也可以定义静态方法)。 接口中的方法定义默认为 public abstract 类型,接口中的成员变量类型默认为 public static final。
下面比较一下两者的语法区别:
1.抽象类可以有构造方法,接口中不能有构造方法。
2.抽象类中可以有普通成员变量,接口中没有普通成员变量
3.抽象类中可以包含非抽象的普通方法,接口中的所有方法必须都是抽象的,不能有非抽象的普通方法。
4. 抽象类中的抽象方法的访问类型可以是 public, protected 和(默认类型,虽然eclipse 下不报错,但应该也不行),但接口中的抽象方法只能是 public 类型的,并且默认即为 public abstract 类型。
5. 抽象类中可以包含静态方法,接口中不能包含静态方法
6. 抽象类和接口中都可以包含静态成员变量,抽象类中的静态成员变量的访问类型可以任意,但接口中定义的变量只能是 public static final 类型,并且默认即为 public static final 类型。
7. 一个类可以实现多个接口,但只能继承一个抽象类。
8.一个类实现接口要实现接口的所有方法,而抽象类不一定。

抽象类其实是可以被实例化的,但是它的实例化方式并不是通过普通的new方式来创建对象,而是通过父类的引用来指向子类的实例间接地实现父类的实例化

接口更多的是在系统架构设计方法发挥作用,主要用于定义模块之间的通信契约。而抽象类在代码实现方面发挥作用,可以实现代码的重用,例如,模板方法设计模式是抽象类的一个典型应用,假设某个项目的所有 Servlet 类都要用相同的方式进行权限判断、记录访问日志和处理异常,那么就可以定义一个抽象的基类,让所有的 Servlet 都继承这个抽象基类,在抽象基类的 service 方法中完成权限判断、记录访问日志和处理异常的代码,在各个子类中只是完成各自的业务逻辑代码。

被static修饰的成员变量和成员方法独立于该类的任何对象。也就是说,它不依赖类特定的实例,被类的所有实例共享。
只要这个类被加载,Java虚拟机就能根据类名在运行时数据区的方法区内定找到他们。因此,static对象可以在它的任何对象创建之前访问,无需引用任何对象。

多线程

为什么要使用多线程?
1.防止阻塞主线程,提高吞吐量
你做WEB,容器帮你做了多线程,但是他只能帮你做请求层面的。简单的说,可能就是一个请求一个线程。或多个请求一个线程。如果是单线程,那同时只能处理一个用户的请求。
2.提高资源的利用率

并行:多个CPU同时执行一个处理逻辑,真正的同时;
并发:通过CPU调度算法,让用户看上去是同时执行的,不是真正同时;
Thread类中的yield方法可以让一个running状态的线程转入runnable。

当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。常用于用户登录控制,如记录session信息。

使用apache和guava或使用ThreadPoolExecutor类创建线程池:
Public static ExecutorService exector = new ThreadPoolExecutor(10,10,
60L, TimeUnit.SECONDS, new ArrayBlockingQueue(10));
1.corePoolSize 核心线程数,打到这个数目新任务放到任务队列中
2.maxPoolSize 核心线程数和任务队列满时创建新的线程,最大线程数
3.keepAliveTime 当线程空闲时间达到keepAliveTime,该线程会退出
4.unit: 参数keepAliveTime的时间单位
4.allowCoreThreadTimeout 是否允许核心线程空闲退出,默认值是false
5.queueCapacity 任务队列容量

线程同步的辅助工具类:
Semaphore,
Semaphore通常用于限制可以访问某些资源(物理or逻辑)的线程数目。
CountDownLatch,CyclicBarrier
可以用于线程同步
● CountDownLatch最大的特征是进行一个数据减法的操作等待,所有的统计操作一旦开始之中就必须执行countDown()方法,如果等待个数不是0,就被一只等待,并且无法重置。
● CyclicBarrier设置一个等待的临界点,并且可以有多个等待线程出现,只要满足了临界点就触发了线程的执行代码后将重新开始进行计数处理操作,也可以直接利用reset()方法执行重置操作。

MVCC(Multi-Version Concurrency Control)即多版本并发控制在大多数情况下代替了行锁,实现了对读的非阻塞,读不加锁,读写不冲突。缺点是每行记录都需要额外的存储空间,需要做更多的行维护和检查工作。

往线程池中提交线程的两种方法:
1.execute,只能提交一个Runnable的对象,方法的返回值是void
2.submit的提交方式:
(1).提交一个Callable接口的对象,返回一个Future对象
(2).提交一个Runable接口的对象,执行成功返回null,线程执行异常会返回异常的信息
(3).提交的除了Runnable对象之外,还有一个result对象。

shutdown()和shutdownNow()
shutDown()
当线程池调用该方法时,线程池的状态则立刻变成SHUTDOWN状态。此时,则不能再往线程池中添加任何任务,否则将会抛出RejectedExecutionException异常。但是,此时线程池不会立刻退出,直到添加到线程池中的任务都已经处理完成,才会退出。
shutdownNow()
根据JDK文档描述,大致意思是:执行该方法,线程池的状态立刻变成STOP状态,并试图停止所有正在执行的线程,不再处理还在池队列中等待的任务,当然,它会返回那些未执行的任务。
它试图终止线程的方法是通过调用Thread.interrupt()方法来实现的,但是大家知道,这种方法的作用有限,如果线程中没有sleep 、wait、Condition、定时锁等应用, interrupt()方法是无法中断当前的线程的。所以,ShutdownNow()并不代表线程池就一定立即就能退出,它可能必须要等待所有正在执行的任务都执行完成了才能退出。

常见的几种线程池:
5.1 newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
这种类型的线程池特点是:
● 工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。
● 如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。
● 在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。
5.2 newFixedThreadPool
创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。
FixedThreadPool是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。
5.3 newSingleThreadExecutor
创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。
5.4 newScheduleThreadPool
创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。

任务拒绝策略:
这里先假设一个前提:线程池有一个任务队列,用于缓存所有待处理的任务,正在处理的任务将从任务队列中移除。因此在任务队列长度有限的情况下就会出现新任务的拒绝处理问题,需要有一种策略来处理应该加入任务队列却因为队列已满无法加入的情况。另外在线程池关闭的时候也需要对任务加入队列操作进行额外的协调处理。
RejectedExecutionHandler提供了四种方式来处理任务拒绝策略
1、直接丢弃(DiscardPolicy)
2、丢弃队列中最老的任务(DiscardOldestPolicy)。
3、抛异常(AbortPolicy)
4、将任务分给调用线程来执行(CallerRunsPolicy)。

BlockingQueue的三种排队策略:
3.1 直接提交 SynchronousQueue
它将任务直接提交给线程而不保存它们。在此,如果不存在可用于立即运行任务的线程,则试图把任务加入队列将失败,因此会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求无界 maximumPoolSizes 以避免拒绝新提交的任务。当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。 SynchronousQueue线程安全的Queue,可以存放若干任务(但当前只允许有且只有一个任务在等待),其中每个插入操作必须等待另一个线程的对应移除操作,也就是说A任务进入队列,B任务必须等A任务被移除之后才能进入队列,否则执行异常策略。你来一个我扔一个,所以说SynchronousQueue没有任何内部容量。
3.2 无界队列 如LinkedBlockingQueue
使用无界队列(例如,不具有预定义容量的 LinkedBlockingQueue)将导致在所有核心线程都在忙时新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize。(因此,maximumPoolSize 的值也就没意义了。)也就不会有新线程被创建,都在那等着排队呢。如果未指定容量,则它等于 Integer.MAX_VALUE。如果设置了Queue预定义容量,则当核心线程忙碌时,新任务会在队列中等待,直到超过预定义容量(新任务没地方放了),才会执行异常策略。你来一个我接一个,直到我容不下你了。FIFO,先进先出。
3.3 有界队列 如ArrayBlockingQueue
操作模式跟LinkedBlockingQueue查不多,只不过必须为其设置容量。所以叫有界队列。new ArrayBlockingQueue(Integer.MAX_VALUE) 跟 new LinkedBlockingQueue(Integer.MAX_VALUE)效果一样。LinkedBlockingQueue 底层是链表结构,ArrayBlockingQueue 底层是数组结构。你来一个我接一个,直到我容不下你了。FIFO,先进先出。

阻塞队列的种类:
ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue:一个由链表结构组成的有界(默认是无界的,可以自行设置和可重入锁相似,默认是非公平锁,但是可以进行设置)阻塞队列。
PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
DelayQueue:一个使用优先级队列实现的无界阻塞队列。
LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

内部类

内部类:内部类是指在一个外部类的内部再定义一个类
优势:
1.可以实现多重继承
2.内部类方法可以访问该类定义所在的作用域的数据,包括私有的数据
3.当想要定义一个回调函数且不想编写大量代码时,使用匿名内部类比较便捷
什么时候使用匿名内部类:
(1)只用到类的一个实例。
(2)类在定义后马上用到。
(3)类非常小(SUN推荐是在4行代码以下)
(4)给类命名并不会导致你的代码更容易被理解。

匿名内部类:
匿名内部类只能使用一次,它通常用来简化代码编写。
Java中的内部类就是一个典型的闭包结构,引用的内部变量需要申明为final因为被匿名内部类引用的变量会被拷贝一份到内部类的环境中。但其后,在外部,该变量如果被修改,则内部外部不一致
双括号初始化:invite(new ArrayList(){{add(“Harry”);add(“Tony”);}});
Java的匿名类,就是提供了一个快捷方便的手段,令继承关系可以方便地变成组合关系
组合:在类中显示调用另一个类,继承是继承并可以进行函数的重写,需要遵守里氏替换原则,比如父类一个方法能够处理null值而重写的子类不具备这个能力那就不满足里氏替换原则。

成员内部类:内部类可以访问外部类的所有方法;外部类不可以访问内部类,需要先实例化;必须使用外部类对象.new 内部类();若有相同的方法或变量可以使用this。
静态内部类:内部类不能访问非静态成员;名称相同使用类名.方法 ,不同则直接使用方法名。直接创建内部类。
方法内部类:只在方法中有效,不能使用访问控制符和static修饰符。
匿名内部类:必须继承一个类或者实现一个借口;不能定义构造函数;不能存在静态成员变量和方法;不能是抽象的。

Object类的九个方法

1.clone方法 ,不重写clone方法并进行调用会发生clonenotsupport异常 2.getClass方法 3.toString()方法 4.finalize方法,用于释放资源
5.equals方法 6.hashCode方法 7.wait方法sleep没有释放锁,wait释放了锁 8.notify方法不能重写 9.notifyAll方法

final,finally,finalize

final修饰类的时候不可继承;修饰方法的时候,方法不可被重写;修饰变量的时候表示该变量为常量,赋值后不可被改变。
finally作为异常处理的一部分,只能用到try/catch中。表示这段语句一定会被执行(不管有没有抛出异常)一般用来释放资源,比如数据库连接,lock释放
finalize是每个对象都有的方法。是在对象需要被回收时调用

equals hashcode ==

hashcode的作用:可以进行快捷的查询;
为什么要重写equals和hashcode方法:
== : 它的作用是判断两个对象的地址是不是相等。对于基本数据类型判断值是否相等,即,判断两个对象是不是同一个对象。
equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况;
情况1,类没有覆盖equals()方法。则通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象。
情况2,类覆盖了equals()方法。一般,我们都覆盖equals()方法来两个对象的内容相等;若它们的内容相等,则返回true(即,认为这两个对象相等)。例如String类和Integer类都重写了equals方法(重载:方法名相同,参数列表不同,返回值和权限修饰符可以不同;重写:方法名和参数列表相同,返回值小于父类范围,异常小于父类范围,权限大于等于父类,父类private发给你发不能被重写)。

一般对于存放到Set集合或者Map中键值对的元素,需要按需要重写hashCode与equals方法,以保证唯一性。对一个对象多次调用hashCode方法返回同一个值,如果equals相等则hashCode也相等,equals不相等hashCode也不相等。

hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。
1)、如果两个对象相等,那么它们的hashCode()值一定相同。这里的相等是指,通过equals()比较两个对象时返回true。
2)、如果两个对象hashCode()相等,它们并不一定相等。因为在散列表中,hashCode()相等,是指经过hash函数后结果相同,实际使用中不可避免不同字符串的hashCode值是相同的。
因此如果向map中存放自定义的对象,就要按照所需的逻辑重写hashcode和equals方法。
如何重写hashcode和equals方法:这个跟具体的功能逻辑相关,例如在集群中每一个数据节点有一个唯一的ID,那么对于一个数据节点对象就可以简单通过ID来判断其是否相等。对于hashcode可以人为的定义哈希函数,例如对于一个字符串可以采用 hash= sum * 31 + int(s.charAt(j))的方式,哈希函数的好坏直接决定哈希冲突的多少。
实现高质量equals方法的诀窍:
· 使用 == 操作符检查“参数是否为这个对象的引用”;
· 使用 instanceof 操作符检查“参数是否为正确的类型”;
· 对于类中的关键属性,检查参数传入对象的属性是否与之相匹配;
· 编写完 equals 方法后,问自己它是否满足对称性、传递性、一致性;
· 重写 equals 时总是要重写 hashCode;
· 不要将 equals 方法参数中的 Object 对象替换为其他的类型,在重写时不要忘掉 @Override 注解。

装箱和拆箱

装箱:自动将基本数据类型转换为包装器类型;拆箱:自动将包装器类型转换为基本数据类型,注意:空类型不能自动拆箱。发生在编译阶段;
装箱操作会创建对象,频繁的装箱操作会消耗许多内存,影响性能,所以可以避免装箱的时候应该尽量避免。
equals(Object o) 因为原equals方法中的参数类型是封装类型,所传入的参数类型(a)是原始数据类型,所以会自动对其装箱,反之,会对其进行拆箱
当两种不同类型用比较时,包装器类的需要拆箱, 当同种类型用比较时,会自动拆箱或者装箱.
Integer,Short会缓存-128–127之间的数。boolean缓存true和false。Byte缓存所有。Character缓存0000–007F。

Java特性1.8 & 1.9

jdk8的一些新特性:
1.语言上:
(1) java8允许我们给接口添加一个非抽象的方法实现,只需要使用default关键字即可,这个特征又叫做扩展方法
(2) Lambda表达式(之前使用匿名内部类)也就是闭包,这个特性是groovy和scala源生支持的
(3) JDK5引入注解,可以使用@Repeatable注解定义重复注解
2.库扩展:
提供了新的工具类(Optional类来解决NullPointer问题)
3.提供了新的Java工具:
1.提供了基于标准Nashorn引擎的命令行工具,可以接受js源码并执行
4.JVM
使用Metaspace代替持久代
jdk8对hashmap数据结构做了一定的改进,之前是数组+链表的形式,现在是数组+链表+红黑树的形式

jdk1.9主要特性:
最大特性:
1.Java平台级模块系统。JVM启动的时候,至少会有30-60M的内存加载,主要是JVM需要加载rt.jar
模块化可以根据模块需要加载程序运行
2.JAVA交互式编程环境JShell
3.改进的javadoc 支持h5,兼容h4
4.集合的of特性
5.私有接口方法
6.Java8带来了接口的默认方法,接口现在也可以包含行为,而不仅仅是方法签名,但是如果在接口上几个默认方法,代码几乎相同,通常将重构这些方法,调用一个可复用的私有方法,但默认方法不能是私有的。将复用代码创建为一个默认方法不是一个解决方案。Java9可以向接口添加私有辅助方法来解决此问题:
7.引入了一个新的HTTP相关模块。不仅支持HTTP1.1而且还支持HTTP2。JDK9之前,JDK提供的HTTP访问功能,几乎都需要依赖HttpURLConnection,但是这个类大家在写代码的时候很少使用,我们一般都会选择Apache的HttpClient
8.钱和货币的相关API
JDK9中引入了新的货比API,用来表示货币,并支持币种之间的转换和各种复杂运算。
9.多版本兼容jar包。多版本兼容jar功能能让你创建仅在特定版本的Java环境中运行库程序选择使用的class版本,自动识别所处java环境然后调用对应的类。

JVM调优

JVM怎么进行调优
年轻代:响应时间优先的应用尽可能设置大,知道接近系统的最低响应时间限制。吞吐量优先的应用尽可能设置大,达到GB。
年老代:响应时间优先的应用年老代采用并发收集器。吞吐量优先的较小的年轻代。
https://blog.csdn.net/john8169/article/details/55802651

jvm调优的命令有哪些? 作用呢? JPS(查看JVM信息),jstat(JVM进程中的类装载,内存,垃圾收集,JIT编译等运行数据),jmap,jstack等
jstat gccause用于查看垃圾收集的统计情况(这个和-gcutil选项一样),如果有发生垃圾收集,它还会显示最后一次及当前正在发生垃圾收集的原因。
如_java_lang_system_gc
_full_gc_alot,
_scavenge_alot,
jstat gcutil 用于查看新生代、老生代及持代垃圾收集的情况

Jstack找出最耗cpu的线程并定位代码:
1.根据java应用名称找出JAVA进程ID,使用ps命令
2.根据进程找到最耗费cpu的线程,使用top -Hp pid
3.使用jstack

string,stringBuffer,StringBuilder

String为字符串常量,在创建时会检查常量池,而StringBuilder和StringBuffer均为字符串变量,底层都是通过可变数组实现(char,9之后用byte,初始16);
String无法进行字符操作,字符变化时需要创建新的对象效率较低,另外两个可以操作字符。
StringBuilder(初始16,继承自abstractstringbuilder)是线程不安全的,而StringBuffer是线程安全的。StringBuffer中很多方法带有synchronized关键字。
JAVA6以后提供了intern()方法,提示JVM把相应的字符串缓存起来,以备重复使用,但是其存在永久代使用不当会造成OOM,后续版本中放在堆中。

一致性hash

1.当缓存服务器数量发生变化时,会引起缓存的雪崩,可能会引起整个系统压力过大而崩溃(大量存储统一时间失效)
2.当缓存服务器数量发生变化时,几乎所有缓存的位置都会发生改边,怎么才能尽量减少受影响的缓存?
一致性hash算法。不是传统的对服务器的数量进行取模,而是对232取模(264次方也有可能)
通过hash(服务器的IP地址) % 2^32次方来映射服务器地址
然后对于需要缓存的内容,hash(缓存内容)%2^32确定hash环的位置,顺时针第一个服务器就是存储所在的服务器(使用虚拟节点,每个物理机对应多个虚拟节点(路由表),可以实现负载均衡和负载管理,机器性能好就多管理一些节点)

多态

1.编译时多态(重载)
2.运行时多态(1.子类继承父类 2.类实现接口)
运行时的多态是面向对象最精髓的东西,要实现多态需要做两件事:
1). 方法重写(子类继承父类并重写父类中已有的或抽象的方法);
2). 对象造型(用父类型引用引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为)。

所谓多态,就是指一个引用(类型)在不同的情况下的多种状态。也可以理解为,多态是指通过指向父类的指针,来调用在不同子类中实现的方法。
// 主人类 存在一種餵食方法
class Master {
// 给动物喂食物,如果没有多态,他要写给猫喂食和给狗喂食两个方法
// 有了多态,以后即使再来好多动物,用这一个函数就可以了
public void feed(Animal an, Food f) {
an.eat();
f.showName();

}

}
Wine a = new JNC();

不可变类

要创建不可变类,要实现下面几个步骤:
(1)将类声明为final,所以它不能被继承
(2)将所有的成员声明为私有的,这样就不允许直接访问这些成员
(3)对变量不要提供setter方法
(4)将所有可变的成员声明为final,这样只能对它们赋值一次
(5)通过构造器初始化所有成员,进行深拷贝(deep copy)
(6)在getter方法中,不要直接返回对象本身,而是克隆对象,并返回对象的拷贝

引用相关

不同的引用类型主要体现在对象不同的可达性状态和对垃圾收集的影响。
强引用:只要还有强引用指向一个对象,就表明这个对象还活着,不会被回收。如果错误的保持了强引用(给static变量赋值)会导致对象没有机会变成弱引用的可达性状态,会产生内存泄露。
软引用:当JVM认为内存不足时会试图回收软引用指向的对象,确保在OOM前清理软引用指向的对象,用来实现内存敏感的缓存。
弱引用:不能使对象豁免垃圾收集,维护一种非强制性的映射关系,如果试图获取时对象还在就使用否则实例化,缓存实现。
幻想引用和虚引用:不能通过它访问对象,监控对象的创建和销毁。当垃圾回收器准备回收一个对象时,如果发现他还有虚引用,就会在回收对象的内存之前把这个虚引用加入到与之关联的引用队列里,程序可以通过判断引用队列中是否已经加入虚引用来了解被引用对象是否将要被垃圾回收。
查看JVM引用情况:-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintReferenceGC

幂等性

数学上的绝对值函数。在分布式环境下表示的是对于同样的请求,在一次或者多次请求的情况下对系统的使用资源是一样的。保证失败重试不会导致提交两次。方法:带版本号的方式;采用数据库唯一索引方式。

Java程序变慢如何排查

(1)检查网络是否存在问题,测试网速,netstat查看网络繁忙程度,可以使用speedtest工具;
(2)top,free,df,iostat(iotop)查看CPU,内存(/proc/meminfo)和磁盘使用情况;如果内存满了可以执行sync将内存缓存区内容立即同步到磁盘。
(3)检查程序日志,看有哪些异常被抛出,有没有类似内部错误的不正常异常被抛出。
(4)定位到进程的具体线程。Ps -ef | grep java(找出最消耗CPU的进程);top -HP “进程ID”(找出最耗时间的Java线程);jstack “进程ID” | grep “线程ID”查看具体线程的问题;使用jstat获取内存统计信息;使用vmstat -1 -10查看上下文切换的数量,如果比较大则说明是不合理的线程调度所导致的。
(5)top命令查看swap的使用情况。
(6)iostat判断磁盘的健康状况。
(7)查看GC日志。
(8)JRF监控应用是否大量出现了某种类型的异常。
(9)检查应用服务器的线程池配置是否合理,请求的排队现象是否严重,如果严重则需要重新设置合理的线程池。检查数据库连接池设置是否合理,增大连接池设置。检查是否有慢sql,如果有则优化。
(10)查看访问慢的服务调用链,查看一下调用链中的每一步响应时间是否合理。
(11)pidstat监控锁竞争
(12)ab测试,ping测试查看网速;/proc/sys中查看内核参数等;

JVM内存配置

初始分配由-Xms指定,为物理内存的1/64,小于1G
最大分配内存由-Xmx指定,物理内存1/4,小于1G
空余小于40%,增大
空余大于70%减小,-XX:MaxHeapFreeRatio指定
参数及其默认值 描述
-XX:LargePageSizeInBytes=4m 设置用于Java堆的大页面尺寸
-XX:MaxHeapFreeRatio=70 GC后java堆中空闲量占的最大比例
-XX:MaxNewSize=size 新生成对象能占用内存的最大值
-XX:MaxPermSize=64m 老生代对象能占用内存的最大值
-XX:MinHeapFreeRatio=40 GC后java堆中空闲量占的最小比例
-XX:NewRatio=2 新生代内存容量与老生代内存容量的比例
-XX:NewSize=2.125m 新生代对象生成时占用内存的默认值
-XX:ReservedCodeCacheSize=32m 保留代码占用的内存容量
-XX:ThreadStackSize=512 设置线程栈大小,若为0则使用系统默认值
-XX:+UseLargePages 使用大页面内存

callable runnable区别

callable:
public interface Callable {
V call() throws Exception;
}
runable:
public interface Runnable {
public abstract void run();
}
相同点:都是接口;都可用来编写多线程;都需要调用start()启动线程;
不同点:实现callable接口的任务可以返回执行结果;实现runable接口的任务不能返回结果;Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛;
注意点:
Callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取‘将来’结果;当不调用此方法时,主线程不会阻塞!

频繁full GC如何排查

Full GC频繁发生,意味着你的内存分配机制存在问题,也许是内存泄露,有大量内存垃圾不断在老年代产生;也许是你的大对象(缓存)过多;也有可能是你的参数设置不好,minor GC清理不掉内存,导致每次minor GC都会触发Full GC;还有可能是你的老年代大小参数设置错误,老年代过小等等原因。
先jstat查看内存情况。jmap -F -dump:format=b,file=heapDump ID在线上开启了 -XX:+HeapDumpBeforeFullGC。dump下来的文件大约1.8g,用jvisualvm,visualVM查看

java并发理解,线程安全理解

并发的关键是你有处理多个任务的能力,不一定要同时,多线程是并发的一种形式。
并行的关键是你有同时处理多个任务的能力。

线程安全
JVM中有一个主存,每个线程有自己的工作内存,当多个线程同时操作一个主存变量时就会产生问题。java中确保线程安全有两种方法 使用锁(加锁释放锁有开销,其他线程挂起,高优先级等待低优先级线程会导致优先级倒置),使用原子类(CAS操作)。

双亲委派被破坏

NDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI,JDBC,JCE,JAXB和JBI等。
双亲委派模型的第三次“被破坏”是由于用户对程序的动态性的追求导致的,例如OSGi的出现。在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为网状结构。

fail fast

fail-fast机制,是一种错误检测机制。它只能被用来检测错误,因为JDK并不保证fail-fast机制一定会发生。若在多线程环境下使用fail-fast机制的集合,建议使用“java.util.concurrent包下的类”去取代“java.util包下的类”。
所以,本例中只需要将ArrayList替换成java.util.concurrent包下对应的类即可。
若 “modCount 不等于 expectedModCount”,则抛出ConcurrentModificationException异常,产生fail-fast事件。

java注解

java死锁

死锁原因:互斥条件;请求保持;资源不能被抢占;循环等待;
根本原因:多线程涉及到多个锁,这些锁是交叉的;默认的锁申请是阻塞的;
死锁问题排查:(1)使用PS命令获取到进程ID ;jstack pid 获取到线程栈信息;查看线程栈中waiting和blocked状态,对比monitor持有状态。(2)使用ThreadMXBean进行定位。(3)如果是因为一个线程出现死循环而导致死锁,可以采用如下方法排查:使用top命令找到CPU利用率较高的进程,使用top -Hp pid找到cpu使用率较大的线程,使用jstack查看具体调用情况,排查问题。
死锁避免方法:(1)避免使用多个锁,只有需要时才持有锁;(2)多个锁要设计好锁的获取顺序;(3)使用带超时的方法;(4)通过静态代码分析查找固定的模式;(5)任务线程规范命名,详细记录逻辑运行日志;避免在同步方法中调用其他对象的延时方法和同步方法;尽量避免使用静态同步方法;threadlocal,堆栈封闭,线程封闭;

(1)创建两个字符串a,b,再创建两个线程AB,访问字符串前都加锁(A先锁a再锁b,B先锁b再锁a),如果A锁住了a,B锁住了b,就出现了死锁。

public class DeadLockSample extends Thread {
	private String first;
	private String second;
	public DeadLockSample(String name, String first, String second) {
    	super(name);
    	this.first = first;
    	this.second = second;
	}

	public  void run() {
    	synchronized (first) {
        	System.out.println(this.getName() + " obtained: " + first);
        	try {
            	Thread.sleep(1000L);
            	synchronized (second) {
                	System.out.println(this.getName() + " obtained: " + second);
            	}
        	} catch (InterruptedException e) {
            	// Do nothing
        	}
    	}
	}
	public static void main(String[] args) throws InterruptedException {
    	String lockA = "lockA";
    	String lockB = "lockB";
    	DeadLockSample t1 = new DeadLockSample("Thread1", lockA, lockB);
    	DeadLockSample t2 = new DeadLockSample("Thread2", lockB, lockA);
    	t1.start();
    	t2.start();
    	t1.join();
    	t2.join();
	}
}

(2)哲学家吃饭问题,五个哲学家五个筷子,获取每个筷子时需要加锁。如果五个哲学家都先拿右边筷子就会产生死锁。 解决:可以让4个哲学家先拿右边,第5个哲学家先拿左边。(筷子可以共享例如筷子可以切割;拿筷子前先看左右两边的筷子是否使用,只有都空闲的情况下再使用;哲学家有等级高优先级可以抢占低优先级;)

maven解决包冲突

maven解决包冲突:版本冲突。在解析pom.xml时,同一个jar包只会保留一个。(1)最短路径优先,最先声明优先(2)移除依赖,使用maven helper。正常项目都是多模块的项目,如moduleA和moduleB共同依赖X这个依赖的话,那么可以将X抽取出来,同时设置其版本号,这样X依赖在升级的时候,不需要分别对moduleA和moduleB模块中的依赖X进行升级,避免太多地方(moduleC、moduleD….)引用X依赖的时候忘记升级造成jar包冲突。

2.43 Threadload

error&exception

定义:都继承了throwable类;exception(IO,数组越界,nullpointer)程序正常运行中可以预料的意外情况可以捕获和处理;error(OOM,SOE)正常情况下不大可能出现的情况,不需要捕获。
捕获异常规范:(1)尽量不要捕获通用异常要捕获特定异常;(2)不要生吞异常;(3)将异常信息输出到日志中;(4)throw early catch late;(5)try catch的开销很大,实例化一个exception都会对线程进行快照开销也很大。

ClassNotFouneException(类名书写错误)和NoClassDefFoundError(打包遗漏,jar报篡改损坏)区别:前者是在动态加载Class的时候找不到类会抛出异常(Class.forname(),classloader.loadclass(0);后者当编译成功以后执行过程中Class找不到导致抛出该错误。

java对象组成

包含三部分:对象头(16字节,哈希码,锁状态,gc年代年龄;类型指针),对象实例(实际数据)和对齐填充(8字节倍数)。

java开发注入攻击

(1)SQL注入,在输入字段输入SQL语句。
(2)操作系统命令注入,java语言提供类似runtime.exec的API。
(3)XML注入攻击。

java面向对象

抽象、封装、继承、多态
抽象:找出一些事物的相似之处归为一类,这个类只考虑这些事物的相似之处。
封装的好处:对类内部的改变不会影响到其他代码;隐藏类的实现细节;对所有用户提供统一的接口;易于维护和扩展;对成员变量进行更精确的控制,例如如果需要在set的时候对set的值进行判断。封装的做法:私有属性,提供public的读写方法;
继承:子类继承父类的属性和非私有方法,构造子类时依次调用父类的构造方法,再调用子类的构造方法;

多态:一个引用变量在程序运行时才确定具体指向哪个类。不同类对象对同一方法做出不同的操作,很好的解决了应用程序函数同名问题,主要包括方法的重载和重写,注意:static方法、final方法、private方法和protected方法无法表现出多态;向上转型是自动的,向下转型需要强转;
多态可以分为:参数多态(向方法中添加子类对象);多态应用在方法返回类型中(可以返回任何子类对象)

你可能感兴趣的:(JAVA)