GC机制

垃圾回收需要完成两件事:找到垃圾,回收垃圾。 找到垃圾一般的话有两种方法:
引用计数法: 当一个对象被引用时,它的引用计数器会加一,垃圾回收时会清理掉引用计数为0的对象。但这种方法有一个问题,比方说有两个对象 A 和 B,A 引用了 B,B 又引用了 A,除此之外没有别的对象引用 A 和 B,那么 A 和 B 在我们看来已经是垃圾对象,需要被回收,但它们的引用计数不为 0,没有达到回收的条件。正因为这个循环引用的问题,Java 并没有采用引用计数法。
可达性分析法: 我们把 Java 中对象引用的关系看做一张图,从根级对象不可达的对象会被垃圾收集器清除。根级对象一般包括 Java 虚拟机栈中的对象、本地方法栈中的对象、方法区中的静态对象和常量池中的常量。
回收垃圾的话有这么四种方法:

  • 标记清除算法: 顾名思义分为两步,标记和清除。首先标记到需要回收的垃圾对象,然后回收掉这些垃圾对象。标记清除算法的缺点是清除垃圾对象后会造成内存的碎片化。
  • 复制算法: 复制算法是将存活的对象复制到另一块内存区域中,并做相应的内存整理工作。复制算法的优点是可以避免内存碎片化,缺点也显而易见,它需要两倍的内存。
  • 标记整理算法: 标记整理算法也是分两步,先标记后整理。它会标记需要回收的垃圾对象,清除掉垃圾对象后会将存活的对象压缩,避免了内存的碎片化。
  • 分代算法: 分代算法将对象分为新生代和老年代对象。那么为什么做这样的区分呢?主要是在Java运行中会产生大量对象,这些对象的生命周期会有很大的不同,有的生命周期很长,有的甚至使用一次之后就不再使用。所以针对不同生命周期的对象采用不同的回收策略,这样可以提高GC的效率。

    • 新生代对象分为三个区域:Eden 区和两个 Survivor 区。新创建的对象都放在 Eden区,当 Eden 区的内存达到阈值之后会触发 Minor GC,这时会将存活的对象复制到一个 Survivor 区中,这些存活对象的生命存活计数会加一。这时 Eden 区会闲置,当再一次达到阈值触发 Minor GC 时,会将Eden区和之前一个 Survivor 区中存活的对象复制到另一个 Survivor 区中,采用的是我之前提到的复制算法,同时它们的生命存活计数也会加一。
    • 这个过程会持续很多遍,直到对象的存活计数达到一定的阈值后会触发一个叫做晋升的现象:新生代的这个对象会被放置到老年代中。 老年代中的对象都是经过多次 GC 依然存活的生命周期很长的 Java 对象。当老年代的内存达到阈值后会触发 Major/Full GC,采用的是标记整理算法。

      JVM内存区域的划分

  • 哪些区域会发生 OOM

JVM 的内存区域可以分为两类:线程私有和区域和线程共有的区域。 线程私有的区域(私有内存):程序计数器、JVM 虚拟机栈、本地方法栈 线程共有的区域(主内存):堆、方法区、运行时常量池

  • 程序计数器:每个线程有有一个私有的程序计数器,任何时间一个线程都只会有一个方法正在执行,也就是所谓的当前方法。程序计数器存放的就是这个当前方法的JVM指令地址。
  • Java栈(JVM虚拟机栈):创建线程的时候会创建线程内的虚拟机栈,栈中存放着一个个的栈帧,对应着一个个方法的调用。JVM 虚拟机栈有两种操作,分别是压栈和出站。栈帧中存放着局部变量表、方法返回值和方法的正常或异常退出的定义等等。
  • 本地方法栈:跟 JVM 虚拟机栈比较类似,只不过它支持的是 Native 方法。
  • 堆:堆是内存管理的核心区域,用来存放对象实例。几乎所有创建的对象实例都会直接分配到堆上。所以堆也是垃圾回收的主要区域,垃圾收集器会对堆有着更细的划分,最常见的就是把堆划分为新生代和老年代。
  • 方法区:方法区主要存放类的结构信息,比如静态属性和方法等等。
  • 运行时常量池:运行时常量池位于方法区中,主要存放各种常量信息。

其实除了程序计数器,其他的部分都会发生 OOM。

  • Java堆:通常发生的 OOM 都会发生在堆中,最常见的可能导致 OOM 的原因就是内存泄漏。
  • JVM虚拟机栈和本地方法栈:当我们写一个递归方法,这个递归方法没有循环终止条件,最终会导致 StackOverflow 的错误。当然,如果栈空间扩展失败,也是会发生 OOM 的。
  • 方法区:方法区现在基本上不太会发生 OOM,但在早期内存中加载的类信息过多的情况下也是会发生 OOM 的。

