目录名称

并发理论基础

请你说说线程和进程的区别

进程:操作系统进行资源调度和分配的基本单位(例如浏览器,APP,JVM)。

线程:进程中的最小执行单位(可以理解为一个顺序的执行流)。

alt

说明:同一个进程内的多个线程共享资源。

  1. 进程有独立的地址空间,线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间;
  2. 进程和线程切换时,需要切换进程和线程的上下文,进程的上下文切换时间开销远远大于线程上下文切换时间,耗费资源较大,效率要差一些;
  3. 进程的并发性较低,线程的并发性较高;
  4. 系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了 CPU 外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源;

得分点: 地址空间、开销、并发性、内存

如何理解并行和并发的区别

并发:多线程抢占CPU,可能不同时执行,侧重于多个任务交替执行。

alt

现在的操作系统无论是windows,linux还是macOS等其实都是多用户多任务分时操作系统,使用这些操作系统的的用户可以“同时”干多件事情。但实际上,对于单机CPU的计算机而言,在同一时间只能干一件事,为了看起来像是“同时干多件事”分时操作系统把CPU的时间划分成了长短进本相同的时间区间,即“时间片”,通过操作系统的管理,把时间片依次轮流的分配给各个线程任务使用。我们看似的“同时干多件事”,其实是通过CPU时间片技术并发完成的。例如:多个线程并发使用一个CPU资源并发执行任务的线程时序图。

alt

并行:线程可以不共享CPU,可每个线程一个CPU同时执行多个任务。

alt

总之:个人认为并行只出现在多CPU或多核CPU中,而并发可理解为并行中的一个子集。

并发是指一个处理器同时处理多个任务。

并行是指多个处理器或者是多核的处理器同时处理多个不同的任务。

多线程的出现是要解决什么问题?本质什么?

CPU、内存、I/O 设备的速度是有极大差异的,为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系结构、操作系统、编译程序都做出了贡献,主要体现为:

  • CPU 增加了缓存,以均衡与内存的速度差异;// 导致可见性问题
  • 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异;// 导致原子性问题
  • 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。// 导致有序性问题

Java是如何解决并发问题的?

理解的第一个维度:核心知识点

JMM本质上可以理解为,Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法。具体来说,这些方法包括:

  • volatile、synchronized 和 final 三个关键字
  • Happens-Before 规则

理解的第二个维度:可见性,有序性,原子性

  1. 原子性 在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。 请分析以下哪些操作是原子性操作:
x = 10;        //语句1: 直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中
y = x;         //语句2: 包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。
x++;           //语句3: x++包括3个操作:读取x的值,进行加1操作,写入新的值。
x = x + 1;     //语句4: 同语句3

上面4个语句只有语句1的操作具备原子性。 也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。

从上面可以看出,Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。

  1. 可见性 Java提供了volatile关键字来保证可见性。

当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

  1. 有序性

在Java里面,可以通过volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。当然JMM是通过Happens-Before 规则来保证有序性的。

线程有哪几种状态?/如何理解线程的生命周期及状态变化?

一个线程从创建,运行,到最后销毁的这个过程称之为线程的生命周期,在这个生命周期过程中线程可能会经历如下几个状态:

alt

这些状态可归纳为:状态分别为新建状态,就绪状态,运行状态,阻塞状态,死亡状态。

  1. 新建(New)

创建后尚未启动。

  1. 可运行(Runnable)

可能正在运行,也可能正在等待 CPU 时间片。

包含了操作系统线程状态中的 Running 和 Ready。

  1. 阻塞(Blocking)

等待获取一个排它锁,如果其线程释放了锁就会结束此状态。

  1. 无限期等待(Waiting)

等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。

进入方法 退出方法
没有设置 Timeout 参数的 Object.wait() 方法 Object.notify() / Object.notifyAll()
没有设置 Timeout 参数的 Thread.join() 方法 被调用的线程执行完毕
LockSupport.park() 方法 -
  1. 限期等待(Timed Waiting)

无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。

调用 Thread.sleep() 方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。

调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述。

睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态。

阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁。而等待是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入。

进入方法 退出方法
Thread.sleep() 方法 时间结束
设置了 Timeout 参数的 Object.wait() 方法 时间结束 / Object.notify() / Object.notifyAll()
设置了 Timeout 参数的 Thread.join() 方法 时间结束 / 被调用的线程执行完毕
LockSupport.parkNanos() 方法 -
LockSupport.parkUntil() 方法 -
  1. 死亡(Terminated)

可以是线程结束任务之后自己结束,或者产生了异常而结束。

通常线程有哪几种使用方式?

目前获取线程有四种方式

  • 实现Runnable接口(无返回值)
  • 实现Callable接口(有返回值)
  • 实例化Thread类
  • 使用线程池获取

实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。

基础线程机制有哪些?

  • Executor

Executor 管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。这里的异步是指多个任务的执行互不干扰,不需要进行同步操作。

主要有三种 Executor:

  1. CachedThreadPool: 一个任务创建一个线程;
  2. FixedThreadPool: 所有任务只能使用固定大小的线程;
  3. SingleThreadExecutor: 相当于大小为 1 的 FixedThreadPool。
  • Daemon

守护线程是程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分。

当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程。

main() 属于非守护线程。使用 setDaemon() 方法将一个线程设置为守护线程。

  • sleep()

Thread.sleep(millisec) 方法会休眠当前正在执行的线程,millisec 单位为毫秒。

sleep() 可能会抛出 InterruptedException,因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。

  • yield()

对静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。

alt

说说你了解的线程同步方式

线程的中断方式有哪些?

一个线程执行完毕之后会自动结束,如果在运行过程中发生异常也会提前结束。

  • InterruptedException

通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。

对于以下代码,在 main() 中启动一个线程之后再中断它,由于线程中调用了 Thread.sleep() 方法,因此会抛出一个 InterruptedException,从而提前结束线程,不执行之后的语句。

public class InterruptExample {

    private static class MyThread1 extends Thread {
        @Override
        public void run() {
            try {
                Thread.sleep(2000);
                System.out.println("Thread run");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new MyThread1();
        thread1.start();
        thread1.interrupt();
        System.out.println("Main run");
    }
}

Main run
java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at InterruptExample.lambda$main$0(InterruptExample.java:5)
    at InterruptExample$$Lambda$1/713338599.run(Unknown Source)
    at java.lang.Thread.run(Thread.java:745)

  • interrupted()

如果一个线程的 run() 方法执行一个无限循环,并且没有执行 sleep() 等会抛出 InterruptedException 的操作,那么调用线程的 interrupt() 方法就无法使线程提前结束。

但是调用 interrupt() 方法会设置线程的中断标记,此时调用 interrupted() 方法会返回 true。因此可以在循环体中使用 interrupted() 方法来判断线程是否处于中断状态,从而提前结束线程。

  • Executor 的中断操作

调用 Executor 的 shutdown() 方法会等待线程都执行完毕之后再关闭,但是如果调用的是 shutdownNow() 方法,则相当于调用每个线程的 interrupt() 方法。

线程之间有哪些协作方式?

当多个线程可以一起工作去解决某个问题时,如果某些部分必须在其它部分之前完成,那么就需要对线程进行协调。

  • join()

在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。

对于以下代码,虽然 b 线程先启动,但是因为在 b 线程中调用了 a 线程的 join() 方法,b 线程会等待 a 线程结束才继续执行,因此最后能够保证 a 线程的输出先于 b 线程的输出。

public class JoinExample {

    private class A extends Thread {
        @Override
        public void run() {
            System.out.println("A");
        }
    }

    private class B extends Thread {

        private A a;

        B(A a) {
            this.a = a;
        }

        @Override
        public void run() {
            try {
                a.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("B");
        }
    }

    public void test() {
        A a = new A();
        B b = new B(a);
        b.start();
        a.start();
    }
}

public static void main(String[] args) {
    JoinExample example = new JoinExample();
    example.test();
}

  • wait() notify() notifyAll()

调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。

它们都属于 Object 的一部分,而不属于 Thread。

只能用在同步方法或者同步控制块中使用,否则会在运行时抛出 IllegalMonitorStateExeception。

使用 wait() 挂起期间,线程会释放锁。这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程,造成死锁。

wait()和sleep()的区别

  • wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法;
  • wait() 会释放锁,sleep() 不会。
  • await() signal() signalAll()

java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。

为什么我们调用start()方法时会执行run()方法,为什么我们不能直接调用run()方法?

这是另一个非常经典的java多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来!

new一个Thread,线程进入了新建状态;调用start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start()会执行线程的相应准备工作,然后自动执行run()方法的内容,这是真正的多线程工作。 而直接执行run()方法,会把run方法当成一个main线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

总结: 调用start方法方可启动线程并使线程进入就绪状态,而run方法只是thread的一个普通方法调用,还是在主线程里执行。

并发关键字

说说synchronize的用法及原理

Synchronized可以作用在哪里?

  • 对象锁
  • 方法锁
  • 类锁

原理

synchronized: 底层使用指令码方式来控制锁的,映射成字节码指令就是增加来两个指令:monitorenter和monitorexit。当线程执行遇到monitorenter指令时会尝试获取内置锁,如果获取锁则锁计数器+1,如果没有获取锁则阻塞;当遇到monitorexit指令时锁计数器-1,如果计数器为0则释放锁。

Synchronized在使用时有何注意事项?

  • 锁对象不能为空,因为锁的信息都保存在对象头里
  • 作用域不宜过大,影响程序执行的速度,控制范围过大,编写代码也容易出错
  • 避免死锁
  • 在能选择的情况下,既不要用Lock也不要用synchronized关键字,用java.util.concurrent包中的各种各样的类,如果不用该包下的类,在满足业务的情况下,可以使用synchronized关键,因为代码量少,避免出错

Synchronized由什么样的缺陷? Java Lock是怎么弥补这些缺陷的?

