Java八股文面试全套真题-中

Java八股文面试全套真题-中

一、多线程-线程基础

1.1、线程和进程的区别?

当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。

一个进程之内可以分为一到多个线程。

一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行

Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。在 windows 中进程是不活动的,只是作为线程的容器

二者对比

  • 进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务
  • 不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间
  • 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程)

1.2、并行和并发有什么区别?

单核CPU

  • 单核CPU下线程实际还是串行执行的

  • 操作系统中有一个组件叫做任务调度器,将cpu的时间片(windows下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于cpu在线程间(时间片很短)的切换非常快,人类感觉是同时运行的 。

  • 总结为一句话就是: 微观串行,宏观并行

一般会将这种线程轮流使用CPU的做法称为并发(concurrent)

多核CPU

每个核(core)都可以调度运行线程,这时候线程可以是并行的。

并发(concurrent)是同一时间应对(dealing with)多件事情的能力

并行(parallel)是同一时间动手做(doing)多件事情的能力

1.3、创建线程的四种方式

共有四种方式可以创建线程

  • 继承Thread类
  • 实现runnable接口
  • 实现Callable接口
  • 线程池创建线程

1、继承Thread类

public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + ": MyThread...run...");
    }

    public static void main(String[] args) {
        // 创建MyThread对象
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();

        // 调用start方法启动线程
        t1.start();
        t2.start();
    }
}

2、实现runnable接口

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + ": MyThread...run...");
    }

    public static void main(String[] args) {
        // 创建MyRunnable对象
        MyRunnable mr = new MyRunnable();

        // 创建Thread对象
        Thread t1 = new Thread(mr);
        Thread t2 = new Thread(mr);

        // 调用start方法启动线程
        t1.start();
        t2.start();
    }
}

3、实现Callable接口

public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        System.out.println(Thread.currentThread().getName() + ": MyThread...run...");
        return "OK";
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建MyCallable对象
        MyCallable mc = new MyCallable();

        // 创建FutureTask
        FutureTask<String> ft = new FutureTask<String>(mc);

        // 创建Thread对象
        Thread t1 = new Thread(ft);
        Thread t2 = new Thread(ft);

        // 调用start方法启动线程
        t1.start();

        // 调用ft的get方法获取执行结果
        String result = ft.get();

        // 输出
        System.out.println(result);
    }
}

4、线程池创建线程

public class MyExecutors implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + ": MyThread...run...");
    }

    public static void main(String[] args) {
        // 创建线程池对象
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        threadPool.submit(new MyExecutors());

        // 关闭线程池
        threadPool.shutdown();
    }
}

1.4、Runnable 和 Callable有什么区别

  1. Runnable 接口run方法没有返回值;Callable接口call方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
  2. Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
  3. Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛。

1.5、线程的 run()和 start()有什么区别?

  • start():用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次。
  • run():封装了要被线程执行的代码,可以被调用多次。

1.6、线程包括哪些状态,状态之间是如何变化的

线程的状态可以参考JDK中的Thread类中的枚举State

public enum State {
        /**
         * 尚未启动的线程的线程状态
         */
        NEW,

        /**
         * 可运行线程的线程状态。处于可运行状态的线程正在 Java 虚拟机中执行,但它可能正在等待来自		 * 操作系统的其他资源,例如处理器。
         */
        RUNNABLE,

        /**
         * 线程阻塞等待监视器锁的线程状态。处于阻塞状态的线程正在等待监视器锁进入同步块/方法或在调          * 用Object.wait后重新进入同步块/方法。
         */
        BLOCKED,

        /**
         * 等待线程的线程状态。由于调用以下方法之一,线程处于等待状态:
		* Object.wait没有超时
         * 没有超时的Thread.join
         * LockSupport.park
         * 处于等待状态的线程正在等待另一个线程执行特定操作。
         * 例如,一个对对象调用Object.wait()的线程正在等待另一个线程对该对象调用Object.notify()			* 或Object.notifyAll() 。已调用Thread.join()的线程正在等待指定线程终止。
         */
        WAITING,

        /**
         * 具有指定等待时间的等待线程的线程状态。由于以指定的正等待时间调用以下方法之一,线程处于定          * 时等待状态:
		* Thread.sleep
		* Object.wait超时
		* Thread.join超时
		* LockSupport.parkNanos
		* LockSupport.parkUntil
         * </ul>
         */
        TIMED_WAITING,

        /**
         * 已终止线程的线程状态。线程已完成执行
         */
        TERMINATED;
    }

状态之间是如何变化的
在这里插入图片描述

新建

  • 当一个线程对象被创建,但还未调用 start 方法时处于新建状态
  • 此时未与操作系统底层线程关联

