java入门到精通

0、线程不安全一般是指多线程情况下导致的数据不一致的问题,单线程下一般是安全的
1、CAS

  • Conmpare And Swap比较和交换,主要用于多个线程对共享内存的变量(全局变量)操作时的线程安全问题。它将内存位置的内容与给定值进行比较,只有在相同的情况下,将该内存位置的内容修改为新的给定值。这是作为单个原子操作完成的。
  • 一个 CAS 涉及到以下操作
    假设现在主内存中的数据为V,旧的预期值A(线程之前从主内存中取出的数据),需要修改的新值B。
    比较 A 与 V 是否相等。(比较)
    如果比较相等,将 B 写入 V。(交换)
    当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。可见 CAS其实是一个乐观锁。
    下图中,主存中保存V值,线程中要使用V值要先从主存中读取V值到线程的工作内存A中,然后计算后变成B值,最后再把B值写回到内存V值中。多个线程共用V值都是如此操作。CAS的核心是在将B值写入到V之前要比较A值和V值是否相同,如果不相同证明此时V值已经被其他线程改变,重新将V值赋给A,并重新计算得到B,如果相同,则将B值赋给V。
    在这里插入图片描述
    ABA 问题
    CAS 由三个步骤组成,分别是“读取->比较->写回”。
    考虑这样一种情况,线程1和线程2同时执行 CAS 逻辑,两个线程的执行顺序如下:
    时刻1:线程1执行读取操作,获取原值 A,然后线程被切换走
    时刻2:线程2执行完成 CAS 操作将原值由 A 修改为 B
    时刻3:线程2再次执行 CAS 操作,并将原值由 B 修改为 A
    时刻4:线程1恢复运行,将比较值(compareValue)与原值(oldValue)进行比较,发现两个值相等。
    然后用新值(newValue)写入内存中,完成 CAS 操作
    如上流程,线程1并不知道原值已经被修改过了,在它看来并没什么变化,所以它会继续往下执行流程。
    ABA的影响:比如链表的头在变化了两次后恢复了原值,但是不代表链表就没有变化。
    对于 ABA 问题,通常的处理措施是对每一次 CAS 操作设置版本号
    ABA问题的解决办法
    1.在变量前面追加版本号:每次变量更新就把版本号加1,则A-B-A就变成1A-2B-3A。
    2.使用atomic包下的AtomicStampedReference类,即带版本号的原子引用类:其compareAndSet方法首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用的该标志的值设置为给定的更新值。

CAS除了ABA问题,仍然存在循环时间长开销大和只能保证一个共享变量的原子操作

  • 循环时间长开销大 自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
  • 只能保证一个共享变量的原子操作
    当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁

synchronized属于悲观锁,它有一个明显的缺点,它不管数据存不存在竞争都加锁,随着并发量增加,且如果锁的时间比较长,其性能开销将会变得很大。有没有办法解决这个问题?答案是基于冲突检测的乐观锁。这种模式下,已经没有所谓的锁概念了,每个线程都直接先去执行操作,检测是否与其他线程存在共享数据竞争,如果没有则让此操作成功,如果存在共享数据竞争则不断地重新执行操作,直到成功为止,重新尝试的过程叫自旋。注意,长时间自旋会给CPU带来压力

2、使用volatile关键字修饰某一个变量,表明这个变量是全局共享的一个变量,同时具有了可见性和有序性。但是却没有原子性。比如说一个常见的操作a++。这个操作其实可以细分成三个步骤:
(1)从内存中读取a
(2)对a进行加1操作
(3)将a的值重新写入内存中
在单线程状态下这个操作没有一点问题,但是在多线程中就会出现各种各样的问题了。因为可能一个线程对a进行了加1操作,还没来得及写入内存,其他的线程就读取了旧值。造成了线程的不安全现象。如何去解决这个问题呢?最常见的方式就是使用AtomicInteger来修饰a。而AtomicInteger底层就是由CAS实现的
3、synchronized是悲观锁,这种线程一旦得到锁,其他需要锁的线程就挂起的情况就是悲观锁。CAS操作的就是乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止
4、synchronized与Lock的区别

  • synchronized是java内置关键字,在jvm层面,Lock是个java接口,有多种锁的实现类;
  • synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
  • synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
  • Lock只有代码块锁,而synchronized既有代码块锁,也有方法锁。

5、java.util.concurrent.locks包下常用的类

  • Lock
    Lock接口中的方法:lock()、tryLock()等是用来获取锁的。unLock()方法是用来释放锁的。
    lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。
  • ReentrantLock
    ReentrantLock,意思是“可重入锁”,ReentrantLock是唯一实现了Lock接口的类
  • ReadWriteLock   
    ReadWriteLock也是一个接口,只有两个方法,一个用来获取读锁,一个用来获取写锁
  • ReentrantReadWriteLock
    ReentrantReadWriteLock实现了ReadWriteLock接口

6、锁的相关概念

  • 可重入锁
    如果锁具备可重入性,则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁,可重入性表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2
  • 可中断锁
    在Java中,synchronized就不是可中断锁,而Lock是可中断锁。如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。
    lockInterruptibly()的用法时已经体现了Lock的可中断性。
  • 公平锁
    公平锁即尽量以请求锁的顺序来获取锁。比如有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该锁,这种就是公平锁。
    非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。
    在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。