  • synchronized的缺陷
  1. 效率低:锁的释放情况少,只有代码执行完毕或者异常结束才会释放锁;试图获取锁的时候不能设定超时,不能中断一个正在使用锁的线程,相对而言,Lock可以中断和设置超时
  2. 不够灵活:加锁和释放的时机单一,每个锁仅有一个单一的条件(某个对象),相对而言,读写锁更加灵活
  3. 无法知道是否成功获得锁,相对而言,Lock可以拿到状态
  • Lock解决相应问题

Lock类这里不做过多解释,主要看里面的4个方法:

  1. lock(): 加锁
  2. unlock(): 解锁
  3. tryLock(): 尝试获取锁,返回一个boolean值
  4. tryLock(long,TimeUtil): 尝试获取锁,可以设置超时

Synchronized只有锁只与一个条件(是否获取锁)相关联,不灵活,后来Condition与Lock的结合解决了这个问题。

多线程竞争一个锁时,其余未得到锁的线程只能不停的尝试获得锁,而不能中断。高并发的情况下会导致性能下降。ReentrantLock的lockInterruptibly()方法可以优先考虑响应中断。 一个线程等待时间过长,它可以中断自己,然后ReentrantLock响应这个中断,不再让这个线程继续等待。有了这个机制,使用ReentrantLock时就不会像synchronized那样产生死锁了。

synchronized和Lock有什么区别

  • 存在层次上

synchronized: Java的关键字,在jvm层面上

Lock: 是一个接口

  • 锁的释放

synchronized: 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁

Lock: 在finally中必须释放锁,不然容易造成线程死锁

  • 锁的获取

synchronized: 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待

Lock: 分情况而定,Lock有多个锁获取的方式,大致就是可以尝试获得锁,线程可以不用一直等待(可以通过tryLock判断有没有锁)

  • 锁的释放(死锁产生)

synchronized: 在发生异常时候会自动释放占有的锁,因此不会出现死锁

Lock: 发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生

  • 锁的状态

synchronized: 无法判断

Lock: 可以判断

  • 锁的类型

synchronized: 可重入 不可中断 非公平

Lock: 可重入 可判断 可公平(两者皆可)

  • 性能

synchronized: 少量同步

Lock: 大量同步

Lock可以提高多个线程进行读操作的效率。(可以通过readwritelock实现读写分离) 在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态;

ReentrantLock提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。在资源竞争不激烈的情形下,性能稍微比synchronized差点点。但是当同步非常激烈的时候,synchronized的性能一下子能下降好几十倍。而ReentrantLock确还能维持常态。

  • 调度

synchronized: 使用Object对象本身的wait 、notify、notifyAll调度机制

Lock: 可以使用Condition进行线程之间的调度

  • 用法

synchronized: 在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。

Lock: 一般使用ReentrantLock类做为锁。在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。

  • 底层实现

synchronized: 底层使用指令码方式来控制锁的,映射成字节码指令就是增加来两个指令:monitorenter和monitorexit。当线程执行遇到monitorenter指令时会尝试获取内置锁,如果获取锁则锁计数器+1,如果没有获取锁则阻塞;当遇到monitorexit指令时锁计数器-1,如果计数器为0则释放锁。

Lock: 底层是CAS乐观锁,依赖AbstractQueuedSynchronizer类,把所有的请求线程构成一个CLH队列。而对该队列的操作均通过Lock-Free(CAS)操作。

说说volatile的用法及原理

说说你对AQS的理解

Java哪些地方使用了CAS

线程安全问题

如何理解线程安全与不安全?

  • 多个线程并发执行时,仍旧能够保证数据的正确性,这种现象称之为线程安全。
  • 多个线程并发执行时,不能能够保证数据的正确性,这种现象称之为线程不安全。

案例分享:如何保证12306中的订票操作的安全。

第一步:编写售票任务类:

class TicketTask implements Runnable{
	   int ticket=10;
	   @Override
	   public void run() {
		    doTicket();
	   }
	   public void doTicket() {
		  while(true) {
			  if(ticket<=0)break;
			  System.out.println(ticket--);
		  }
	   }
   }

第二步:编写售票测试方法:

public static void main(String[] args) {
	     TicketTask task=new TicketTask();
	     Thread t1=new Thread(task);
	     Thread t2=new Thread(task);
	     Thread t3=new Thread(task);
	     
	     t1.start();
	     t2.start();
	     t3.start();  
   }

导致线程不安全的因素有哪些?

  1. 多个线程并发执行。
  2. 多个线程并发执行时存在共享数据集(临界资源)。
  3. 多个线程在共享数据集上的操作不是原子操作。

例如:现有一生产者消费者模型,生产者和消费者并发操作容器对象。 alt

线程安全有哪些实现思路

  1. 互斥同步

synchronized 和 ReentrantLock。对共享进行限制访问.

  1. 非阻塞同步

互斥同步最主要的问题就是线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。

互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,那就肯定会出现问题。无论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。

  • CAS

随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略: 先进行操作,如果没有其它线程争用共享数据,那操作就成功了,否则采取补偿措施(不断地重试,直到成功为止)。这种乐观的并发策略的许多实现都不需要将线程阻塞,因此这种同步操作称为非阻塞同步。

乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。硬件支持的原子性操作最典型的是: 比较并交换(Compare-and-Swap,CAS)。

CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B。

  • AtomicInteger

J.U.C 包里面的整数原子类 AtomicInteger,其中的 compareAndSet() 和 getAndIncrement() 等方法都使用了 Unsafe 类的 CAS 操作。

  1. 无同步方案-取消共享,每个线程一个对象实例

要保证线程安全,并不是一定就要进行同步。如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性。

  • 栈封闭

多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有的。

  • 线程本地存储(Thread Local Storage)

如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。

说明:Java中的线程安全问题的主要关注点有3个:可见性,有序性,原子性; Java内存模型(JMM)解决了可见性和有序性问题,而JUC解决了原子性问题。

请你说说乐观锁和悲观锁

悲观锁

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。

乐观锁

总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。

假设不会发生冲突,只在提交操作时检查是否违反数据完整性,例如java中可借助CAS( Compare And Swap)算法实现(此算法依赖硬件CPU)。

乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

两种锁的使用场景

从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。

1)悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。

2)乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升

悲观锁&乐观锁应用案例分析

悲观锁实现计数器:

方案1:

class Counter{
		private int count;
		public synchronized int count() {
			count++;
			return count;
		}
}

方案2:

class Counter{
		private int count;
		private Lock lock=new ReentrantLock();
		public  int count() {
			lock.lock();
			try {
			 count++;
             return count;
			}finally {
			 lock.unlock();
			}
		}
	}

乐观锁实现计数器:

class Counter{
	private AtomicInteger at=new AtomicInteger();
	public  int count() {
		return at.incrementAndGet();
	}
}

其中 AtomicInteger 是基于CAS算法实现。

乐观锁的两种实现方式

  1. 版本号机制

一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

举一个简单的例子: 假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。

  1. 操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 5050( 100-$50 )。
  2. 在操作员 A 操作的过程中,操作员B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 2020 ( 100-$20 )。
  3. 操作员 A 完成了修改工作,将数据版本号加一( version=2 ),连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。
  4. 操作员 B 完成了操作,也将版本号加一( version=2 )试图向数据库提交数据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。

这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员A 的操作结果的可能。

  1. CAS算法

compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数

  • 需要读写的内存值 V
  • 进行比较的值 A
  • 拟写入的新值 B

当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试

乐观锁的缺点

  1. ABA问题

如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。

JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

  1. 循环时间长开销大

自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。 如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

  1. 只能保证一个共享变量的原子操作

CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作。

CAS与synchronized的使用情景

简单的来说CAS适用于写比较少的情况下(多读场景,冲突一般较少),synchronized适用于写比较多的情况下(多写场景,冲突一般较多)

  1. 对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
  2. 对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。

补充: Java并发编程这个领域中synchronized关键字一直都是元老级的角色,很久之前很多人都会称它为 “重量级锁” 。但是,在JavaSE 1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的 偏向锁轻量级锁 以及其它各种优化之后变得在某些情况下并不是那么重了。synchronized的底层实现主要依靠 Lock-Free 的队列,基本思路是 自旋后阻塞竞争切换后继续竞争锁稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。

如何理解线程的上下文切换?

一个线程得到CPU执行的时间是有限的。当此线程用完为其分配的CPU时间以后,cpu会切换到下一个线程执行。但是在这之前,线程需要将当前的状态进行保存,以便下次再次获得CPU时间片时可以加载对应的状态以继续执行剩下的任务。而这个切换过程是需要耗费时间的,会影响多线程程序的执行效率,所以在在使用多线程时要减少线程的频繁切换。那如何实现呢?

减少多线程上下文切换的方案如下:

  • 无锁并发编程:锁的竞争会带来线程上下文的切换
  • CAS算法:CAS算法在数据更新方面,可以达到锁的效果
  • 使用最少线程:避免不必要的线程等待
  • 使用协程:单线程完成多任务的调度和切换,避免多线程

说说死锁定义及发生的的条件

定义: 当两个线程被阻塞, 每个线程在等待另一个线程时就发生死锁.

多个线程互相等待已经被对方线程正在占用的锁,导致陷入彼此等待对方释放锁的状态,这个过程称之为死锁。

产生死锁的条件:

  1. 互斥条件---资源竞争: 一个资源每次只能被一个进程使用.
  2. 请求与保持条件: 一个进程因请求资源而阻塞时, 对已获取的资源保持不放.
  3. 不剥夺条件: 进程已获得的资源, 在未使用完之前, 不能强行剥夺.
  4. 循环等待条件: 若干进程之间形成一种头尾相接的循环等待资源关系.

如何避免死锁: 破坏其中的一个条件即可.

如何避免死锁呢?

  • 避免一个线程中同时获取多个锁
  • 避免一个线程在一个锁中获取其他的锁资源
  • 考虑使用定时锁来替换内部锁机制,如lock.tryLock(timeout)。

可能出现死锁的案例分享

class SyncThread implements Runnable {
	private Object obj1;
	private Object obj2;
	public SyncThread(Object o1, Object o2) {
		this.obj1 = o1;
		this.obj2 = o2;
	}
	@Override
	public void run() {
		synchronized (obj1) {
			work();
			synchronized (obj2) {
				work();
			}
		}
	}
	private void work() {
		try {Thread.sleep(30000);} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

死锁测试

public class TestDeadLock01 {
	public static void main(String[] args)throws Exception {
		 Object obj1 = new Object();
		 Object obj2 = new Object();
		 Thread t1 = new Thread(new SyncThread(obj1, obj2), "t1");
		 Thread t2 = new Thread(new SyncThread(obj2, obj1), "t2");
		 t1.start();
		 t2.start();
	}
}

线程通讯与进程通讯应用增强

请你说说线程和进程间的通信方式

进程与线程的区别是什么?

线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。

与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

另外,也正是因为共享资源,所以线程中执行时一般都要进行同步和互斥。总的来说,进程和线程的主要差别在于它们是不同的操作系统资源管理方式。

  • 线程通讯:java中的多线程通讯主要是共享内存(变量)等方式。
  • 进程通讯:java中进程通讯(IPC)主要是Socket,MQ等。

alt

线程间的通信方式:

1、锁机制

  • 互斥锁:提供了以排它方式阻止数据结构被并发修改的方法。
  • 读写锁:允许多个线程同时读共享数据,而对写操作互斥。
  • 条件变量:可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。

2、信号量机制:包括无名线程信号量与有名线程信号量

3、信号机制:类似于进程间的信号处理。

线程间通信的主要目的是用于线程同步,所以线程没有象进程通信中用于数据交换的通信机制。

基于wait/nofity/notifyall实现

wait()/notify()/notifyall()方法定义说明:

  • Wait:阻塞正在使用监视器对象的线程,同时释放监视器对象
  • notify: 唤醒在监视器对象上等待的单个线程,但不释放监视器对象,此时调用该方法的代码继续执行,直到执行结束才释放对象锁
  • notifyAll: 唤醒在监视器对象上等待的所有线程,但不释放监视器对象,此时调用该方法的代码继续执行,直到执行结束才释放对象锁

wait()/notify()/notifyall()方法应用说明

1) 这些方法必须应用在同步代码块或同步方法中 2) 这些方法必须由监视器对象(对象锁)调用

说明:使用wait/notify/notifyAll的作用一般是为了避免轮询带来的性能损失。

wait()/notify()/notifyall()应用案例实现:

手动实现阻塞式队列,并基于wait()/notifyAll()方法实现实现线程在队列上的通讯。

/**
 * 有界消息队列:用于存取消息
 * 1)数据结构:数组(线性结构)
 * 2)具体算法:FIFO(先进先出)-First in First out
 */
public class BlockContainer<T> {//类泛型 
	/**用于存储数据的数组*/
	private Object[] array;
	/**记录有效元素个数*/
	private int size;
	public BlockContainer () {
		this(16);//this(参数列表)表示调用本类指定参数的构造函数
	}
    public BlockContainer (int cap) {
		array=new Object[cap];//每个元素默认值为null
	}
}

向容器添加put方法,用于放数据。

  /**
     * 生产者线程通过put方法向容器放数据
     * 数据永远放在size位置
     * 说明:实例方法内部的this永远指向
     * 调用此方法的当前对象(当前实例)
     * 注意:静态方法中没有this,this只能
     * 应用在实例方法,构造方法,实例代码块中
     */
    public synchronized void put(T t){//同步锁:this
    	//1.判定容器是否已满,满了则等待
    	while(size==array.length)
    	try{this.wait();}catch(Exception e){}
    	//2.放数据
    	array[size]=t;
    	//3.有效元素个数加1
    	size++;
    	//4.通知消费者取数据
    	this.notifyAll();
    }

向容器类添加take方法,用于从容器取数据。

/**
     * 消费者通过此方法取数据
     * 位置:永远取下标为0的位置的数据
     * @return
     */
    @SuppressWarnings("unchecked")
	public synchronized T take(){
    	//1.判定容器是否为空,空则等待
    	while(size==0)
    	try{this.wait();}catch(Exception e){}
    	//2.取数据
    	Object obj=array[0];
    	//3.移动元素
    	System.arraycopy(
    			array,//src 原数组
    			1, //srcPos 从哪个位置开始拷贝
    			array,  //dest 放到哪个数组
    			0, //destPost 从哪个位置开始放
    			size-1);//拷贝几个
    	//4.有效元素个数减1
    	size--;
    	//5.将size位置为null
    	array[size]=null;
    	//6.通知生产者放数据
    	this.notifyAll();//通知具备相同锁对象正在wait线程
    	return (T)obj;
    }

基于Condition实现

Condition 类定义说明

Condition 是一个用于多线程间协同的工具类,基于此类可以方便的对持有锁的线程进行阻塞或唤醒阻塞的线程。它的强大之处在于它可以为多个线程间建立不同的Condition,通过signal()/signalall()方法指定要唤醒的不同线程。

Condition 类应用说明

  1. 基于Lock对象获取Condition对象
  2. 基于Condition对象的await()/signal()/signalall()方法实现线程阻塞或唤醒。

Condition类对象的应用案例实现:

手动实现阻塞式队列,并基于wait()/notifyAll()方法实现实现线程在队列上的通讯。

/**
 * 有界消息队列:用于存取消息
 * 1)数据结构:数组(线性结构)
 * 2)具体算法:FIFO(先进先出)-First in First out
 */
public class BlockContainer<T> {//类泛型 
	
	/**用于存储数据的数组*/
	private Object[] array;
	/**记录有效元素个数*/
	private int size;
	public BlockContainer() {
		this(16);//this(参数列表)表示调用本类指定参数的构造函数
	}
    public BlockContainer(int cap) {
		array=new Object[cap];//每个元素默认值为null
	}
    //JDK1.5以后引入的可重入锁(相对于synchronized灵活性更好)
    private ReentrantLock lock=new ReentrantLock(true);// true表示使用公平锁,默认是非公平锁
    private Condition producerCondition=lock.newCondition();//通讯条件
private Condition consumerCondition=lock.newCondition();//通讯条件
}

向容器中添加put方法,用于向容器放数据

   /**
     * 生产者线程通过put方法向容器放数据
     * 数据永远放在size位置
     * 说明:实例方法内部的this永远指向
     * 调用此方法的当前对象(当前实例)
     * 注意:静态方法中没有this,this只能
     * 应用在实例方法,构造方法,实例代码块中
     */
    public void put(T t){//同步锁:this
    	System.out.println("put");
    	lock.lock();
    	try{
    	//1.判定容器是否已满,满了则等待
    	while(size==array.length)
        //等效于Object类中的wait方法
    	try{producerCondition.await();}catch(Exception e){e.printStackTrace();}
    	//2.放数据
    	array[size]=t;
    	//3.有效元素个数加1
    	size++;
    	//4.通知消费者取数据
    	consumerCondition.signalAll();//等效于object类中的notifyall()
    	}finally{
    	lock.unlock();
    	}
}

在容器类中添加take方法用于从容器取数据

  /**
     * 消费者通过此方法取数据
     * 位置:永远取下标为0的位置的数据
     * @return
     */
    @SuppressWarnings("unchecked")
	public  T take(){
    	System.out.println("take");
    	lock.lock();
    	try{
    	//1.判定容器是否为空,空则等待
    	while(size==0)
    	try{consumerCondition.await();}catch(Exception e){}
    	//2.取数据
    	Object obj=array[0];
    	//3.移动元素
    	System.arraycopy(
    			array,//src 原数组
    			1, //srcPos 从哪个位置开始拷贝
    			array,  //dest 放到哪个数组
    			0, //destPost 从哪个位置开始放
    			size-1);//拷贝几个
    	//4.有效元素个数减1
    	size--;
    	//5.将size位置为null
    	array[size]=null;
    	//6.通知生产者放数据
        producerCondition.signalAll();//通知具备相同锁对象正在wait线程
    	return (T)obj;
    	}finally{
    	lock.unlock();
    	}
    }

进程间通信:

  1. 管道(pipe):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有血缘关系的进程间使用。进程的血缘关系通常指父子进程关系。管道分为pipe(无名管道)和fifo(命名管道)两种,有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间通信。
  2. 信号量(semophore):信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它通常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  3. 消息队列(message queue):消息队列是由消息组成的链表,存放在内核中 并由消息队列标识符标识。消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。消息队列与管道通信相比,其优势是对每个消息指定特定的消息类型,接收的时候不需要按照队列次序,而是可以根据自定义条件接收特定类型的消息。
  4. 信号(signal):信号是一种比较复杂的通信方式,用于通知接收进程某一事件已经发生。
  5. 共享内存(shared memory):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问,共享内存是最快的IPC方式,它是针对其他进程间的通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量配合使用,来实现进程间的同步和通信。
  6. 套接字(socket):socket,即套接字是一种通信机制,凭借这种机制,客户/服务器(即要进行通信的进程)系统的开发工作既可以在本地单机上进行,也可以跨网络进行。也就是说它可以让不在同一台计算机但通过网络连接计算机上的进程进行通信。也因为这样,套接字明确地将客户端和服务器区分开来。

1)管道(Pipe):管道可用于具有亲缘关系进程间的通信,允许一个进程和另一个与它有共同祖先的进程之间进行通信。管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有血缘关系的进程间使用。进程的血缘关系通常指父子进程关系。管道分为pipe(无名管道)和fifo(命名管道)两种,有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间通信。

