Java杂记

JAVA

  • 线程安全

全局变量A,10个线程同时对其累加1,最后可能不会到10;

无论是堆内存还是栈内存,当被用到的时候一定会被复制到CPU的缓存行和寄存器,也就是当前线程的本地内存。全局变量A被线程内调用时,A的值会被复制一份到本地内存。10个线程这个变量A被复制到10个线程栈中,累加后都变成A+1,然后store到全局变量A中。这也就出现了10个线程累加10次最后全局变量不是A+10而是A+1。

valotile 可以保证多线程可见但是并不能保证线程安全,比如第1个线程累加完成 -> store到全局变量 -> 让所有线程可见,但是线程2得到新的可见值之前可能就已经完成了累加。

那怎么让结果是A+10,这里就是加锁,包括两种,乐观锁和悲观锁。

准确的说法应该是Java工作内存快,因为Java虚拟机及硬件系统会让工作内存优先存储于寄存器和CPU Cache中,所以肯定比堆的纯内存存储快了很多个数量级

因为有空间局部性(locality)栈的访问不像堆一样那么随意,而是在变化不大的一部分空间内访问,在访问数据的时候一整块的数据都会被加载进缓存,就是那个L1.所以栈的miss率很小啊,所以就接近SDRAM的访问速率了。Java中 栈内存的存取速度仅次于寄存器,如何做到的? - 知乎

  • CAS 乐观锁

当线程1中的A写回到主内存的全局变量A时,如果发现A的值是期望的旧值并没有改变过,则将自己SET进去,如果发现不是旧值,则快速失败。这在汇编底层调用test_and_set指令,是原子不可中断的;这比较适合偶尔竞争甚至没有竞争的场景,速度更快;如果竞争多比较适合用悲观锁,排队等。

  • 悲观锁synchronized

① 检测 锁对象的Mark Word(Java对象数据结构的一部分)中锁状态为01(偏向锁状态),偏向锁的标志位为1,则可确认为可偏向状态 。然后检测 锁对象Mark Word里的线程ID是不是当前线程的,如果是,表示当前线程持有偏向锁,则可运行接下来的同步代码 

② 如果不是,则使用CAS竞争性地将Mark Word中的线程ID替换为当前线程的ID,如果成功则表示当前线程获得偏向锁

③ 如果失败,则说明发生竞争,到达全局安全点(短暂stw)撤销偏向锁并升级为轻量级锁,锁状态为00(轻量锁状态)。

④ 当前线程使用CAS竞争性地将 锁对象Mark Word的 锁记录(Lock Record)指针 赋值(指向)为在当前线程栈中开辟的一个Lock Record空间(锁对象Mark Word的拷贝,存储当前线程Mark Word ,实现线程切换),如果成功,当前线程获得此轻量级锁 

⑤ 如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。如果自旋获取锁成功则依然处于轻量级状态。

⑥ 如果自旋失败,则升级为重量级锁(底层悲观式的阻塞或唤醒一个线程就需要操作系统的帮忙,从用户态转换到核心态,非常耗时,尽量乐观式的CAS)。

  • AQS

Java通过AQS实现基础的生产者消费者模型。

1. AQS提供了同步队列,独占式锁的获取和释放(类似写锁),共享锁的获取和释放(类似读锁),超时等待锁获取这些特性的实现,好多同步组件的基础。独占 锁对象后,其他线程无论想独占这个锁还是共享占有都会失败;共享锁对象,其他线程想独占不可以,但是同样可以共享占有。

2. 以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state自加1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。

BlockQueue用可重入锁实现:

lock = new ReentrantLock(fair); notEmpty = lock.newCondition(); notFull =  lock.newCondition();//Condition中的await()方法相当于Object的wait()方法,Condition中的signal()方法相当于Object的notify()方法 put 操作:lock.lock(); while (count == items.length) notFull.await(); 入队; count++; notEmpty.signal(); lock.unlock();        take操作:lock.lock(); while (count ==0) notEmpty.await(); 出队; count--; notFull.signal(); lock.unlock();

公平锁与非公平锁体现在持有锁的线程释放锁的时候是从等待队列里取线程给予锁还是直接给新来的线程。

3. 以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程都可以在执行完后tryReleaseShared释放共享锁(既调用countDown),state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()等待线程,然后等待线程就会从await()函数返回,继续后余动作。(跟线程池的invokeAll类似,都要记得解决死锁或者有一个线程没完成导致无法归零问题)。

cdl =new CountDownLatch(7); for循环7次new Thread(()->{ ... cdl.countDown() }).start(); cdl.await();主线程等待

CountDownLatch主要是实现了1个或N个线程需要等待其他线程完成某项操作之后才能继续往下执行操作。CyclicBarrier主要是实现了多个线程之间相互等待,直到所有的线程都满足了条件之后各自才能继续执行后续的操作。