可运行

  • 调用了 start 方法,就会由新建进入可运行
  • 此时与底层线程关联,由操作系统调度执行

死亡

  • 线程内代码已经执行完毕,由可运行进入终结
  • 此时会取消与底层线程关联

阻塞

  • 当获取锁失败后,由可运行进入 Monitor 的阻塞队列阻塞,此时不占用 cpu 时间
  • 当持锁线程释放锁时,会按照一定规则唤醒阻塞队列中的阻塞线程,唤醒后的线程进入可运行状态

等待

  • 当获取锁成功后,但由于条件不满足,调用了 wait() 方法,此时从可运行状态释放锁进入 Monitor 等待集合等待,同样不占用 cpu 时间
  • 当其它持锁线程调用 notify() 或 notifyAll() 方法,会按照一定规则唤醒等待集合中的等待线程,恢复为可运行状态

有时限等待

  • 当获取锁成功后,但由于条件不满足,调用了 wait(long) 方法,此时从可运行状态释放锁进入 Monitor 等待集合进行有时限等待,同样不占用 cpu 时间
  • 当其它持锁线程调用 notify() 或 notifyAll() 方法,会按照一定规则唤醒等待集合中的有时限等待线程,恢复为可运行状态,并重新去竞争锁
  • 如果等待超时,也会从有时限等待状态恢复为可运行状态,并重新去竞争锁
  • 还有一种情况是调用 sleep(long) 方法也会从可运行状态进入有时限等待状态,但与 Monitor 无关,不需要主动唤醒,超时时间到自然恢复为可运行状态

1.7、新建 T1、T2、T3 三个线程,如何保证它们按顺序执行?

在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。

为了确保三个线程的顺序你应该先启动最后一个(T3调用T2,T2调用T1),这样T1就会先完成而T3最后完成

public class JoinTest {
    public static void main(String[] args) {
        // 创建线程对象
        Thread t1 = new Thread(() -> {
            System.out.println("t1");
        });
        Thread t2 = new Thread(() -> {
            try {
                // 加入线程t1,只有t1线程执行完毕以后,再次执行该线程
                t1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t2");
        });

        Thread t3 = new Thread(() -> {
            try {
                // 加入线程t2,只有t2线程执行完毕以后,再次执行该线程
                t2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t3");
        });

        // 启动线程
        t1.start();
        t2.start();
        t3.start();
    }
}

1.8、notify()和 notifyAll()有什么区别?

  • notifyAll:唤醒所有wait的线程
  • notify:只随机唤醒一个 wait 线程
public class WaitNotifyTest {

    static boolean flag = false;
    static Object lock = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (lock) {
                while (!flag) {
                    System.out.println(Thread.currentThread().getName() + "...wating...");
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName() + "...flag is true");
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (lock) {
                while (!flag) {
                    System.out.println(Thread.currentThread().getName() + "...wating...");
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName() + "...flag is true");
            }
        });

        Thread t3 = new Thread(() -> {
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + " hold lock");
                lock.notifyAll();
                flag = true;
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.start();
        t2.start();
        t3.start();
    }
}

1.9、在 java 中 wait 和 sleep 方法的不同?

共同点

  • wait() ,wait(long) 和 sleep(long) 的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态。

不同点

  • 方法归属不同
    • sleep(long) 是 Thread 的静态方法。
    • 而 wait(),wait(long) 都是 Object 的成员方法,每个对象都有。
  • 醒来时机不同
    • 执行 sleep(long) 和 wait(long) 的线程都会在等待相应毫秒后醒来。
    • wait(long) 和 wait()还可以被 notify 唤醒,wait() 如果不唤醒就一直等下去。
    • 它们都可以被打断唤醒。
  • 锁特性不同(重点)
    • wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制。
    • wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃 cpu,但你们还可以用)
    • 而 sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃 cpu,你们也用不了)

1.10、如何停止一个正在运行的线程?

有三种方式可以停止线程

  • 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
  • 使用stop方法强行终止(不推荐,方法已作废)
  • 使用interrupt方法中断线程

代码参考如下:

1、使用退出标志,使线程正常退出

public class MyInterrupt1 extends Thread {
    // 线程执行的退出标记
    volatile boolean flag = false;

    @Override
    public void run() {
        while (!flag) {
            System.out.println("MyThread...run...");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 创建MyThread对象
        MyInterrupt1 t1 = new MyInterrupt1();
        t1.start();

        // 主线程休眠6秒
        Thread.sleep(6000);

        // 更改标记为true
        t1.flag = true;
    }
}

2、使用stop方法强行终止

public class MyInterrupt2 extends Thread {
    // 线程执行的退出标记
    volatile boolean flag = false;