(2)命名管道(named pipe):命名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关 系 进程间的通信。命名管道在文件系统中有对应的文件名。命名管道通过命令mkfifo或系统调用mkfifo来创建。

(3)信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数)。

(4)消息(Message)队列:消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺.消息队列是由消息组成的链表,存放在内核中 并由消息队列标识符标识。消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。消息队列与管道通信相比,其优势是对每个消息指定特定的消息类型,接收的时候不需要按照队列次序,而是可以根据自定义条件接收特定类型的消息。

(5)共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。

(6)内存映射(mapped memory):内存映射允许任何多个进程间通信,每一个使用该机制的进程通过把一个共享的文件映射到自己的进程地址空间来实现它。

(7)信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。

(8)套接口(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和System V的变种都支持套接字。

基于BIO实现的简易server服务器

public class BioMainServer01 {
    private Logger log=LoggerFactory.getLogger(BioMainServer01.class);	
	private ServerSocket server;
	private volatile boolean isStop=false;
	private int port;
	public BioMainServer01(int port) {
		this.port=port;
	}
	public void doStart()throws Exception {
		server=new ServerSocket(port);
		while(!isStop) {
			Socket socket=server.accept();
			log.info("client connect");
			doService(socket);
		}
		server.close();
	}
	public void doService(Socket socket) throws Exception{
		InputStream in=socket.getInputStream();
		byte[] buf=new byte[1024];
		int len=-1;
		while((len=in.read(buf))!=-1) {
			String content=new String(buf,0,len);
			log.info("client say {}", content);
		}
		in.close();
		socket.close();
	}
	public void doStop() {
		isStop=false;
	}
	public static void main(String[] args)throws Exception {
		BioMainServer01 server=new BioMainServer01(9999);
		server.doStart();
	}
}

启动服务,然后打开浏览器进行访问或者通过如下客户端端访问

public class BioMainClient {
	public static void main(String[] args) throws Exception{
		Socket socket=new Socket();
		socket.connect(new InetSocketAddress("127.0.0.1", 9999));
		OutputStream out=socket.getOutputStream();
		Scanner sc=new Scanner(System.in);
		System.out.println("client input:");
		out.write(sc.nextLine().getBytes());
		out.close();
		sc.close();
		socket.close();
	}
}

JUC全局观/请你说说JUC

JUC框架包含五个部分.

alt

主要包含: (注意: 上图是网上找的图,无法表述一些继承关系,同时少了部分类;但是主体上可以看出其分类关系也够了)

  • Lock框架和Tools类(把图中这两个放到一起理解)
  • Collections: 并发集合
  • Atomic: 原子类
  • Executors: 线程池

Lock框架和Tools哪些核心的类?

alt

JUC并发集合哪些核心的类?

alt

JUC原子类哪些核心的类?

JUC线程池哪些核心的类?

alt

JUC原子类

Java中的无锁对象应用?

我们先看个案例,关键代码如下:

int count=1;
int count() {
		return count++;
}

其中:count() 这个方法不是线程安全的,问题就出在变量count的可见性和count++的原子性上。可见性问题可以用volatile来解决,而原子性问题我们前面一直都是采用的互斥锁方案。但这种方案,性能上会有一定的损失。其实对于简单的原子性问题,还有一种无锁方案。JUC并发包将这种无锁方案封装提炼之后,实现了一系列的原子类。

AtomicLong 类型对象案例应用:

AtomicLong al=new AtomicLong(1);
	long atomicCount(){
		return al.getAndIncrement();
	}



AtomicLong atomicLong=new AtomicLong(0);
		LongStream.range(0, 1000)
		          .parallel()
		          .forEach((t)->atomicLong.incrementAndGet());
		System.out.println(atomicLong.get());

无锁方案相对互斥锁方案,最大的好处就是性能。互斥锁方案为了保证互斥性,需要执行加锁、 解锁操作,而加锁、解锁操作本身就消耗性能。同时拿不到锁的线程还会进入阻塞状态,进而触发线程切换,线程切换对性能的消耗也很大。 相比之下,无锁方案则完全没有加锁、解锁的性能消耗,可谓绝佳方案。

Java中的无锁对象原理分析?

无锁化实现的原理其实也很简单,硬件支持而已。CPU 为了解决并发问题,提供了 CAS 指令(CAS,全称是 Compare And Swap,即“比较并交换”)。CAS 指令包含 3 个参数:共享变量的内存地址 A、用于比较的值 B 和共享变量的新值 C;并且只有当内存中地址 A 处的值等于 B时,才能将内存中地址 A 处的值更新为新值 C。作为一条 CPU 指令,CAS 指令本身是能够保证原子性的。

简易的CAS算法的实现,关键代码如下:

class SimulatedCAS{
	int count;
	synchronized int cas(int expect, int newValue){
		// 读⽬前 count 的值
		int curValue = count;
		// ⽐较⽬前 count 值是否 == 期望值
		if(curValue == expect){
			// 如果是,则更新 count 的值
			count = newValue;
		}
		// 返回写⼊前的值
		return curValue;
	}
}

说明:原子类的方法都是针对一个共享变量的,如果你需要解决多个变量的原子性问题,建议还是使用互斥锁方案。

Java中的无锁对象问题分析?

Java中的无锁方案相对于互斥锁方案,优点非常多,首先性能好,其次是基本不会出现死锁问题,但也会有一些问题,例如:

(1)ABA问题。 对于一个旧的变量值A,线程2将A的值改成B又改成可A,此时线程1通过CAS看到A并没有变化,但实际A已经发生了变化,这就是ABA问题。解决这个问题的方法很简单,记录一下变量的版本就可以了,在变量的值发生变化时,对应的版本也做出相应的变化,然后CAS操作时比较一下版本就知道变量有没有发生变化。此时可借助在java的atomic包下的AtomicStampedReference类进行实现。

(2)自旋问题。 无锁对象会多次尝试CAS操作直至成功或失败,这个过程叫做自旋。通过自旋的过程我们可以看出自旋操作不会将线程挂起,从而避免了内核线程切换,但是自旋的过程也可以看做CPU死循环,会一直占用CPU资源。这种情形在单CPU的机器上是不能容忍的,因此自旋一般都会有个次数限制,即超过这个次数后线程就会放弃时间片,等待下次机会。因此自旋操作在资源竞争不激烈的情况下确实能提高效率,但是在资源竞争特别激烈的场景中,CAS操作会的失败率就会大大提高,这时使用中重量级锁的效率可能会更高。当前,也可以使用LongAdder类来替换,它则采用了分段锁的思想来解决并发竞争的问题。

LongStream.range(0, 1000)
		          .parallel()
		          .forEach((t)->longAdder.increment());
		System.out.println(longAdder.sumThenReset());

JUC锁

Lock接口及ReentrantLock对象分析及应用?

并发编程领域,有两大核心问题:一个是互斥,即同一时刻只允许一个线程访问共享资源;另一个是同步,即线程之间如何通信、协作。这两大问题,在Java SDK 并发包可通过 Lock 和 Condition 两个接口来实现,其中Lock 用于解决互斥问题,Condition 用于解决同步问题。Java SDK 并发包里的 Lock 接口中,不仅有支持类似 synchronized 的隐式加锁方法,还支持超时、非阻塞、可中断的方式获取锁, 这三种方式为我们编写更加安全、健壮的并发程序提供了很大的便利。我们来一起看看Lock接口常用方法,关键方法如下:

1)void lock() 获取锁对象,优先考虑是锁的获取,而非中断。

2)void lockInterruptibly() 获取锁,但优先响应中断而非锁的获取。

3)boolean tryLock() 试图获取锁。

4)boolean tryLock(long timeout, TimeUnit timeUnit) 试图获取锁,并设置等待时长。

5)void unlock()释放锁对象。

Java SDK 并发包里的ReentrantLock实现了Lock接口,是一个可重入的互斥锁(“独占锁”), 同时提供了”公平锁”和”非公平锁”的支持。所谓公平锁和非公平锁其含义如下:

1)公平锁:在多个线z程争用锁的情况下,公平策略倾向于将访问权授予等待时间最长的线程。也就是说,相当于有一个线程等待队列,先进入等待队列的线程后续会先获得锁。

2)非公平锁:在多个线程争用锁的情况下,能够最终获得锁的线程是随机的(由底层OS调度)。

ReetrantLock简易应用如下(默认为非公平策略)。

class Counter{
    ReentrantLock lock = new ReentrantLock();
    int count = 0;
	void increment() {
	       lock.lock();
	    try {
	        count++;
	    } finally {
	        lock.unlock();
	}
}

其中,这里的锁通过lock()方法获取锁,通过unlock()方法释放锁。重要的是将代码包装成try/finally块,以确保在出现异常时解锁。这个方法和synchronized关键字修饰的方法一样是线程安全的。在任何给定的时间,只有一个线程可以持有锁。

ReetrantLock对象在构建时,可以基于ReentrantLock(boolean fair)构造方法参数,设置对象的”公平锁”和”非公平锁”特性。其中fair的值true表示“公平锁”。这种公平锁,会影响其性能,但是在一些公平比效率更加重要的场合中公平锁特性就会显得尤为重要。关键代码示例如下:

public void performFairLock(){
		    //...
		  ReentrantLock lock = new ReentrantLock(true);
		   try {
		            //Critical section here
		        } finally {
		            lock.unlock();
		        }
		    //...
}

ReetrantLock对象在基于业务获取锁时,假如希望有等待时间,可以借助tryLock实现,关键代码示例如下:

 public void performTryLock(){
		    //...
		    boolean isLockAcquired = lock.tryLock(1, TimeUnit.SECONDS);
		     
		    if(isLockAcquired) {
		        try {
		            //Critical section here
		        } finally {
		            lock.unlock();
		        }
		    }
		    //...
		}

“锁”是为了保护竞争资源,防止多个线程同时操作线程而出错,ReentrantLock在同一个时间点只能被一个线程获取(当某线程获取到“锁”时,其它线程就必须等待);ReentrantLock是通过一个FIFO的等待队列来管理获取该锁所有线程的。在“公平锁”的应用机制下,线程依次排队获取锁;而“非公平锁”,在锁是可获取状态时,不管自己是不是在队列的开头都会获取锁。

谈谈 synchronized 和 ReentrantLock 的区别

① 两者都是可重入锁

两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。

② synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API

synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。

③ ReentrantLock 比 synchronized 增加了一些高级功能

相比synchronized,ReentrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)

  • ReentrantLock提供了一种能够中断等待锁的线程的机制,通过 lock.lockInterruptibly() 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
  • ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
  • synchronized关键字与wait()和notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。

如果你想使用上述功能,那么选择ReentrantLock是一个不错的选择。

④ 两者的性能已经相差无几

在JDK1.6之前,synchronized 的性能是比 ReentrantLock 差很多。具体表示为:synchronized 关键字吞吐量岁线程数的增加,下降得非常严重。而ReentrantLock 基本保持一个比较稳定的水平。我觉得这也侧面反映了, synchronized 关键字还有非常大的优化余地。后续的技术发展也证明了这一点,我们上面也讲了在 JDK1.6 之后 JVM 团队对 synchronized 关键字做了很多优化。JDK1.6 之后,synchronized 和 ReentrantLock 的性能基本是持平了。所以网上那些说因为性能才选择 ReentrantLock 的文章都是错的!JDK1.6之后,性能已经不是选择synchronized和ReentrantLock的影响因素了!而且虚拟机在未来的性能改进中会更偏向于原生的synchronized,所以还是提倡在synchronized能满足你的需求的情况下,优先考虑使用synchronized关键字来进行同步!优化后的synchronized和ReentrantLock一样,在很多地方都是用到了CAS操作。

Condition接口对象分析与应用?

J.U.C包提供的Conditon接口,用以对原生的Object.wait()、Object.notify()进行增强。我们可以借助Condition对象,然后基于锁实现线程之间的通讯。

Condition相关方法介绍:

1)await()方法相当于Object的wait()方法。

2)signal()方法相当于Object的notify()方法。

3)signalAll()方法相当于Object的notifyAll()方。

Condition对象应用分析,基于Condition实现阻塞式栈对象。关键代码如下:

class BlockingStack{
    Stack<String> stack = new Stack<>();
    int CAPACITY = 5;
    ReentrantLock lock = new ReentrantLock();
    Condition stackEmptyCondition = lock.newCondition();
    Condition stackFullCondition = lock.newCondition();
 
    public void pushToStack(String item){
        try {
            lock.lock();
            while(stack.size() == CAPACITY) {
                stackFullCondition.await();
            }
            stack.push(item);
            stackEmptyCondition.signalAll();
        } finally {
            lock.unlock();
        }
    }
 
    public String popFromStack() {
        try {
            lock.lock();
            while(stack.size() == 0) {
                stackEmptyCondition.await();
            }
            return stack.pop();
        } finally {
            stackFullCondition.signalAll();
            lock.unlock();
        }
    }
}

Condition的强大之处在于它可以为多个线程间建立不同的Condition。我们知道对于栈而言,假设栈中数据已经满了,那么阻塞的肯定是写线程,唤醒的肯定是读线程,相反,阻塞的肯定是读线程,唤醒的肯定是写线程,那么假设只有一个Condition会有什么效果呢,缓存队列中已经存满,这个Lock不知道唤醒的是读线程还是写线程了,如果唤醒的是读线程,皆大欢喜,如果唤醒的是写线程,那么线程刚被唤醒,又被阻塞了,这时又去唤醒,这样就浪费了很多时间。

ReadWriteLock接口及实现类分析与应用?

ReadWriteLock是一个是读写锁接口。其中“读锁”又称“共享锁”,能同时被多个线程获取。“写锁”又称独占锁,只能被一个线程获取。读写锁与互斥锁的一个重要区别就是读写锁允许多个线程同时读共享变量,而互斥锁是不允许的,这是读写锁在读多写少的场景下性能优于互斥锁的关键。但读写锁的写操作是互斥的,当一个线程在写共享变量的时候,是不允许其他线程执行写操作和读操作的。

构建一个简易的线程安全的MapCache对象,并允许多个线程同时从cache读数据。具体应用案例分析如下:

class MapCache{
	private Map<String,Object> map=new HashMap<>();
	private ReadWriteLock readWriteLock=new ReentrantReadWriteLock();
	public void writeObject(String key,Object value){
		readWriteLock.writeLock().lock();
		try {
		 map.put(key, value);
		}finally {
		 readWriteLock.writeLock().unlock();
		}
	}
	public Object readObject(String key) {
		readWriteLock.readLock().lock();
		try {
		return map.get(key);
		}finally {
		readWriteLock.readLock().unlock();
		}
	}
}

ReentrantReadWriteLock可以让多个读线程可以同时持有读锁(只要写锁未被占用),而写锁是独占的。但是,假如读写锁使用不当,很容易产生“饥饿”问题:比如在读线程非常多,写线程很少的情况下,很容易导致写线程“饥饿”,虽然使用“公平”策略可以一定程度上缓解这个问题,但是“公平”策略是以牺牲系统吞吐量为代价的。

ReadWriteLock中的锁不支持升级操作,比方说我们在读锁内获取写锁,这个过程我们通常理解为锁的升级。代码分析如下:

static void doMethod01() {
		ReadWriteLock rtLock = new ReentrantReadWriteLock();
		rtLock.readLock().lock();
		System.out.println("get readLock.");
		rtLock.writeLock().lock();
		System.out.println("get writeLock");
}

ReadWriteLock中的锁虽不支持升级操作,但支持降级操作,比方说我们在写锁内获取读锁,这个过程我们通常理解为锁的降级。代码分析如下:

static void doMethod02() {
		ReadWriteLock rtLock = new ReentrantReadWriteLock();
		rtLock.writeLock().lock();
		System.out.println("get writeLock.");
		rtLock.readLock().lock();
		System.out.println("get readLock");
}

课堂练习:分析如下代码检查是否存在问题?

 r.lock(); 
		try {
			v = m.getObject(key); 
			if (v == null) {
				w.lock();
				try {
					//假如缓存没有从数据库或一级缓存查询
					//然后更新缓存(代码省略)
				} finally{
					w.unlock();
				}
			}
		} finally{
			r.unlock(); 
		}

对于如上代码,看上去好像是没有问题的,先是获取读锁,然后再升级为写锁。可惜 ReadWriteLock 并不支持这种升级。在上面的代码示例中,读锁还没有释放,此时获取写锁,会导致写锁永久等待,最终导致相关线程都被阻塞,永远也没有机会被唤醒。那如何修改呢?参考代码如下:

 r.lock(); 
		try {
			v = m.getObject(key); 
			if (v == null) {
                r.unlock();
				w.lock();
				try {
					//假如缓存没有从数据库或一级缓存查询
					//然后更新缓存(代码省略)
                 r.lock();
				} finally{
					w.unlock();
				}
			}
		} finally{
			r.unlock(); 
		}

StampedLock对象分析与应用?

StampedLock类,在JDK1.8时引入,是对读写锁ReadWriteLock的增强,该类优化了读锁、写锁的应用,同时使读写锁之间可以互相转换,实现了更加细粒度的并发控制。StampedLock 支持三种模式,分别是写锁、悲观读锁和乐观读。其中,写锁、悲观读锁的语义和 ReadWriteLock 的写锁、读锁的语义非常类似,允许多个线程同时获取悲观读锁,但是只允许一个线程获取写锁,写锁和悲观读锁是互斥的。不同的是,StampedLock 里的写锁和悲观读锁加锁成功之后,都会返回一个 stamp;然后解锁的时候,需要传入这个 stamp。

基于StampedLock 实现一个线程安全的cache对象。其关键代码试下如下:

class StampedMapCache{
	private Map<String,Object> map=new HashMap<>();
	private StampedLock lock=new StampedLock ();
	public void writeObject(String key,Object value){
		long stamp=lock.writeLock();
		try {
		map.put(key, value);
		}finally {
			lock.unlock(stamp);
		}
	}
	public Object readObject(String key) {
		long stamp=lock.readLock();
		try {
		return map.get(key);
		}finally {
			Lock.unlock(stamp);
		}
	}
}

StampedLock 的性能之所以比 ReadWriteLock 还要好,其关键是 StampedLock 支持乐观读的方 式。ReadWriteLock 支持多个线程同时读,但是当多个线程同时读的时候,所有的写操作会被阻塞;而 StampedLock 提供的乐观读,是允许一个线程获取写锁的,也就是说不是所有的写操作 都被阻塞。 注意这里,我们用的是“乐观读”这个词,而不是“乐观读锁”,是要提醒你,乐观读这个操 是无锁的,所以相比较 ReadWriteLock 的读锁,乐观读的性能更好一些

