并发编程面试题

文章目录

    • 1、为什么要使用并发编程?
    • 2、缺点
    • 3、并发编程三要素
      • 3.1 线程安全问题的原因
      • 3.2 解决办法
    • 4、并行和并发
    • 5、线程和进程的区别
    • 6、如何在 Windows 和 Linux 上查找哪个线程cpu利用率最高?
    • 7、线程的创建方式
      • 7.1 继承Thread类
      • 7.2 实现Runnable接口
      • 7.3 匿名内部类
      • 7.4 Lambad 表达式
      • 7.5 实现Callable接口
      • 7.6 基于线程池构建线程
    • 8、Runnable和Callable的区别
    • 9、run和start的区别
    • 10、线程的状态
    • 11、线程调度算法
    • 12、线程同步以及线程调度相关的方法
    • 13、Java程序中怎么保证多线程的运行安全?
    • 14、synchronized
      • 14.1 作用:
      • 14.2 用法
      • 14.3 双重检验锁方式实现单例模式的原理(线程安全)
      • 14.4 synchronized的底层实现原理
      • 14.5 synchronized的锁升级
      • 14.6 锁的相关概念
      • 14.7 synchronized、volatile、CAS 比较
      • 14.8 synchronized 和 Lock 有什么区别?
      • 14.9 synchronized 和 ReentrantLock 区别是什么?
    • 15、volatile
    • 16、Lock体系
      • 16.1 概念
      • 16.2 乐观锁和悲观锁的理解及如何实现,有哪些实现方式?
      • 16.3 CAS
        • 16.3.1 CAS带来的问题
      • 16.4 死锁
      • 16.5 AQS(AbstractQueuedSynchronizer)详解与源码分析
      • 16.6 ReentrantLock(重入锁)实现原理与公平锁非公平锁区别
      • 16.7 读写锁ReentrantReadWriteLock
    • 17、线程池
      • **优点**:
      • **状态**:
      • 线程池中 submit() 和 execute() 方法有什么区别?
      • 线程池之ThreadPoolExecutor详解
    • 18、原子操作类
      • 18.1 概念:

1、为什么要使用并发编程?

  • 充分利用CPU的计算能力:将多核CPU的计算能力发挥到极致,提升性能。
  • 业务拆分,特殊业务场景先天就适合并发编程,比串行程序更适合业务需求。

多线程:程序中包含多个执行流,同时运行多个不同的线程执行不同的任务。

好处:提高CPU利用率,一个线程必须等待时,CPU可以运行其他线程,允许创建多个并行执行的线程完成各自任务。

劣势:线程需要占用更多的内存,CPU上下文切换(多线程协调和管理)需要时间跟踪线程,各线程对共享资源的访问相互影响(竞争资源问题)。

2、缺点

内存泄漏、上下文切换、线程安全、死锁

3、并发编程三要素

原子性:一个或多个操作要么全部成功要么全部失败。

可见性:一个线程对共享变量的修改,其他县城也能立刻看见。

有序性:程序执行的顺序按照代买的先后顺序执行。

3.1 线程安全问题的原因

  1. 线程切换带来的原子性问题
  2. 缓存导致的可见性问题
  3. 编译优化带来的有序性问题(处理器可能对指令重新排序)

3.2 解决办法

  1. JDK Atomic开头的原子类、synchronized、LOCK可以解决原子性问题
  2. synchronized、volatile、LOCK可以解决可见性问题
  3. Happens-Before规则可以解决有序性问题

4、并行和并发

并发:多个任务在同一个CPU核上,按细分的时间片轮流(交替执行/上下文切换),逻辑上是同时执行

并行:单位时间内,多个处理器或多核处理器同时执行多个任务,真正的同时执行。

串行:n个任务,一个线程按顺序执行。

5、线程和进程的区别

进程:一个在内存中运行的程序。每个进程都有独立的内存空间,一个进程可以有多个线程

操作系统资源分配的基本单位。

进程与进程之间独立,访问其他进程资源开销较大,保护模式下进程崩溃不会影响其他进程。

线程:进程中的一个执行任务(控制单元),负责进程中程序的执行。多个线程可共享数据。

处理器任务调度和执行的基本单位。