详情

  • https://www.cnblogs.com/dolphin0520/p/3613043.html

    java的内存模型

    Java内存模型规定了所有的变量都存储在主内存中(堆:放对象,常量池:基本类型),每条线程还有自己的私有内存,线程的私有内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在私有内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方私有内存中的变量,线程间变量的传递均需要自己的私有内存和主内存之间进行数据同步进行。
    原子性
    一个操作或一系列是不可中断的。即使是在多个线程一起执行的时候,这些操作一旦开始,就不会被其他线程干扰;在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。
    在Java中可以使用synchronized来保证方法和代码块内的操作是原子性的。
    可见性
    Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值的这种依赖主内存作为传递媒介的方式来实现的。
    Java中的volatile关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。因此,可以使用volatile来保证多线程操作时变量的可见性。
    除了volatile,Java中的synchronizedfinal两个关键字也可以实现可见性。只不过实现方式不同,这里不再展开了。
    有序性
    在Java中,可以使用synchronizedvolatile来保证多线程之间操作的有序性。实现方式有所区别:
    volatile关键字会禁止指令重排。synchronized关键字保证同一时刻只允许一条线程操作。
    好了,这里简单的介绍完了Java并发编程中解决原子性、可见性以及有序性可以使用的关键字。读者可能发现了,好像synchronized关键字是万能的,他可以同时满足以上三种特性,这其实也是很多人滥用synchronized的原因。
    但是synchronized是比较影响性能的,虽然编译器提供了很多锁优化技术,但是也不建议过度使用

    类加载过程

    Java 中类加载分为 3 个步骤:加载、链接、初始化

  • 加载:查找和导入Class文件;

  • 链接:把类的二进制数据合并到JRE中
    • 校验:校验载入的字节信息是否符合规范,避免恶意信息和不规范数据危害JVM运行安全。
    • 准备:给类的静态变量分配存储空间
    • 解析:将符号引用转成直接引用
  • 初始化:对类的静态变量,静态代码块执行初始化操作

说明

  • 符号引用(Symbolic Reference)
    • 符号引用以一组符号来描述所引用的目标,符号引用可以是任何形式的字面量,符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经在内存中。
  • 准备

    • 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
    • 这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。

      双亲委派模型

      类加载器大致分为3类:启动类加载器、扩展类加载器、应用程序类加载器。
  • 启动类加载器主要加载 jre/lib下的jar文件。

  • 扩展类加载器主要加载 jre/lib/ext 下的jar文件。
  • 应用程序类加载器主要加载 classpath 下的文件。

所谓的双亲委派模型就是当加载一个类时,会优先使用父类加载器加载,当父类加载器无法加载时才会使用子类加载器去加载。这么做的目的是为了避免类的重复加载。

HashMap 的原理

HashMap 的内部可以看做数组+链表的复合结构。数组被分为一个个的桶(bucket)。哈希值决定了键值对在数组中的寻址。具有相同哈希值的键值对会组成链表。需要注意的是当链表长度超过阈值(默认是8)的时候会触发树化,链表会变成树形结构。
把握HashMap的原理需要关注4个方法:hash、put、get、resize。

  • hash方法。 将 key 的 hashCode 值的高位数据移位到低位进行异或运算。这么做的原因是有些 key 的 hashCode 值的差异集中在高位,而哈希寻址是忽略容量以上高位的,这种做法可以有效避免哈希冲突。
  • put 方法。put 方法主要有以下几个步骤:
  • 通过 hash 方法获取 hash 值,根据 hash 值寻址。
  • 如果未发生碰撞,直接放到桶中。
  • 如果发生碰撞,则以链表形式放在桶后。
  • 当链表长度大于阈值后会触发树化,将链表转换为红黑树。
  • 如果数组长度达到阈值,会调用 resize 方法扩展容量。
  • get方法。get 方法主要有以下几个步骤:
  • 通过 hash 方法获取 hash 值,根据 hash 值寻址。
  • 如果与寻址到桶的 key 相等,直接返回对应的 value。
  • 如果发生冲突,分两种情况。如果是树,则调用 getTreeNode 获取 value;如果是链表则通过循环遍历查找对应的 value。
  • resize 方法。resize 做了两件事:
  • 将原数组扩展为原来的 2 倍
  • 重新计算 index 索引值,将原节点重新放到新的数组中。这一步可以将原先冲突的节点分散到新的桶中。

    线程的几种状态

    图片示例
    image.png
    文字说明
  1. 新建状态:
    使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
  2. 就绪状态:
    当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
  3. 运行状态:
    如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
  4. 阻塞状态:如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
    • 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
    • 同步阻塞:线程在获取 synchronized同步锁失败(因为同步锁被其他线程占用)。
    • 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
  5. 死亡状态:
    一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
  6. 参考:https://www.cnblogs.com/zxfei/p/11074492.html

    多线程问题

  • 可以这么答:
    1. 当只有一个线程写,其它线程都是读的时候,可以用volatile修饰变量
    2. 当多个线程写,那么一般情况下并发不严重的话可以用Synchronized,Synchronized并不是一开始就是重量级锁,在并发不严重的时候,比如只有一个线程访问的时候,是偏向锁;当多个线程访问,但不是同时访问,这时候锁升级为轻量级锁;当多个线程同时访问,这时候升级为重量级锁。所以在并发不是很严重的情况下,使用Synchronized是可以的。不过Synchronized有局限性,比如不能设置锁超时,不能通过代码释放锁。
    3. ReentranLock 可以通过代码释放锁,可以设置锁超时。
    4. 高并发下,Synchronized、ReentranLock 效率低,因为同一时刻只有一个线程能进入同步代码块,如果同时有很多线程访问,那么其它线程就都在等待锁。这个时候可以使用并发包下的数据结构,例如ConcurrentHashMapLinkBlockingQueue,以及原子性的数据结构如:AtomicInteger