    @Override
    public void run() {
        while (!flag) {
            System.out.println("MyThread...run...");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 创建MyThread对象
        MyInterrupt2 t1 = new MyInterrupt2();
        t1.start();

        // 主线程休眠2秒
        Thread.sleep(6000);

        // 调用stop方法
        t1.stop();
    }
}

3、使用interrupt方法中断线程

public class MyInterrupt3 {
    public static void main(String[] args) throws InterruptedException {
        // 1.打断阻塞的线程
        /*Thread t1 = new Thread(()->{
            System.out.println("t1 正在运行...");
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t1");
        t1.start();
        Thread.sleep(500);
        t1.interrupt();
        System.out.println(t1.isInterrupted());*/

        //2.打断正常的线程
        Thread t2 = new Thread(() -> {
            while (true) {
                Thread current = Thread.currentThread();
                boolean interrupted = current.isInterrupted();
                if (interrupted) {
                    System.out.println("打断状态:" + interrupted);
                    break;
                }
            }
        }, "t2");
        t2.start();
        Thread.sleep(500);
        t2.interrupt();
    }
}

二、多线程-线程安全

2.1、讲一下synchronized关键字的底层原理?

2.1.1、基本使用

Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。

如下抢票的代码,如果不加锁,就会出现超卖或者一张票卖给多个人。

public class TicketDemo {
    private int ticketNum = 10;

    public synchronized void getTicket() {
        synchronized (this) {
            if (this.ticketNum <= 0) {
                return;
            }
            System.out.println(Thread.currentThread().getName() + "\t抢到一张票, 剩余:" + ticketNum);
            // 非原子性操作
            this.ticketNum--;
        }
    }

    public static void main(String[] args) {
        TicketDemo ticketDemo = new TicketDemo();
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                ticketDemo.getTicket();
            }).start();
        }
    }
}

2.1.2、Monitor

Monitor 被翻译为监视器,是由jvm提供,c++语言实现。

在代码中想要体现monitor需要借助javap命令查看clsss的字节码,比如以下代码:

public class SyncTest {

    static final Object lock = new Object();
    static int counter = 0;

    public static void main(String[] args) {
        synchronized (lock) {
            counter++;
        }
    }
}

找到这个类的class文件,在class文件目录下执行javap -v SyncTest.class,反编译效果如下:
在这里插入图片描述

  • monitorenter:上锁开始的地方
  • monitorexit:解锁的地方
  • 其中被monitorentermonitorexit包围住的指令就是上锁的代码。
  • 有两个monitorexit的原因,第二个monitorexit是为了防止锁住的代码抛异常后不能及时释放锁。

在使用了synchornized代码块时需要指定一个对象,所以synchornized也被称为对象锁。

monitor主要就是跟这个对象产生关联,如下图:
在这里插入图片描述
Monitor内部具体的存储结构

  • Owner:存储当前获取锁的线程的,只能有一个线程可以获取。
  • EntryList:关联没有抢到锁的线程,处于Blocked状态的线程。
  • WaitSet:关联调用了wait方法的线程,处于Waiting状态的线程。

具体的流程

  • 代码进入synchorized代码块,先让lock(对象锁)关联的monitor,然后判断Owner是否有线程持有。
  • 如果没有线程持有,则让当前线程持有,表示该线程获取锁成功。
  • 如果有线程持有,则让当前线程进入entryList进行阻塞,如果Owner持有的线程已经释放了锁,在EntryList中的线程去竞争锁的持有权(非公平)。
  • 如果代码块中调用了wait()方法,则会进去WaitSet中进行等待。

参考回答

  • Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】。
  • 它的底层由monitor实现的,monitor是jvm级别的对象( C++实现),线程获得锁需要使用对象(锁)关联monitor
  • monitor内部有三个属性,分别是ownerentrylistwaitset
    • owner是关联的获得锁的线程,并且只能关联一个线程;
    • entrylist关联的是处于阻塞状态的线程;
    • waitset关联的是处于Waiting状态的线程。

2.2、synchronized关键字的底层原理-进阶

Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。

  • 重量级锁:底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。
  • 轻量级锁:线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是CAS操作,保证原子性
  • 偏向锁:一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只需要判断mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令

一旦锁发生了竞争,都会升级为重量级锁。

2.3、JMM(Java 内存模型)

  • JMM(Java Memory Model)Java内存模型,定义了共享内存多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性。
  • JMM把内存分为两块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享区域(主内存)。
  • 线程跟线程之间是相互隔离,线程跟线程交互需要通过主内存。

2.4、CAS 你知道吗?

