JAVA和JVM

1. JVM结构

在程序执行期间,JVM维护了若干运行时数据区(run-time data area),有些数据区是JVM启动时创建的,当JVM退出时销毁,有些数据区是分配给线程的,随着线程创建/销毁而创建/销毁。JDK8去掉了PermGen,取而代之的是在本地内存中的MetaSpace。

JVM运行时结构:https://www.cnblogs.com/jhxxb/p/10896386.html

在这里插入图片描述

2. 加载,链接,初始化

2.1. 加载

上图描述了JVM ClassLoader的工作方式:将.class文件中的字节码加载到代码区,然后在Method Area中创建Class对象,用于创建对象。ClassLoader一共分为4种:

  • BootStrap ClassLoader:C++实现,负责加载jdk/jre/lib目录,或-Xbootclasspath指定的目录。
  • Extension ClassLoader:实现在sun.misc.Launcher$ExtClassLoader,负责加载jdk/jre/lib/ext目录。
  • Application ClassLoader:实现在sun.misc.Launcher$AppClassLoader,负责加载应用程序。
  • User ClassLoader:开发人员实现,继承ClassLoader,加载通过IO等方式获取的代码。

2.1.1. 双亲委派模型

JVM通过双亲委派模型加载类,工作方式如下:

Class loadClass(String name) {
    // 检查是否已经被加载
    Class c = findLoadedClass(name);
    if (c == null) {
        if (parent != null) {
            // 交给父类加载,父类调用loadClass加载
            c = parent.loadClass(name, false);
        } else {
            // parent是null,则为BootStrap ClassLoader,调用findBootstrapClassOrNull加载
            c = findBootstrapClassOrNull(name);
        }
        // 父类找不到该类,则自己加载
        if (c == null) {
            // 加载类
            c = findClass(name);
        }
    }
    return c;
}

加载类的分两种:

  • ClassLoader.loadClass:加载类,初始化类(初始化static变量,运行static代码块)。
  • Class.forName:加载类,通过参数控制是否初始化类。

2.1.2. 自定义ClassLoader

自定义ClassLoader的需要继承ClassLoader,重载findClass方法。findClass读取.class文件,结合defineClass方法返回Class对象。系统内置的三种类加载器均不能加载同名的类,如果想加载同名的类,可以自己实现ClassLoader,一个ClassLoader负责加载一个同名类,相同的自定义ClassLoader不能加载同名的类,会报attempted duplicate class definition for name异常。如下所示的代码MyClassLoader可以多次创建,每个都可以加载multiLoad类。

class MyClassLoader extends URLClassLoader {
    public MyClassLoader(String jarPath) {
        super(new URL[]{}, null); // 设置parent为null
        loadJar(jarPath);
    }
    public void loadJar(String jarPath) {
        File jarFile = new File(jarPath);
        Method method = null;
        try {
            method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
        } catch (NoSuchMethodException | SecurityException e1) {
            e1.printStackTrace();
        }
        boolean accessible = method.isAccessible();
        try {
            method.setAccessible(true);
            URL url = jarFile.toURI().toURL();
            method.invoke(this, url);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            method.setAccessible(accessible);
        }
    }

    public Class<?> loadClass(String name) throws ClassNotFoundException {
        if (name.equals("multiLoad")) {
            return loadClass(name, false); // parent为null,自己加载multiLoad class
        } else {
            return Thread.currentThread().getContextClassLoader().loadClass(name);
        }
    }
}

2.1.3. 类加载应用

用类加载机制实现单例模式(initialization-on-demand holder),JVM启动时并不加载静态内部类Holder,而是JVM发现Holder被引用到了才会去加载。加载类的过程是线程安全的

public class Singleton {
    private static class Holder {
        public static final Singleton instance = new Singleton(); // 类初始化
    }
	
    public static Singleton getInstance( ) {
        return Holder.instance; // 类加载是线程安全的
    }
}

2.2. 链接

链接分为验证、准备、解析

2.2.1. 验证

验证保证类或者接口的二进制表示结构上正确。

2.2.2. 准备

准备是为类静态变量分配内存并初始化这些变量为默认值(并非用户定义的值)。如果类静态变量被final修饰了,则在准备阶段就给这个变量赋值为用户定义的值。

2.2.3. 解析

解析是将常量池里的符号引用替换为直接引用的过程。

2.3. 初始化

给类变量赋值,执行static方法块。

3. 垃圾回收

GC广泛应用于虚拟机类型语言,发生在heap,GC会暂停应用线程(Stop-The-World, STW),步骤如下:

  • 找到符合回收条件的对象(垃圾)。
  • 释放垃圾占用的内存。
  • 整理heap,使其获得更多连续的内存。