Synchronized 锁的升级

大家对Synchronized的理解可能就是重量级锁,但是Java1.6对 Synchronized 进行了各种优化之后,有些情况下它就并不那么重,Java1.6 中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁。
偏向锁:大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。

当一个线程A访问加了同步锁的代码块时,会在对象头中存 储当前线程的id,后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。

轻量级锁:在偏向锁情况下,如果线程B也访问了同步代码块,比较对象头的线程id不一样,会升级为轻量级锁,并且通过自旋的方式来获取轻量级锁。
重量级锁:如果线程A和线程B同时访问同步代码块,则轻量级锁会升级为重量级锁,线程A获取到重量级锁的情况下,线程B只能入队等待,进入BLOCK状态。

sleep 和 wait 的区别

  • sleep 方法是 Thread 类中的静态方法,wait 是 Object 类中的方法
  • sleep 并不会释放同步锁,而 wait 会释放同步锁
  • sleep 可以在任何地方使用,而 wait 只能在同步方法或者同步代码块中使用
  • sleep 中必须传入时间,而 wait 可以传,也可以不传,不传时间的话只有 notify 或者 notifyAll 才能唤醒,传时间的话在时间之后会自动唤醒

    join 的用法

    join 方法通常是保证线程间顺序调度的一个方法,它是 Thread 类中的方法。比方说在线程 A 中执行线程 B.join(),这时线程 A 会进入等待状态,直到线程 B 执行完毕之后才会唤醒,继续执行A线程中的后续方法。
    join 方法可以传时间参数,也可以不传参数,不传参数实际上调用的是 join(0)。它的原理其实是使用了 wait 方法,join 的原理如下:

    1. public final synchronized void join(long millis)
    2. throws InterruptedException {
    3. long base = System.currentTimeMillis();
    4. long now = 0;
    5. if (millis < 0) {
    6. throw new IllegalArgumentException("timeout value is negative");
    7. }
    8. if (millis == 0) {
    9. while (isAlive()) {
    10. wait(0);
    11. }
    12. } else {
    13. while (isAlive()) {
    14. long delay = millis - now;
    15. if (delay <= 0) {
    16. break;
    17. }
    18. wait(delay);
    19. now = System.currentTimeMillis() - base;
    20. }
    21. }
    22. }

    volatile

    一般提到 volatile,就不得不提到内存模型相关的概念。我们都知道,在程序运行中,每条指令都是由 CPU 执行的,而指令的执行过程中,势必涉及到数据的读取和写入。程序运行中的数据都存放在主内存中,这样会有一个问题,由于 CPU 的执行速度是要远高于主内存的读写速度,所以直接从主内存中读写数据会降低 CPU 的效率。为了解决这个问题,就有了高速缓存的概念,在每个 CPU 中都有高速缓存,它会事先从主内存中读取数据,在 CPU 运算之后在合适的时候刷新到主内存中。
    这样的运行模式在单线程中是没有任何问题的,但在多线程中,会导致缓存一致性的问题。举个简单的例子:i=i+1 ,在两个线程中执行这句代码,假设i的初始值为0。我们期望两个线程运行后得到2,那么有这样的一种情况,两个线程都从主存中读取i到各自的高速缓存中,这时候两个线程中的i都为0。在线程1执行完毕得到i=1,将之刷新到主存后,线程2开始执行,由于线程2中的i是高速缓存中的0,所以在执行完线程2之后刷新到主内存的i仍旧是1。
    所以这就导致了对共享变量的缓存一致性的问题,那么为了解决这个问题,提出了缓存一致性协议:当 CPU 在写数据时,如果发现操作的是共享变量,它会通知其他 CPU 将它们内部的这个共享变量置为无效状态,当其他 CPU 读取缓存中的共享变量时,发现这个变量是无效的,它会从新从主存中读取最新的值。
    在Java的多线程开发中,有三个重要概念:原子性、可见性、有序性。

  • 原子性:一个或多个操作要么都不执行,要么都执行。

  • 可见性: 一个线程中对共享变量(类中的成员变量或静态变量)的修改,在其他线程立即可见。
  • 有序性: 程序执行的顺序按照代码的顺序执行。 把一个变量声明为volatile,其实就是保证了可见性和有序性。 可见性我上面已经说过了,在多线程开发中是很有必要的。这个有序性还是得说一下,为了执行的效率,有时候会发生指令重排,这在单线程中指令重排之后的输出与我们的代码逻辑输出还是一致的。但在多线程中就可能发生问题,volatile在一定程度上可以避免指令重排。