CAS的全称是: Compare And Swap(比较再交换),它体现的一种乐观锁的思想,在无锁情况下保证线程操作共享数据的原子性。

  • CAS使用到的地方很多:AQS框架、AtomicXXX类
  • 在操作共享变量的时候使用的自旋锁,效率上更高一些。
  • CAS的底层是调用的Unsafe类中的方法,都是操作系统提供的,其他语言实现。

乐观锁和悲观锁

  • CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
  • synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。

2.5、请谈谈你对 volatile 的理解

volatile 是一个关键字,可以修饰类的成员变量、类的静态成员变量,主要有两个功能

  • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存。
  • 禁止进行指令重排序,可以保证代码执行有序性。底层实现原理是,添加了一个内存屏障,通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。

2.6、什么是AQS?

2.6.1、概述

全称是 AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架,它是构建锁或者其他同步组件的基础框架

AQS与Synchronized的区别

synchronizedAQS
关键字,c++ 语言实现java 语言实现
悲观锁,自动释放锁悲观锁,手动开启和关闭
锁竞争激烈都是重量级锁,性能差锁竞争激烈的情况下,提供了多种解决方案

AQS常见的实现类

  • ReentrantLock 阻塞式锁
  • Semaphore 信号量
  • CountDownLatch 倒计时锁

2.6.2、工作机制

  • 在AQS中维护了一个使用了volatile修饰的state属性来表示资源的状态,0表示无锁,1表示有锁
  • 提供了基于 FIFO 的等待队列,类似于 MonitorEntryList
  • 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 MonitorWaitSet

在这里插入图片描述

  • 线程0来了以后,去尝试修改state属性,如果发现state属性是0,就修改state状态为1,表示线程0抢锁成功
  • 线程1和线程2也会先尝试修改state属性,发现state的值已经是1了,有其他线程持有锁,它们都会到FIFO队列中进行等待,
  • FIFO是一个双向队列,head属性表示头结点,tail表示尾结点

如果多个线程共同去抢这个资源是如何保证原子性的呢?
在这里插入图片描述
在去修改state状态的时候,使用的cas自旋锁来保证原子性,确保只能有一个线程修改成功,修改失败的线程将会进入FIFO队列中等待

AQS是公平锁吗,还是非公平锁?

  • 新的线程与队列中的线程共同来抢资源,是非公平锁

  • 新的线程到队列中等待,只让队列中的head线程获取锁,是公平锁

比较典型的AQS实现类ReentrantLock,它默认就是非公平锁,新的线程与队列中的线程共同来抢资源

2.6.3、参考回答

AQS的话,其实就一个jdk提供的类AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架。

内部有一个属性 state 属性来表示资源的状态,默认state等于0,表示没有获取锁,state等于1的时候才标明获取到了锁。通过cas 机制设置 state 状态。

在它的内部还提供了基于 FIFO 的等待队列,是一个双向列表,其中:

  • tail 指向队列最后一个元素
  • head 指向队列中最久的一个元素

其中我们刚刚聊的ReentrantLock底层的实现就是一个AQS。

2.7、ReentrantLock的实现原理

2.7.1、概述

ReentrantLock翻译过来是可重入锁,相对于synchronized它具备以下特点:

  • 可中断
  • 可以设置超时时间
  • 可以设置公平锁
  • 支持多个条件变量
  • 与synchronized一样,都支持重入

在这里插入图片描述

2.7.2、实现原理

ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似

构造方法接受一个可选的公平参数(默认非公平锁),当设置为true时,表示公平锁,否则为非公平锁。公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。

查看ReentrantLock源码中的构造方法:
在这里插入图片描述
提供了两个构造方法,不带参数的默认为非公平

如果使用带参数的构造函数,并且传的值为true,则是公平锁

其中NonfairSync和FairSync这两个类父类都是Sync
在这里插入图片描述
而Sync的父类是AQS,所以可以得出ReentrantLock底层主要实现就是基于AQS来实现的
在这里插入图片描述

工作流程

在这里插入图片描述

  • 线程来抢锁后使用cas的方式修改state状态,修改状态成功为1,则让exclusiveOwnerThread属性指向当前线程,获取锁成功
  • 假如修改状态失败,则会进入双向队列中等待,head指向双向队列头部,tail指向双向队列尾部
  • 当exclusiveOwnerThread为null的时候,则会唤醒在双向队列中等待的线程
  • 公平锁则体现在按照先后顺序获取锁,非公平体现在不在排队的线程也可以抢锁

2.8、synchronized和Lock有什么区别 ?

  • 语法层面
    • synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现。
    • Lock 是接口,源码由 jdk 提供,用 java 语言实现。
    • 使用 synchronized 时,退出同步代码块锁会自动释放,而使用 Lock 时,需要手动调用 unlock 方法释放锁。
  • 功能层面
    • 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能。
    • Lock 提供了许多 synchronized 不具备的功能,例如获取等待状态、公平锁、可打断、可超时、多条件变量。
    • Lock 有适合不同场景的实现,如 ReentrantLock, ReentrantReadWriteLock。
  • 性能层面
    • 在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖。
    • 在竞争激烈时,Lock 的实现通常会提供更好的性能。