7、线程池
频繁创建线程和销毁线程需要消耗大量时间资源(其实java中不仅是线程,创建和销毁任何对象都是要消耗不少资源的)。
使用线程池使得线程可以复用:每个任务过来,就去线程池里面拿线程,处理完后,把线程放回线程池,避免频繁创建线程

  • 在ThreadPoolExecutor类中有几个非常重要的方法:
    execute()
    submit() //实际上也是调用execute()方法
    shutdown() //等待所有任务执行完毕再关掉线程池
    shutdownNow() //立即关闭线程池

  • ThreadPoolExecutor构造函数中的重要参数
    public ThreadPoolExecutor(int corePoolSize, intmaximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue workQueue,hreadFactory threadFactory,RejectedExecutionHandler handler)

    1)corePoolSize(核心池大小)和maximumPoolSize
    如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;
    如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务;
    如果当前线程池中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理;
    corePoolSize就是常规线程池大小,maximumPoolSize可以看成是线程池的一种补救措施,即任务量突然过大时的一种补救措施,最大的线程池只能到这里。
    2)workQueue该线程池中的任务队列:维护着等待执行的 Runnable 对象。当所有的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务

  • 我们可以使用Executors类中提供的几个静态方法来创建线程池,它们实际上也是调用了ThreadPoolExecutor,只不过参数都已配置好了:
    Executors.newCachedThreadPool(); //创建一个缓冲线程池,缓冲池容量大小为Integer.MAX_VALUE
    Executors.newSingleThreadExecutor(); //创建容量为1的缓冲池
    Executors.newFixedThreadPool(int); //创建固定容量大小的缓冲池
    即上面的线程池的创建其实是调用类似下面的方法,只不过Java不推荐我们这样用
    ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 200,TimeUnit.MILLISECONDS,new ArrayBlockingQueue(5));

  • 线程池工作原理
    当提交一个新任务到线程池中时,线程池的处理流程如下:

    1、线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则进入下个流程。
    2、线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
    3、线程池判断最大线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务
    在这里插入图片描述

8、Java中线程间通信:

  • syncrhoized加锁的线程的Object类的wait()/notify()/notifyAll()
  • ReentrantLock类加锁的线程的Condition类的await()/signal()/signalAll()
  • 利用volatile
  • 利用AtomicInteger

9、悲观锁适合写多读少,要确保数据安全的场景。乐观锁适合读多写少,提高系统吞吐的场景
10、垃圾回收器
查看默认的垃圾回收器:java -XX:+PrintCommandLineFlags -version
jdk8环境下,默认使用 Parallel Scavenge(新生代)+ Serial Old(老年代)
jdk9环境下,默认使用G1回收器
垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存,内存泄露是指该内存空间使用完毕之后未回收。
垃圾回收操作需要消耗CPU、线程、时间等资源,所以容易理解的是垃圾回收操作不是实时的发生(对象死亡马上释放),当内存消耗完或者是达到某一个指标(Threshold,使用内存占总内存的比列,比如0.75)时,触发垃圾回收操作
几个相关概念:
并行收集:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。
并发收集:指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另一个CPU上。
吞吐量:即CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 ))。例如:虚拟机共运行100分钟,垃圾收集器花掉1分钟,那么吞吐量就是99%
垃圾回收器有以下7种:
新生代垃圾回收器采用复制算法,年老代垃圾回收器采用标记整理或者标记清除算法

  • 1)Serial new收集器(新生代收集器)(复制算法)
    Serial收集器是一个基本的,古老的新生代垃圾收集器。这个垃圾收集器是一个单线程的收集器,在它进行垃圾回收的时候,其他的工作线程都会被暂停,直到它收集结束。尽管Serial收集器有如此多的缺点,但是从JDK1.3开始到JDK1.7都一直是默认的运行在Client模式下的新生代收集器。原因在于Serial收集器是一个简单高效的收集器,没有线程切换的开销等等,在一般的Client应用中需要回收的内存也不是很大,垃圾回收停顿的时间不是很长,是可以接受的

  • 2)ParallelNew收集器(新生代收集器)(复制算法)
    ParNew收集器是Serial收集器的多线程版本,除了使用多线程进行垃圾收集之外,其余行为跟Serial收集器一样。虽然ParNew收集器是多线程收集,但是它的性能并不一定比Serial收集器好。因为线程切换等开销的因素,在单CPU环境中它的性能是不如Serial收集器的,就算有2个CPU也不一定能说绝对比Serial好。但是随着CPU核数的增多,其最终效果肯定是优于Serial收集器的

  • 3)Parallel Scavenge收集器(新生代收集器)(复制算法)
    ParNew主要关注的是回收内存的速度,尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。

  • 4)Serial Old收集器(老年代)(标记-整理算法)
    Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器

  • 5)Parallel Old收集器(老年代)(标记-整理算法)
    Parallel Old收集器是Parallel Scavenge收集器的老年代版本。直到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的应用组合。在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器组合。

  • 6)CMS收集器(标记-清除算法)
    并发标记清除垃圾回收器,垃圾回收线程和用户线程可以并发执行,也可以产生短时间的stw,但是会产生碎片

  • 7)G1收集器
    和CMS一样,可以并发收集,不会产生碎片。
    采用分治思想,将内存划分成许多不连续的region,每个region可以是年轻代也可以是年老代

    新生代收集器:SerialNew、ParallelNew、Parallel Scavenge
    老年代收集器:Serial Old、Parallel Old、CMS
    整堆收集器: G1
    小数据量和小型应用,使用串行垃圾回收器即可。
    对于对响应时间无特殊要求的,可以使用并行垃圾回收器和并发标记垃圾回收器。(中大型应用)
    对于heap可以分配很大的中大型应用,使用G1垃圾回收器比较好,进一步优化和减少了GC暂停时间。
    垃圾回收算法:
    算法是方法论,回收器是算法落地实现

  • 1)引用计数法
    假设有一个对象A,任何一个对象对A的引用,那么对象A的引用计数器+1,当引用失败时,对象A的引用计数器就-1,如果对象A的计算器的值为0,就说明对象A没有引用了,可以被回收
    无法解决循环引用问题。(最大的缺点)

  • 2) 复制算法
    复制算法的核心就是,将原有的内存空间一分为二,每次只用其中的一块,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收。

  • 3)标记清除算法
    先把所有活动的对象标记出来,然后把没有被标记的对象统一清除掉

  • 4)标记整理算法
    标记压缩算法是在标记清除算法的基础之上,做了优化改进的算法。和标记清除算法一样,也是从根节点开始,对对象的引用进行标记,在清理阶段,并不是简单的清理未标记的对象,而是将存活的对象压缩到内存的一端,然后清理边界以外的垃圾,从而解决了碎片化的问题。

    上述算法中,判断对象是否存活方法是gcroot可达性分析:可直接或间接引用到gcroot对象的对象,那么还是在使用的,不可回收