volatile的原理是在生成的汇编代码中多了一个lock前缀指令,这个前缀指令相当于一个内存屏障,这个内存屏障有3个作用:

  • 确保指令重排的时候不会把屏障后的指令排在屏障前,确保不会把屏障前的指令排在屏障后。
  • 修改缓存中的共享变量后立即刷新到主存中。
  • 当执行写操作时会导致其他CPU中的缓存无效。

    volatile和synchronize的区别

    volatile
    它所修饰的变量不保留拷贝,直接访问主内存中的。
    在Java内存模型中,有main memory,每个线程也有自己的memory (例如寄存器)。为了性能,一个线程会在自己的memory中保持要访问的变量的副本。这样就会出现同一个变量在某个瞬间,在一个线程的memory中的值可能与另外一个线程memory中的值,或者main memory中的值不一致的情况。 一个变量声明为volatile,就意味着这个变量是随时会被其他线程修改的,因此不能将它cache在线程memory中。
    使用场景
    您只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:
    1)对变量的写操作不依赖于当前值。
    2)该变量没有包含在具有其他变量的不变式中。
    volatile最适用一个线程写,多个线程读的场合。
    如果有多个线程并发写操作,仍然需要使用锁或者线程安全的容器或者原子变量来代替。
    synchronized
    当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。
  1. 当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
  2. 然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。
  3. 尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。
  4. 当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。

区别

  1. volatile是变量修饰符,而synchronized则作用于一段代码或方法。
  2. volatile只是在线程私有内存和“共享”内存间同步某个变量的值;而synchronized通过锁定和解锁某个监视器同步所有变量的值, 显然synchronized要比volatile消耗更多资源。
  3. volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
  4. volatile保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存中和公共内存中的数据做同步。
  5. volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

线程安全包含原子性和可见性两个方面,Java的同步机制都是围绕这两个方面来确保线程安全的。
关键字volatile主要使用的场合是在多个线程中可以感知实例变量被修改,并且可以获得最新的值使用,也就是多线程读取共享变量时可以获得最新值使用。
关键字volatile提示线程每次从主内存中读取变量,而不是私有内存中读取,这样就保证了同步数据的可见性。但是要注意的是:如果修改实例变量中的数据

ThreadLocal的作用

ThreadLocal的作用是提供线程内的局部变量,说白了,就是在各线程内部创建一个变量的副本,相比于使用各种锁机制访问变量,ThreadLocal的思想就是用空间换时间,使各线程都能访问属于自己这一份的变量副本,变量值不互相干扰,减少同一个线程内的多个函数或者组件之间一些公共变量传递的复杂度。
get函数用来获取与当前线程关联的ThreadLocal的值,如果当前线程没有该ThreadLocal的值,则调用initialValue函数获取初始值返回,initialValue是protected类型的,所以一般我们使用时需要继承该函数,给出初始值。而set函数是用来设置当前线程的该ThreadLocal的值,remove函数用来删除ThreadLocal绑定的值,在某些情况下需要手动调用,防止内存泄露。

Java中生产者与消费者模式