3.1. 如何确定对象为垃圾

主要分两种办法:引用计数和GC Roots Tracing。

3.1.1. 引用计数

记录对象的引用次数:新增引用加1,引用失效减1,当引用归零时,可回收。举例如下:

// 堆中存在对象A和B,B被b和a.refB引用(计数为2),A被a引用(计数为1)。
// 当goodRefCount执行完毕,a失效,A可回收(计数为0),a.refB失效,b也失效,于是B可回收(计数为0)。
void goodRefCount(){
    A a = new A();
    B b = new B();
    a.refB = b;
}
// 引用计数失效:循环引用。
// badRefCount执行完,A对象和B对象引用计数为1,无法回收,内存泄漏。
void badRefCount() {
    A a = new A();
    B b = new B();
    a.refB = b;
    b.refA = a;
}

3.1.2. GC Roots Tracing

从GC roots出发,不可达的对象,则可回收。常见的GC roots包括:

  • Thread stack(包括JVM stack和native method stack)引用的对象。
  • 类静态变量。
static S s = new S();
B newSomeObjs() {
    A a = new A();
    B b = new B();
    C c = new C();
    s.refA = a;
    return b;
}
// 调用GC时,GC roots包括对象静态对象s(s->a),栈变量b1(b1->b)。
// c会被回收,a和b不被回收。
void GCRootTracing() {
    B b1 = newSomeObjs();
    System.gc();
}

3.1.3. 弱引用

一个对象上只有弱引用,下次GC时会被回收。

HashMap<String, WeakReference<User>> userMap = new HashMap<String, WeakReference<User>>();
User user = new User();
userMap.put("user1", new WeakReference<User>(user));
user = null;
System.gc();
System.out.println(userMap.get("user1").get());

3.1.4. 软引用

一个对象上只有软引用,下次GC时,如果内存充足,则不会被回收,否则被回收。

HashMap<String, SoftReference<WebPage>> pageCache = new HashMap<String, SoftReference<WebPage>>();
pageCache.put("page1", new SoftReference<WebPage>(new WebPage())); // Load from DB
System.gc();
System.out.println(pageCache.get("page1").get());

3.1.5. finalize

GC时,如果对象不可达,没有覆盖finalize方法,且没有执行过finalize方法,则被回收。否则放入finalizer queue,由低优先级线程执行finalize方法。可见,覆盖finalize方法的对象至少需要两轮GC才能回收。

3.2. 内存回收算法

  • 标记-清除(Sweep):先标记出垃圾,然后将垃圾内存释放掉,会产生内存碎片。Concurrent。(适用于老年代,CMS并发清理)
  • 复制算法:将内存分成2块,GC时把存活的对象复制到另一块内存,当前回收的内存全部回收。STW,复制会移动对象,改变对象的地址,如果和应用程序并发执行,会出现错误的引用。(适用于年轻代,存活的对象比较少)
  • 标记-整理:GC时将存活的对象移动到当前内存的一端,然后回收边界外的内存。适用于老年代的回收(G1 Mixed GC)
  • 分代收集算法:JVM将heap分为YoungGen和OldGen。分代设计利用了大部分对象存活时间短的特点,降低垃圾回收给应用程序带来的影响。

3.3. 分代GC

3.3.1. YOUNG GC

在这里插入图片描述

为了避免线程争用内存,每个线程在YoungGen都有独立的TLAB(Thread Local Allocation Buffer)。对象先在尝试在TLAB分配,如果TLAB分配失败,则会尝试分配到到YoungGen。
YoungGen有3个区域,Eden和survivor from, survivor to。
最开始是在Eden区分配的,如果其满了,则进行YoungGC,清理Eden,将存活的对象以及from存活的对象移动到to区,from会被清空。如果对象在survivor区存活了大于TenuringThreshold,会被放到OldGen。整个过程中,如果对象在YoungGen放不下了,则会被放到OldGen。结束后,from和to的角色互换。

3.3.2. Full GC

young GC后,OldGen收到分配对象的请求,如果OldGen内存满了,STW,整理OldGen,这个过程为full GC。可见full GC会整理整个heap。

3.4. JVM收集器

收集器组合使用:https://www.cnblogs.com/grey-wolf/p/10222758.html
在这里插入图片描述

3.4.1. Serial收集器

单线程执行YOUNG GC,STW。

3.4.2. ParNew收集器

多线程执行YOUNG GC,STW。

3.4.3. CMS(Concurrent Mark-Sweep)收集器

CMS收集器工作流程如下:

  • YoungGen满了则STW,执行young GC。
  • OldGen使用率超过一定阈值(CMSInitiatingOccupancyFraction),在OldGen执行一次并发收集
  • 可能会执行一次full GC。