10、java中有两种方法:JAVA方法和本地方法。JAVA方法由JAVA编写,编译成字节码,存储在class文件中;本地方法由其它语言编写的,编译成和处理器相关的机器代码

11、java内存区域:

  • (1)程序计数器
    一个比较小的内存区域,用于指示当前线程的字节码执行到了第几行,可以理解为是当前线程的行号指示器。线程私有,JVM内存中唯一没有定义OOM的区域

  • (2)虚拟机栈
    运行java方法。线程私有,每创建一个线程,虚拟机就会为这个线程创建一个虚拟机栈,虚拟机栈表示Java方法执行的内存模型,每调用这个线程的一个方法就会为每个方法生成一个栈帧(Stack Frame),用来存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法被调用和完成的过程,都对应一个栈帧从虚拟机栈上入栈和出栈的过程。虚拟机栈的生命周期和线程是相同的。局部变量表中存储着方法的相关局部变量,包括各种基本数据类型,对象的引用,返回地址等

  • (3)本地方法栈
    运行本地方法,其他跟虚拟机栈差不多。线程私有

  • (4)堆区:
    JVM内存中最大的一块。也是JVM GC管理的主要区域。存储对象实例。所有线程共享。如果在执行垃圾回收之后,仍没有足够的内存分配,也不能再扩展。则OOM

  • (5)方法区
    线程共享,存储已经被虚拟机加载的类信息、常量、静态变量(static)、即时编译器编译后的代码等。一般的,方法区上执行的垃圾收集是很少的,这也是方法区被称为永久代的原因之一(对于HotSpot虚拟机来说),但这也不代表着在方法区上完全没有垃圾收集,其上的垃圾收集主要是针对常量池的内存回收和对已加载类的卸载。
    字符串常量池中的字符串只存在一份。
    String s1 = “hello,world!”;
    String s2 = “hello,world!”; 即执行完第一行代码后,常量池中已存在
    “hello,world!”,那么s2不会在常量池中申请新的空间,而是直接把已存在的字符串内存地址返回给s2。 所以s1=s2

    直接内存:除了JVM内存外的内存,比如机器一共有8G内存,JVM占了2G,直接内存就是6G

12、一般来说,一个Java的引用访问涉及到3个内存区域:JVM栈,堆,方法区。以最简单的本地变量引用:Object obj = new Object()为例:
Object obj表示一个本地引用,存储在JVM栈的本地变量表中,表示一个引用类型数据; new Object()作为实例对象数据存储在堆中;堆中还记录了这个类–Object类本身的的类型信息(接口、方法、field、对象类型等)的地址,这些地址所执行的数据存储在方法区中;
13、GC内存区域
根据对象的存活时间,把对象划分到不同的内存区域:年轻代,年老代,永久代

  • (1)年轻代:对象被创建时,内存的分配首先发生在年轻代(大对象可以直接被创建在年老代),大部分的对象在创建后很快就不再使用,因此很快变得不可达,于是被年轻代的GC机制清理掉,这个GC机制被称为Minor GC或叫Young GC。注意,MinorGC并不代表年轻代内存不足,一般来说只是年轻代的某个区域满了:eden区或者存活区,整个年轻代内存还是够的。

    年轻代可以分为3个区域:Eden区和两个存活区Survivor 0 、Survivor 1
    大多数刚创建的对象会被分配在Eden区,其中的大多数对象很快就会消亡。Eden区是连续的内存空间,因此在其上分配内存极快;当Eden区满的时候,执行Minor GC将消亡的对象清理掉,并将剩余的对象复制到一个存活区Survivor0(此时,Survivor1是空白的,两个Survivor总有一个是空白的);此后,每次Eden区满了就执行一次Minor GC,并将剩余的对象添加到Survivor0;当Survivor0也满的时候,将其中仍然活着的对象直接复制到Survivor1,以后Eden区执行MinorGC后,将剩余的对象添加Survivor1(此时Survivor0是空白的)。当两个存活区切换了几次(HotSpot虚拟机默认15次,用-XX:MaxTenuringThreshold控制,大于该值进入老年代)之后,仍然存活的对象(其实只有一小部分,比如,我们自己定义的对象),将被复制到老年代。由于绝大部分的对象都是短命的,甚至存活不到Survivor中,所以,Eden区与Survivor的比例较大,HotSpot默认是 8:1,即分别占新生代的80%,10%,10%

  • (2)年老代:对象如果在年轻代存活了足够长的时间而没有被清理掉(即在几次 YoungGC后存活了下来),则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时,将执行Major GC,也叫Full GC

  • (3)永久代:也就是方法区,其上的垃圾收集主要是针对常量池的内存回收(没有引用了就回收)和对已加载类的卸载

14、Java内存模型(与Java内存区域属于不同层次的概念)
java虚拟机有自己的内存模型(Java Memory Model,JMM),JMM决定一个线程对共享变量的写入何时对另一个线程可见,JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。由此就会导致某个线程对共享变量的修改对其他线程不可见
17、Vector、HashTable、Properties是线程安全的。
ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap等都是线程不安全的。
stringbuffer是线程安全的,stringbuilder是非线程安全的, String是不可变类,所以是线程安全的。所有不可变类都是线程安全的
18、类加载过程

  • 装载:将编译后的二进制文件(.class文件)加载进JVM;

  • 链接:
    验证:确保被加载类的字节流信息符合虚拟机要求,不会危害到虚拟机安全
    准备:为类的静态变量分配内存,并将其初始化为默认值(比如静态int变量a,默认初始值为0);
    解析:把类中的符号引用转换为直接引用

  • 初始化:为类的静态变量赋予正确的初始值(a的正确初始值为5);