StampedLock 对象基于乐观读的方式从缓存对象中获取数据。关键代码如下:

 public String readWithOptimisticLock(String key) {
    	    long stamp = lock.tryOptimisticRead();
    	    String value = map.get(key);
    	    if(!lock.validate(stamp)) {
    	        stamp = lock.readLock();
    	        try {
    	            return map.get(key);
    	        } finally {
    	            lock.unlock(stamp);               
    	        }
    	    }
    	    return value;
    	}

其中:在代码中,首先通过调用 lock对象的tryOptimisticRead()方法 获取了一个 stamp,这里的 tryOptimisticRead() 就是我前面提到的乐观读。需要注意的是,由于 tryOptimisticRead() 是无锁的,因此最后读完之后,还需要再次验证一下是否存在写操作(这个验证操作是通过调 用 validate(stamp) 来实现的),来保证数据的一致性。

StampedLock对象在应用时需要注意如下几个点:

  • 所有获取锁的方法,都返回一个邮戳(Stamp),Stamp为0表示获取失败,其余都表示成功。
  • 获取锁时返回一个Stamp值,值为0表示获取失败,其余都表示成功。
  • 释放锁时需要一个Stamp值,这个值必须是和成功获取锁时得到的Stamp值是一致的。
  • StampedLock是不可重入的。(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)
  • StampedLock有三种访问模式:Reading,Writing,Optimistic reading。
  • StampedLock支持读锁和写锁的相互转换。
  • 无论写锁还是读锁,都不支持Conditon等待。 总之: 相比ReadWriteLock读写锁,StampedLock通过提供乐观读在多线程多写线程少的情况下可以提供更好的性能,因为乐观读不需要进行CAS设置锁的状态

Java中的锁对象的最佳应用设置推荐?

我们在使用锁时,要尽量在更新对象的成员变量时加锁,在访问可变的成员变量时加锁,不在调用其他对象的方法时加锁。这三条规则,最后一条你可能会觉得过于严苛。为什么不再访问其它对象方法时加锁呢?因为双重加锁就很有可能会导致死锁。

JUC集合类

JUC线程池/说说你对线程池的理解

Java线程池简述

Java中创建线程对象远不像创建一个普通对象那么简单。创建一般的对象,可能仅仅是在JVM的堆里分配一块内存而已;而创建一个线程,却需要调用操作系统内核的API,然后操作系统要为线程分配一系列的资源,这个创建成本一般会很高,所以可以把线程理解为一个重量级的对象,应该避免频繁创建和销毁。Java中为了优化线程对象应用,提供了一些线程池类型的对象。如图所示:

alt

ThreadPoolExecutor对象应用

JUC包中最核心的线程池类型为ThreadPoolExecutor,其常用构造函数如下:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

构造函数中参数说明:

1)corePoolSize:表示线程池保有的最小线程数。有些项目很闲,但是也不能把人都撤了,至少要留 corePoolSize个人坚守阵地。

2)maximumPoolSize:表示线程池创建的最大线程数。当项目很忙时,就需要加人,但是也不能无限制地 加,最多就加到maximumPoolSize个人。当项目闲下来时,就要撤人了,最多能撤到corePoolSize个人。

3)keepAliveTime & unit:上面提到项目根据忙闲来增减人员,那在编程世界里,如何定义忙和闲呢?很简单,一个线程如果在一段时间内,都没有执行任务,说明很闲,keepAliveTime 和 unit 就是用来定义这 个“一段时间”的参数。也就是说,如果一个线程空闲了keepAliveTime & unit这么久,而且线程池的线程数大于 corePoolSize ,那么这个空闲的线程就要被回收了。

4)workQueue:工作队列,和上面示例代码的工作队列同义。

5)threadFactory:通过这个参数你可以自定义如何创建线程,例如你可以给线程指定一个有意义的名字。

6)handler:通过这个参数你可以自定义任务的拒绝策略。如果线程池中所有的线程都在忙碌,并且工作队 列也满了(前提是工作队列是有界队列),那么此时提交任务,线程池就会拒绝接收。至于拒绝的策略,你可以通过handler这个参数来指定。

ThreadPoolExecutor已经提供了以下4种策略

1)CallerRunsPolicy:提交任务的线程自己去执行该任务。

  1. AbortPolicy:默认的拒绝策略,会throws RejectedExecutionException

  2. DiscardPolicy:直接丢弃任务,没有任何异常抛出。

  3. DiscardOldestPolicy:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入 到工作队列。

ThreadPoolExecutor 应用案例实现,创建TaskExecutorUtil工具类,然后在类中添加创建线程池和关闭池的方法。

第一步:定义创建池对象的方法。

public static ThreadPoolExecutor doCreateThreadPoolExecutor() {
		int corePoolSize=3;
		int maximumPoolSize=5;
		long keepAliveTime=60;
		BlockingQueue<Runnable> workQueue=
             new ArrayBlockingQueue<>(3);
		RejectedExecutionHandler handler=
		new ThreadPoolExecutor.AbortPolicy();
		ThreadFactory threadFactory=new ThreadFactory() {
			AtomicInteger at=new AtomicInteger(1);
			@Override
			public Thread newThread(Runnable r) {
				return new Thread(r,
                        "pool-thread->"+at.getAndIncrement());
			}
		};
		ThreadPoolExecutor pExecutor=
		new ThreadPoolExecutor(corePoolSize, 
				maximumPoolSize, 
				keepAliveTime, 
				TimeUnit.SECONDS, 
				workQueue, 
				threadFactory, 
				handler);
		return pExecutor;
	}

第二步:定义关闭池对象的方法。

public  static void doCloseExecutor(ThreadPoolExecutor executor) {
		try {
		    System.out.println("attempt to shutdown executor");
		    executor.shutdown();
		    executor.awaitTermination(5, TimeUnit.SECONDS);
		}catch (InterruptedException e) {
		    System.err.println("tasks interrupted");
		}finally {
		    if (!executor.isTerminated()) {
		        System.err.println("cancel non-finished tasks");
		    }
		    executor.shutdownNow();
		    System.out.println("shutdown finished");
		}
	}

shutdown只是将线程池的状态设置为SHUTWDOWN状态,正在执行的任务会继续执行下去,新的任务将会被拒绝。但这个方法不会等待提交的任务执行完,我们可以用awaitTermination来等待任务执行完。shutdownNow()方法是线程池处于STOP状态,此时线程池不再接受新的任务,并且会去尝试终止正在执行的任务,然后清空并返回队列。整个过程类似超市或商场关门。

第三步:应用池对象执行任务。

private static void doTestPoolExecutor01() throws Exception {
	    ThreadPoolExecutor pExecutor = 
            doCreateThreadPoolExecutor();
	    Future<Integer> future = 
          pExecutor.submit(new Callable<Integer>() {
	    	@Override
	    	public Integer call() throws Exception {
	    		TimeUnit.SECONDS.sleep(5);
	    		return new Random().nextInt();
	    	}
		});
	    System.out.println("future done? " + future.isDone());
	    Integer result=future.get();
	    System.out.println("future done? " + future.isDone());
	    System.out.println(result);
	    doCloseExecutor(pExecutor);
	}

线程池通过线程执行任务时,假如需要获取任务的执行结果,一般建议使用submit方法,而此方法的返回结果为Futrue类型,此类型常用的方法有5个, 它们分别是取消任务的方法cancel()、判断任务是否已取消的方法isCancelled()、判断任务是否已结束的方法isDone()以及2个获得任务执行结果的get()和get(timeout, unit),其中最后一个get(timeout, unit)支持超时机制。通过Future接口的这5个方法你会发现,我们提交的任务不但能够获取任务执行结果,还可以取消任务。不过需要注意的是,这两个get()方法都是阻塞式的,如果被调用的时候,任务还没有执行完,那么调用get()方法的线程会阻塞,直到任务执行完才会被唤醒。

通过线程池执行批量任务,关键代码如下:

private static void doTestPoolExecutor02() throws Exception {
	   ThreadPoolExecutor pExecutor = doCreateThreadPoolExecutor();
	   List<Callable<String>> callables = Arrays.asList(
		        () -> "task1",
		        () -> "task2",
		        () -> "task3");
		pExecutor.invokeAll(callables)
		         .stream()
		         .map(future -> {
		             try {
		                 return future.get();
		             }
		             catch (Exception e) {
		                 throw new IllegalStateException(e);
		             }
		         }).forEach(System.out::println);
		doCloseExecutor(pExecutor);
	}

ScheduledThreadPoolExecutor对象应用

ScheduledThreadPoolExecutor继承自ThreadPoolExecutor类,其内部将所有的Runnable任务包装成RunnableScheduledFuture类型,用于满足任务的延迟和周期性调度。案例分析如下:

案例一:创建一延迟任务并执行,关键代码如下:

static void doTestSchedule01() {
		ScheduledExecutorService executor =
		new ScheduledThreadPoolExecutor(1);
		Runnable task = () ->
		System.out.println("Scheduling: " + 
            System.nanoTime());
		executor.schedule(task, 3, TimeUnit.SECONDS);
}

案例二:创建一按固定频率执行的任务,启动频率与任务执行时长无关,关键代码如下:

 static void doTestSchedule02() {
		ScheduledExecutorService executor = 
           new ScheduledThreadPoolExecutor(1);
		Runnable task = () -> System.out.println("Scheduling: " +  
		           System.currentTimeMillis());
		int initialDelay = 0;
		int period = 1;
		executor.scheduleAtFixedRate(task, initialDelay, period, TimeUnit.SECONDS);
}