CyclicBarrier barrier = new CyclicBarrier(3); ExecutorService executor = Executors.newFixedThreadPool(3);    executor.submit(new Thread(new Runner(barrier, "1号选手"))); executor.submit(new Thread(new Runner(barrier, "2号选手"))); class Runner implements Runnable { private CyclicBarrier barrier; public Runner(CyclicBarrier barrier) {this.barrier =barrier;} void run(){ ...barrier.await();//有3个await此barrier就都启动 syso(起跑); } }

  • 双重检查锁的单例模式

public class SafeDoubleCheckedLocking { private volatile static Instance instance;//volatile防止指令重排,出现读取到instance不为null但是instance引用的对象有可能还没有完成初始化的情况;static保证下面的static方法中能使用,并保证唯一 public static Instance getInstance() { if (instance == null) { //提前检测,可以防止每次都进入同步块 synchronized (SafeDoubleCheckedLocking.class) { if (instance == null)  //防止两个线程都通过第一个检测,破坏了单例 instance = new Instance();          }      } return instance;   } }

。。。。。什么时候看aqs的源码就这么难了。?一个队列。一个volatile的int的值status。一个原子操作。一个 unsafe.park unpark。一堆方法。2个抽象方法。各种实现类去实现。

Direct Memory的回收机制:Direct Memory是受GC控制的,例如ByteBuffer bb = ByteBuffer.allocateDirect(1024),这段代码的执行会在堆外占用1k的内存,Java堆内只会占用一个对象的指针引用的大小,堆外的这1k的空间只有当bb对象被回收时,才会被回收,这里会发现一个明显的不对称现象,就是堆外可能占用了很多,而堆内没占用多少,导致还没触发GC,那就很容易出现Direct Memory造成物理内存耗光。

jmap -heap pid 整体当前的各个区域状态(以及各个区域的已经向操作系统申请的内存)

jmap–histo:live 3772 查看存活对象

jmap -dump:format=b,file=heapdump.phrof 下载下来查看当时个对象占用的大小

jstack

jstat -gcutil 31798 1000 10 查看gc的过程

• JVM内存分布

java8之前,hotspot使用在内存中划分出一块区域来存储类的元信息、类变量以及内部字符串(interned string)等内容,称之为永生代,把它作为方法区来使用(永生代是hotspot中的概念,其他jvm实现未必有)。java8将 原先永生代中类的元信息会被放入本地内存(元数据区,metaspace),将类的静态变量和内部字符串放入到java堆中,整体还作为方法区。

Java线程通过LWP(轻量级进程)一对一映射到OS的内核进程上,底层调用pthread_create。多线程时,有多少线程就开多少红色部分。

将一个用户线程设为守护线程的方法就是调用start启动线程之前调用setDaemon(true)。

• GC算法

能盛下的对象都在Eden区诞生(盛不了进入old区),如果其Eden区盛不下新数据触发MinorGC->短暂STW,(假设to现在为空的)将Eden区和from区仍存活的对象复制到to区(如果to区盛不下进入old区)并将年龄+1(满15岁进入old区),然后清空Eden区和from区。

如果old 区盛不下新数据就会触发FullGC。

• FullGC垃圾收集过程:

初始标记(仅标记与GCRoots关联的,独占CPU需短暂STW),

并发标记(GC Roots Tracing标记,与用户线程并行,时间很长),

重新标记(修正标记期间改变的标记,独占CPU会短暂STW),

并发清除(如回收Old区的CMS,为达到与用户线程并行使用标记-清除,会产生不连续碎片但还得用,因为 整理 地址会变影响并行的用户线程)

/筛选回收(注意这里没有了明确的那个内存区域属于哪个代而是分为一个个Region进行管理如上图,并且G1有内存整理过程,使用Remembered Set管理引用避免全表扫描,根据用户期望的GC停顿时间回收)。

JVM不是一个普通的进程,JVM将许多本来属于操作系统管理范畴的东西,移植到了JVM内部,减少系统调用的次数;同时加强对内存的管理,自动回收不使用的内存。

CMS的concurrent mode failure:还没FullGC完又有大量的数据进入old区造成频繁FullGC-->young设小点。

• GC中判断对象是否存活一般有两种方式:

引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。

可达性分析:从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不可达对象。在Java语言中,GC Roots包括:虚拟机栈中引用的对象,方法区中类静态属性实体引用的对象,方法区中常量引用的对象,本地方法栈中JNI引用的对象。

• JVM 启动参数

-Xmx:限制向OS申请的最大内存,防止与OS争内存造成冲突造成死机(默认是物理内存的1/4)-Xms:初始申请的内存大小(最小内存) -Xmn:年轻代大小 -XXSurvivorRatio:年轻代中Eden区与Survivor区的大小比值。

JVM启动是向OS申请Xms大小的内存(Total Memory) -> 可以在资源管理器中看到commited内存,当GC后发现空余内存小到一定阈值时JVM就会向OS申请更多的内存(直到最大限制-Xmx);空余内存较多时JVM会减少申请的内存直到最小限制-Xms。因此服务器一般设置-Xms、-Xmx 相等以避免在每次GC 后调整堆的大小。-Xss 为jvm启动的每个线程分配的内存大小,默认JDK1.4中是256K,JDK1.5+中是1M(越小能分配的线程越多,但容易爆栈)。CMS垃圾回收器归还内存比较缓慢,防止万一还需要那么多的内存(大概率),而G1每次GC主动归还不保留,降低内存率。

• GC日志分析

[PSYoungGen: 6912K GC前Young堆已用大小 ->368K GC后剩余已用Young堆大小 (8960K young堆总大小)] 6912K GC前总堆已用大小->6512K GC后总堆剩余已用大小(19200K 总堆大小), 0.0054194 secs]

MinorGC晋升到Old区的大小为(6912-368,Young区减少的)-(6912-6512,清除掉的)=6144KB(如果old区满了会继续触发FullGC)

GC清理分析,很强大 • JVM 性能调优监控工具字节码相关问题

• Java中String对象的指向可以变,但是指向的字符串是不变的。StringBuffer(线程安全) & StringBuilder(不安全)指向的字符串均可以动态改变。String不变好处:省空间,hash不变,线程安全,黑客不可改。

String类中有这样的一个数组成员:private final char value[];加d=d.intern();后就都成h_llo啦。d.intern会从常量池中找到equal(d)的字符串返回它的地址(如果没有便在池里创建一个并返回-->1.8是创建一个地址(指向堆中的d)hash值)• 密码最好用char数组,因为String对象赋值为null一段时间内还存在,不安全。

•  虚拟机规定:5种主动引用(比如:用new实例化一个类时、读取或者设置类的静态字段时(不包括被final修饰的静态字段,因为他们此前已经被塞进常量池并可见了)、以及执行静态方法,反射,Class.forName)必须 触发类的初始化(如果没加载类就会先加载Class文件字节流到内存,验证,准备静态变量空间并赋零值,符号引用解析为直接的地址),使静态变量可见(静态变量赋初值),并调用静态块(只执行一次),而其他普通变量只在new或forName得到实例的时候才有内存。

•  类加载过程:

双亲委派模式每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成,这不就是传说中的实力坑爹。保证安全。

•  JRE=/jre/bin目录(JVM -> PATH中需要,指定java命令搜索路径) +  /jre/lib目录(有rt.jar是Java基础类库 -> CLASSPATH中需要); JDK=JRE运行环境+ Java开发工具(有tools.jar);

•  Java是静态多分配(重载时看方法的返回值【静态类型】和方法参数),动态单分配(重写时看谁【实际类型】调用)。Java中obj.printf(“hello”);obj的静态类型是PrintfStream,那么实际类型一定是PrintfStream的子类(编译器保存了obj的完整符号引用);而动态语言的类型检查在运行期,obj的实际类型只要有printf的方法就可以调用成功(变量obj本身没类型限制,只有值有类型);JVM通过invokedynamic实现支持动态语言。

• Java解释器就是虚拟机,前端编译器(如javac),其将 Java 源代码编译为 Java 字节码文件(jvm解释执行)。JIT 即时编译器(如 HotSpot 虚拟机中的 Client Compiler 和 Server Compiler),运行是其将 Java 字节码编译为本地机器代码。而 AOT 编译器则能将源代码直接编译为本地机器码。数组越界从运行期检查提到编译期完成。

• Java泛型通过类型擦除再强制转换完成,导致void method(List list)与void method(List list)无法重载,因为编译后都变成了List,但是改为Integer method(List list)后可以了(返回值不参与重载,但是class文件允许描述不一致的两个方法共存)。

•  object result = Stack[top --];此时Stack[top]仍有强引用,导致其所指的对象一直无法被GC掉 ,造成内存泄漏,可以加一句Stack[top] = null;      

强引用:默认的引用方式。一个对象只有软引用:JVM有内存是跟强引用一样,JVM没内存了跟没引用一样,避免OOM。一个对象只有弱引用:跟没引用一样,下次GC时被回收。虚引用:跟没引用一样,与弱引用不同的是JVM会将虚引用放到与之关联的ReferenceQueue中去,只有等到ReferenceQueue中虚引用被你remove取出(你可以进行一些处理)他所指的实例对象才会被GC掉。

 • 线程状态转换

对于IO堵塞时在jstack中显示是Runnable的问题解释jstack案例

• 资源竞争才会需要锁,本质还是一个生产者消费者同步队列

1. AQS提供了同步队列,独占式锁的获取和释放(类似写锁),共享锁的获取和释放(类似读锁),超时等待锁获取这些特性的实现,好多同步组件的基础。独占 锁对象后,其他线程无论想独占这个锁还是共享占有都会失败;共享 锁对象,其他线程想独占不可以,但是同样可以共享占有。

2. 以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state自加1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。

BlockQueue用可重入锁实现:

lock = new ReentrantLock(fair); notEmpty = lock.newCondition(); notFull =  lock.newCondition();//Condition中的await()方法相当于Object的wait()方法,Condition中的signal()方法相当于Object的notify()方法 put 操作:lock.lock(); while (count == items.length) notFull.await(); 入队; count++; notEmpty.signal(); lock.unlock();        take操作:lock.lock(); while (count ==0) notEmpty.await(); 出队; count--; notFull.signal(); lock.unlock();

公平锁与非公平锁体现在持有锁的线程释放锁的时候是从等待队列里取线程给予锁还是直接给新来的线程。

3. 以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程都可以在执行完后tryReleaseShared释放共享锁(既调用countDown),state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()等待线程,然后等待线程就会从await()函数返回,继续后余动作。(跟线程池的invokeAll类似,都要记得解决死锁或者有一个线程没完成导致无法归零问题)。

cdl =new CountDownLatch(7); for循环7次new Thread(()->{ ... cdl.countDown() }).start(); cdl.await();主线程等待

CountDownLatch主要是实现了1个或N个线程需要等待其他线程完成某项操作之后才能继续往下执行操作。CyclicBarrier主要是实现了多个线程之间相互等待,直到所有的线程都满足了条件之后各自才能继续执行后续的操作。

CyclicBarrier barrier = new CyclicBarrier(3); ExecutorService executor = Executors.newFixedThreadPool(3);    executor.submit(new Thread(new Runner(barrier, "1号选手"))); executor.submit(new Thread(new Runner(barrier, "2号选手"))); class Runner implements Runnable { private CyclicBarrier barrier; public Runner(CyclicBarrier barrier) {this.barrier =barrier;} void run(){ ...barrier.await();//有3个await此barrier就都启动 syso(起跑); } }

• synchronized锁:

① 检测 锁对象的Mark Word(Java对象数据结构的一部分)中锁状态为01(偏向锁状态),偏向锁的标志位为1,则可确认为可偏向状态 。然后检测 锁对象Mark Word里的线程ID是不是当前线程的,如果是,表示当前线程持有偏向锁,则可运行接下来的同步代码 

② 如果不是,则使用CAS竞争性地将Mark Word中的线程ID替换为当前线程的ID,如果成功则表示当前线程获得偏向锁

③ 如果失败,则说明发生竞争,到达全局安全点(短暂stw)撤销偏向锁并升级为轻量级锁,锁状态为00(轻量锁状态)。

④ 当前线程使用CAS竞争性地将 锁对象Mark Word的 锁记录(Lock Record)指针 赋值(指向)为在当前线程栈中开辟的一个Lock Record空间(锁对象Mark Word的拷贝,存储当前线程Mark Word ,实现线程切换),如果成功,当前线程获得此轻量级锁 

⑤ 如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。如果自旋获取锁成功则依然处于轻量级状态。

⑥ 如果自旋失败,则升级为重量级锁(底层悲观式的阻塞或唤醒一个线程就需要操作系统的帮忙,从用户态转换到核心态,非常耗时,尽量乐观式的CAS)。

•  在数据库中实现乐观锁:在基于数据库表的版本解决方案(MVVC)中,一般是通过为数据库表添加一个”version”字段来实现读取出数据时,将此版本号一同读出,之后更新时,对此版本号+1。此时,将提交数据的版本号与数据库表对应记录版本号进行比对,如果提交的数据版本号大于数据当前版本号,则予以更新,否则认为是过去数据。

• 在Redis中,使用watch命令实现乐观锁(watch key): watch命令会监视给定的key,当exec时,如果监视的key从调用watch后发生过变化,则事务会失败,也可以调用wathc多长监视多个key。这样就可以对指定key加乐观锁了。注意watch的可以是对整个连接有效的。事务也一样。如果连接断开,监视和事务都会被自动清除。当然exec,discard,unwatch命令都会清除连接中的所有监视。(实现并发抢红包)

• volatile a; 加入内存屏障(两个作用:告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行;另一个作用是改a后强制更新一次不同线程中的local a`变量缓存值,使所有线程可见)。但是volatile并不完全保证原子性,例如Thread1执行b = a+1,在store->b之前可能插入改a的操作,volatile保证可见然后更新a` ,而此时a`+1的值已经计算好就要store b了。(插入读b也有问题)(类比:c语言优化是指将源数据(可能是持续变化的模拟信号)读到寄存器中缓存起来,多次调用就快了,但是不能保证是最新的。volatile关键字不优化,保持最新)

//线程1:context = loadContext(); //语句1   inited = true; //语句2 //线程2: while(!inited ){ sleep() }    doSomethingwithconfig(context);

• 上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此时线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。解决inited =  true ;加volatile内存屏障,使之一定程度上有序,并且这是个赋值操作,是原子操作,那就没问题了。通过synchronized和Lock也能够保证可见性,因为synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步操作,并且在释放锁之前会将对变量的修改刷新到主存当中。但synchronized关键字是保证此操作同一时刻只有一个线程执行(语句1和2加锁),那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。

----------------------------- 分割线 ----------------------------------

 https://www.jianshu.com/p/74cbec2e6f73 如何优雅的关闭Java线程池 - 清泉^_^ - 博客园

• 断点模拟多线程是一个很好的思路。

• wait()和notify()方法来实现线程间的协作:

1. A线程取得锁,执行wait(),释放锁; 

2. B线程取得锁,完成业务后执行notify(),再释放锁; 

3. B线程释放锁之后,A线程取得锁,继续执行wait()之后的代码;

例如实现两个线程交替打印奇偶100次

void run() { while (num.i <= 100) { synchronized (num) { if (num.flag){ num.wait(); } else { num.i++;num.flag = true; num.notify(); } } } }

1、按值传递bai:值传递是指在调用函数时将du实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。简单来说就是直接复制了一份数据过去,因为是直接复制,所以这种方式在传递时如果数据量非常大的话,运行效率自然就变低了,所以java在传递数据量很小的数据是值传递,比如java中的各种基本类型:int,float,double,boolean等类型的,具体可以自己测试。

2、按引用传递:引用传递其实就弥补了上面说的不足,如果每次传参数的时候都复制一份的话,如果这个参数占用的内存空间太大的话,运行效率会很底下,所以引用传递就是直接把内存地址传过去,也就是说引用传递时,操作的其实都是源数据,这样的话修改有时候会冲突,记得用逻辑弥补下就好了,具体的数据类型就比较多了,比如Object,二维数组,List,Map等除了基本类型的参数都是引用传递。代码:

Caffeine Cache-高性能Java本地缓存组件

MeshUrlResponseTimeData meshUrlResponseTimeData = new MeshUrlResponseTimeData(100, "1", 1.0F, 1L); Function<MeshUrlResponseTimeData, Integer> f = MeshUrlResponseTimeData::getMeshUrlId; System.out.println(f.apply(meshUrlResponseTimeData));

minor gc 会发生stop the world 现象吗? - 知乎

每个thread中,都有一个run,run里执行 longThreadLocal.get , 会从这个thread的localmap中取出 key为longThreadLocal的值

• Iterator可remove(线程安全)。Eumeration增加了泛型操作,但不能remove。ListIterator增加了add,set,next/previousIndex方法。sublist是快照,sulist插入会影响原list。

• ArrayList基于数组,默认大小是10(如果不指定的话),不够用时扩充为原来大小的1.5倍。

Vector基于数组,默认扩充为原来2倍,线程安全的(加了synchronized)。

LinkedList采用双向链表。数组可以随机访问,链表插入性能好但占空间大。

ArrayList没有实现Random这个MarkAccess接口,用for循环遍历的话花费很长时间(每次get(i)都从从头到i),因此最好用迭代器。

• Arrays.sort()的改进 单轴快排(SinglePivotQuickSort)和双轴快排(DualPivotQuickSort)及其JAVA实现_Holmofy的博客-优快云博客  新的快速排序算法: 《Dual-Pivot QuickSort》阅读笔记 - 简书

• Collection.sort() :当参数类型为对象数组 -> TimSort 算法为了减少对升序部分的回溯和对降序部分的性能倒退,将输入按其升序和降序特点进行了分区,每一个区为一个run。再到了JDK8,对大集合增加了Arrays.parallelSort()函数,使用fork-Join框架,充分利用多核,对大的集合进行切分然后再归并排序,而在小的连续片段里,依然使用TimSort与DualPivotQuickSort。 Timsort原理介绍_猴子数据分析的博客-优快云博客_timsort原理

bit-split quicksort 首先想法是将快排与桶排序结合,桶不能无限多,因此可以取余进行hash,但是对于排序来说余数的不是首要的参考价值(一般也要先排整数部分),什么既是首要参考还可以控制范围,那就是数据化成二进制时的前几位,因此将快排的划分方式由大于小于pivot改为从前面数第1,2,3...位是0还是1,这样如果是数据类型确定比如说int,那么递归深度就是32层是一个常数,避免经典快排一边倒的特殊情况。后来百度了一下,没有看见相关的快排改进,以为自己第一个发现了新大陆,后来Google 了一下,最后竟然找到了类似的,清华大学一个叫黄兴(PennySort夺冠)的论文中提到了这种方式,并且每次比较n位放到对应的桶中,最后再排序,也就是我一开始时的那个想法。直接排序省空间的。

• HashMap阈值为其hash数组Capacity(默认16)*加载因子(默认0.75),超过阈值进行rehash成倍扩充,允许一条key为null的记录,线程不安全但是采用fast-fail的遍历:Iterator在读他的时候,如果其他线程在改就会触发ConcurrentModificationException异常。(fail-safe的遍历基于容器的克隆体,对容器的修改不影响遍历,如ConcurrentHashMap的Iterator)。get寻找key时通过hashcode和equal。

还有JDK1.8中对HashMap的增强,如果一个桶上的节点数量过多,链表+数组的结构就会转换为红黑树。

HashMap源码中:h & (length-1)相当于h % length,但是h % length效率比较低(HashTable中是这儿干的)。HashMap规定length为2的幂(rehash扩容时也是*2),那么length就可以表示成100......00的形式(表示至少1个0),那么length-1就是01111....11。h& (length-1)就只能截取h对应01111....11中位全1的地,也就是取余数。

HashMap在并发resize时可能出现环的情况,首先不同线程resize前的数组是同一个,而resize后的数组是各自线程里重新开辟的,还有两个线程共享A,B结点。

• HashTable 阈值为其hash数组Capacity(默认11)*加载因子(默认0.75),当其中的键值对数量超过此阈值后进行rehash:newCapacity = oldCapacity*2+1; --> 现在用ConcorrentHashMap

•  ConcurrentHashMap 1.7中将hash表分为16个segment(每一个看成一个Hashtable),get操作不需要锁(volatile的value使得读到数据在各线程一致),put,remove操作只需要锁当前segment而不是整个hash,提高并发能力。 (因为key和next都是final,remove时将要删除的结点前的所有结点复制一遍,然后把复制的Hash链的最后一个结点指向待删除结点的后继结点,删除效率是有点低)。1.8中又改成Node数组+每一个元素加synchronize(已优化的)+CAS插入,链表过大会变成红黑树。class Node implements Map.Entry { final int hash;final K key;  volatile V val;volatile Node next; ... 省略}                                   

HashTable是通过synchronize来锁整个表实现线程安全。

• 对于hash,加载因子过大,空间利用率高了,但是冲突增加,导致链更长。过小,那么空间利用率变低了,但是冲突的机会减小了,这样链表就不会太长。时空矛盾。

Hash冲突的处理:拉链法(冲突的key组成一条链,如HashMap);开放定址法(当前存不了就往下一地址存或跳着存)

•  List只可往里写T类型的对象,List只可读到T类型。

• 编译器把一个变量对应一个地址,这个地址存有对应的数据。函数调用时在函数内部会产生形参副本来接收实参的数据。直接对形参赋值的对实参没啥影响,给实参.参数 赋值的对实参有影响。

• 一个对象没有构造方法,系统默认提供无参构造器可用;子类未显示调用父类构造器,会自动调用父类无参的,父若无报错。注意:构造器无法被继承(可能会被子类自动调用)被重写。

• 关于Java的继承问题:Father【静态类型】 a = new Son()【实际类型】中Father是范围限定(限定的是子类中父类的部分,由两部分组成:原父类的静态部分is linked to【静态部分从属于类】,而非静态部分is copied to),其中父类的非private方法可被子类重写覆盖掉,这是实现多态的基础。但是注意:子类重写或重载父类private方法,相当于定义一个新方法,达不到效果。Son中非Father范围无法调用Father范围的private属性(确实继承到了Son中,只不过限定在Father范围 对Son部分不可见 且 无法被Son重写),但可以通过继承来的public getter()来获取。

• 小范围接收大数要强制转换并被截断,大范围接收小数会进行自动扩展

• 还是Father a = new Son(),编译器在重载时通过范围限定(学名:Static Type)来进行判断。Class Test{ void test(Father f){...}  void test(Son s){...}   } Test t ; t.test (a);调用前者。

• 如果子类中有与 父类或接口 中变量重名,那就有两个;如果只是extend的父类和implements的接口中有同名变量而子类没有(因为字段解析时如果子类有了就不往上解析了,如果子类没有往上解析同时出现俩,咋办),编译报错这个变量is ambitious。

• 接口like a ,有……的能力(...able),实现接口的类必须重写接口里的方法(而其他地方此类的对象会调用此方法)。Interface与abstract相比,虽然都不能实例化且强制派生类重写方法,但是interface变量默认为public final static,不能继承且不变,只有方法(动作)能被继承,不像abstract有普通成员变量可以被继承(也可以有构造器用来派生类调用父类时初始化父类变量),因此interface比abstract更抽象,而抽象的意味着不变,而多变的东西尽量把他们放到子类中。单继承多接口。default关键字在接口中修饰方法时,方法可以有方法体(Java8中)。

• 子类继承的implement是父类的。子父变量同名,那就有两个。构造方法不能被继承。

• 创建实例时,先静态(父变量,父代码块,子变量,子代码块),后非静态(父变量,父类代码块,父构造方法,子变量,子代码块,子构造方法)。

• 重载时返回值可以不同,但不能出现只有返回值不同的重载,重写时返回值必须一致。重载访问权限可以随便改,重写时子类方法不能缩小访问权限,不能抛出更多未知的异常,否则多态时代码处理不了;

• Final方法不能被重写;final类不能被继承;final变量赋值后不能改(final类里的变量可改);final不能和abstract在一起;byte b1=1; byte b2=3;  byte b3=b1+b2;//出错,因为b1、b2可以自动转换成int类型的变量,运算时Java虚拟机对它进行了转换,结果导致把一个int赋值给byte  如果对b1 b2加上final就不会出错                          

• static方法中不能使用this或者super;private的static不能在其他类中使用。           

• 最小的double为2^(-1074);1.0/0.0是正无穷,打印为Infinity;i=i++后i值不变,先把i存到临时变量tmp,i自加1,i被赋值tmp;小数默认double,float的赋值1.0f;byte,char,short运算+=时不会类型转换;null可以被强制转换为任意对象;

  跳出多重for循环:out:....break out;.....  

• short s; s = s +1 会出现编译错误,s+1运算自动提为int类型,然后赋给short当然不对。s+=1 是可以的,+=会自动强制转换,相当于s = (short)s+1。

包装类除了用new Integer()显式的创建对象,否则都复用一个同值的包装类。Integer i01 = 59; Integer i02 =Integer.valueOf(59); i01==i02。

byte b = (byte)0xfe; (byte的强制转换必须有,否则0xfe是int类型的-2)

new Integer(1) 和Integer a = 1不同,前者会创建对象,存储在堆中,而后者因为在-128到127的范围内,不会创建新的对象,而是从IntegerCache中获取的。

• i = i3位,扩大 2*2*2(2^3)倍

• protected成员可以继承到其他包(但是自动降成private)>无(只包内可见)>private

A a=new A(); a.clone()会报错,因为虽然A继承自Object(Object有clone方法),但是clone本身在Object中是protected,越出包将为private,不能直接调用。因此A类需要重写 Object 内定义的 clone()方法 ,同时需要实现 Cloneable 接口(虽然这个接口内并没有定义 clone() 方法),否则会抛出异常,因为 Cloneable 接口是个合法调用 clone() 的标识。

• 模板中T代表在作用域中T是某个确定的类型,? super A中的 ? 代表只要是A或继承A的类都行。

• 十进制的0.1化为二进制为无限循环小数。区分0与’0’。

• enum Direction {LEFT, RIGHT}编译的时候还是会变成下面的类:                     

class Direction extends Enum {   public static final Direction LEFT, RIGHT;。。。。

• String b = Integer.toString(Integer.valueOf(q,7),8) ;///这样7进制就变成8进制了

• 静态成员,transient成员不能被静态化;子类实现Serializable父类没有,属于父类的属性不能被序列化。

• 正则表达式:.*A 贪婪模式,一直吃 . 类型(. 就是任何字符都行)的字符,不能吃了倒着吐,每次都检测是否整体匹配(这里看是否有字符能匹配 A),匹配就退出。  .*?A懒惰模式,吃一个.类型的字符就检测一次是否匹配。

• Long sum = 0L; for(i=0;..) sum += i; 每一次for循环都会创建Long对象,应优先用long

• Equal(Object o)方法的重写:Object不能改(否则无法重写);先使用 == 检查地址是否一样(地址一样一定相等),优化速度;再使用instanceof检查类型是否相等(或是否实现同一接口);检查关键域是否相等(把最有可能不等的放前面,因为一般是多个域有一个不等就整个对象不等,每个域值i*31 = (i一个好的hash函数尽量为不相等的对象产生不相等的Hash值),Hash类型的数据结构必须重写两者。写完之后检查是否满足对称性,传递性,多次调用一致性。

• 如果你使用有关排序的东西,你必须实现Comparable接口,也必须重写compareTo方法。建议(x.compareTo(y)==0)==(x.equals(y)),BigDecimal(“1.0”)和(“1.00”)的两者不同,导致HashSet和TreeSet也不一样。其实也可以 Arrays.sort(arrs,new Comparator(){ Public int compare(String s1,String s2){..} });的 匿名内部类实现,如果是重复执行很多次,可以将其储存到一个私有的静态final域中。

• 主类中的accept方法传入vistor的实例,调用其visit方法并传入this,实现访问。

• 构造器绝对不能调用能够被覆盖掉的方法(因为父类构造器先执行此时子类未初始化,构造器调用的被重写的方法中可能使用子类未初始化的变量);让abstract类型的骨架类implements接口更好,骨架类中有部分方法的实现。接口中最好不要有常量,因为常量纯粹是内部类的实现细节。

• List与List没有任何的关系。为什么不能有泛型数组,因为数组是具化的,填入的必须是有完整信息的数据,而不是被擦除的。

反射

• 首先搞明白一点为什么要要复制内核区数据到用户区,是因为内核区的数据是在缓冲区里,他负责读取硬件数据到FIFO队列,然后当用户代码read完时要把FIFO中队头已经read的数据清除,在一个为了安全,在用户层与硬件层加一层操作系统内核区,对不安全的操作进行监测和禁止。

 Java 字符char是两个字节,能储存Unicode的一个汉字。而UTF-8,GBK编码都是Unicode的具体实现方式。编码就是对二进制数据进行 分组打包然后映射显示 的方式。最底层一定都是二进制数据无疑。

只有Stream的是字节流(一次读一个字节,本身可以通过read(byte[] b)读一个数组的量提高速度,在File读取中经常用到),只有Writer的是字符流(一次读一个字符,需编码格式才能确定一个字符的二进制长度);Buffered有缓冲效率高(Buffered类初始化时会创建一个较大的byte数组,一次性从底层输入流中读取多个字节来填充byte数组,当程序读取一个或多个字节时,可直接从byte数组中获取---> 更像Cache。写操作时如果此时没有close或flush不会立即写到磁盘中,更适合网络等不稳定的流)。 程序内存write入out对象(建立输出到硬盘file的通道),主语是程序内存。一定别忘关闭流。

Socket中的流:br = new BufferedReader(new InputStreamReader(socket.getInputStream()));

pw = new PrintWriter(socket.getOutputStream(),true);

Throwable:

Error:严重错误,不可恢复,属于逻辑错误,如OutOfMemoryError

Exception分为:

1.Runtime Exception不强制捕获(unchecked)。如果不捕获让jvm自己处理时,会同时结束程序。如NullPointer,ClassCast,IndexOutBound,Arithmetic

2.Other Exception编译器会让你必须人工try ...catch...捕获(checked)。如IO异常,SQL异常

多个catch是if..else if..else..的模式,捕获了一个就不往下捕获了。即使try中有return,会把值保存起来,执行完finally后再返回。(如果finally中也有return会被更新)

• 原生JDBC使用的设计模式

• 当笛卡尔积{奶油,奶茶..}*{加盐,加糖..}都存在时(继承将导致类太多),用装饰者。

•上面的是Visitor模式将经常变的方法、不同类经常用的方法集中到Visitor中管理,哪个类要用就接受visitor到访,让visitor执行带来的visit方法。反射能够使其更灵活。

• 被观察的类持有 观察者们(List) 的实例,当其变动时调用观察者们的方法(notify)。 

• 静态工厂方法 (简单工厂)代替构造器的好处: BigInteger.probablePrime()名字更精确;Boolean.valueOf()相比于Boolean(“true”)不必每次调用都产生新对象;EnumSet可以返回不同的子对象;简化多参数的构造器;最大的缺点是:不含public或protected的构造器,不能子类化。(工厂模式是:创建产品的工厂接口,让子类决定实例化哪个类,增强扩展功能)

Builder(作为内部类) 模式:NutritionFacts cocaCola = new NutritionFacts.Builder().calories(100).fat(10).sodium(23).build();参数有了明确的名字,好写

•  私有构造器强化Singleton属性:class Elvis { public static final Elvis INSTANCE = new Elvis();private Elvis(){..} ...  只有自己初始化时这一次开实例的机会(以后无),枚举更简洁                         

• 责任链模式: 很多的对象由每一个对象对其下家的引用而联接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。

• 模板模式:父类建一个骨架,子类去重写骨架里的各个步骤

桥接是先有桥才有两端的东西,适配是先有两边的东西才有适配器。

UML设计图

• 代码重构的方法:消除重复代码。不要把许多意图放到同一个大类或大方法中。成群的数据抽象成独立的对象。多个if-else合成一个方法 用符号常量替换魔法数字 。两个类之间彼此过多get set访问private内容,合成一个新类吧。

OOP原则:单一职责(一个元素做一件事);开闭(不修改原有模块基础上进行扩展);里氏替换(任何基类可以出现的地方,子类一定可以出现);依赖倒置(依赖抽象,小明可以看书,而不是只可以看童话书);接口隔离(使用多个专门的接口比使用单一的总接口要好);迪米特(对于被依赖的类,向外公开的方法应该尽可能的少)。

GO

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值