19、静态变量是在类加载的时候就初始化,所以它属于类,不属于某个对象。非静态变量就是在执行代码的时候初始化
20、方法重载是一个类中定义了多个方法名相同,而他们的参数的数量不同或数量相同而类型和次序不同。方法重写是在子类存在方法与父类的方法的名字相同,而且参数的个数与类型一样,返回值也一样的方法,就称为重写(Overriding)。方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现
22、ArrayList和LinkedList,Vector,CopyOnWriteArrayList 的大致区别

  • ArrayList是实现了基于动态数组的数据结构,适合读取操作
  • LinkedList是基于链表结构,适合写入操作
  • Vector也是基于数组结构,由于使用了synchronized方法-线程安全,所以性能上比ArrayList要差
  • CopyOnWriteArrayList兼顾了线程安全的同时,又提高了并发性,性能比Vector有不少提高。读写分离,写时复制出一个新的数组,完成插入、修改或者移除操作后将新数组赋值给array。这和 ReentrantReadWriteLock 读写锁的思想非常类似,也就是 读读共享、写写互斥、读写互斥、写读互斥。JDK中提供了 CopyOnWriteArrayList 类,相比于在读写锁的思想又更进一步。为了将读取的性能发挥到极致,CopyOnWriteArrayList 读取是完全不用加锁的,并且更厉害的是:写入也不会阻塞读取操作

24、栈区存引用和基本数据类型,不能存对象,而堆区存对象。
“ == ”是比较地址,equals()比较对象内容。

  • 1)String str1 = “abcd"的实现过程
    首先栈区创建str引用,然后在String池(独立于栈和堆而存在,存储不可变量)中寻找其指向的内容为"abcd"的对象, 如果String池中没有,则创建一个,然后str指向String池中的对象,如果有,则直接将str1指向"abcd”";如果后来又定义了字符串变量str2 = “abcd”,则直接将str2引用指向String池中已经存在的“abcd”,不再重新创建对象; 此时进行str1 ==str3操作,返回值为true
  • 2)String str3 = new String(“abcd”)的实现过程
    直接在堆中创建对象。如果后来又有String str4 = new String(“abcd”),str4不会指向之前的对象,而是重新创建一个对象并指向它。所以进行str3==str4返回值是false,因为两个对象的地址不一样,如果是str3.equals(str4),返回true,因为内容相同。

25、为什么hashmap是线程不安全的
如果多个线程同时使用put方法添加元素,而且假设正好存在两个put的key的hash值一样,这样可能会发生多个线程同时对Node数组进行扩容,扩容的时候就容易造成死循环
26、如何线程安全的使用HashMap
了解了HashMap为什么线程不安全,那现在看看如何线程安全的使用HashMap。这个无非就是以下三种方式:
Hashtable
ConcurrentHashMap
Synchronized Map
例子:

  //Hashtable
  Map<String, String> hashtable = new Hashtable<>();
  //synchronizedMap
  Map<String, String> synchronizedHashMap = Collections.synchronizedMap(new HashMap<String, String>());
  //ConcurrentHashMap
  Map<String, String> concurrentHashMap = new ConcurrentHashMap<>();

27、hashmap, hashtable, concurrenthashmap

  • 线程不安全的HashMap
    多线程环境下,使用Hashmap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap。

  • 效率低下的HashTable容器
    HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下

  • ConcurrentHashMap分段锁技术
    ConcurrentHashMap是Java5中支持高并发、高吞吐量的线程安全HashMap实现。ConcurrentHashMap中的分段锁称为Segment,类似于HashMap的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在哪一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。而且,其可以做到读取数据不加锁。线程占用其中一个Segment时,其他线程可正常访问其他段数据。Segment是一种可重入锁ReentrantLock。

    ConcurrentHashMap的并发度是什么
    CCHM的并发度就是segment的数量,默认为16,这意味着最多同时可以有16条线程操作CCHM,这也是CCHM对Hashtable的最大优势。
    concurrenthashmap使用注意事项: ConcurrentHashmap、Hashtable不支持key或者value为null,所以需要处理value不存在和存在两种情况。而HashMap是支持的

28、单例模式
单例的意思是这个类只有一个实例。单例有好几种写法,这里只列出两种:

1)饿汉单例模式
	public class EHanSingleton {
		//static final单例对象,类加载的时候就初始化
		private static final EHanSingleton instance = new EHanSingleton();
		//私有构造方法,使得外界不能直接new
		private EHanSingleton() {
		}
		//公有静态方法,对外提供获取单例接口
		public static EHanSingleton getInstance() {
			return instance;
		}
	}
	缺点:如果有大量的类都采用了饿汉单例模式,那么在类加载的阶段,会初始化很多暂时还没有用到的对象,这样肯定会浪费内存,影响性能
2)静态内部类实现单例模式(推荐使用这种模式)
	public class StaticClassSingleton {
		//私有的构造方法,防止new
		private StaticClassSingleton() {
	
		}
		public static StaticClassSingleton getInstance() {
			return StaticClassSingletonHolder.instance;
		}
		//静态内部类
		private static class StaticClassSingletonHolder {
			//第一次加载内部类的时候,实例化单例对象
			private static final StaticClassSingleton instance = new StaticClassSingleton();
		}
	}
	第一次加载StaticClassSingleton类时,并不会实例化instance。只有第一次调用getInstance方法时,
	Java虚拟机才会去加载内部类:StaticClassSingletonHolder类,继而实例化instance,这样延时实例化instance,节省了内存,并且也是线程安全的

29、实现并启动线程有两种方法
1)写一个类继承自Thread类,重写run方法。用start方法启动线程;
2)写一个类实现Runnable接口,实现run方法。用new Thread(Runnable target).start()方法来启动。
在start方法里面其实就是调用的run方法。那为什么不直接调用run方法?
因为如果直接调用run方法,并不会创建另一个线程,程序中只有主线程一个线程,所以并不是多线程。而start方法就是单独开一个线程去跑run里面的代码,与此同时,主线程也会同时进行。实现真正的多线程
30、synchronized和ReentrantLock的区别
synchronized是和if、else、for、while一样的关键字,ReentrantLock是类,这是二者的本质区别。既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock比synchronized的扩展性体现在几点上:
(1)ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁
(2)ReentrantLock可以获取各种锁的信息
(3)ReentrantLock可以灵活地实现多路通知