2.9、死锁产生的条件是什么?

死锁:一个线程需要同时获取多把锁,这时就容易发生死锁

例如:
t1 线程获得A对象锁,接下来想获取B对象的锁
t2 线程获得B对象锁,接下来想获取A对象的锁

代码如下:

public class Deadlock {
    public static void main(String[] args) {
        Object A = new Object();
        Object B = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (A) {
                System.out.println("lock A");
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (B) {
                    System.out.println("lock B");
                    System.out.println("操作...");
                }
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            synchronized (B) {
                System.out.println("lock B");
                try {
                    sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (A) {
                    System.out.println("lock A");
                    System.out.println("操作...");
                }
            }
        }, "t2");
        t1.start();
        t2.start();
    }
}

控制台输出结果

lock A
lock B

此时程序并没有结束,这种现象就是死锁现象…线程t1持有A的锁等待获取B锁,线程t2持有B的锁等待获取A的锁。

2.10、如何进行死锁诊断?

当程序出现了死锁现象,我们可以使用jdk自带的工具:jps jstack

步骤如下:

第一:查看运行的线程
在这里插入图片描述

PS D:\IdeaProjects\demo01\demo01> jps
10496 Launcher
28384 Jps
20852 Deadlock
12104 
10268 RemoteMavenServer36

第二:使用jstack查看线程运行的情况,下图是截图的关键信息

运行命令:

jstack -l 20852

在这里插入图片描述

其他解决工具,可视化工具

  • jconsole

用于对jvm的内存,线程,类 的监控,是一个基于 jmx 的 GUI 性能监控工具

打开方式:java 安装目录 bin目录下 直接启动 jconsole.exe 就行

  • VisualVM:故障处理工具

能够监控线程,内存情况,查看方法的CPU时间和内存中的对 象,已被GC的对象,反向查看分配的堆栈

打开方式:java 安装目录 bin目录下 直接启动 jvisualvm.exe就行

2.11、ConcurrentHashMap

  1. 底层数据结构:
    • JDK1.7底层采用分段的数组+链表实现
    • JDK1.8采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。
  2. 加锁的方式
    • JDK1.7采用Segment份段锁,底层使用的是ReentrantLock。
    • JDK1.8采用CAS添加新节点,采用Synchronized锁定链表或红黑二叉树的首节点,相对Segment分段锁粒度更细,性能更好。

2.12、导致并发程序出现问题的根本原因是什么

Java并发编程三大特性

  • 原子性
  • 可见性
  • 有序性

原子性

  • 一个线程在CPU中操作不可暂停,也不可中断,要不执行完成,要不不执行

解决方案:

  1. synchronized:同步加锁
  2. JUC里面的lock:加锁

内存可见性

  • 让一个线程对共享变量的修改对另一个线程可见

解决方案:

  • synchronized
  • volatile(推荐)
  • LOCK

有序性

  • 指令重排:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

解决方案:

  • volatile

三、线程池

3.1、说一下线程池的核心参数(线程池的执行原理知道嘛)

线程池核心参数主要参考ThreadPoolExecutor这个类的7个参数的构造函数

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
  • corePoolSize 核心线程数目

  • maximumPoolSize 最大线程数目 = (核心线程+救急线程的最大数目)

  • keepAliveTime 生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放

  • unit 时间单位 - 救急线程的生存时间单位,如秒、毫秒等

  • workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务

  • threadFactory 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等

  • handler 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略

工作流程

在这里插入图片描述
1、任务在提交的时候,首先判断核心线程数是否已满,如果没有满则直接添加到工作线程执行

2、如果核心线程数满了,则判断阻塞队列是否已满,如果没有满,当前任务存入阻塞队列

3、如果阻塞队列也满了,则判断线程数是否小于最大线程数,如果满足条件,则使用临时线程执行任务,如果核心或临时线程执行完成任务后会检查阻塞队列中是否有需要执行的线程,如果有,则使用非核心线程执行任务

4、如果所有线程都在忙着(核心线程+临时线程),则走拒绝策略

拒绝策略:

  1. AbortPolicy:直接抛出异常(默认策略)
  2. CallerRunsPolicy:用调用者所在的线程来执行任务;
  3. DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
  4. DiscardPolicy:直接丢弃任务;

3.2、线程池中有哪些常见的阻塞队列

workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务