案例三:创建一个按固时间执行的任务,与任务执行时长有关(如果不执行完第n次任务是永远不会再执行第n+1次任务的)。

  static void doTestSchedule03() {
		ScheduledExecutorService executor =new ScheduledThreadPoolExecutor(1);
		Runnable task = () -> {
		    try {
		        TimeUnit.SECONDS.sleep(2);
		        System.out.println("Scheduling: " + System.currentTimeMillis());
		    }
		    catch (InterruptedException e) {
		        System.err.println("task interrupted");
		    }
		};
		executor.scheduleWithFixedDelay(task, 0, 1, TimeUnit.SECONDS);
}

为什么要用线程池?

线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。

这里借用《Java并发编程的艺术》提到的来说一下使用线程池的好处:

  • 降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。 当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

Java 提供了哪几种线程池?他们各自的使用场景是什么?

Java 主要提供了下面4种线程池

  • FixedThreadPool: 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
  • SingleThreadExecutor: 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
  • CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
  • ScheduledThreadPoolExecutor: 主要用来在给定的延迟后运行任务,或者定期执行任务。ScheduledThreadPoolExecutor又分为:ScheduledThreadPoolExecutor(包含多个线程)和SingleThreadScheduledExecutor (只包含一个线程)两种。

各种线程池的适用场景介绍

  • FixedThreadPool: 适用于为了满足资源管理需求,而需要限制当前线程数量的应用场景。它适用于负载比较重的服务器;
  • SingleThreadExecutor: 适用于需要保证顺序地执行各个任务并且在任意时间点,不会有多个线程是活动的应用场景;
  • CachedThreadPool: 适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器;
  • ScheduledThreadPoolExecutor: 适用于需要多个后台执行周期任务,同时为了满足资源管理需求而需要限制后台线程的数量的应用场景;
  • SingleThreadScheduledExecutor: 适用于需要单个后台线程执行周期任务,同时保证顺序地执行各个任务的应用场景。

创建的线程池的方式

(1) 使用 Executors 创建

我们上面刚刚提到了 Java 提供的几种线程池,通过 Executors 工具类我们可以很轻松的创建我们上面说的几种线程池。但是实际上我们一般都不是直接使用Java提供好的线程池,另外在《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

Executors 返回线程池对象的弊端如下:

FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM。
CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。

(2) ThreadPoolExecutor的构造函数创建

我们可以自己直接调用 ThreadPoolExecutor 的构造函数来自己创建线程池。在创建的同时,给 BlockQueue 指定容量就可以了。示例如下:

private static ExecutorService executor = new ThreadPoolExecutor(13, 13,
        60L, TimeUnit.SECONDS,
        new ArrayBlockingQueue(13));

这种情况下,一旦提交的线程数超过当前可用线程数时,就会抛出java.util.concurrent.RejectedExecutionException,这是因为当前线程池使用的队列是有边界队列,队列已经满了便无法继续处理新的请求。但是异常(Exception)总比发生错误(Error)要好。

(3) 使用开源类库

Hollis 大佬之前在他的文章中也提到了:“除了自己定义ThreadPoolExecutor外。还有其他方法。这个时候第一时间就应该想到开源类库,如apache和guava等。”他推荐使用guava提供的ThreadFactoryBuilder来创建线程池。下面是参考他的代码示例:

public class ExecutorsDemo {

    private static ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
        .setNameFormat("demo-pool-%d").build();

    private static ExecutorService pool = new ThreadPoolExecutor(5, 200,
        0L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());

    public static void main(String[] args) {

        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            pool.execute(new SubThread());
        }
    }
}

通过上述方式创建线程时,不仅可以避免OOM的问题,还可以自定义线程名称,更加方便的出错的时候溯源。

JUC工具类

JUC工具类包括

  • ThreadLocal
  • CountDownLatch
  • Semaphore
  • Phaser
  • Exchanger

说说你对ThreadLocal的理解

什么是线程局部变量ThreadLocal?

线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。

ThreadLoal的作用是什么?

简单说ThreadLocal就是一种以空间换时间的做法在每个Thread里面维护了一个ThreadLocal.ThreadLocalMap把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了.

CountDownLatch对象分析及应用

CountDownLatch是一个辅助同步类,用来作计数使用,它的作用有点类似于生活中的倒数计数器,先设定一个计数初始值,当计数降到0时,将会触发一些事件。 CountDownLatch对象的初始计数值,在构造CountDownLatch对象时传入,每调用一次 countDown() 方法,计数值就会减1。线程可以调用CountDownLatch的await方法进入阻塞,当计数值降到0时,所有之前调用await阻塞的线程都会释放。其应用原理如图所示:

alt CountDownLatch 应用案例1:

public class TestCountDownLatch01 {
	static String content;
	public static void main(String[] args)throws Exception {
		CountDownLatch cdl=new CountDownLatch(1);
		new Thread(new Runnable() {
			@Override
			public void run() {
			   content="helloworld";
			   cdl.countDown();
			}
		}).start();
		while(content==null)cdl.await();
		System.out.println(content.toUpperCase());
	}
}

说明:CountDownLatch的初始计数值一旦降到0,无法重置。如果需要重置,可以考虑使用CyclicBarrier。

CyclicBarrier对象分析及应用

CyclicBarrier可以认为是一个栅栏,其作用是阻挡前行。与CountDownLatch不同是,CyclicBarrier是一个可以循环使用的栅栏,它做的事情就是:让线程到达栅栏时被阻塞(调用await方法),直到到达栅栏的线程数满足指定数量要求时,栅栏才会打开放行。这个应用,其实有点像军训报数,报数总人数满足教官认为的总数时,教官才会安排后面的训练。 CyclicBarrier 对象是让一组线程相互等待,所有的执行结束以后,才继续向后执行,如图所示:

alt

CyclicBarrier 应用案例分析,关键代码如下:

public class TestCyclicBarrier01{
	static CyclicBarrier cBarrier=
	new CyclicBarrier(3,new Runnable() {
		@Override
		public void run() {
			System.out.println("run()");
		}
	});
    static class SumTask implements Runnable{
    	@Override
    	public void run() {
    		try{
    		String tName=
    		Thread.currentThread().getName();
    		System.out.println("开始计算:"+tName);
    		//TimeUnit.SECONDS.sleep(2);
    		System.out.println("计算完成:"+tName);
    		cBarrier.await();
    		}catch(Exception e){e.printStackTrace();}
    	}
    }
	public static void main(String[] args) {
		SumTask task=new SumTask();
		for(int i=0;i<3;i++){
			new Thread(task).start();
		}
	}
}

CyclicBarrier典型应用是一组任务,它们并行执行工作,然后在进行下一个步骤之前进行等待,直至所有的任务都完成。

Semaphore对象分析及应用

Semaphore,又名信号量,这个类的作用有点类似于“许可证”。有时,我们因为一些原因需要控制同时访问共享资源的最大线程数量,比如出于系统性能的考虑需要限流,或者共享资源是稀缺资源,我们需要有一种办法能够协调各个线程,以保证合理的使用公共资源。

Semaphore维护了一个许可集,其实就是一定数量的“许可证”。 当有线程想要访问共享资源时,需要先获取(acquire)的许可;如果许可不够了,线程需要一直等待,直到许可可用。当线程使用完共享资源后,可以归还(release)许可,以供其它需要的线程使用。 另外,Semaphore支持公平/非公平策略,这和ReentrantLock类似。基于Semaphore实现限流操作,关键代码如下

class LimitService {
    private final Semaphore permit = new Semaphore(10, true);
    public void process(){
        try{
            permit.acquire();
            //业务逻辑处理
            String tName=Thread.currentThread().getName();
            System.out.println(tName+":process");
            try{Thread.sleep(2000);}
catch(Exception e) {e.printStackTrace();}
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            permit.release();
        }
    }
	public static void main(String[] args) {
		LimitService lService=new LimitService();
		for(int i=0;i<30;i++) {
			new Thread() {
				public void run() {
					lService.process();
				};
			}.start();
		}
	}

}

说明,当许可数 ≤ 0代表共享资源不可用。许可数 > 0,代表共享资源可用,且多个线程可以同时访问共享资源。

JVM

我们为什么要学习JVM?

深入理解JVM可以帮助我们从平台角度提高解决问题的能力,例如:

  1. 有效防止内存泄漏(Memory leak)。
  2. 优化线程锁的使用 (Thread Lock)。
  3. 科学进行垃圾回收 (Garbage collection)。
  4. 提高系统吞吐量 (throughput)。
  5. 降低延迟(Delay),提高其性能(performance)。

市场上有哪些主流的JVM呢?

JVM是一种规范基于这种规范,不同公司就对此规范做了具体实现,例如市场上的一些主流JVM如下:

  1. JRockit VM (BEA公司研发,后在2008年由Oracle公司收购)。
  2. HotSpot VM (Sun公司研发,后在2010年由Oracle公司收购)。
  3. J9 VM (IBM 内部使用)。 说明:HotSpot目前是甲骨文公司最主要的一款JVM虚拟机,也是我们现在最常用的一种。

JVM的体系结构是怎样的?/说说你了解的JVM内存模型