线程切换开销较小,一个线程崩溃整个进程撕掉。

6、如何在 Windows 和 Linux 上查找哪个线程cpu利用率最高?

Windows上可以使用任务管理器,Linux上可以用top这个工具看。

  • 找出Cpu好用厉害的进程pid,执行top命令,shift+p查找cpu利用最厉害的pid号
  • top -H -p pid。shift+p,查找出cpu利用率最高的线程号,比如 top -H -p 1328
  • 线程号转16进制
  • 使用jstack工具将进程信息打印,jstack pid > /tmp/t.dat,比如jstack31365 > /tmp/t.dat
  • 编辑 /tmp/t.dat文件,查找到线程号对应的信息

也可以使用arthas,通过线程日志,直接找到对应的代码块。

7、线程的创建方式

7.1 继承Thread类

创建Thread类的子类,重写run方法,方法中是线程具体要执行的业务,通过start方法启动线程

public class One extends Thread {
    private String name;
    public One(String name) {
        this.name = name;
    }
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " run()方法正在执行...");
        for (int i = 0; i < 10; i++) {
            System.out.println("i:"+i+";"+name);
            try {
                sleep(1000);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        One one1 = new One("A");
        One one2 = new One("B");
        one1.start();
        one2.start();
        System.out.println(Thread.currentThread().getName() + " main()方法执行结束");
    }

7.2 实现Runnable接口

  • 实现Runnable接口,重写run方法
  • 创建实例,以此作为target创建Thread对象(真正的线程对象),通过start方法启动线程
public class Two implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " run()方法执行中...");
    }

    public static void main(String[] args) {
        Two two = new Two();
        Thread thread = new Thread(two);
        thread.start();
        System.out.println(Thread.currentThread().getName() + " main()方法执行结束");
    }
}

7.3 匿名内部类

相当于实例化了了Runnable接口中的run方法,然后放到Thread对象中执行生成一个线程

    public static void main(String[] args) {
        //匿名内部类创建线程
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println(i+";线程A");
                    try {
                        Thread.sleep(1000);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println(i+";线程B");
                    try {
                        Thread.sleep(1000);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
            }
        });
        thread1.start();
        thread2.start();
    }

7.4 Lambad 表达式