比较常见的有4个,用的最多是ArrayBlockingQueueLinkedBlockingQueue

  1. ArrayBlockingQueue:基于数组结构的有界阻塞队列,FIFO。
  2. LinkedBlockingQueue:基于链表结构的有界阻塞队列,FIFO。
  3. DelayedWorkQueue :是一个优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前的
  4. SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。

ArrayBlockingQueue的LinkedBlockingQueue区别

LinkedBlockingQueueArrayBlockingQueue
默认无界,支持有界强制有界
底层是链表底层是数组
是懒惰的,创建节点的时候添加数据提前初始化 Node 数组
入队会生成新 NodeNode需要是提前创建好的
两把锁(头尾)一把锁

左边是LinkedBlockingQueue加锁的方式,右边是ArrayBlockingQueue加锁的方式

  • LinkedBlockingQueue读和写各有一把锁,性能相对较好
  • ArrayBlockingQueue只有一把锁,读和写公用,性能相对于LinkedBlockingQueue差一些

在这里插入图片描述

3.3、如何确定核心线程数

在设置核心线程数之前,需要先熟悉一些执行线程池执行任务的类型

  • IO密集型任务

一般来说:文件读写、DB读写、网络请求等

推荐:核心线程数大小设置为2N+1 (N为计算机的CPU核数)

  • CPU密集型任务

一般来说:计算型代码、Bitmap转换、Gson转换等

推荐:核心线程数大小设置为N+1 (N为计算机的CPU核数)

java代码查看CPU核数

// 获取CPU核数
int availableProcessors = Runtime.getRuntime().availableProcessors();

参考回答:

1、高并发、任务执行时间短 -->( CPU核数+1 ),减少线程上下文的切换

2、并发不高、任务执行时间长

  • IO密集型的任务 --> (CPU核数 * 2 + 1)

  • 计算密集型任务 --> ( CPU核数+1 )

3、并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)

3.4、线程池的种类有哪些

在java.util.concurrent.Executors类中提供了大量创建连接池的静态方法,常见就有四种

3.4.1、创建使用固定线程数的线程池

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
  • 核心线程数与最大线程数一样,没有救急线程

  • 阻塞队列是LinkedBlockingQueue,最大容量为Integer.MAX_VALUE

  • 适用场景:适用于任务量已知,相对耗时的任务

  • 案例:

public class FixedThreadPoolCase {

    static class FixedThreadDemo implements Runnable {
        @Override
        public void run() {
            String name = Thread.currentThread().getName();
            for (int i = 0; i < 2; i++) {
                System.out.println(name + ":" + i);
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        //创建一个固定大小的线程池,核心线程数和最大线程数都是3
        ExecutorService executorService = Executors.newFixedThreadPool(3);

        for (int i = 0; i < 5; i++) {
            executorService.submit(new FixedThreadDemo());
            Thread.sleep(10);
        }
        executorService.shutdown();
    }
}

3.4.2、单线程化的线程池

它只会用唯一的工作线程来执行任 务,保证所有任务按照指定顺序(FIFO)执行

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
  • 核心线程数和最大线程数都是1

  • 阻塞队列是LinkedBlockingQueue,最大容量为Integer.MAX_VALUE

  • 适用场景:适用于按照顺序执行的任务

  • 案例:

  public class NewSingleThreadCase {
  
      static int count = 0;
  
      static class Demo implements Runnable {
          @Override
          public void run() {
              count++;
              System.out.println(Thread.currentThread().getName() + ":" + count);
          }
      }
  
      public static void main(String[] args) throws InterruptedException {
          //单个线程池,核心线程数和最大线程数都是1
          ExecutorService exec = Executors.newSingleThreadExecutor();
  
          for (int i = 0; i < 10; i++) {
              exec.execute(new Demo());
              Thread.sleep(5);
          }
          exec.shutdown();
      }
  }

3.4.3、可缓存线程池

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}
  • 核心线程数为0

  • 最大线程数是Integer.MAX_VALUE

  • 阻塞队列为SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。

  • 适用场景:适合任务数比较密集,但每个任务执行时间较短的情况

  • 案例:

  public class CachedThreadPoolCase {
  
      static class Demo implements Runnable {
          @Override
          public void run() {
              String name = Thread.currentThread().getName();
              try {
                  //修改睡眠时间,模拟线程执行需要花费的时间
                  Thread.sleep(100);
  
                  System.out.println(name + "执行完了");
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
      }
  
      public static void main(String[] args) throws InterruptedException {
          //创建一个缓存的线程,没有核心线程数,最大线程数为Integer.MAX_VALUE
          ExecutorService exec = Executors.newCachedThreadPool();
          for (int i = 0; i < 10; i++) {
              exec.execute(new Demo());
              Thread.sleep(1);
          }
          exec.shutdown();
      }
  }

3.4.4、提供了“延迟”和“周期执行”功能的ThreadPoolExecutor

public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
            new ScheduledThreadPoolExecutor.DelayedWorkQueue());
}

public ScheduledThreadPoolExecutor(int corePoolSize,
                                   ThreadFactory threadFactory) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
            new ScheduledThreadPoolExecutor.DelayedWorkQueue(), threadFactory);
}