32、static的使用:
(1)static修饰的变量,方法或者类,在类加载到jvm的时候,就已经实例化了。只实例化一次,在内存中只有一份。一般工具类或者常量这么做
(2)只初始化一次。然后再用时都是直接从内存中得到。不需要再初始化
(3)如果这个方法是作为一个工具来使用,就声明为static,不用new一个对象出来就可以使用了,直接用类进行调用
(4)不用new就能直接调用,这样也能省去new对象的内存使用,提高效率
33、HashMap原理
本质是一个一定长度的数组,数组中存放的是链表。

  • 数组
    数组存储区间是连续的,占用内存严重,故空间复杂的很大。但数组的二分查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入和删除困难;

  • 链表
    链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N)。链表的特点是:寻址困难,插入和删除容易。

  • hashmap是一个默认长度为16的entry数组,即Entry[],即里面每一个元素都是都是Entry类型,这个类重要的属性有 key, value, next,hash(key的hash值)。因为这个next链表属性,所以可以把Entry类型看作链表类型

  • hashmap元素(k-v)存放位置index的规则:
    传统方式:index = hash(key) % 16
    hashmap:index = hash(key) & (16-1)
    简单理解可以通过比较传统的hash(key)%len获得,也就是元素的key的哈希值对数组长度取模得到。比如12%16=12,28%16=12,所以12、28都存储在数组下标为12的位置,但是其实hashmap内部并不是用取模运算,“模”运算的消耗还是比较大的。hashmap用了很多位运算(左移右移,异或)来获取index,位运算的方式比取模效率高一个量级
    Hashmap为什么大小是2的幂次?
    因为在计算元素该存放的位置的时候,用到的算法是将元素的hashcode与当前map长度-1进行与运算。源码:

    static int indexFor(int h, int length) {
    // assert Integer.bitCount(length) == 1 : “length must be a non-zero power of 2”;
    return h & (length-1);
    }
    如果map长度为2的幂次,那长度-1的二进制一定为11111…这种形式,进行与运算就看元素的hashcode,但是如果map的长度不是2的幂次,比如为15,那长度-1就是14,二进制为1110,无论与谁相与最后一位一定是0,0001,0011,0101,1001,1011,0111,1101这几个位置就永远都不能存放元素了,空间浪费相当大。也增加了添加元素是发生碰撞的机会。减慢了查询效率。所以Hashmap的大小是2的幂次。

  • Entry类里面有一个next属性,作用是指向下一个Entry。打个比方,
    第一个键值对A进来,通过计算其key的hash得到的index=5,然后会生成一个entryA对象,此时Entry[5] = entryA。一会后又进来一个键值对B,通过计算其index也等于5,现在怎么办?HashMap会这样做:
    entryB.next = entryA //entryB里面包含next属性,所以先要对entryB的next赋值
    Entry[5] = entryB
    如果又进来entryC,index也等于5,那么
    entryC.next = entryB,Entry[5] =entryC;
    这样我们发现index=5的地方其实存取了A,B,C三个键值对的Entry对象,他们通过next这个属性链接在一起。也就是说数组中存储的是最后插入的元素。而Entry[5]其实就是该链表的头部,把最后插入的元素插在头部,所以插入效率很高,这就是头插法

//向hashmap中put一个k-v元素源码
public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value); //null总是放在数组的第一个链表中
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length); //得到该k-v元素在数组中的位置i
        //遍历位置i上面的链表
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //如果key在链表中已存在,则替换为新value
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        //如果key不存在,则在链表上新增一个Entry对象
        addEntry(hash, key, value, i);
        return null;
    }

void addEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];  //取出位置i上的当前链表
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e); //重新给位置数组位置i赋值,这个值即为新的Entry,包含k的hash值,当前要插入的k,v,以及当前位置的上一个Entry,e是一个引用,指向上一个Entry
    //如果size超过threshold,则扩充table大小。再散列
    if (size++ >= threshold)
            resize(2 * table.length);
}

# get源码
 public V get(Object key) {
        if (key == null)
            return getForNullKey();
        int hash = hash(key.hashCode());
        //先定位到数组元素,再遍历该元素处的链表
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                return e.value;
        }
        return null;
}

  • hashmap的resize
    当hashmap中的元素越来越多的时候,碰撞的几率也就越来越高(因为数组的长度是固定的),即链表长度越来越大。所以为了提高查询的效率,就要对hashmap的数组进行扩容。 而在hashmap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。
    那么hashmap什么时候进行扩容呢?当hashmap中的元素个数超过数组大小loadFactor时,并且当前put操作的index上已经有元素了,就会进行数组扩容,如果put操作是插到一个没有元素的index上,即使超过了负载因子也不会扩容。loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过160.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。比如说,我们有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适

  • hashmap线程不安全
    hashmap扩容,会使链表反序,这个在单线程下是没有问题的。然而多线程下当多个线程同时扩容的时候,反序链表会导致环形链表的出现。一旦出现了环那么在while(null !=p.next){}的循环的时候.就会出现死循环导致线程阻塞。
    HashMap扩容导致死循环的主要原因在于扩容后链表中的节点在新的hash桶使用头插法插入。新的hash桶会倒置原hash桶中的单链表,那么在多个线程同时扩容的情况下就可能导致产生一个存在闭环的单链表,从而导致死循环。

  • JDK8的修复
    首先通过上面的分析我们知道JDK 1.7中HashMap扩容发生死循环的主要原因在于扩容后链表倒置以及链表过长。
    那么在JDK 1.8中HashMap扩容不会造成死循环的主要原因就从这两个角度去分析一下。
    由于扩容是按两倍进行扩,即 N 扩为 N + N,因此就会存在低位部分 0 - (N-1),以及高位部分 N - (2N-1),所以在扩容时分为 loHead (low Head) 和 hiHead (high head)。这样能减少一半链表长度。
    然后将原hash桶中单链表上的节点按照尾插法插入到loHead和hiHead所引用的单链表中。由于使用的是尾插法,不会导致单链表的倒置,所以扩容的时候不会导致死循环。
    如果单链表的长度达到 8 ,就会自动转成红黑树,而转成红黑树之前产生的单链表的逻辑也是借助loHead (low Head) 和hiHead (high head),采用尾插法。然后再根据单链表生成红黑树,也不会导致发生死循环。
    这里虽然JDK 1.8中HashMap扩容的时候不会造成死循环,但是如果多个线程同时执行put操作,可能会导致同时向一个单链表中插入数据,从而导致数据丢失的。
    所以不论是JDK 1.7 还是 1.8,HashMap线程都是不安全的,要使用线程安全的Map可以考虑ConcurrentHashMap。

  • hashmap jdk1.7和1.8的区别
    在jdk1.7中,HashMap中有个内置Entry类,它实现了Map.Entry接口;而在jdk1.8中,这个Entry类不见了,变成了Node类,也实现了Map.Entry接口,与jdk1.7中的Entry是等价的。也就是node数组和Entry数组是等价的
    链表如何转红黑树?其实就是循环链表的每一个元素,然后根据该元素new一个树节点,最后由这些树节点组成一棵红黑树