相当于匿名内部类的简化写法

    public static void main(String[] args) {
        Thread thread1 = new Thread(()->{
            for (int i = 0; i < 10; i++) {
                System.out.println(i+";线程A");
                try {
                    Thread.sleep(1000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        });
        Thread thread2 = new Thread(()->{
            for (int i = 0; i < 10; i++) {
                System.out.println(i+";线程B");
                try {
                    Thread.sleep(1000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        });
        thread1.start();
        thread2.start();
    }

7.5 实现Callable接口

配合FutureTask使用,用于需要有返回结果的执行方法同步非阻塞

  • 创建实现Callable接口的实现类myCallable
  • 以myCallable作为参数,创建FutureTask对象
  • 将FutureTask作为参数创建Thread对象
  • 通过调用start()方法启动
public class Five implements Callable {
    private String name;

    public Five(String name) {
        this.name = name;
    }

    @Override
    public Object call() throws Exception {
        int i;
        for (i = 0; i < 10; i++) {
            System.out.println(i + ";线程A");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return i;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask futureTask1 = new FutureTask(new Five("A"));
        FutureTask futureTask2 = new FutureTask(new Five("B"));
        Thread thread1 = new Thread(futureTask1);
        Thread thread2 = new Thread(futureTask2);
        thread1.start();
        thread2.start();
        System.out.println(futureTask1.get());
        System.out.println(futureTask2.get());
    }
}

Future接口表示异步任务,用于获取结果。

FutureTask表示一个异步运算的任务,传入一个Callable的实现类,可以对这个异步运算的任务结果进行获取、判断是否已完成、取消任务等操作。

7.6 基于线程池构建线程

使用 Executors 工具类创建线程池

Executors提供了一系列工厂方法创建线程池,返回的线程池都实现了ExecutorService接口。

8、Runnable和Callable的区别

  • 两者都是接口,都采用Thread.start()来启动
  • Runnable接口的ruh方法没有返回值,且只能抛出运行时异常,无法捕获处理
  • Callable接口的call方法有返回值,允许抛出异常,可以捕获异常信息
  • Callbale接口可以通过FutuerTask.get()得到返回值,此方法会阻塞主线程。

9、run和start的区别

run:线程体,可以重复调用,只是线程中的一个函数,直接调用run()必须等待run()方法中的内容执行完毕。

start:启动线程,并使线程进入就绪状态。只能调用一次,无序等待run方法执行完毕,可以直接开启新的线程。

10、线程的状态

并发编程面试题_第1张图片

1、创建线程(new):新创建了一个线程对象。

Thread t = new Thread();

2、可运行(runnable):对象创建后,调用start方法,此时处于就绪状态,等待被线程调度选中,获取CPU的使用权。

3、运行(running):可运行的线程获取到了cpu时间片,执行程序。

4、阻塞(block):运行状态中,暂时放弃对CPU的使用权,直到其再次进入就绪状态,才有机会被CPU调用。

  1. 等待阻塞:执行了wait()方法,JVM会将线程放入等待队列
  2. 同步阻塞:线程回去synchronized同步锁失败(其他线程占用),JVM会讲线程放入锁池中
  3. 其他阻塞:执行sleep()、join()或发出来IO请求时,进入阻塞状态。当sleep超时、join等待县城终止或超时、IO处理完毕时,线程会自己进入就绪状态。

5、死亡(dead):线程run()、main()方法执行结束,或者因为异常退出了run方法,线程生命周期结束。

11、线程调度算法

分时调度模型:所有线程轮流获得CPU使用权,平均分配每个线程占用的时间片。

抢占式调度模型:优先让可运行池中优先级高的线程占用CPU,优先级相同就随机选择。处于运行状态的线程会一直执行,直到其自己放弃CPU。

12、线程同步以及线程调度相关的方法

wait():使线程处于等待(阻塞)状态,释放所持有的对象锁。属于Object类,一般用于线程间通讯,需要notify()或notifyAll()方法唤醒。一般在循环中使用,在循环中检查等待条件。

synchronized (monitor) {
    //  判断条件谓词是否得到满足
    while(!locked) {
        //  等待唤醒
        monitor.wait();
    }
    //  处理其他的业务逻辑
}

sleep():使线程休眠,静态方法。不会释放锁。属于Three类,一般用于暂停执行,时间结束会自己恢复成就绪状态

notify():唤醒处于等待的线程,具体唤醒哪一个线程由虚拟机控制。

notifyAll():唤醒所有处于等待的线程。继续去进行锁竞争,竞争失败则留在锁池等待锁被释放再次参与竞争。

13、Java程序中怎么保证多线程的运行安全?

  • 使用安全类,比如使用Atom开头的原子类
  • 使用自动锁synchronized。
  • 使用手动锁Lock。
Lock lock = new ReentrantLock();
lock. lock();
try {
    System. out. println("获得锁");
} catch (Exception e) {
    // TODO: handle exception
} finally {
    System. out. println("释放锁");
    lock. unlock();
}

14、synchronized

14.1 作用:

控制代码块不被多个线程同时执行,可以修饰类、方法、变量、代码块。

JDK1.6之前,synchronized属于重量级锁,监视器锁(monitor)依赖于底层操作系统,Java线程是映射到操作系统的原生线程上的。如果挂起或者唤醒一个线程,需要操作系统完成,需要从用户态切换到内核态,时间成本较大。

JDK1.6之后,对锁的实现进行了大量的优化,自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等。

14.2 用法

修饰实例方法:当前实例对象加锁。

修饰静态方法:给当前类加锁,作用于类的所有对象实例。静态成员不属于任何一个实例(static声明该类的一个静态资源,不过new了多少个对象,只有这一份)。

修饰代码块:指定加锁对象,进入代码库前,需要获得指定对象锁。

尽量不要使用字符串类型作为锁,因为JVM中,字符串常量池具有缓存功能。

14.3 双重检验锁方式实现单例模式的原理(线程安全)

public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public static Singleton getUniqueInstance() {
        //先判断对象是否已经实例过,没有实例化过才进入加锁代码
        if (uniqueInstance == null) {
            //类对象加锁
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

uniqueInstance = new Singleton()的执行步骤

  1. 为 uniqueInstance 分配内存空间
  2. 初始化 uniqueInstance
  3. 将 uniqueInstance 指向分配的内存地址

多线程环境下,JVM可能进行指令重排,所以需要用volatile修饰,禁止JVM的指令重排。

14.4 synchronized的底层实现原理

Java对象头:对象在内存中的存储布局分为三个区域

  • 对象头
  • 实例数据
  • 对齐填充

对象头主要包括两部分

  1. 类型指针:对象指向它的元数据的指针,虚拟机通过这个指针确定这个对象是哪个类的实例。
  2. 标记字段:存储对象自身的运行时数据,哈希码、GC分代年龄、锁状态、线程持有的锁等。

synchronized使用的锁对象,存储在Java对象头的标记字段里面。

Monitor监视器

保证每次只能有一个线程进入被保护的数据,进入房间持有Monitor,退出房间释放Monitor。

synchronized加锁的同步代码块在字节码引擎中执行时,主要是通过锁对象的monitor的取用(monitor)和释放(monitorexit)实现的。有两个monitorexit是为了保证线程异常退出,锁也能被释放,防止死锁

Monitor上的线程流转:对象监视器会设置几种状态区分请求的线程:

  • Contention List:所有请求锁的线程都将被首先放置到该竞争队列
  • Entry List:Contention List中有资格的候选人的线程被移到Entry List
  • Wait Set:调用wait方法被阻塞的线程放入Wait Set
  • OnDeck:最多只能有一个线程在竞争锁
  • Owner:获得所得线程
  • IOwner:释放锁的线程

14.5 synchronized的锁升级

锁解决了数据的安全性,但是也带来了性能的下降,调查发现加锁的代码总是有一个线程多次获得。

基于这个问题,JDK1.6后,为减少获得锁和释放锁带来的性能开销,引入了偏向锁、轻量级锁,锁的状态从低到高不断升级。

无锁:没有对资源进行锁定,所有线程均能访问,但仅有一个可以修改成功。

偏向锁:偏向于第一个获得它的线程,如果接下来该锁没有被其他线程获取,则持有该锁的线程永远不需要同步。

轻量级锁:锁位偏向锁时,被其他线程访问,升级为该锁,其他线程会通过自旋的形式尝试获取,不会阻塞(提高性能)

重量级锁:原始的synchronized的实现。其他线程视图获取锁时,都会被阻塞,只有持有锁的线程释放锁,其他线程才会被唤醒。

14.6 锁的相关概念

重入锁:一个线程获取到锁之后,该线程还可以继续获得该锁。底层原理:维护一个计数器,获得锁时+1,再次获得再+1,释放时-1,当计数器为0时,表示没有被任何线程占用,可以竞争。

自旋:等待的锁不被阻塞,在边界做循环尝试获取锁。(线程阻塞涉及用户态和内核态的切换)

14.7 synchronized、volatile、CAS 比较

  • synchronized是悲观锁,抢占式,引起其他线程阻塞
  • volatile提供多线程共享变量的可见性和禁止指令重排
  • CAS是基于冲突检测的乐观锁(非阻塞)

14.8 synchronized 和 Lock 有什么区别?

  • synchronized是关键字,Lock是Java类
  • synchronized可以修饰方法、类、代码块;Lock只能给代码块加锁
  • synchronized不需要手动获取、释放锁,发生异常会自动释放锁(两次monitorexit);Lock需要自己加锁和释放锁
  • synchronized无法得知有没有成功获取锁,Lock可以知道

14.9 synchronized 和 ReentrantLock 区别是什么?

  • synchronized是关键字,ReentrantLock是类,lock的子类,都是可重入锁
  • ReentrantLock作为类使用更加灵活,需要手动获取释放,synchronized不需要
  • ReentrantLock只适用于代码块,synchronized可以修饰方法、类、代码块

15、volatile

作用:保证可见性和禁止指令重排。确保一个线程的修改能对其他线程的可见性。

一个共享变量被修改,确保其立即被更新到主内存,其他线程可以立即读取到它的新值。

常用于多线程环境下的单次操作(读或写)

16、Lock体系

16.1 概念

相比较于同步方法和同步块,Lock接口提供了更具扩展性的锁操作。允许更灵活的结构,可以具有完全不同的性质。

优势

  • 锁更公平
  • 使线程在等待锁的时候响应中断
  • 让线程尝试获取锁,并在无法获取锁的时候立即返回,或等待一段时间
  • 可以在不同范围,以不同顺序获取和释放锁

synchronized的扩展版,提供了无条件的、可轮询的、定时的、可中断的、可多条件队列的锁操作。

实现类基本都支持非公平锁和公平锁,synchronized仅支持非公平锁。

16.2 乐观锁和悲观锁的理解及如何实现,有哪些实现方式?

悲观锁:总是假设最坏的情况,每次拿数据都认为会被其他人修改,因此每次拿数据的时候都上锁。(行锁、表锁、读锁、写锁、synchronized)

乐观锁:每次拿数据都认为不会有人修改,不会上锁,但是更新时会去判断在此期间别人有没有更新,可以使用版本号机制。适用于多读的应用类型,提高吞吐量。

乐观锁的实现方式

  1. 使用版本号来确定独到的数据和提交时是否一致。提交后更新版本标识,不一致则采取丢弃和再次尝试的策略。
  2. CAS,当多个线程尝试使用CAS同时更新一个变量时,只有其中一个线程能更新变量的值,其他线程都失败,但不会被挂起,而是被告知这次竞争中的失败,并可以再次尝试。

16.3 CAS

compare and swap,即交换。

基于乐观锁,CAS操作中包含三个操作数:需要读写的内存位置(V),进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置的V的值与A匹配,处理器自动将该位置更新为B值,否则不做处理。

通过无限循环来获取数据,如果第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能有机会执行。

16.3.1 CAS带来的问题

ABA问题:线程1从内存位置取出A,线程2也取出了A并将其改成B,然后线程2又将V的数据变成A,此时线程1会发现内存中仍是A,然后线程1操作成功。但可能存在隐藏问题。

循环时间长开销大:资源竞争严重的情况,CAS自旋的概率较大,浪费过多CPU资源。

只能保证一个共享变量的原子操作:多个共享变量操作时,循环CAS无法保证操作的原子性,需要用到锁。

16.4 死锁

概念:线程A持有锁a,尝试去获取锁b时,线程B持有锁b,且尝试获取锁a,两个线程互相持有对方需要的锁,发生阻塞。

产生死锁的条件

  1. 互斥条件:线程在某一时间独占资源。
  2. 请求与保持条件:一个线程因请求资源而阻塞,对已获得的资源保持不放。
  3. 不剥夺条件:线程已获得的资源,使用完之前,不能强行剥夺。
  4. 循环等待条件:多个线程之间,头尾相连,循环等待资源关系。

只要破坏了其中一个条件,死锁则破除。

防止死锁的方法

  • 尽量使用tryLock的方法(ReentrantLock、ReentrantReadWriteLock),设置超时时间,超时可以退出防止死锁。
  • 尽量使用java.util,concurrent并发类代替自己手写锁
  • 尽量降低锁的使用粒度,不要几个功能用同一把锁
  • 减少同步的代码块

16.5 AQS(AbstractQueuedSynchronizer)详解与源码分析

抽象类 ,在java.util.concurrent.locks包下面。

作用:一个用来构建锁和同步器的框架,比如ReentrantLock、ReentrantReadWriteLock,SynchronousQueue,FutureTask等,还可以自定义同步器。

原理:如果被请求的共享资源空闲,将当前请求资源的线程设置为有效工作线程,且将共享资源设置为锁定状态。如果被请求的资源被占用,就需要一套线程阻塞等待以及被唤醒时锁分配的机制。

CLH队列锁实现。将暂时获取不到锁的线程加入到队列中。

CLH是一个虚拟的双向队列,AQS将每条请求共享资源的线程封装成一个CLH锁队列的节点(Node)来实现锁的分配。

定义了两种资源共享方式

独占:只要一个线程能执行,又分为公平锁和非公平锁。

  • 公平锁:按照线程队列中的排队顺序,先到先拿
  • 非公平锁:浙江舟山人队列顺序,直接抢锁

共享:多个线程可同时执行。

//返回同步状态的当前值
protected final int getState() {  
        return state;
}
 // 设置同步状态的值
protected final void setState(int newState) { 
        state = newState;
}
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

16.6 ReentrantLock(重入锁)实现原理与公平锁非公平锁区别

概念:Lock的实现类,支持重入性,即可以给共享资源重复加锁,当前线程再次获取该锁不会被阻塞。

synchronized锁升级的偏向锁也支持重入,通过获取自增,释放自减实现重入。

重入性的实现原理

  1. 线程获取锁时,如果已经获取锁的线程是当前线程,则直接获取成功
  2. 因为锁会被获取n次,只要锁再被释放同样的n次之后,才算完全释放

16.7 读写锁ReentrantReadWriteLock

作用:如果使用 ReentrantLock,多个线程读取时也进行了加锁,降低程序性能。而ReentrantReadWriteLock是一个读写锁,提升性能的锁分离技术,本身也是ReentrantLock的子类。

实现了读写的分离,读锁共享,写锁独占,提升了读写的性能。

特性

  1. 公平选择性:支持非公平和公平的锁获取方式。
  2. 重进入:读锁和写锁都支持线程重进入
  3. 锁降级:遵循获取写锁=》获取读锁=》释放写锁的顺序,写锁能够降级为读锁。

17、线程池

作用:减少每次获取资源的消耗,提高资源的利用率。

本质:先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程,而不需要自行创建;使用完也不需要销毁线程,而是放回池中,减少了创建、销毁小程对象的开销。

Executor接口的子类型即线程池接口ExecutorService提供了一些静态工厂方法生成一些常用线程池。

ThreadPoolExecutor 可以创建自定义线程池。

  1. newSingleThreadExecutor:单线程的线程池。串行化执行所有任务。
  2. newFixedThreadPool:固定大小的线程池。每次提交任务创建一个线程,直到线程池装满,装满后线程数量保持不变。
  3. newCachedThreadPool:可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程数量,就会回收部分空闲的线程(60s不执行任务);任务数量增加时,线程池自行添加新的线程处理任务。线程池大小依赖于操作系统(或JVM)能够创建的最大线程数。
  4. newScheduledThreadPool:创建一个无限大小的线程池。支持定时、周期性执行任务。

优点

  • 降低资源消耗:重用存在的线程,减少创建、销毁带来的开销。
  • 提高响应速度。有效控制最大并发线程数,提高资源使用率,避免过多资源竞争,避免阻塞。
  • 提高线程的可管理性。统一分配,调优和监控。
  • 附加功能:提供定时执行、定期执行、单线程、并发数控制等功能。

状态

  • RUNNING:接受新任务,处理等待队列中的任务
  • SHUTDOWN:不接受新任务,继续处理等待队列中的任务
  • STOP:既不接受新任务,也不处理等待队列中的任务,中断正在执行的线程
  • TIDYING:所有任务都销毁了。
  • TERMINATED:terminated()方法结束后,线程池的状态就会变成这个。

线程池中 submit() 和 execute() 方法有什么区别?

接受参数:execute()只能执行Runnable类型的任务。submit()可以执行Runnable和Callable类型的任务

返回值:submit()方法可以返回持有计算结果的Future对象。

异常处理:submit()方便Exception管理。

线程池之ThreadPoolExecutor详解

Executors 各个方法的弊端:

  • newFixedThreadPool (固定大小的线程池)和 newSingleThreadExecutor(多线程的线程池):堆积的请求处理队列可能会耗费非常大的内存。
  • newCachedThreadPool(可以缓存的线程池) 和 newScheduledThreadPool(可以定时或周期性执行任务的线程池):可能会创建非常多的线程,导致OOM。

而ThreadPoolExecutor是通过构造函数自定义线程池(阿里巴巴开发规范中明确规范的线程池创建方式)

核心参数

  • corePoolSize:核心线程数,定义了最小可同时运行的线程数
  • maximumPoolSize:线程池中可运行存在的工作线程的最大数量
  • workQueue:新任务来的时候,先判断当前运行的线程数是否达到核心线程数,达到则会放入队列中。

其他参数

  • keepAliveTime:线程池中的参数大于核心线程数时,如果没有新任务提交,核心外的线程会等待超过keepAliveTime时间后,被销毁。
  • unit:keepAliveTime参数的时间单位。
  • threadFactory:为线程池提供创建新线程的线程工厂。
  • handler:线程池任务队列超过可运行的最大工作线程数量之后的拒绝策略。

饱和策略

  • ThreadPoolExecutor.AbortPolicy:抛出RejectedExecutionException异常来拒绝新任务。
  • ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务,不会扔掉任何一个任务。
  • ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃。
  • ThreadPoolExecutor.DiscardOldestPolicy:丢弃最早的未处理的任务请求。

线程池实现原理

  • 提交任务
  • 判断核心线程数是否已满,未满则创建线程
  • 已满,则判断等待队列是否已满,未满则加入队列
  • 已满,则判断线程池是否已满,未满则创建新的线程
  • 已满,则按照饱和策略处理

首先创建一个 Runnable 接口的实现类

public class MyRunnable implements Runnable {

    private String command;

    public MyRunnable(String command) {
        this.command = command;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date() + ";command = " + command);
        sleepTest();
        System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date() + ";command = " + command);
    }

    private void sleepTest() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public String toString() {
        return this.command;
    }

}

使用 ThreadPoolExecutor 构造函数自定义参数的方式来创建线程池。

public class ThreadPoolExecutorDemo {

    //核心线程数
    private static final int CORE_POOL_SIZE = 5;
    //最大工作线程数
    private static final int MAX_POOL_SIZE = 10;
    //队列容量
    private static final int QUEUE_CAPACITY = 100;
    //非核心线程销毁等待时间
    private static final Long KEEP_ALIVE_TIME = 1L;

    public static void main(String[] args) {
        //使用阿里巴巴推荐的创建线程池的方式
        //通过ThreadPoolExecutor构造函数自定义参数创建
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                CORE_POOL_SIZE,//核心线程数
                MAX_POOL_SIZE,//最大工作线程数
                KEEP_ALIVE_TIME,//非核心线程销毁等待时间
                TimeUnit.SECONDS,//KEEP_ALIVE_TIME的时间单位
                new ArrayBlockingQueue<>(QUEUE_CAPACITY),//队列容量
                new ThreadPoolExecutor.CallerRunsPolicy());//淘汰策略

        for (int i = 0; i < 10; i++) {
            Runnable worker = new MyRunnable(""+i);
            executor.execute(worker);
        }
        //终止线程池
        executor.shutdown();
        //死循环判断线程是否已关闭
        while (!executor.isTerminated()) {
        }
        System.out.println("所有的子线程都结束了!");
    }
}

18、原子操作类

18.1 概念:

原子操作:不可被中断的一个或一些列操作。多线程环境下避免数据不一致问题必须得手段。

处理器使用基于对缓存加锁或总线加锁的方式实现多处理器之间的原子操作。

Java中通过锁和循环CAS的方式实现原子操作

jdk1.5后推出了java.util.concurrent 包,提供了一组原子类。

基本特性:多线程环境下,多个线程同时执行这些类的方法时,具有排他性,即不会被其他线程打断,此时别的线程就像自旋锁一样,直到该方法执行完成,才由JVM从等待队列中选择另一个线程进入。

原子类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference

原子数组:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

原子属性更新器:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater

解决 ABA 问题的原子类:AtomicMarkableReference(通过引入一个 boolean来反映中间有没有变过),AtomicStampedReference(通过引入一个 int 来累加来反映中间有没有变过)

AtomicInteger 类的部分源码

通过CAS+volatile和native方法保证原子操作,避免了synchronized的高开销,效率提升。

CAS的原理是将期望的值和原本的值进行比较,相同则更新成新的值。Unsafe类的objectFieldOffset方法是一个native方法,用来拿到“原来的值”的内存地址。value被volatile修饰,内存中可见,JVM可以保证任何时刻线程总能拿到改变量最新的值。

// setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替换”的作用)
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

static {
	try {
		valueOffset = unsafe.objectFieldOffset
		(AtomicInteger.class.getDeclaredField("value"));
	} catch (Exception ex) { throw new Error(ex); }
}

private volatile int value;

你可能感兴趣的:(面试题,java,面试,jvm)