学习笔记
java
内容持续更新…
基础类型与String相关
基本类型范围
一个字节占八位,即8bit,byte取值为00000000-11111111,即0-255,由于java中是有符号类型,所以第一位作为符号
那么byte取值就是01111111为最大值127,11111111为最小值-127,此处引入原码,反码,补码的概念,反码用于负数的算数运算,而补码用于正负数跨零的算数运算,计算机都是用补码计算和存储的,因此多出了-128,原理如下:原码反码补码
int是基本数据类型,Integer是int的封装类,是引用类型。int默认值是0,Integer默认值是null,所以Integer能区分出0与null的情况,一旦java看到null,就知道这个引用没有指向某个对象,在任何引用使用前,必须为其指定一个对象,否则会报错
基本数据类型在声明时系统会自动给他分配空间(方法区的常量池),而引用类型声明时只分配了引用空间(线程的java虚拟机栈),必须通过实例化开辟数据空间后才可以赋值(实例化即在java堆中创建对象实例,将引用指向java堆中对象)
虽然定义了boolean这种数据类型,但java虚拟机中操作的所有boolean值,在编译之后都是用了int类型来代替,而boolean数组将会被编译为byte数组
基本类型的转换
byte计算自动转换int
byte b1=3,b2=4,b;
b=b1+b2; //错误 运算时会自动转换为int类型 而int类型的值不能赋值给byte 需要强制类型转换、
b=3+4;//正确 常量具有常量类型优化机制 可以直接识别为byte(原因:常量运算,先把结果算出来再赋给一个变量)
b1+=b2;//正确 +=操作会自动类型转换
byte计算自动转换原因:jvm的32位架构最基础计算单元为int
基本类型与包装类
自动装箱与拆箱,java基本类型与其相应的包装类之间会自动进行类型转换
public class Main{
public static void main(String[] args){
Integer i1=100;
Integer i2=100;
Integer i3=200;
Integer i4=200;
System.out.println(i1==i2);
System.out.println(i3==i4);
}
}
运行结果:
true
false
原因是Integer的valueOf方法具体实现如下
public static Integer valueOf(int i){
if(i>=-128 && i<=IntergerCache.high){
return IntegerCache.cache[i+128];
else
return new Integr(i);
}
在通过valueOf创建Integer对象时,如果在-128~127之间便会返回IntegerCache.cache中已经存在的对象的引用,否则创建一个新的Integer对象
public class Main{
public static void main(String[] args){
Double i1=100.0;
Double i2=100.0;
Double i3=200.0;
Double i4=200.0;
System.out.println(i1==i2);
System.out.println(i3==i4);
}
}
运行结果:
false
false
原因:在一个范围中的浮点数个数不是有限的,存在精度问题,如果要进行比较,使用BigDecimal并使用BigDecimal提供的对象方法进行计算,构造时使用String作为入参防止丢失精度,若用数字比如1解析出来可能是1.0000000000000000000000013之类的,还有计算时要保留位数,尤其除法否则无限循环会报错,然后BigDecimal和String一样不可变,累加需要赋值。
equals与==的区别
equals一般比较对象内容是否相等,而==比较两者的栈中保存的对象堆地址值是否相等,即是否指向同一个对象,equals在object中就存在,如不重写的话两者没有区别,重写时一般使用hashcode方法结合,比较相等首先判断hashcode是否相等,相等再进行真实值的比较,大大降低比较时间,最多两次即可判断。
string stringbuffer stringbuilder
string是一个对象,底层为final类型的字符数组,所引用的字符串不可改变,每次+操作都是隐式的在堆上new一个与原字符串相同的stringbuilder对象进行append操作,stringbuffer加了同步锁,线程安全,stringbuilder非线程安全
集合比较与常用集合原理
List,Map,Set
Array是基于索引index的数据结构,因此getByIndex搜索复杂度是O(1),而且可以进行范围查找
ArrayList和LinkedList是List的两种实现,前者可以自动扩容,底层实现是array,自动扩容原理为新建一个两倍长度的数组再将数据复制到新数组中,LinkedList是一个双向链表,因此在添加和删除时会优于ArrayList,数据不经常更新可以使用ArrayList否则使用LinkedList,而如果不是为了范围查找,最常见还是使用map存储,数组还有一种线程安全的实现方式Vector,很少使用
List的队列和栈数据模型:List的队列和栈数据模型
map是以键值对的形式存储数据,主要的实现为hashmap,hashtable,其中hashmap实现为数组加链表的形式,添加数据的原理为每当数据来临,调用hashcode方法计算哈希并找到元素存储位置,若该处无数据则添加,若有数据则调用equals在链表中进行比较,随着hash冲突的增加,链表的比较开销较大,因此在1.8之后设计为HashMap设计原理
hashmap的死循环原因触发于多线程和扩容状态下,因为头插法扩容时原先的a-b-c就会变为c-b-a,扩容也是一个一个放置到新的map中,原map最上面的就先放入最终变成最下面的,这时候如果两个线程一个执行扩容变成c-b-a,另一个在扩容前是a-b-c那么该线程存储的a的next是b,等扩容完去获取b的next就是a,死循环发生,选择头插是认为新插入数据使用可能较大放前面搜索快,改为尾插不会发生死循环,但是并发不能保证
hashtable是同步的,内部方法使用synchronize修饰,效率很低,几乎不使用,syncronizedmap构造方法将this指向mutex,所有方法加synchronized锁住map,只能一个线程访问,如下:
...
private final Map<k,v> m;
final Object mutex;
SynchronizeMap(Map<k,v> m){
this.m=m;
mutex=this;
...
public int size(){
synchronized(mutex){
return m.size();}
}
...
}
concurrenthashmap使用分段锁来保证在多线程下的性能。一次锁住一个桶。默认将hash表分为16个桶, 诸如 get put remove等常见操作只锁当前需要用到的桶。能同时有16个写线程执行,并发性能的提升是显而易见的。HashMap在多线程情况下put超过了负载因子0.8总容量就会rehash扩容,此时可能发生死循环,虽然改为尾插不会死循环但多个put也会线程不安全,而hashTable方法都用了Synchronize效率太低,因此ConcurrentHashMap使用分段锁的概念,因为不同的数据集hash不同根本不需要去竞争同一个锁,1.7中数据结构为一个segement数组,默认长度16,然后每个Segement对于一个HashEntry,HashEntry就类似hashmap的结构,当put的时候会hash两次,首先hash确定放入哪个Segement中然后hash确定放入HashEntry哪个位置中,并且在放置的时候因为Segement继承了ReentrantLock所以使用tryLock来判断是否能获取锁,获取到的才会put获取不到就进入Lock的AQS中的CLH队列中等待了,get操作和HashMap类似只不过要两次hash,size操作,因为并发size不准确,提供了两个方案,先不加锁去算最多三次,比较size不变那就返回结果,如果不行那就给每个Segement上锁后再计算返回,这里让我想起了Innodb不像Myisam一样提供数据条数总量的字段的问题,因为有事务的情况所以条数不准确。而到了1.8就放弃了Segement的方式直接使用Node数组+链表+红黑树的结构来实现,保留Segement是为了兼容之前的代码。并发使用Synchronize和CAS来操作,Node是存储的基本单元,继承于HashMap中的Entry,就是一个链表,只能查询不能修改
TreeNode继承自Node,数据结构换成了二叉树,一条链路是一个Bucket,Bucket中可能是链表也可能是红黑树,取决于长度是否大于8。put操作先判断key和value是否为null,然后计算key的hash进入无限循环确保可以插入数据,先判断table是否空如果是初始化table数组表,然后后根据key的hash值取table中节点,不存在则CAS放入,否则判断节点hash是否是MOVED,是则说明在扩容,帮助转移,不是的话synchronize开始对该桶Bucket进行遍历并比较,相等就修改,遍历完都不想等就在末尾生成一个新结点,然后计算长度如果达到阈值就转为红黑树。get函数则是根据key的hash判断在哪个桶,如果首结点符合就返回,如果遇上遇到扩容会调用ForwardingNode的find方法取查找(如果该桶没扩容完可以找到,扩容完会把这个指向原来的引用供查找),如果都不符合就遍历节点,匹配就返回,不匹配就返回null。扩容原理是这样的,首先扩容是n<<1即原来的大小乘2,这样会对桶的结点重新计算存放的hash位置,因为N的最高位是1,哈希码和N的最高位一样都是1那么说明是新位置,否则是原来位置,只有这两种情况,因为以前少一位,如果现在第一位还是0说明就是以前,变为1才说明是到了新的位置,然后每个线程参加扩容都只会分配属于自己的区域。put或者get的时候就会判断节点状态并帮助扩容。
为什么线程安全的map都不允许key为null?
hashtable和concurrenthashmap都不允许key为null,而hashmap却可以,这是出于对并发的考虑,多线程情况下一个线程判断该位置有无元素得到null值时无法确定是没有元素还是其他元素在此时插入了一个null值数据
TreeMap 有序,元素无序要实现comparable
set用于非重复,保证机制还是hashcode加equals方法
Collection是集合类的上级接口包含有关集合操作的静态方法用于实现集合的搜索排序线程安全化操作,不能实例化。
跳表skiplist是一种有序的数据结构,可以作为平衡树的一种替代,因为平衡树比如红黑树之类的实现起来复杂而且要旋转变色在并发场景下锁的性能比较差,跳表是有序链表加稀疏索引的方式,首先底层是有序的链表,比如12345678,第一层索引比如有三个值
1-4-8分别可以指向链表中的148元素,再上一层索引可能只有1-8分别指向第一层索引的18,这样当搜索的时候通过几层索引就可以迅速找到数据在链表中的位置,而不需要一个个找下去,显然索引会随着链表的变化而要变化,因此将每层的中点作为索引不合适
这样每次一个元素修改就要更改索引,所以用随机化,比如设置N个数据的链表第k层有N/2^k个节点
BitMap位图,用每一个位表示某个状态,适用处理海量数据,本质是hash表,原理就是将一个int数据映射到对应的位上,将该位由0变为1,一个int存储要32bit,bitmap只要1bit所以节约了32倍
数据分布不均匀的话中间空值占了很多反而浪费了资源,这就需要用一位存数据,多几位存到下一个数据的跨度信息,合起来作为一个数据的整体,比如10位为一个数据。然后对于字符型的可能就要
使用布隆过滤器(比较难维护,生成之后如果值删除了要去除比较麻烦)。
stream中的flatMap是扁平化处理,list中的子list可以用flatMap来操作会得到该子list的所有数据
Optional用来解决校验问题,如果一个对象中多层嵌套的判断是否为空用if太多层了,用Optional.ofNullable然后用map当一个if去链式处理最后用orElse或者orElseGet来收尾会更优雅
@Conditional注解的作用是给需要装载的bean添加一个条件判断只有满足的时候才加载,在bean的描述逻辑中加上这个注解并重写matchs方法自定义装载条件
反射机制与真实使用场景
java反射机制是在运行时,对于任意一个类,都能够知道这个类的所有属性和方法,对于任意一个对象,都能调用它的任意一个方法,只要给定类的名字就可以获取它的全部信息,field,constructor,method都可以被获取,提高灵活性但是性能较低,可以通过setAcessible(True)来关闭jdk的安全检查来提高性能
动态代理与使用范例
异常
Exception于error,error是JVM出现的问题,不是代码原因,比如oom,exception有runtimeException和编译时异常,前者有空指针,数组溢出等,运行时才有可能发生,需要try/catch。后者有ParserException,IOExceptio等在编译的时候就要进行处理否则编译无法通过,我们也可以实现Throwable定义自己的异常
类加载机制与热加载实现与反编译
内存模型与threadLocal与syncronize
java的四类引用,强弱软虚
直接创建对象就是强引用,包括new,newInstance,clone和序列化,弱引用weakReference,只要GC就会回收,软引用SoftReference会在内存不足的时候回收,虚引用phantomReference回收机制和弱引用差不多,只要GC就回收,但回收前会被放入ReferenceQueue中,其他引用都是被jvm回收后才被传入ReferenceQueue中,由于这个机制,虚引用大多用于引用被销毁前的处理工作,因此创建虚引用必须带有ReferenceQueue
深拷贝与浅拷贝的区别:深拷贝将对象完全拷贝一份,包括属性和对其他对象的引用,浅拷贝只会拷贝属性,对象的引用复用,所以浅拷贝的两个对象在对引用属性修改时是共享的,深拷贝则完全独立。clone是浅拷贝,深拷贝需要自己实现,即每一层对象都用浅拷贝。
java中不想序列化的字段用transient关键字修饰,不会序列化,反序列化时也不会被持久化和恢复,只能修饰变量,不能用来修饰方法和类。
threadLocal的弱引用产生的内存泄漏隐患,与线程池的结合
syncronize的锁升级机制与AQS算法,monitorenter与monitorexit指令
AQS双向链表等待队列
jvm的GC与调优处理
JVM的内存结构主要源于数据在主存和工作内存的交流方式产生,分别是线程内私有的java虚拟机栈,用于存储局部变量表,方法操作栈等,本地方法栈用于执行native方法,程序计数器用于记录当线程的cpu时间片耗尽时程序执行的位置,执行native时置为空,线程间的共享有堆和方法区两块,堆用于存放几乎所有的对象实例,即新生代和老年代,方法区用于存储常量静态变量和类的信息,即永久代。类的加载和卸载遵循双亲委派机制,加载-验证-准备(设置初始值)-解析-初始化(赋值),该机制保证类不被重复加载,且能保证java核心api不被篡改,bootstrap-ext-application-自定义,以加载器+全类名为唯一标识,GC算法在新生代为标记赋值算法,eden-survivor1-survivor2是minorGC,老年代是标记清楚算法是FullGC,判断是否回收通过引用计数和可达性分析,引用计数简单但不能解决循环引用,可达性分析为从GCRoots向下搜索,搜索走过的路径称作引用链,当一个对象没有任何引用链,说明是不可达的,JVM性能调优通过-Xmx设置堆内存,不宜太小否则都去老年代了。
GC问题:cpu报警,接口失败率逐渐上升逐渐奔溃说明服务节点还在但是应用系统内有问题,cpu报警业务上无计算密集型也没有大量io所以判断是不是内存,查看触发GC算法fullGC频繁,所以查看原因发现新上的代码使用single线程池来插入数据,sql从入参来
入参很大导致sql很大然后一次插入要1秒并且后续的sql都在阻塞队列中作为大对象等待导致fullGC,然后限流没限住是因为请求并没到达阈值是处理太慢了,处理慢没触发降级熔断是因为用了异步线程去做rt没有增加。。
JUC设计原理
java解决多线程并发访问共享资源的时候提供了两种解决方案
一种是synchronize锁方式,用时间换空间,有无锁的CAS以及乐观锁方式,也有lock包下的锁与synchronize关键字,对共享变量的同时修改采用互斥的方式,synchronize利用monitor的机器指令,锁住的是对象,锁信息置于对象头中,所以没有公平锁的概念,释放资源后产生羊群效应,其中还包括锁优化即偏向锁到轻量级锁到重量级锁的锁升级机制,lock使用的AQS,锁住的是锁本身,用state去控制访问的权限,等待竞争的线程会在CLH队列中,所以可以保证公平锁。但是synchronize可以锁类,lock只能锁方法。
一种是ThreadLocal复制方式,用空间换时间,通过对共享变量的复制,使得每个线程访问并修改自身存储的值,ThreadLocal本身不存储数据,使用线程中threadlocals的一个属性,其类型由threadlocal中threadlocalmap定义,这个还要去看源码。
Condition类:主要用来替代Object中的wait和notify实现线程间的写作,相比Object来说Condition中提供awaitUntil的deadline等待以及awaitUninterrptibly的非中断等待等更有效的方式
场景例子有arrayBlockingQueue的实现:
public class ConTest {
final Lock lock=new ReentrantLock();
final Condition condition=lock.newCondition();
final Lock lockFake=new ReentrantLock();//condition必须和对应lock配合使用如果用lockFake上锁,condition通知会报错
public static void main (String args[]){
//AbstractQueuedSynchronizer AQS抽象队列同步器
//ArrayBlockingQueue 中Condition notEmpty在enqueue添加元素后signal,notFull在dequeue删除元素后signal,在take中count为0则notEmpty.await,在put中count=item.length则notFull.await
ConTest test=new ConTest();
Producer p=test.new Producer();
Consumer c=test.new Consumer();
c.start();
p.start();
}
class Consumer extends Thread{
public void run(){
try{
lock.lock();
System.out.println("This Thread is waiting a sign"+ currentThread().getName());
condition.await();
}catch (InterruptedException e){
}finally {
System.out.println("getting a sign"+currentThread().getName());
lock.unlock();
}
}
}
class Producer extends Thread{
public void run(){
try{
lock.lock();
System.out.println("This Thread holding on the lock"+ currentThread().getName());
condition.signalAll();
System.out.println("advertise a sign"+ currentThread().getName());
}finally {
lock.unlock();
}
}
}
}
以Condition实现生产消费者模式:
public class ConTestTwo {
private int queueSize = 10;
private PriorityQueue<Integer> queue = new PriorityQueue<>(queueSize);
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
public static void mai