并发收集步骤如下:

  • 初始标记(Initial Mark):STW。寻找OldGen中的GC roots直接引用的对象,以及被YoungGen直接引用的对象。
  • 并发标记(Concurrent Mark):根据初始标记的结果,在OldGen中寻找GC roots可达的对象X。
  • 并发Preclean(Concurrent Preclean):并发标记过程中,应用程序可能会修改引用,或对象从YoungGen移至OldGen,导致OldGen中某可达对象不在X中。Preclean会找到OldGen中新增的可达对象,加入到X中。Preclean是提前做了重新标记的工作,降低重新标记STW的时间。
  • 重新标记(Remark):STW。在并发标记过程中,对象的引用关系发生了变化,可能会造成存活的对象被标记为垃圾(https://www.zhihu.com/question/37028283),需要remark修正并发标记的结果。此过程可能会触发耗时较长的类卸载,可以通过设置-XX:+UseLargePagesInMetaspace解决,或者换成G1收集器。参考文章:https://blogs.oracle.com/poonam/long-class-unloading-pauses-with-jdk8
  • 并发清理(Concurrent Sweep):清除OldGen中无用的对象。

CMS清理OldGen时,应用线程可能触发young GC,会有对象从YoungGen移至OldGen,如果CMS不能及时清理掉OldGen,OldGen空间可能不足,这时就会触发concurrent mode fail

当发生concurrent mode fail时,会触发full GC,CMS用一个线程整理OldGen。可以通过如下办法避免concurrent mode fail

  • 提高堆的大小。
  • 尽早的触发并发收集。

3.4.4. (Garbage First)收集器

G1的目的是解决CMS内存碎片问题,降低concurrent mode fail的频率。G1将整个heap分为若干(约2000)固定大小的区域,区域状态如下:

  • E(den):被Eden占用。
  • S(urvivor):被Survivor占用。
  • O(ld):被OldGen占用。
  • H(umongous):存放大对象。
  • U(nused):未被占用。

步骤如下:

  • YoungGen满了则STW,执行young GC。
  • OldGen使用率超过一定阈值(InitiatingHeapOccupancyPercent),执行一次并发标记
  • Mixed GC。
  • 可能会执行一次full GC。

并发标记步骤如下:

  • 初始标记(Initial Mark):STW,寻找GC roots直接引用的对象。
  • 扫描根区域(Root Region Scan):并发执行,标记根区域(Survivor区)对象,被根区域引用的对象不会被收集。在扫描根区域完毕前,禁止执行young GC。
  • 并发标记(Concurrent Mark):根据前两步的结果,得到可达对象X。
  • 重新标记(Remark):修正并发标记的结果。
  • Cleanup:为下阶段的整理准备空间,回收空闲空间等。

并发标记结束后,并不会立刻执行mixed GC,而是通过策略决定。Mixed GC会同时回收YoungGen和OldGen。回收哪些Old区域(Collection Set,通常是垃圾比例高的区域)也是通过策略决定的,存活的对象会复制到空闲区域。Mixed GC会STW。

G1通过Remembered Sets维护区域之间的引用。每个区域都有一个Remembered Set(与card table类似),记录了其它区域到它的引用,方便快速分析可达性。整理时需要改变Remembered Set,这也是整理的主要开销。

与CMS类似,G1同样会触发concurrent mode fail。如果整理阶段没有空闲的区域,还会触发to-space overflow

G1适合4G以上的内存,目标是暂停MaxGCPauseMillis(200ms)时间。

4. 多线程和锁

实现多线程的两种方式:

// 1.实现Runnable接口
new Thread(new Runnable() { // Runnable对象可以在线程之间共享
    @Override
    public void run() {
        System.out.println("my runnable");
    }
}).start();

// 2.继承Thread类
new Thread() {
     @Override
     public void run() {
         System.out.println("my thread");
     }
}.start();

4.1. 线程池技术

4.1.1. ThreadPoolExecutor

ThreadPoolExecutor有如下参数:

  • corePoolSize:线程池中运行的核心线程数,即使其无任务可以运行,也不关闭这些线程(除非设置了allowCoreThreadTimeOut)。

  • maximumPoolSize:线程池中允许运行最大的线程数量。

  • keepAliveTime:如果运行的非核心线程超过keepAliveTime时间没有被分配任务,则关闭这些线程。

  • workQueue:存放任务的队列。

  • threadFactory:创建线程的工厂对象,一般为namedThreadFactory,如下:

new ThreadFactoryBuilder().setNameFormat("xxx-%d").build();
  • handler:线程池无法分配任务时调用handler。

execute方法:

public void execute(Runnable command) { 
   int c = ctl.get();    
   // 如果当前运行的线程数小于corePoolSize    
   if (workerCountOf(c) < corePoolSize) {        
   // 创建一个新的线程,command作为其第一个运行的任务        
       if (addWorker(command, true))            
          return;        
       c = ctl.get();    
    }
    // 如果当前运行线程数大于等于corePoolSize,先尝试将其放于队列
    if (isRunning(c) && workQueue.offer(command)) {        
        // 成功放于队列,double check        
        int recheck = ctl.get();        
        // 当前线程池关闭,从队列删除,拒绝该任务        
        if (! isRunning(recheck) && remove(command))            
            reject(command);        
        // 当前运行线程数为0,则新建一个线程        
        else if (workerCountOf(recheck) == 0)            
           addWorker(null, false);    
   }
   // 如果放于队列失败,且运行线程数少于maximumPoolSize,则新建线程
   // 否则reject    
   else if (!addWorker(command, false))        
       reject(command);
   	}

4.1.2. callable和Future

如果不关心线程的执行结果,可以使用execute,否则使用submit提交Callable对象,submit返回Future对象,调用Future的get方法可以阻塞当前线程,直到线程返回结果。

Future<String> f = executorService.submit(new Callable<String>() {
    @Override    
    public String call() throws Exception {
            return "Hello world";    
    }});
    System.out.println(f.get());

4.1.3. CompletableFuture

Java8扩展了Future为CompletableFuture,CompletableFuture提供一种非阻塞获取Future执行结果的方法,使用两种方式创建CompletableFuture对象:

  • 在外部创建CompletableFuture对象:调用CompletableFuture的complete等方法设置结果。
  • 通过runAsync或supplyAsync等静态方法创建(可以指定executor)。

通过调用CompletableFuture对象如下方法,可以将CompletableFuture的结果传入lambda表达式,返回同一个CompletableFuture对象:

  • thenApply:Function
  • thenAccept:Consumer
  • thenRun:Runnable
CompletableFuture<String> helloFuture = CompletableFuture.supplyAsync(() -> "hello");
CompletableFuture<String> helloworldFuture = helloFuture.thenApply(s -> s+ " world");

可以调用CompletableFuture的thenCompose方法,可以将CompletableFuture的结果传入另一个CompletableFuture对象的计算流程,即合并两个CompletableFuture,返回一个新的CompletableFuture。

CompletableFuture<String> helloFuture = CompletableFuture.supplyAsync(() -> "hello");
CompletableFuture<String> helloworldFuture = helloFuture.thenCompose(s -> CompletableFuture.supplyAsync(() -> s + " world"));

this.applyToEither(other, fn)将this和other优先执行完的那个作为参数传入到fn,举例如下:

CompletableFuture<String> helloFuture = CompletableFuture.supplyAsync(() -> {   
          Thread.sleep(1200);   
           return "hello";});
CompletableFuture timeoutFuture = new CompletableFuture();

Executors.newSingleThreadScheduledExecutor().schedule(() -> 
  timeoutFuture.completeExceptionally(new TimeoutException("timeout")),
               1000, TimeUnit.MILLISECONDS);
               
  CompletableFuture<String> eitherFuture = 
    helloFuture.applyToEither(timeoutFuture, Function.identity());

    eitherFuture.exceptionally((e) -> { 
    if (e.getCause() instanceof TimeoutException) {   
        System.out.println("timeout exception");  
     }
     return null;
 });

/*输出:timeout exception*/

4.1.4. 常用的线程池

4.1.4.1. FixedThreadPool

常用于服务端

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>());
}
4.1.4.2. CachedThreadPool