public ScheduledThreadPoolExecutor(int corePoolSize,
                                   RejectedExecutionHandler handler) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
            new ScheduledThreadPoolExecutor.DelayedWorkQueue(), handler);
}

public ScheduledThreadPoolExecutor(int corePoolSize,
                                   ThreadFactory threadFactory,
                                   RejectedExecutionHandler handler) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
            new ScheduledThreadPoolExecutor.DelayedWorkQueue(), threadFactory, handler);
}
  • 适用场景:有定时和延迟执行的任务

  • 案例:

  public class ScheduledThreadPoolCase {
  
      static class Task implements Runnable {
          @Override
          public void run() {
              try {
                  String name = Thread.currentThread().getName();
  
                  System.out.println(name + ", 开始:" + new Date());
                  Thread.sleep(1000);
                  System.out.println(name + ", 结束:" + new Date());
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
      }
  
      public static void main(String[] args) throws InterruptedException {
          //按照周期执行的线程池,核心线程数为2,最大线程数为Integer.MAX_VALUE
          ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2);
          System.out.println("程序开始:" + new Date());
  
          /**
           * schedule 提交任务到线程池中
           * 第一个参数:提交的任务
           * 第二个参数:任务执行的延迟时间
           * 第三个参数:时间单位
           */
          scheduledThreadPool.schedule(new Task(), 0, TimeUnit.SECONDS);
          scheduledThreadPool.schedule(new Task(), 1, TimeUnit.SECONDS);
          scheduledThreadPool.schedule(new Task(), 5, TimeUnit.SECONDS);
  
          Thread.sleep(5000);
  
          // 关闭线程池
          scheduledThreadPool.shutdown();
      }
  }

3.5、为什么不建议用Executors创建线程池

参考阿里开发手册《Java开发手册-嵩山版》
在这里插入图片描述

四、多线程-线程使用场景问题

4.1、项目中哪里用到了多线程

  • 批量导入:使用了线程池+CountDownLatch批量把数据库中的数据导入到了ES任意)中,避免OOM。
  • 数据汇总:调用多个接口来汇总数据,如果所有接口(或部分接口)的没有依赖关系,就可以使用线程池+future来提升性能。
  • 异步线程(线程池):为了避免下一级方法影响上一级方法(性能考虑),可更用异步线程调用下一入方法(不需要下一级法返回值),可以提升方法响应时间。

4.2、线程池使用场景CountDownLatch、Future

4.2.1、CountDownLatch

CountDownLatch(闭锁/倒计时锁)用来进行线程同步协作,等待所有线程完成倒计时(一个或者多个线程,等待其他多个线程完成某件事情之后才能执行)

  • 其中构造参数用来初始化等待计数值
  • await() 用来等待计数归零
  • countDown() 用来让计数减一

在这里插入图片描述

案例代码:

public class CountDownLatchDemo {

    public static void main(String[] args) throws InterruptedException {
        //初始化了一个倒计时锁 参数为 3
        CountDownLatch latch = new CountDownLatch(3);

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"-begin...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            //count--
            latch.countDown();
            System.out.println(Thread.currentThread().getName()+"-end..." +latch.getCount());
        }).start();
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"-begin...");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            //count--
            latch.countDown();
            System.out.println(Thread.currentThread().getName()+"-end..." +latch.getCount());
        }).start();
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"-begin...");
            try {
                Thread.sleep(1500);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            //count--
            latch.countDown();
            System.out.println(Thread.currentThread().getName()+"-end..." +latch.getCount());
        }).start();
        String name = Thread.currentThread().getName();
        System.out.println(name + "-waiting...");
        //等待其他线程完成
        latch.await();
        System.out.println(name + "-wait end...");
    }
}

4.2.2、案例一(es数据批量导入)

在我们项目上线之前,我们需要把数据库中的数据一次性的同步到es索引库中,但是当时的数据好像是1000万左右,一次性读取数据肯定不行(oom异常),当时我就想到可以使用线程池的方式导入,利用CountDownLatch来控制,就能避免一次性加载过多,防止内存溢出

整体流程就是通过CountDownLatch+线程池配合去执行
在这里插入图片描述
详细实现流程:
在这里插入图片描述

4.2.3、案例二(数据汇总)