生产者消费者模式要保证的是当缓冲区满的时候生产者不再生产对象,当缓冲区空时,消费者不再消费对象。实现机制就是当缓冲区满时让生产者处于等待状态,当缓冲区为空时让消费者处于等待状态。当生产者生产了一个对象后会唤醒消费者,当消费者消费一个对象后会唤醒生产者。
三种种实现方式:wait 和 notify、await 和 signal、BlockingQueue。

  • wait 和 notify

    1. //wait和notify
    2. import java.util.LinkedList;
    3. public class StorageWithWaitAndNotify {
    4. private final int MAX_SIZE = 10;
    5. private LinkedList<Object> list = new LinkedList<Object>();
    6. public void produce() {
    7. synchronized (list) {
    8. while (list.size() == MAX_SIZE) {
    9. System.out.println("仓库已满:生产暂停");
    10. try {
    11. list.wait();
    12. } catch (InterruptedException e) {
    13. e.printStackTrace();
    14. }
    15. }
    16. list.add(new Object());
    17. System.out.println("生产了一个新产品,现库存为:" + list.size());
    18. list.notifyAll();
    19. }
    20. }
    21. public void consume() {
    22. synchronized (list) {
    23. while (list.size() == 0) {
    24. System.out.println("库存为0:消费暂停");
    25. try {
    26. list.wait();
    27. } catch (InterruptedException e) {
    28. e.printStackTrace();
    29. }
    30. }
    31. list.remove();
    32. System.out.println("消费了一个产品,现库存为:" + list.size());
    33. list.notifyAll();
    34. }
    35. }
    36. }
  • await 和 signal

    1. import java.util.LinkedList;
    2. import java.util.concurrent.locks.Condition;
    3. import java.util.concurrent.locks.ReentrantLock;
    4. class StorageWithAwaitAndSignal {
    5. private final int MAX_SIZE = 10;
    6. private ReentrantLock mLock = new ReentrantLock();
    7. private Condition mEmpty = mLock.newCondition();
    8. private Condition mFull = mLock.newCondition();
    9. private LinkedList<Object> mList = new LinkedList<Object>();
    10. public void produce() {
    11. mLock.lock();
    12. while (mList.size() == MAX_SIZE) {
    13. System.out.println("缓冲区满,暂停生产");
    14. try {
    15. mFull.await();
    16. } catch (InterruptedException e) {
    17. e.printStackTrace();
    18. }
    19. }
    20. mList.add(new Object());
    21. System.out.println("生产了一个新产品,现容量为:" + mList.size());
    22. mEmpty.signalAll();
    23. mLock.unlock();
    24. }
    25. public void consume() {
    26. mLock.lock();
    27. while (mList.size() == 0) {
    28. System.out.println("缓冲区为空,暂停消费");
    29. try {
    30. mEmpty.await();
    31. } catch (InterruptedException e) {
    32. e.printStackTrace();
    33. }
    34. }
    35. mList.remove();
    36. System.out.println("消费了一个产品,现容量为:" + mList.size());
    37. mFull.signalAll();
    38. mLock.unlock();
    39. }
    40. }
  • BlockingQueue

    1. import java.util.concurrent.LinkedBlockingQueue;
    2. public class StorageWithBlockingQueue {
    3. private final int MAX_SIZE = 10;
    4. private LinkedBlockingQueue<Object> list = new LinkedBlockingQueue<Object>(MAX_SIZE);
    5. public void produce() {
    6. if (list.size() == MAX_SIZE) {
    7. System.out.println("缓冲区已满,暂停生产");
    8. }
    9. try {
    10. list.put(new Object());
    11. } catch (InterruptedException e) {
    12. e.printStackTrace();
    13. }
    14. System.out.println("生产了一个产品,现容量为:" + list.size());
    15. }
    16. public void consume() {
    17. if (list.size() == 0) {
    18. System.out.println("缓冲区为空,暂停消费");
    19. }
    20. try {
    21. list.take();
    22. } catch (InterruptedException e) {
    23. e.printStackTrace();
    24. }
    25. System.out.println("消费了一个产品,现容量为:" + list.size());
    26. }
    27. }

    final、finally、finalize区别

    final 可以修饰类、变量和方法。修饰类代表这个类不可被继承。修饰变量代表此变量不可被改变。修饰方法表示此方法不可被重写 (override)。
    finally 是保证重点代码一定会执行的一种机制。通常是使用 try-finally 或者 try-catch-finally 来进行文件流的关闭等操作。
    finalize 是 Object 类中的一个方法,它的设计目的是保证对象在垃圾收集前完成特定资源的回收。finalize 机制现在已经不推荐使用,并且在 JDK 9已经被标记为 deprecated。

    Java 中单例模式

    Java 中常见的单例模式实现有这么几种:饿汉式、双重判断的懒汉式、静态内部类实现的单例、枚举实现的单例。 这里着重讲一下双重判断的懒汉式和静态内部类实现的单例。
    双重判断的懒汉式:

    1. public class SingleTon {
    2. //需要注意的是volatile
    3. private static volatile SingleTon mInstance;
    4. private SingleTon() {
    5. }
    6. public static SingleTon getInstance() {
    7. if (mInstance == null) {
    8. synchronized (SingleTon.class) {
    9. if (mInstance == null) {
    10. mInstance=new SingleTon();
    11. }
    12. }
    13. }
    14. return mInstance;
    15. }
    16. }

    双重判断的懒汉式单例既满足了延迟初始化,又满足了线程安全。通过 synchronized 包裹代码来实现线程安全,通过双重判断来提高程序执行的效率。这里需要注意的是单例对象实例需要有 volatile 修饰,如果没有 volatile 修饰,在多线程情况下可能会出现问题。原因是这样的,mInstance=new SingleTon()这一句代码并不是一个原子操作,它包含三个操作:

  1. 给 mInstance 分配内存
  2. 调用 SingleTon 的构造方法初始化成员变量
  3. 将 mInstance 指向分配的内存空间(在这一步 mInstance 已经不为 null 了)