常用于客户端

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                60L, TimeUnit.SECONDS,
                new SynchronousQueue<Runnable>());
}
4.1.4.3. ScheduledExecutorService

ScheduledExecutorService适合运行定时任务,如果抛出异常,将会影响后续任务执行,如下:

final int[] counter = {0};
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
    @Override    
    public void run() {
            counter[0]++;
            System.out.println("run runnable" + counter[0]);        
            if (counter[0] == 5) {            
                 throw new RuntimeException("exception");        
            }    
 }}, 0, 1000, TimeUnit.MILLISECONDS);
 /*输出:run runnable1
 run runnable2
 run runnable3
 run runnable4
 run runnable5*/

4.1.5. ForkJoinPool

使用示例

// 100为线程池大小
ForkJoinPool forkJoinPool = new ForkJoinPool(100);
forkJoinPool.submit(...)

4.1.6 线程池处理分治任务—如何避免死锁

看一个错误的例子,在线程池中使用Arrays.parallelSort

public class Deadlock {
    ForkJoinPool forkJoinPool = new ForkJoinPool(100);
    public void doDeadlock() {
        System.out.println(forkJoinPool.getParallelism());
        for (int i = 0; i < 100; i++) {
            int finalI = i;
            forkJoinPool.submit(new ForkJoinTask<Object>() {
                @Override
                protected boolean exec() {
                    if (finalI <99) {
                        Thread.sleep(1000000);
                    }
                    System.out.println("第"+finalI+"TASK在执行");
                    List<Integer> arr = new ArrayList<>();
                    for (int k=0;k<5000000;k++) {
                        arr.add(k);
                    }
                    Arrays.parallelSort(arr.toArray(), (o1, o2) -> 0);
                    System.out.println("第"+finalI+"TASK执行完毕");
                    return false;
                }
            });
        }
}

// 子任务放到当前线程的线程池里执行
 ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ?
            (wt = (ForkJoinWorkerThread)t).pool.
            awaitJoin(wt.workQueue, this, 0L) :

分析:当执行到第100个任务时,前99个任务在线程池里sleep,第100个任务开始执行,Arrays.parallelSort会根据数组长度进行拆分,拆分成子任务进行归并排序,子任务是放到当前线程所在的线程池里执行,而此时线程池已满,第100个任务出不来(要等子任务做完),子任务进不去队列,发生了死锁。发生这种情况,只能等前99个任务执行结束,如果前99个任务又要依赖第100个任务,则会无限期死锁。
如何避免