34、hashmap和hashtable的区别
两者都是基于哈希表实现。底层都是通过数组加链表的结构实现

  • HashMap中key和value都允许为null。key为null的键值对永远都放在以table[0]为头结点的链表中。ashtable不允许
  • HashMap在并发时,如果多个线程同时使用put方法添加元素,而且假设正好存在两个put的key的hash值一样,这样可能会发生多个线程同时对Node数组进行扩容,扩容的时候就容易造成死循环。 所以hashmap是线程不安全的,hashtable使用了synchronized,在修改数据时锁住整个HashTable
  • hashtable初始size为11,扩容:newsize = olesize2+1。hashmap初始size为16,扩容:newsize = oldsize2,size一定为2的n次幂
  • 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。

35、ConcurrentHashMap
ConcurrentHashMap是Java中的一个线程安全且高效的HashMap实现。平时涉及高并发如果要用map结构,那第一时间想到的就是它。

  • 在ConcurrentHashMap中有个重要的概念就是Segment。我们知道HashMap的结构是数组+链表形式,其实每个segment就类似于一个HashMap
  • ConcurrentHashMap当中每个Segment各自持有一把锁。在保证线程安全的同时降低了锁的粒度,让并发操作效率更高。上面我们说过,每个Segment就好比一个HashMap,其实里面的操作原理都差不多,只是Segment里面加了锁
  • JDK1.7和JDK1.8中ConcurrentHashMap的区别
    在JDK1.8中ConcurrentHashMap的实现方式有了很大的改变,在JDK1.7中采用的是Segment + HashEntry,而Sement继承了ReentrantLock,所以自带锁功能,而在JDK1.8中则取消了Segment,作者认为Segment太过臃肿,采用Node+ CAS + Synchronize。其中node为Node数组+链表 / 红黑树。其实node数组就跟Entry数组的作用类似。
    即1.8之后,不管是hashmap还是concurrnthashmap都是采用node+链表+红黑树的形式。数组可以扩容,链表可以转化为红黑树。concurrnthashmap已经没有了段的概念。数据结构跟hashmap是差不多的,只是多了一些锁的操作。采用table数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率。
   //concurrnthashmap的node
   static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        volatile V val;
        volatile Node<K,V> next;
}
   //hashmap的node
	static class Node<K,V> implements Map.Entry<K,V> {
        final int hash; // hash值,不可变
        final K key; // 键,不可变
        V value; // 值
        Node<K,V> next; // 下一个节点
    
 
  • jdk1.7中ConcurrentHashMap中的HashEntry相对于HashMap中的Entry有一定的差异性:HashEntry中的value以及next都被volatile修饰,这样在多线程读写过程中能够保持它们的可见性,代码如下:
	 static final class HashEntry<K,V> {
           final int hash;
           final K key;
           volatile V value;
           volatile HashEntry<K,V> next;
     }

36、volatile
内存模型在多核CPU多线程编程环境下,对共享变量读写的原子性、可见性和有序性。只是一种规范,是抽象的,不真实存在的,为了实现这种规范,需要借助一些关键字或者类实现,比如volatile

  • volatile不保证原子性可能的原因:1、线程a将变量更新到主内存后,还没来得及通知其他线程(毫秒级时间内,概率很小),其他线程就更新了主内存变量 2、对于++的操作,将工作内存的变量置为失效后,丢失了一次++操作
  • 内存模型有序性:指令重排是指编译器和处理器在不影响代码单线程执行结果的前提下,对源代码的指令进行重新排序执行。这种重排序执行是一种优化手段,目的是为了处理器内部的运算单元能尽量被充分利用,提升程序的整体运行效率。
    指令重排只能保证单线程执行下的正确性,在多线程环境下,指令重排会带来一定的问题(一个硬币具有两面性,指令重排带来性能提升的同时也增加了编程的复杂性)。即指令重排后,代码的执行顺序跟我们写的源代码的顺序就不一样了,但是在单线程下重排和不重排的执行结果都是一样的。
    在JMM中,提供了以下三种方式来保证有序性:
    happens-before原则
    synchronized机制
    volatile机制:禁止指令重排

37、queue也是一种collection
38、同步队列是阻塞队列的一种,只有一个元素,生产一个元素必须消费一个元素
39、hashset里面存储的元素都具有无序性,标识唯一性。hashset里面大多数的内容都是在hashmap的基础上进行修改的。实际上是HashMap中value=null的实现
40、公平锁:线程获得锁的顺序跟申请锁的顺序一致。
可重入锁:又称递归锁,假如在同步方法A中又调用了同步方法B,那么如果一个线程获得了A的锁,它也会获得B的锁
可重入读写锁:一般的重入锁不管是读操作还是写操作,都不是共享的,即只有一个线程能进行读操作,或者写操作。这样性能很差,比如缓存系统,对于读操作,锁应该是共享的。对于写操作,锁应该是独占的,这样保证写的原子性。而可重入读写锁能实现读时锁共享,写时锁独占,提升整体性能
41、多线程判断要用while而不是if
42、wait-notify: 线程间通信
wait:使当前线程挂起,并马上释放掉对象的锁
notify:通知当前线程锁住的对象上挂起的线程,唤醒任一线程,但是此时并不是马上释放锁,只有当同步代码块执行完了,才会释放锁

public class Demo {

    public static void main(String[] args) {

        Thread thread1 = new Thread(() -> {
            //两个线程都对同一对象加锁
            synchronized (Demo.class) {
                try {
                    System.out.println(new Date() + " Thread1 is running");
                    Demo.class.wait(); //运行到wait,线程1立马挂起,并释放锁
                    System.out.println(new Date() + " Thread1 ended");
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        });
        thread1.start();

        Thread thread2 = new Thread(() -> {
            //两个线程都对同一对象加锁
            synchronized (Demo.class) {
                try {
                    System.out.println(new Date() + " Thread2 is running");
                    Demo.class.notify();    //运行到notify,同一对象-Demo.class上阻塞的线程会被唤醒,但是此时锁还没释放
                    System.out.println(new Date() + " Thread2 notify");
                    Thread.sleep(2000);
                    System.out.println(new Date() + " Thread2 release lock");
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
            //程序除了synchronized代码块,线程2释放锁。线程1立马继续执行。打印Thread1 ended。线程2休眠2S后,打印Thread2 ended
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(new Date() + " Thread2 ended");
        });
        thread2.start();
    }
}

43、生产者消费者模式
生产者和消费者操作同一对象,当对象为空时,消费者线程阻塞。当对象满了,生产者线程阻塞

//使用wait-notify实现生产者消费者模式
public class ProCon {
    private static String lock = "lock";

    public static void main(String[] args) throws InterruptedException {
        final List<Integer> list = new ArrayList<>(10); //生产者和消费者操作的同一对象
        produce produce1 = new produce(list);
        consumer consumer = new consumer(list);
        //消费者线程先开启,此时list为空,消费者线程阻塞
        consumer.start();
        Thread.sleep(2000);
        produce1.start();
    }

    static class produce extends Thread {
        final List<Integer> list;
        public produce(List<Integer> list) {
            this.list = list;
        }
        @Override
        public void run() {
            while (true) {
            //生产者消费者必须都进行同步,且用同一把锁,即锁住同一对象,否则生产者还没生产好对象,消费者就去消费了,消费的对象就不完整
                synchronized (lock) {
                    while (list.size() == 3) {
                        try {
                            System.out.println("生产数据已满  线程" + Thread.currentThread().getName() + "已停止");
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    list.add(1);
                    lock.notifyAll();   //生产者已经生产数据,唤醒阻塞的消费者线程
                    System.out.println("线程" + Thread.currentThread().getName() + "正在生产数据" + "size " + list.size());
                }
            }
        }
    }

    static class consumer extends Thread {
        final List<Integer> list;
        public consumer(List<Integer> list) {
            this.list = list;
        }
        @Override
        public void run() {
            while (true) {
                synchronized (lock) {
                    while (list.isEmpty()) {
                        try {
                            System.out.println("消费数据已空  线程" + Thread.currentThread().getName() + "已停止");
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    int random = list.remove(0);
                    lock.notifyAll();
                    System.out.println("线程" + Thread.currentThread().getName() + "正在消费数据");
                }
            }
        }
    }
}

//使用阻塞队列实现生产者消费者模式:上面的加锁,队列为空消费阻塞、队列已满生产阻塞等等机制阻塞队列内部已经帮你实现,不用管
public class ProCon2 {
    
    public static void main(String[] args) {
        final ArrayBlockingQueue<Integer> queue=new ArrayBlockingQueue<>(10);
        produce produce=new produce(queue);
        consumer consumer=new consumer(queue);
        consumer.start();
        produce.start();
    }

    static class produce extends Thread{
        final  ArrayBlockingQueue<Integer> integerArrayBlockingQueue;
        public produce(ArrayBlockingQueue<Integer> integerArrayBlockingQueue) {
            this.integerArrayBlockingQueue = integerArrayBlockingQueue;
        }

        @Override
        public void run() {
            while (true){
                Integer random=new Random().nextInt(10);
                integerArrayBlockingQueue.add(random);
                System.out.println("生产数据" + random);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    static class consumer extends Thread{
        final ArrayBlockingQueue<Integer> integerArrayBlockingQueue;

        public consumer(ArrayBlockingQueue<Integer> integerArrayBlockingQueue) {
            this.integerArrayBlockingQueue = integerArrayBlockingQueue;
        }

        @Override
        public void run() {
            while (true){
                try {
                    Integer element=integerArrayBlockingQueue.take();
                    System.out.println("消费数据 "+element);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

44、synchronize,重入锁等只在这些锁只在单个jvm(单个tomcat,或者说单个机器节点)下有效,在分布式条件下不能保证数据一致性
45、oom

java.lang.OutOfMemoryError:Java heap spacess :强调内存不够,装不下对象,此时GC可能还是正常的
java.lang.OutOfMemoryError: GC overhead limit exceeded Gc回收时间过长会发生outofmemoryerror,过长的定义是,如果超过98%的时间来做GC,并且回收了不到2%的堆内存。强调一直GC却不起作用
java.lang.OutOfMemoryError: Unable to create new native thread意味着Java应用程序已达到其可以启动线程数的限制。
StackOverflowError 原因 : 函数调用栈太深了,注意代码中是否有了循环调用方法而无法退出的情况

46、Java1.8新特性

  • default关键字
    在java里面,我们通常都是认为接口里面是只能有抽象方法,不能有任何方法的实现的,那么在jdk1.8里面打破了这个规定,引入了新的关键字default,通过使用default修饰方法,可以让我们在接口里面定义具体的方法实现
  • Lambda 表达式   
    Lambda表达式是jdk1.8里面的一个重要的更新,这意味着java也开始承认了函数式编程,并且尝试引入其中
  • stream流
    Stream是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作,配合lambda表达式能提升编码效率和程序的可读性。
    List list=new ArrayList();
    int max =list1.stream().map(x->x.getSalary()).reduce(0,(x,y)->Integer.max(x,y));
    其实就类似于spark rdd数据集的操作

47、对于很多的垃圾收集器来说,都会采用Stop the World机制来进行垃圾回收。具体来讲,在Java虚拟机的Serial, ParNew, Parallel Scanvange, ParallelOld, Serial Old全程都会Stop the world,JVM这时候只运行GC线程,不运行用户线程。而CMS部分采用STW,因为它会尽量减少STW的次数。所谓的Stop the World机制,简称STW,即在执行垃圾收集算法时,Java应用程序的其他所有除了垃圾收集收集器线程之外的线程都被挂起。
Stop-The-World对系统性能存在影响,因此垃圾回收的一个原则是尽量减少“Stop-The-World”的时间。
例如采用复制算法时,为了保证在复制存活的对象的时候,对象的一致性,必须让所有线程停止,否则在回收的时候,不停的产生新垃圾,会影响数据一致性,一直清理不完。
48、public interface List extends Collection List是一个接口,继承自接口Collection
public class ArrayList extends AbstractList ArrayList是一个类,继承自抽象类AbstractList,抽象类AbstractList是List接口的一个实现。
List是一个接口,不能实例化,创建对象时要使用他的实现类ArrayList,LinkList。
LinkList是基于链表实现。
ArrayList底层是用数组实现的,那么它和数组的区别,最大的好处就在于ArrayList不需要定义长度,容量可动态改变但牺牲效率,如果是知道长度的,还是用数组比较好。
新建字符串数组: String[] s = new String[4];
49、wait, notify, notifyAll
这几个都是Object对象的方法。Java中规定,在调用这三个方法时,当前线程必须获得对象锁。因此就得配合synchronized关键字来使用。
当我们调用wait()时,这会强制当前线程等待,并自动释放占有的对象锁。直到某个其他线程在同一个对象上调用notify()或notifyAll()。
notify:随机唤醒一个正在wait当前对象的线程,并让被唤醒的线程拿到对象锁
notifyAll:唤醒所有正在wait当前对象的线程,但是被唤醒的线程会再次去竞争对象锁。因为一次只有一个线程能拿到锁,所有其他没有拿到锁的线程会被阻塞。推荐使用。

synchronized(object) {
    while(contidion) {
        object.wait();
    }
    //object.notify();
    //object.notifyAll();
}

或者

public synchronized void methodName() {
    while(contidion) {
        object.wait();
    }
    //object.notify();
    //object.notifyAll();
}

50、Java强引用,软引用,弱引用

  • 强引用是使用最普遍的引用,也就是如果没有生命任何类型引用,那就是强引用。如果一个对象具有强引用(有引用,说明此对象还存活),那垃圾回收器绝不会回收它。如下:
    Object strongReference = new Object();
    strongReference就是强引用,指向一个Object对象。当内存空间不足时,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象(对象还有引用,说明对象还存活)来解决内存不足的问题。如果强引用对象不使用时,需要弱化从而使GC能够回收,如下:strongReference = null; 显式地设置strongReference对象为null,或让其超出对象的生命周期范围,则gc认为该对象不存在引用,这时就可以回收这个对象
  • 软引用(SoftReference),如果一个对象只具有软引用,则内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足了,gc的时候就会回收这些对象的内存
    String str = new String(“abc”);
    SoftReference softReference = new SoftReference(str);
  • 弱引用:不管当前内存空间足够与否,gc时都会回收它的内存

51、GcRoot是垃圾回收器算法中判断一个对象是否可以回收的一种算法。就是对象到达GcRoot的路径是否还有可达,即是否有可引用链,如果有,这表明对象还存在着引用,如果没有,则表明该对象没有引用,在下一次垃圾回收时就会被回收。
GcRoot的种类
1.虚拟机栈:栈帧中的本地变量表引用的对象
2.native方法引用的对象
3.方法区中的静态变量和常量引用的对象
用一个大白话说,常量、全局变量、静态变量,以及方法内的局部变量,在GC视野里,都是GC Root对象。
通过一系列的名为“GC Root”的对象作为起点,从这些节点向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Root没有任何引用链相连时,则该对象不可达,该对象是不可使用的,垃圾收集器将回收其所占的内存。
52、在java.util.concurrent核心并发包下,JDK为我们提供了一个线程池工厂类—Executors.
阿里巴巴开发手册其中有一条就是强制大家不要去使用Executors创建线程池,这是为什么呢?比如newFixedThreadPool()使用的是LinkedBlockingQueue,该线程池的corePoolSize=maximumPoolSize,所以线程数量不会有变动,当新的任务提交频繁时,该队列会一直存储,直到资源耗尽。所以建议使用ThreadPoolExecutor根据应用的具体情况自定义合适的线程池。
53、JVM参数分为标准参数和非标准参数:

标准参数: "-"开头的参数,如java -version, java -jar
非标准参数: "-X""-XX"开头的参数,最常用的是-XX参数
"-XX"开头的参数是非稳定参数,随时可能被修改或者移除
-XX参数的常见语法有:
-XX:+[PARAM], 开启该参数的功能
-XX:-[PARAM], 关闭该参数的功能
-XX:PARAM=VALUE, 设置参数的值,如-XX:SurvivorRatio=80,设置eden/survivor的比值
下面几个都是XX参数
-Xss并不是"-X"参数,其实就是-XX:ThreadStackSize
-Xmx[value] 设置堆内存最大值:-Xmx1g
-Xms[value] 设置堆内存最小值:一般与-Xmx设置一样大:-Xmx1g
ps -ef表示查看所有进程
jps表示查看java相关的进程
jinfo -flag MaxHeapSize 6687表示查看6687这个进程的jvm参数MaxHeapSize的大小
java -XX:+PrintFlagsInitial 该命令可以查看所有JVM参数启动的初始值。
java -XX:+PrintFlagsFinal -version  查看最终值(初始值可能被修改掉)
其中,=表示默认值,:=表示被用户或者JVM修改后的值
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值