我们知道 JVM 会发生指令重排,正常的执行顺序是1-2-3,但发生指令重排后可能会导致1-3-2。我们考虑这样一种情况,当线程 A 执行到1-3-2的3步骤暂停了,这时候线程 B 调用了 getInstance,走到了最外层的if判断上,由于最外层的 if 判断并没有 synchronized 包裹,所以可以执行到这一句,这时候由于线程 A 已经执行了步骤3,此时 mInstance 已经不为 null 了,所以线程B直接返回了 mInstance。但其实我们知道,完整的初始化必须走完这三个步骤,由于线程 A 只走了两个步骤,所以一定会报错的。
解决的办法就是使用 volatile 修饰 mInstance,我们知道 volatile 有两个作用:保证可见性和禁止指令重排,在这里关键在于禁止指令重排,禁止指令重排后保证了不会发生上述问题。

静态内部类实现的单例:

  1. class SingletonWithInnerClass {
  2. private SingletonWithInnerClass() {
  3. }
  4. private static class SingletonHolder{
  5. private static SingletonWithInnerClass INSTANCE = new SingletonWithInnerClass();
  6. }
  7. public SingletonWithInnerClass getInstance() {
  8. return SingletonHolder.INSTANCE;
  9. }
  10. }

由于外部类的加载并不会导致内部类立即加载,只有当调用 getInstance 的时候才会加载内部类,所以实现了延迟初始化。由于类只会被加载一次,并且类加载也是线程安全的,所以满足我们所有的需求。静态内部类实现的单例也是最为推荐的一种方式。

Java中引用类型的区别,具体的使用场景