  • 使用2个线程池,不要在同一个线程池里产生依赖
  • 配置minimumRunnable参数(jdk11以上的版本)
    官方对minimumRunnable的注释:
    the minimum allowed number of core threads not blocked by a join. To ensure progress, when too few unblocked threads exist and unexecuted tasks may exist, new threads are constructed, up to the given maximumPoolSize.
    说明,ForkJoinPool里的任务是可能被阻塞的,至于为什么被阻塞,上面死锁例子可能是一个原因,当这些任务被阻塞,就会创建至多maximumPoolSize个线程执行这些阻塞的任务。

更新
上面第二种方法也无法完全避免死锁,还是有概率会死锁。

4.2. 线程状态

Java线程状态如下图所示。Java线程在Linux中对应pthread(高版本Linux为内核线程),Java为抢占式调度,线程调度依赖于系统,基于优先级的时间片轮转策略,优先级越高的线程在ready状态下越容易被选中执行,可以手动设置优先级,系统可以给优先级低的线程更高的优先级来避免线程饿死。

参考:https://zhuanlan.zhihu.com/p/61892830

在这里插入图片描述

4.3. 线程同步

4.3.1. synchronized

synchronized关键字做的优化:

偏向锁,轻量级锁,重量级锁,自旋锁。https://tech.meituan.com/2018/11/15/java-lock.html

Java对象头里的Mark Word(32Bit/64Bit)默认存储了对象的hashCode,分代年龄和锁标志。JDK1.6为了降低获取和释放锁的开销引入了偏向锁和轻量级锁。

  • 偏向锁:Mark Word存储了拥有该偏向锁的线程ID,线程进入临界区时只需要探测是否存储的是指向该线程的偏向锁,如果是,直接进入临界区。否则看当前的锁标志是否为1(即偏向锁),如果是,则CAS将偏向锁指向该线程,如果是无锁状态,则CAS竞争锁。当发生竞争时,偏向锁尝试进行撤销,如果占用偏向锁的线程还活着,锁会升级,否则将Mark Word里的锁状态清除。
  • 轻量级锁:当前线程的stack会创建一块lock record的空间,将对象的Mark Word复制到lock record,然后CAS替换Mark word的内容为指向lock record的指针,并将lock record指向Mark word,如果CAS成功则获取到轻量级锁,否则自旋等待一定次数,超过一定条件后,转为重量级锁。

4.3.2. wait集合和通知机制

使用synchronized,wait,notify,notifyAll实现线程间同步,需要注意的是wait会释放锁。

4.3.3. 同步模版

// A和B在变量X上同步
public void AThread() {
    synchronized (X) {
      while (!A的执行条件) {
        // release lock
        X.wait(); 
        // regain lock 
      }     
     // 执行A线程的逻辑   
    // 改变条件     
    // 唤醒B
     X.nofityAll();  
  }
}

public void BThread() {
    synchronized (X) {
       while (!B的执行条件) {
         X.wait(); 
       }
       // 执行B线程的逻辑   
      // 改变条件
      // 唤醒A 
      X.nofityAll();
  }
}

4.4. 内存模型

4.4.1. 多核共享内存

多核CPU中,每个核都有自己的执行单元,寄存器等结构,它们共享内存。在不影响运算结果的前提下,CPU会重新排列指令,提高缓存命中,加速程序执行。

指令重排带来的问题是:每个核的内存状态对其它核是不可见的,需要通过内存屏障等方法使其可见。举例如下:

//初始化
int a = 0;
int b = 0;

//线程1
a = 100;
b = 1;

//线程2
while (b == 0);
System.out.println(a);

正常的理解是:线程1中,a=100发生在b=1之前,那么线程2只能输出a=100。但是指令重排会让a=100发生在b=1之后,于是线程2可能输出a=0。

4.4.2. JVM内存模型规范

java通过volatile和final关键字来保证内存可见性,下面是JVM规范对volatile关键字的约定:

  • volatile写之前的操作不能重排到volatile写之后;
  • volatile读之后的操作不能重排到volatile读之前;
  • volatile读不能重排到volatile写之前。

常见例子:

// 初始化
volatile boolean flag = false;
int x = 0;

// 写
void write() {
    x=1;
    flag=true;
}

// 读
void read() {
    if (flag) {        
        System.out.println(x);
    }
}
// 结果是 x如果输出,则一定为1

用volatile实现Java单例模式(double-checked locking),这里需要注意:

  • 如果不声明instance为volatile,当执行instance=new Singleton()时可能发生:构造函数被inline,对象内存分配完毕,instance被赋值,构造函数未执行完。另外一个线程访问了构造一半的对象。
public class Singleton {
    private static volatile Singleton instance = null;    
    public static Singleton getInstance() {        
        if (instance == null) {            
             synchronized(Singleton.class) {  
                 if (instance == null) {    
                    instance = new Singleton();  
                 }    
             }  
        } 
        return instance;
   }
}

4.4.3. final

JVM会在类中的final字段可以保证获取对象引用时,final字段初始化完毕。利用这个特点实现另一版本的double-checked locking:

class FinalWrapper<T> { 
   final T value;
   public FinalWrapper(T t) {        
      value = t;    
   }}
   public class Singleton { 
      private static FinalWrapper<Singleton> finalWrapper = null;  
      public static Singleton getInstance() {     
         if (finalWrapper == null) {  
             synchronized(Singleton.class) { 
                 if (finalWrapper == null) {
                    finalWrapper = new FinalWrapper<Singleton>(new Singleton());                
                    }
                 }
            }
           return finalWrapper.value;
    }
}

4.5. 悲观锁和CAS

4.5.1. AQS

AQS是一种构建并发工具(包括锁等同步器)的框架,由一个volatile int的state和队列构成,分为独占(锁)和共享(信号量等)两种类型。

  • 公平锁:如果state=0且队列为空,执行CAS(state, 0, 1),成功则获取锁,否则入队

  • 非公平锁:直接CAS(state, 0, 1),成功则获取锁,如果失败,判断state是否为0,如果是,再执行一次CAS(state, 0, 1),成功则获取锁,否则入队。

队列中的每个节点都会自旋判断当前节点是否为头部节点且CAS获取同步状态成功,如果没有达到条件,则阻塞等待被唤醒;如果达到条件,则从队列移除,获取到同步状态。

4.5.2. CountdownLatch

CountdownLatch有类似计数器功能,如下初始化一个计数器(大小为5):

CountDownLatch counter = new CountDownLatch(5)

CountDownLatch主要有两个方法:countDown和await,后者会阻塞当前线程,直到其他线程调用countDown(减1)将计数器减为0。await超时不会抛出异常

4.6. 线程安全

4.6.1 线程安全集合

List是非线程安全的,Vector是线程安全的。

在使用迭代器对一般的集合进行遍历时,是不允许修改集合的。CopyOnWriteArrayList允许一边修改List,一边对List进行迭代。它的原理是写时(on write),拷贝(copy)一份原有List,在此拷贝的List上做修改,修改完毕后将新List地址覆盖到老的List地址,保证遍历不会出错。

HashMap为什么线程不安全:

Java7链表采用的是头插法,在resize时会出现环链表,或者数据丢失的情况。Java8采用的是尾插法,再resize时还是会出现数据丢失的情况(当前数组没有节点,并发插入)。

Java7的ConcurrentHash采用的是分段加锁,粒度较粗,Java8采用的是CAS+syncronized,扩大粒度(粒度为数组的每个Node),减小争用:

  • 当前节点为空,则CAS插入数据,不成功则自旋直至成功。
  • 当前节点hashcode==MOVED,则需要扩容
  • 否则synchronized写入数据
  • 如果节点数超过一定数量,则需要转化为红黑树。

HashMap key和value都可以为null,ConcurrentHashMap key和value都不能为null。

HashMap数组为2的次幂的原因:

  • 分布均匀,计算index的方式为val&(length-1)
  • 扩容时,计算index更快,只需要判断val在新增的那个位(x)是否为1,如果为1,直接将2^x加上之前的index。否则不需要变。

参考:

https://www.cnblogs.com/developer_chan/p/10450908.html

https://crossoverjie.top/2018/07/23/java-senior/ConcurrentHashMap/

https://www.cnblogs.com/yangfeiORfeiyang/p/9694383.html

4.6.2. ThreadLocal

为避免多线程并发操作同一个非线程安全的对象,可以用ThreadLocal赋予每个线程独立的对象。这个独立的对象T可以用ThreadLocal.withInitial初始化。
每个Thread对象持有一个threadLocals的引用,指向了ThreadLocalMap对象。

  • ThreadLocalMap的key是ThreadLocal对象的弱引用 // threadLocal对象,线程公用
  • value是T类型对象的强引用。 // 每个线程都有自己的T对象
    在这里插入图片描述

ThreadLocal的结构参考上图,当脱离ThreadLocal的作用域后,ThreadLocal的引用为0,虚线为key指向ThreadLocal的弱引用,ThreadLocal将会被回收,存在这样一条强引用:
Thread->ThreadLocalMap->Entry->value
除非线程被关闭,否则当key指向的ThreadLocal被回收时,value将无法被回收,即发生潜在的内存泄露。
一种方法是ThreadLocal定义为static,如果类不被卸载,一直存在ThreadLocal的强引用,不会发生泄露。如果类被卸载了,static的ThreadLocal也会被回收,这时仍然可能存在内存泄露。
另一种方法是下一次调用ThreadLocalMap的set(),get(),remove()方法,会清理掉table中为null的key的value,即将这类value置为null。

弱引用的好处是当ThreadLocal对象引用为0时,可以被回收,此时key为null,value仍没有被回收,当调用set/get/remove方法可以清理ThreadLocalMap中为null的key,进而回收value。

private static final ThreadLocal<Animal> animalLocal
            = ThreadLocal.withInitial(() -> {
        return new Animal();
    });

animalLocal.get();

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

5. 基础

5.1. 函数式编程

lambda表达式

接口入参出参
Function<T, R>TR
ConsumerT
PredicateTBoolean
SupplierT

5.2. 类型

BigDecimal的roundingMode:https://blog.csdn.net/Crystalqy/article/details/98080236

Json库jackson自动将Json字符串转化成java类型:

Map<String, Object> map = JsonUtils.fromJson("{\"k\": 1}",
                new TypeReference<Map<String, Object>>() {
                });
System.out.println("type=" + map.get("k").getClass());

map = JsonUtils.fromJson("{\"k\": 11111111111}",
                new TypeReference<Map<String, Object>>() {
                });
System.out.println("type=" + map.get("k").getClass());

map = JsonUtils.fromJson("{\"k\": 111111111111111111111}",
                new TypeReference<Map<String, Object>>() {
                });
System.out.println("type=" + map.get("k").getClass());

map = JsonUtils.fromJson("{\"k\": 1.22}",
                new TypeReference<Map<String, Object>>() {
                });
System.out.println("type=" + map.get("k").getClass());

map = JsonUtils.fromJson("{\"k\": false}",
                new TypeReference<Map<String, Object>>() {
                });
System.out.println("type=" + map.get("k").getClass());

//输出结果为
type=class java.lang.Integer
type=class java.lang.Long
type=class java.math.BigInteger
type=class java.lang.Double
type=class java.lang.Boolean

下面代码输出为false:

System.out.println(new Long(1).equals(new Integer(1)));

POJO类成员全部为包装类型,如果为Boolean,则不要加is前缀:

Boolean success

如果不为包装类型,则会填入默认值,无法区分是否输入默认值还是因为缺失字段填入了默认值。如果为isSuccess,则对应的get方法为isSuccess,某些框架会获取不到success属性导致错误。

System.out.println(new Long(36000000*1000)); // 整数X整数 溢出 Bad
System.out.println(new Long(new Long(36000000)*1000)); // Good

对于过长的long,js会截取一部分,注意给前端的long类型丢失精度问题。

 List<Long>  ll = new ArrayList<>();
        ll.add(1634083200000L);
        ll.add(1633996800000L);
        ll.add(1636761600000L);
        ll.add(1636675200000L);
        ll.add(1636588800000L);

        ll.add(1636502400000L);
        ll.add(1636416000000L);
        ll.add(1636329600000L);
        ll.add(1636243200000L);
        ll.add(1636156800000L);