在一个电商网站中,用户下单之后,需要查询数据,数据包含了三部分:订单信息、包含的商品、物流信息;这三块信息都在不同的微服务中进行实现的,我们如何完成这个业务呢?
在这里插入图片描述

详细实现代码,请查看当天代码

  • 在实际开发的过程中,难免需要调用多个接口来汇总数据,如果所有接口(或部分接口)的没有依赖关系,就可以使用线程池+future来提升性能

  • 报表汇总

在这里插入图片描述

4.2.4、案例二(异步调用)

在这里插入图片描述
在进行搜索的时候,需要保存用户的搜索记录,而搜索记录不能影响用户的正常搜索,我们通常会开启一个线程去执行历史记录的保存,在新开启的线程在执行的过程中,可以利用线程提交任务

4.3、如何控制某个方法允许并发访问线程的数量?

它提供了两个方法

  • semaphore.acquire():请求信号量,可以限制线程的个数,是一个正数,如果信号量是-1,就代表已经用完了信号量,其他线程需要阻塞了。
  • semaphore.release():代表是释放一个信号量,此时信号量的个数+1。

五、多线程-ThreadLocal

5.1、概述

ThreadLocal是多线程中对于解决线程安全的一个操作类,它会为每个线程都分配一个独立的线程副本从而解决了变量并发访问冲突的问题。ThreadLocal 同时实现了线程内的资源共享。
案例:使用JDBC操作数据库时,会将每一个线程的Connection放入各自的ThreadLocal中,从而保证每个线程都在各自的 Connection 上进行数据库的操作,避免A线程关闭了B线程的连接。
在这里插入图片描述

5.2、ThreadLocal基本使用

三个主要方法:

  • set(value) 设置值
  • get() 获取值
  • remove() 清除值
public class ThreadLocalTest {
    static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        new Thread(() -> {
            String name = Thread.currentThread().getName();
            threadLocal.set("itcast");
            print(name);
            System.out.println(name + "-after remove : " + threadLocal.get());
        }, "t1").start();
        new Thread(() -> {
            String name = Thread.currentThread().getName();
            threadLocal.set("itheima");
            print(name);
            System.out.println(name + "-after remove : " + threadLocal.get());
        }, "t2").start();
    }

    static void print(String str) {
        //打印当前线程中本地内存中本地变量的值
        System.out.println(str + " :" + threadLocal.get());
        //清除本地内存中的本地变量
        threadLocal.remove();
    }

}

5.3、ThreadLocal的实现原理&源码解析

ThreadLocal本质来说就是一个线程内部存储类,从而让多个线程只操作自己内部的值,从而实现线程数据隔离
在这里插入图片描述
在ThreadLocal中有一个内部类叫做ThreadLocalMap,类似于HashMap

ThreadLocalMap中有一个属性table数组,这个是真正存储数据的位置

set方法
在这里插入图片描述
get方法/remove方法
在这里插入图片描述

5.4、ThreadLocal-内存泄露问题

Java对象中的四种引用类型:强引用、软引用、弱引用、虚引用

  • 强引用:最为普通的引用方式,表示一个对象处于有用且必须的状态,如果一个对象具有强引用,则GC并不会回收它。即便堆中内存不足了,宁可出现OOM,也不会对其进行回收
    在这里插入图片描述
  • 弱引用:表示一个对象处于可能有用且非必须的状态。在GC线程扫描内存区域时,一旦发现弱引用,就会回收到弱引用相关联的对象。对于弱引用的回收,无关内存区域是否足够,一旦发现则会被回收
    在这里插入图片描述
    每一个Thread维护一个ThreadLocalMap,在ThreadLocalMap中的Entry对象继承了WeakReference。其中key为使用弱引用的ThreadLocal实例,value为线程变量的副本
    在这里插入图片描述
    在使用ThreadLocal的时候,强烈建议:务必手动remove

参考回答

  1. ThreadLocal可以实现【资源对象】的线程隔离,让每个线程客用客的【资源对象】,避免争用引发的线程安全问题
  2. ThreadLocal同时实现了线程内的资源共享
  3. 每个线程内有一个ThreadLocalMap类型的成员变量,用来存储资源对象
    • 调用set方法,就是以ThreadLocal自己已作为key,资源对象作为value,放入当前线程的ThreadLocalMap集合中。
    • 调用get方法,就是以ThreadLocal自己作为key,到当前线程中查找关联的资源值。
    • 调用remove方法,就是以ThreadLocal自己作为key,移除当前线程关联的资源值。
  4. ThreadLocal内存泄漏问题
    ThreadLocalMap中的key是弱引用,值为强引用;key会被Gc释放内存,关联value的内存并不会释放。建议主动remove释放key,这样就能避免内存溢出。。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值