Java中引用类型分为四类:强引用、软引用、弱引用、虚引用。

  • 强引用: 强引用指的是通过 new 对象创建的引用,垃圾回收器即使是内存不足也不会回收强引用指向的对象。
  • 软引用: 软引用是通过 SoftRefrence 实现的,它的生命周期比强引用短,在内存不足,抛出 OOM 之前,垃圾回收器会回收软引用引用的对象。软引用常见的使用场景是存储一些内存敏感的缓存,当内存不足时会被回收。
  • 弱引用: 弱引用是通过 WeakRefrence 实现的,它的生命周期比软引用还短,GC 只要扫描到弱引用的对象就会回收。弱引用常见的使用场景也是存储一些内存敏感的缓存。
  • 虚引用: 虚引用是通过 FanttomRefrence 实现的,它的生命周期最短,随时可能被回收。如果一个对象只被虚引用引用,我们无法通过虚引用来访问这个对象的任何属性和方法。它的作用仅仅是保证对象在 finalize 后,做某些事情。虚引用常见的使用场景是跟踪对象被垃圾回收的活动,当一个虚引用关联的对象被垃圾回收器回收之前会收到一条系统通知。

    Exception 和 Error的区别

    Exception 和 Error 都继承于 Throwable,在 Java 中,只有 Throwable 类型的对象才能被 throw 或者 catch,它是异常处理机制的基本组成类型。
    Exception 和 Error 体现了 Java 对不同异常情况的分类。Exception 是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应的处理。
    Error 是指在正常情况下,不大可能出现的情况,绝大部分 Error 都会使程序处于非正常、不可恢复的状态。既然是非正常,所以不便于也不需要捕获,常见的 OutOfMemoryError 就是 Error 的子类。
    Exception 又分为 checked Exception 和 unchecked Exception。

  • checked Exception 在代码里必须显式的进行捕获,这是编译器检查的一部分。

  • unchecked Exception 也就是运行时异常,类似空指针异常、数组越界等,通常是可以避免的逻辑错误,具体根据需求来判断是否需要捕获,并不会在编译器强制要求。

    线程的几个常见方法的比较

  1. Thread.sleep(long millis),一定是当前线程调用此方法,当前线程进入TIMED_WAITING状态,但不释放对象锁,millis后线程自动苏醒进入就绪状态。作用:给其它线程执行机会的最佳方式。
  2. Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的CPU时间片,但不释放同步锁,由运行状态变为就绪状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。该方法与sleep()类似,只是不能由用户指定暂停多长时间。
  3. thread.join()/thread.join(long millis)当前线程里调用其它线程thread的join方法,当前线程进入WAITING/TIMED_WAITING状态,当前线程不会释放已经持有的对象锁。线程thread执行完毕或者millis时间到,当前线程进入就绪状态。
  4. thread.interrupt(),当前线程里调用其它线程thread的interrupt()方法,中断指定的线程。
    如果指定线程调用了wait()方法组或者join方法组在阻塞状态,那么指定线程会抛出InterruptedException
  5. Thread.interrupted,一定是当前线程调用此方法,检查当前线程是否被设置了中断,该方法会重置当前线程的中断标志,返回当前线程是否被设置了中断。
  6. thread.isInterrupted()当前线程里调用其它线程thread的isInterrupted()方法,返回指定线程是否被中断
  7. object.wait()当前线程调用对象的wait()方法,当前线程释放同步锁,进入等待队列。依靠notify()/notifyAll()唤醒或者wait(long timeout) timeout时间到自动唤醒。
  8. object.notify()唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的所有线程。

    Object.wait() / Object.notify() Object.notifyAll()

    任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括wait()、
    wait(long timeout)、notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以
    实现等待/通知模式

  9. 使用的前置条件
    当我们想要使用Object的监视器方法时,需要或者该Object的锁,代码如下所示
    synchronized(obj){ …. //1 obj.wait();//2 obj.wait(long millis);//2 ….//3 }
    一个线程获得obj的锁,做了一些时候事情之后,发现需要等待某些条件的发生,调用obj.wait(),该线程会释放obj的锁,并阻塞在上述的代码2处
    obj.wait()和obj.wait(long millis)的区别在于
    obj.wait()是无限等待,直到obj.notify()或者obj.notifyAll()调用并唤醒该线程,该线程获取锁之后继续执行代码3
    obj.wait(long millis)是超时等待,我只等待long millis 后,该线程会自己醒来,醒来之后去获取锁,获取锁之后继续执行代码3
    obj.notify()是叫醒任意一个等待在该对象上的线程,该线程获取锁,线程状态从BLOCKED进入RUNNABLE
    obj.notifyAll()是叫醒所有等待在该对象上的线程,这些线程会去竞争锁,得到锁的线程状态从BLOCKED进入RUNNABLE,其他线程依然是BLOCKED,得到锁的线程执行代码3完毕后释放锁,其他线程继续竞争锁,如此反复直到所有线程执行完毕。
    synchronized(obj){ …. //1 obj.notify();//2 obj.notifyAll();//2 }
    一个线程获得obj的锁,做了一些时候事情之后,某些条件已经满足,调用obj.notify()或者obj.notifyAll(),该线程会释放obj的锁,并叫醒在obj上等待的线程,
    obj.notify()和obj.notifyAll()的区别在于
    obj.notify()叫醒在obj上等待的任意一个线程(由JVM决定)
    obj.notifyAll()叫醒在obj上等待的全部线程

  10. 使用范式
    synchronized(obj){ //判断条件,这里使用while,而不使用ifwhile(obj满足/不满足 某个条件){ obj.wait() } }
    放在while里面,是防止处于WAITING状态下线程监测的对象被别的原因调用了唤醒(notify或者notifyAll)方法,但是while里面的条件并没有满足(也可能当时满足了,但是由于别的线程操作后,又不满足了),就需要再次调用wait将其挂起

    强软弱虚引用以及使用场景

    强引用:
    1. 正常创建的对象,只要引用存在,永远不会被GC回收,即使OOM
    Object obj = new Object();
    2. 如果要中断强引用和某个对象的关联,为其赋值null,这样GC就会在合适的时候回收对象
    3. Vector类的clear()方法就是通过赋值null进行清除
    软引用
    1. 内存溢出之前进行回收,GC时内存不足时回收,如果内存足够就不回收
    2. 使用场景:在内存足够的情况下进行缓存,提升速度,内存不足时JVM自动回收
    3. 可以和引用队列ReferenceQueue联合使用,如果软引用所引用的对象被JVM回收,这个软引用就会被加入到与之关联的引用队列中
    弱引用
    1. 每次GC时回收,无论内存是否足够
    2. 使用场景:a. ThreadLocalMap防止内存泄漏 b. 监控对象是否将要被回收
    3. 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被JVM回收,这个软引用就会被加入到与之关联的引用队列中
    虚引用
    1. 每次垃圾回收时都会被回收,主要用于监测对象是否已经从内存中删除
    2. 虚引用必须和引用队列关联使用, 当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中
    3. 程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动

    http 与 https 的区别?https 是如何工作的?

http 是超文本传输协议,而 https 可以简单理解为安全的 http 协议。https 通过在 http 协议下添加了一层 ssl 协议对数据进行加密从而保证了安全。https 的作用主要有两点:建立安全的信息传输通道,保证数据传输安全;确认网站的真实性。

http 与 https 的区别主要如下:

  • https 需要到 CA 申请证书,很少免费,因而需要一定的费用
  • http 是明文传输,安全性低;而 https 在 http 的基础上通过 ssl 加密,安全性高
  • 二者的默认端口不一样,http 使用的默认端口是80;https使用的默认端口是 443

    http 1.0 、1.1和2.0的区别

  • 1.0每次都要重新建立连接

  • 1.1连接可以复用
  • 2.0可以实现并发多路复用,长连接服务器可主动向客户端推消息

    https 的工作流程

    提到 https 的话首先要说到加密算法,加密算法分为两类:对称加密和非对称加密。

  • 对称加密: 加密和解密用的都是相同的秘钥,优点是速度快,缺点是安全性低。常见的对称加密算法有 DES、AES 等等。

  • 非对称加密: 非对称加密有一个秘钥对,分为公钥和私钥。一般来说,私钥自己持有,公钥可以公开给对方,优点是安全性比对称加密高,缺点是数据传输效率比对称加密低。采用公钥加密的信息只有对应的私钥可以解密。常见的非对称加密包括RSA等。

在正式的使用场景中一般都是对称加密和非对称加密结合使用,使用非对称加密完成秘钥的传递,然后使用对称秘钥进行数据加密和解密。二者结合既保证了安全性,又提高了数据传输效率。

https 的具体流程如下:

  1. 客户端(通常是浏览器)先向服务器发出加密通信的请求
  • 支持的协议版本,比如 TLS 1.0版
  • 一个客户端生成的随机数 random1,稍后用于生成”对话密钥”
  • 支持的加密方法,比如 RSA 公钥加密
  • 支持的压缩方法
  1. 服务器收到请求,然后响应
  • 确认使用的加密通信协议版本,比如 TLS 1.0版本。如果浏览器与服务器支持的版本不一致,服务器关闭加密通信
  • 一个服务器生成的随机数 random2,稍后用于生成”对话密钥”
  • 确认使用的加密方法,比如 RSA 公钥加密
  • 服务器证书
  1. 客户端收到证书之后会首先会进行验证
  • 首先验证证书的安全性
  • 验证通过之后,客户端会生成一个随机数 pre-master secret,然后使用证书中的公钥进行加密,然后传递给服务器端
  1. 服务器收到使用公钥加密的内容,在服务器端使用私钥解密之后获得随机数 pre-master secret,然后根据 radom1、radom2、pre-master secret 通过一定的算法得出一个对称加密的秘钥,作为后面交互过程中使用对称秘钥。同时客户端也会使用 radom1、radom2、pre-master secret,和同样的算法生成对称秘钥。
  2. 然后再后续的交互中就使用上一步生成的对称秘钥对传输的内容进行加密和解密。

    http头部的字段以及含义

  • Accept : 浏览器(或者其他基于HTTP的客户端程序)可以接收的内容类型(Content-types),例如 Accept: text/plain
  • Accept-Charset:浏览器能识别的字符集,例如 Accept-Charset: utf-8
  • Accept-Encoding:浏览器可以处理的编码方式,注意这里的编码方式有别于字符集,这里的编码方式通常指gzip,deflate等。例如 Accept-Encoding: gzip, deflate
  • Accept-Language:浏览器接收的语言,其实也就是用户在什么语言地区,例如简体中文的就是 Accept-Language: zh-CN
  • Authorization:在HTTP中,服务器可以对一些资源进行认证保护,如果你要访问这些资源,就要提供用户名和密码,这个用户名和密码就是在Authorization头中附带的,格式是“username:password”字符串的base64编码
  • Cache-Control:这个指令在request和response中都有,用来指示缓存系统(服务器上的,或者浏览器上的)应该怎样处理缓存,因为这个头域比较重要,特别是希望使用缓 存改善性能的时候
  • Connection:告诉服务器这个user agent(通常就是浏览器)想要使用怎样的连接方式。值有keep-alive和close。http1.1默认是keep-alive。keep-alive就是浏览器和服务器 的通信连接会被持续保存,不会马上关闭,而close就会在response后马上关闭。但这里要注意一点,我们说HTTP是无状态的,跟这个是否keep-alive没有关系,不要认为keep-alive是对HTTP无状态的特性的改进。
  • Cookie:浏览器向服务器发送请求时发送cookie,或者服务器向浏览器附加cookie,就是将cookie附近在这里的。例如:Cookie:user=admin
  • Content-Length:一个请求的请求体的内存长度,单位为字节(byte)。请求体是指在HTTP头结束后,两个CR-LF字符组之后的内容,常见的有POST提交的表单数据,这个Content-Length并不包含请求行和HTTP头的数据长度。
  • Content-MD5:使用base64进行了编码的请求体的MD5校验和。例如:Content-MD5: Q2hlY2sgSW50ZWdyaXR5IQ==
  • Content-Type:请求体中的内容的mime类型。通常只会用在POST和PUT方法的请求中。例如:Content-Type: application/x-www-form-urlencoded
  • Date:发送请求时的GMT时间。例如:Date: Tue, 15 Nov 1994 08:12:31 GMT
  • From:发送这个请求的用户的email地址。例如:From: user@example.com
  • Host:被服务器的域名或IP地址,如果不是通用端口,还包含该端口号,例如:Host: www.some.com:182
  • Proxy-Authorization:连接到某个代理时使用的身份认证信息,跟Authorization头差不多。例如:Proxy-Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
  • User-Agent:通常就是用户的浏览器相关信息。例如:User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:12.0) Gecko/20100101 Firefox/12.0
  • Warning:记录一些警告信息。

参考