        ll.add(1636070400000L);
        ll.add(1635984000000L);
        ll.add(1635897600000L);
        ll.add(1635811200000L);
        ll.add(1635638400000L);


        ll.sort(new Comparator<Long>() {
            @Override
            public int compare(Long o1, Long o2) { // BAD ,溢出风险
                return (int) (o1-o2);
            }
        });
 // GOOD. Long::compartor

        System.out.println((ll));

5.3. 面向对象

接口可以多继承,即一个接口可以多继承多个接口,类不能多继承。

5.4. 泛型

5.4.1. 泛型的作用

在JAVA编码中,不指定类的成员变量或者方法(非静态和静态)的参数/返回值的类型,而是通过一个类型占位符,让某个类或者方法使用于多种类型。

5.4.2. 泛型的种类

<>中可以存放多个类型占位符。

5.4.2.1 类泛型,方法泛型

类泛型<>定义在类的后面
方法泛型<>定义在方法的返回值前面

public class Person<T> {

    private T name;
    
    // 方法泛型可以公用类定义的泛型,也可以定义自己的泛型
    public <K, V> T getName(K arg1, V arg2) {
        return name;
    }

    public void setName(T name) {
        this.name = name;
    }

    // 静态方法泛型,与类上定义的泛型无关
    public static <E> E echo(E arg) {
        return arg;
    }
}
5.4.2.2. 接口泛型

接口实现类泛型的写法有两种

public interface PersonInt<T> {
    T getName();
}

第一种是实现类中依然使用类型占位符:

public class PersonIntImpl<T> implements PersonInt<T> {
    @Override
    public T getName() {
        return null;
    }
}

另外第一种是实现类中指定类型:

public class PersonIntImpl implements PersonInt<String> {
    @Override
    public String getName() {
        return null;
    }
}

5.4.3 泛型擦除

JAVA中泛型只存在与编译阶段,运行期间泛型的类型是会被擦除的,取而代之的是Object类型,在构建泛型对象时,如果不指定类型,有些问题在编译阶段就被忽略掉了,而是在运行时报错,比如类型转换错误等。

5.4.4 通配符,上边界和下边界

通配符
JAVA中的继承关系,在泛型中不做任何声明修饰的情况下是无法识别的,需要引入通配符,使用?表示,表示泛型中所有类的父类。当需要在泛型中表示继承关系的时候可以使用通配符。
下面是一个错误的例子,因为在泛型中,无法识别JAVA中的继承关系,需要使用通配符

public class Person<T> {

    private T age;

    public T getAge() {
        return age;
    }

    public void setAge(T age) {
        this.age = age;
    }

    public void display(Person<T> person) {
        setAge(person.getAge());
    }

    public static void main(String[] args) {
        Person<Number> person = new Person<>();
        Person<Integer> person1 = new Person<>();
        // Error:(22, 24) java: 不兼容的类型: <java.lang.Integer>无法转换为<java.lang.Number>
        person.display(person1);
    }
}

使用通配符能解决这个问题:

public void display(Person<?> person) {
    setAge((T)person.getAge());
}

通配符上界
用extends表示,写法是<? extends T>,代表的是可以传入的类型只能是T或者T的子类的类型。在读取T这类型数据的时候,但不写入,则使用上边界。比如定义list的类型为List<? extends Number>,可以读取这个list,因为list的每个元素都继承了Number,但是无法写入,因为如果这是一个Double类型的list,写入了Integer类型的数据就错了。
上面通配符的例子,可以用通配符上界。

public void display(Person<? extends Number> person) {
    setAge((T)person.getAge());
}

Person<Number> person = new Person<>();
Person<Integer> person1 = new Person<>();
person.display(person1);

通配符下界
用super表示,写法是<? supers T>,代表的是可以传入的类型只能是T或者T的父类的类型。需要写入T这个类型的数据的时候,但不需要读取,则使用下边界。比如定义list的类型为List<? supers Integer>,无法读取这个list,因为list可能指向一个Number类型的list,也可能指向Object类型的list。因为Object和Number是Integer的父类,无论list指向何种list,均可以向list添加Integer对象。

将上面通配符的例子进行修改,此种场景使用通配符下界:

public void display(Person<? super Integer> person) {
    // person的类型不确定,读取person的数据可能存在问题
    // 这里的age为double类型
    System.out.println(person.getAge());
    // 写入则是安全的,可以写入Integer类型
    person.setAge(Integer.MAX_VALUE);
}

public static void main(String[] args) {
    Person<Number> person = new Person<>();
    person.setAge(Double.MAX_VALUE);
    Person<Integer> person1 = new Person<>();
    person1.display(person);
}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值