JVM (Java Hotspot Architecture:主要分为三大部分,

  1. 类加载系统 (ClassLoader System) :负责加载类到内存。
  2. 运行时数据区 (Runtime Data Area):负责存储数据信息。
  3. 执行引擎 (Execution Engine):负责调用对象执行业务。

其中:

  1. ClassLoader作用是什么?(负责将类从磁盘或网络加载内存)
  2. ClassLoader 可以自己定义吗?(可以,参考Tomcat,MyBatis,Spring等,他们都有自定义类加载器)
  3. JVM 中的方法区(Method Area) 如何理解?(逻辑上的一种定义,不同JVM有不同实现,比方说有JVM中称元数据区,有的称持久代)
  4. HotSpot JDK8虚拟机在创建对象时,所有的对象都会分配在堆中吗?(不一定,小对象未逃逸,可以直接分配在栈上)
  5. 如何知道类的加载顺序?(可通过配置JVM 参数-XX:+TraceClassLoading 检查类的加载过程)

你知道JVM有哪些运行模式吗?

JVM有两种运行模式Server与Client。两种模式的区别在于,Client模式启动速度较快,Server模式启动较慢;但是启动进入稳定期之后Server模式的程序运行速度比Client要快很多。这是因为Server模式启动的JVM采用的是重量级的虚拟机,对程序采用了更多的优化;而Client模式启动的JVM采用的是轻量级的虚拟机。所以Server启动慢,但稳定后速度比Client远远要快。 现在64位的jdk中默认都是server模式(可通过 java -version进行查看)。当虚拟机运行在-client模式的时候,使用的是一个代号为C1的轻量级编译器, 而server模式启动的虚拟机采用相对重量级,代号为C2的编译器.c1、c2都是JIT编译器, C2比C1编译器编译的相对彻底,服务起来之后,性能更高。

如何理解Java中的字节码对象?

每个类在加载(将类读到内存)时都会创建一个字节码对象,其类型为Class类型,且这个对象在一个JVM内存中是唯一的.此对象中存储的是类的结构信息(元数据信息),节码对象的获取方式常用的有如下三种:

a) 类名.class

b) Class.forName(“包名.类名”)

c) 类的实例对象.getClass();

代码演示:

package com.java.oop;
//呈现类加载过程(通过配置JVM参数实现)
//-XX:+TraceClassLoading 
public class TestClassObject01 {
	static public void main(String[] args)throws Exception {
		Class<Object> c1=Object.class;
		Class<?> c2=Class.forName("java.lang.Object");
		System.out.println(c1==c2);	
		Class<?> c3=new Object().getClass();
		System.out.println(c2==c3);
	}
}

说说类加载机制

类加载器层次

alt

  • 启动类加载器: Bootstrap ClassLoader,负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。
  • 扩展类加载器: Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
  • 应用程序类加载器: Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
  • 自定义类加载器: 因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点: 在执行非置信代码之前,自动验证数字签名。 动态地创建符合用户特定需要的定制化构建类。 从特定的场所取得java class,例如数据库中和网络中。

JVM类加载机制有哪些?

  • 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
  • 父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
  • 缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效.
  • 双亲委派机制, 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

说说JVM的双亲委派模型

  1. 当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
  2. 当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
  3. 如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
  4. 若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。

具体过程

说说Java运行时数据区

alt

Java 虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的,这些与线程一一对应的数据区域会随着线程开始和结束而创建和销毁。

  • 线程私有:程序计数器、虚拟机栈、本地方法栈
  • 线程共享:堆、方法区, 堆外内存(Java7的永久代或JDK8的元空间、代码缓存)

程序计数器

PC 寄存器用来存储指向下一条指令的地址,即将要执行的指令代码。由执行引擎读取下一条指令。

虚拟机栈

  • 特点?
  1. 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器
  2. JVM 直接对虚拟机栈的操作只有两个:每个方法执行,伴随着入栈(进栈/压栈),方法执行结束出栈
  3. 栈不存在垃圾回收问题
  4. 可以通过参数-Xss来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度
  • 该区域有哪些异常?
  1. 如果采用固定大小的 Java 虚拟机栈,那每个线程的 Java 虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量,Java 虚拟机将会抛出一个 StackOverflowError 异常
  2. 如果 Java 虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那 Java 虚拟机将会抛出一个OutOfMemoryError异常
  • 栈帧的内部结构?
  1. 局部变量表(Local Variables)
  2. 操作数栈(Operand Stack)(或称为表达式栈)
  3. 动态链接(Dynamic Linking):指向运行时常量池的方法引用
  4. 方法返回地址(Return Address):方法正常退出或异常退出的地址
  5. 一些附加信息

alt

主管 Java 程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。每个线程在创建的时候都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次 Java 方法调用,是线程私有的,生命周期和线程一致。

本地方法栈

Java 虚拟机栈用于管理 Java 方法的调用,而本地方法栈用于管理本地方法的调用.

  1. 堆是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java世界里“几乎”所有的对象实例都在这里分配内存。堆是垃圾收集器管理的内存区域,因此一些资料中它也被称作“GC堆”。
  2. 堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的Java虚拟机都是按照可扩展来实现的。如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,JVM将会抛出OutOfMemoryError异常。

Java对象在堆中生命周期

  1. 在 JVM 内存模型的堆中,堆被划分为新生代和老年代
  • 新生代又被进一步划分为 Eden区 和 Survivor区,Survivor 区由 From Survivor 和 To Survivor 组成
  • 年轻代是所有新对象创建的地方。当填充年轻代时,执行垃圾收集。这种垃圾收集称为 Minor GC。年轻一代被分为三个部分——伊甸园(Eden Memory)和两个幸存区(Survivor Memory,被称为from/to或s0/s1),默认比例是8:1:1 alt
  1. 当创建一个对象时,对象会被优先分配到新生代的 Eden 区
  • 此时 JVM 会给对象定义一个对象年轻计数器(-XX:MaxTenuringThreshold)
  1. 当 Eden 空间不足时,JVM 将执行新生代的垃圾回收(Minor GC)
  • JVM 会把存活的对象转移到 Survivor 中,并且对象年龄 +1
  • 对象在 Survivor 中同样也会经历 Minor GC,每经历一次 Minor GC,对象年龄都会+1
  1. 如果分配的对象超过了-XX:PetenureSizeThreshold,对象会直接被分配到老年代

方法区

  1. 方法区(method area)只是 JVM 规范中定义的一个概念,用于存储类信息、常量池、静态变量、JIT编译后的代码等数据,并没有规定如何去实现它,不同的厂商有不同的实现。而永久代(PermGen)是 Hotspot 虚拟机特有的概念, Java8 的时候又被元空间取代了,永久代和元空间都可以理解为方法区的落地实现。

  2. 运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译期生成的各种字面量与符号引用。 根据Java虚拟机规范的规定,如果方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常。

栈,堆,方法区的交互

alt

具体操作

说说JVM的垃圾回收机制

垃圾回收机制主要完成三件事情:

  1. 哪些内存需要回收:废弃的常量,不再被引用的对象等
  2. 什么时候回收:当类不再被使用
  3. 如何回收:用引用计数法判断对象是否存活,采用垃圾收集器进行回收

请你说说内存溢出

具体内容

请你说说内存泄漏

具体内容

说说JVM的垃圾回收算法

如何判断一个对象是否可以回收?

  • 引用计数算法
  1. 给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。
  2. 两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。

正因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。

  • 可达性分析算法
  1. 通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收。

alt

2.Java 虚拟机使用该算法来判断对象是否可被回收,在 Java 中 GC Roots 一般包含以下内容:

  • 虚拟机栈中引用的对象
  • 本地方法栈中引用的对象 *方法区中类静态属性引用的对象
  • 方法区中的常量引用的对象

有哪些基本的垃圾回收算法?

  • 标记-清除
  • 标记-整理
  • 复制
  • 分代收集 现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。

一般将堆分为新生代和老年代。

新生代使用: 复制算法

老年代使用: 标记 - 清除 或者 标记 - 整理 算法

具体内容

请你讲下G1垃圾回收器

具体内容

请你讲下CMS垃圾回收器

具体内容

说说JVM问题排查过程

alt

具体使用

说说类的实例化过程

在JVM中,对象的创建遵循如下过程:

  1. 当JVM遇到一条字节码new指令时,首先会去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
  2. 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务实际上便等同于把一块确定大小的内存块从Java堆中划分出来。
  3. 内存分配完成之后,虚拟机必须将分配到的内存空间都初始化为零值,如果使用了TLAB的话,这一项工作也可以提前直TLAB分配时顺便进行。这步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,使程序能访问到这些字段的数据类型所对应的零值。
  4. 接下来,虚拟机还要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。根据虚拟机当前运行状态的不同,如是否启动偏向锁等,对象头会有不同的设置方式。
  5. 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但是从Java程序的视角来看,对象创建才刚刚开始——构造函数,即Class文件中的init()方法还没执行,所有的字段都为默认的零值,对象需要的其他资源和状态信息也还没有按照预订的意图构造好。一般来说,new指令之后会接着执行init()方法,按照程序猿的意愿对对象进行初始化,这样一个真正可用的对象才能完全被构造出来。

出现 OOM 后你会怎么排查问题

有过GC调优的经历么?

(有点虚,答得不是很好)

说说GC的过程

说了young gc和full gc的触发条件和回收过程以及对象创建的过程

CMS GC有什么问题

并发清除算法,浮动垃圾,短暂停顿

怎么避免产生浮动垃圾?

记得有个VM参数设置可以让扫描新生代之前进行一次young gc,但是因为gc是虚拟机自动调度的,所以不保证一定执行。但是还有参数可以让虚拟机强制执行一次young gc

强制young gc会有什么问题

STW停顿时间变长

知道G1回收过程吗?

young gc、并发阶段、混合阶段、full gc,说了Remember Set

你提到的Remember Set底层是怎么实现的

jvm最大内存设置原理

实例化对象(两个属性,四个方法)100次,内存中是什么状态,多少个对象,属性,方法