【面试】并发编程

文章目录

基础知识

并发编程的优缺点

优点

  • 充分利用多核CPU的计算能力
  • 方便业务拆分,提升系统的并发能力和性能

缺点

  • 并发可能会导致内存泄漏、线程安全、死锁等问题。

并发三要素是什么?Java怎么保证多线程的运行安全
三要素:

  1. 原子性: 一个或多个操作要么全部执行成功要么全部执行失败
  2. 可见性:一个线程对共享变量的修改,另一个线程能够立刻看到
  3. 有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)

出现线程安全的原因:

  • 线程切换带来的原子性问题
  • 缓存导致的可见性问题
  • 编译优化带来的有序性问题

解决办法:

  • 原子性问题:使用JDK Atomic开头的原子类、synchronized、LOCK
  • 可见性问题:Synchronized、volatile、LOCK
  • 有序性问题:Happens-Before规则

并行和并发有什么区别?

  • 并发:多个任务在同一个CPU上,按细分的时间片轮流执行,从逻辑上来看那些任务是同时执行。
  • 并行:单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的“同时进行”
  • 串行:有n个任务,由一个线程按顺序执行。

线程和进程的区别

进程
一个在内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程可以有多个线程。
线程
线程中的一个指向任务负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可以共享数据。
进程和线程的区别

  • 根本区别:进程是操作系统资源分配的基本单位,而线程是任务处理器任务调度和执行的基本单位。
  • 资源开销、包含关系、内存分配、影响关系、执行过程等。

线程死锁

死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程)。
在这里插入图片描述

代码模拟死锁情况:

public class DeadLockDemo {
    private static Object resource1 = new Object();//资源 1
    private static Object resource2 = new Object();//资源 2

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "线程 1").start();

        new Thread(() -> {
            synchronized (resource2) {
                System.out.println(Thread.currentThread() + "get resource2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource1");
                synchronized (resource1) {
                    System.out.println(Thread.currentThread() + "get resource1");
                }
            }
        }, "线程 2").start();
    }
}

输出结果:

Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]get resource2
Thread[线程 1,5,main]waiting get resource2
Thread[线程 2,5,main]waiting get resource1

形成死锁的四个必要条件

  1. 互斥条件:资源具有排他性,一个资源只能被一个线程占用,知道该线程
  2. 请求与保持条件:一个线程因请求被占用资源而发生阻塞时,对获得资源保持不放
  3. 不剥夺条件:已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  4. 循环等待条件:当发生死锁时,所等待的线程必定会形成一个环路,造成永久阻塞。

如何避免死锁

破坏死锁产生四个条件中的一个就行了。
破坏互斥条件
这个条件没办法破坏,因为锁的目的就是让他们互斥
破坏请求与保持条件
一次性申请所有的资源
破坏不剥夺条件
占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
破坏循环等待条件
按序申请资源来预防。按照某一顺序申请资源,资源释放则反序释放

创建线程的四种方式

  1. 继承Thread类;
  2. 实现Runnable接口;
  3. 实现Callable接口;
  4. 使用Execuors工具类创建线程池

继承thread类

  1. 定义一个Thread子类,重写run方法, run()方法就是要执行的业务
  2. 创建自定义的线程子类对象
  3. 调用子类实例的star()方法来启动线程
public class MyThread extends Thread {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " run()方法正在执行...");
    }
}


public class TheadTest {

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

}

实现Runnable接口
步骤

  1. 实现Runnable接口实现类MyRunnable,并重写run()方法
  2. 创建MyRunnable实例myRunnable,以myRunnable作为target创建Thread,该Thread对象才是真正的线程对象
  3. 调用线程对象的start()方法
public class MyRunnable implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " run()方法执行中...");
    }
}


public class RunnableTest {

    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
        System.out.println(Thread.currentThread().getName() + " main()方法执行完成");
    }

}

实现Callable接口

  1. 创建实现Callable接口的类myCallable
  2. 以myCallable为参数创建FutureTask对象
  3. 以FutureTask作为参数创建Thread对象
  4. 调用线程对象的start()方法
public class MyCallable implements Callable<Integer> {

    @Override
    public Integer call() {
        System.out.println(Thread.currentThread().getName() + " call()方法执行中...");
        return 1;
    }

}


public class CallableTest {

    public static void main(String[] args) {
        FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyCallable());
        Thread thread = new Thread(futureTask);
        thread.start();

        try {
            Thread.sleep(1000);
            System.out.println("返回结果 " + futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " main()方法执行完成");
    }

}

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

主要有newFixedThreadPool,newCachedThreadPool,newSingleThreadExecutor,newScheduledThreadPoll四种线程池。

public class MyRunnable implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " run()方法执行中...");
    }

}

public class SingleThreadExecutorTest {

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        MyRunnable runnableTest = new MyRunnable();
        for (int i = 0; i < 5; i++) {
            executorService.execute(runnableTest);
        }

        System.out.println("线程任务开始执行");
        executorService.shutdown();
    }
}

runnable和callable有什么区别?

相同点

  • 都是接口
  • 都可以编写多线程程序
  • 都是采用Thread.start()启动线程

主要区别

  • Runnable接口run方法无返回值;Callable接口call方法有返回值,是个泛型,和Future、FutureTask配合可以获取异步执行的结果
  • Runnable接口run方法只能抛出运行时异常,且无法捕获处理;Callable接口call方法允许抛出异常,可以获取异常信息。

注:Callable接口支持返回执行结果,需要调用FutureTask.get()得到,但是会阻塞主进程的继续往下执行,如果不调用不会阻塞。

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

start()方法用于启动线程,run()方法用于执行线程的运行时代码。run()可以重复调用,而start()只能调用一次。

start()方法用来启动一个线程,真正实现了多线程的运行,调用start方法无需等待run方法体代码执行完毕,可以直接继续执行其他代码;此时线程是处于就绪状态,并没有运行。然后通过Thread类调用方法run()来完成其运行状态,run()方法运行结束,线程重视。

如果直接调用run()方法等于是调用一个main()线程下的普通方法,是会同步阻塞的,不是多线程工作。

什么是Callable、Future和FutureTask?

实现Runnable接口和继承Thread类,线程任务完成后都无法获取返回结果。Callable接口类似于Runnable,但Runnable不会返回结果,并且无法抛出结果的异常,而Callable功能更强大一些,被线程执行后,配合Future和FutureTask可以取得返回的结果。

Future是用来获取异步计算结果的,说白了就是对具体的Runable或者Callable对象执行的结果进行获取get(),取消cancel(),判断是否完成等操作。

FutureTask除了实现Future接口还实现了Runnable接口,所以FutureTask除了可以获取异步计算结果,还可以直接提交给Executor执行。

线程的状态和基本操作

线程的声明周期和五种基本状态

在这里插入图片描述

  1. 新建:新创建了一个线程对象
  2. 就绪状态:线程对象创建后,当调用线程对的start()方法,该线程处于就绪状态,等待被线程调度选中,获取CPU使用权。
  3. 运行:可运行状态的线程获得了cpu时间片,执行程序代码。
  4. 阻塞:由于运行状态中的线程因为某种原因,暂时放弃对CPU的使用权,进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用进入到运行状态。
    • 等待阻塞:运行状态线程执行wait()方法,JVM会把该线程放入到等待队列中
    • 同步阻塞:线程在获取Synchronized同步锁失败,JVM会把该线程放入到锁池中,线程会进入到同步阻塞状态;
    • 其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求,线程会进入到阻塞状态。
  5. 死亡:线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。

Java中用到的线程调度算法是什么?

有两种调度模型:分时调度模型和抢占式调度模型

分时调度模型是指让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占用的 CPU 的时间片这个也比较好理解。

Java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃 CPU。

请说出线程同步以及线程调度相关的方法

  1. wait():使一个线程处于阻塞状态,并且释放所持有对象的锁
  2. sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法需要处理InterruptedException
  3. notify():唤醒一个处于等待状态的线程,由JVM确定唤醒哪个线程,与优先级无关
  4. notifyAll():唤醒所有处于等待状态的线程,让它们竞争,只有获得锁的线程才能进入就绪状态。

sleep()和wait()有什么区别?

  • sleep()是Thread线程类的静态方法,wait()是Object类方法。
  • sleep()不释放锁,wait()释放锁。
  • wait()通常用于线程间通信,sleep通常用于暂停执行
  • wait()被调用后,需要notify()或者notifyAll()唤醒。sleep()方法执行完成后,线程自动苏醒。

Thread类中yield()方法有什么作用

使当前线程从运行状态变为就绪状态。

线程到了就绪状态,接下来执行的线程,可能是当前线程也可能是其它线程,看系统的分配。

Thread类的sleep()和yield()方法区别

  1. sleep()方法给其他线程运行时不考虑线程优先级;yield()方法只会给相同优先级以及更高优先级线程运行机会
  2. 线程执行sleep()后转入阻塞状态,执行yield()方法后转入就绪状态
  3. sleep()方法抛出InterruptedException,而yield()方法没有声明任何异常
  4. sleep()方法比yield()方法具有更好的可移植性

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

  1. 使用安全类,比如java.util.concurrent下的类,使用原子类AtomicInteger
  2. 使用自动锁Synchronized
  3. 使用手动锁Lock

并发理论

重排序与数据依赖性

为什么代码会重排序

在执行程序时,为了提供性能,处理器和编译器会进行重排序。但是要满足两个条件:

  • 在单线程环境下不能改变程序运行的结果
  • 存在数据依赖关系的不允许重排序

as-if-serial规则和happends-before规则的区别

  • as-if-serial语义保证单线程内程序的执行结果不被改变,happends-before关系保证正确同步的多线程程序的执行结果不被改变
  • 目的是为了在不改变执行结果的前提下,尽可能提高程序执行的并行度

并发关键字

synchronized

Synchronized的作用

Synchronized关键字是用来控制线程同步的,在多线程环境下,控制Synchronized代码段不被多个线程同时执行。Synchronized可以修饰类、方法、变量。

在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

怎么使用Synchronized关键字,在项目中用到了吗

synchronized关键字最主要的三种使用方式

  • 修饰实例方法:给当前对象实例加锁,进入同步代码之前要获得当前对象实例的锁
  • 修饰静态方法:给当前类加锁,会做用于类的所有对象实例。因为党文静态synchronized方法占用的锁是当前类的锁,而访问非静态synchronized方法占用的锁是当前实例对象锁。
  • 修饰代码块:指定加锁对象,给给定对象加锁,进入同步代码库前要获得给定对象的锁。

小结:synchronized关键字加到static静态方法和synchronized(class)代码块上都是是给Class类上锁。synchronized关键字加到实例方法上是给对象实例上锁。

双重校验锁实现对象单例(线程安全)

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;
    }
}

说一下synchrnized底层实现原理?

synchronized是JVM实现的一种互斥同步访问方式,底层是基于每个对象的监视器(monitor)来实现的。被synchronized修饰的代码,在被编译器编译后在被修饰的代码前后加上了一组字节指令。
在代码开始加入了monitorenter,在代码后面加入了monitorexit,这两个字节码指令配合完成了synchronized关键字修饰代码的互斥访问。
在虚拟机执行到monitorenter指令的时候,会请求获取对象的monitor锁,基于monitor锁又衍生出一个锁计数器的概念。
当执行monitorenter时,若对象未被锁定时,或者当前线程已经拥有了此对象的monitor锁,则锁计数器+1,该线程获取该对象锁。
当执行monitorexit时,锁计数器-1,当计数器为0时,此对象锁就被释放了。那么其他阻塞的线程则可以请求获取该monitor锁。

什么是自旋

很多synchronized里面的代码都是一些很简单的代码,执行时间非常快。此时等待的线程加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题,影响性能。既然synchronized代码运行的非常快,那么不妨让等待锁的线程不要被阻塞,而是在synchronized的边界做忙循环,这就是自旋。如果做了多次循环还没有获得锁,再阻塞。

synchronized锁升级的原理是什么?

在锁对象头里面有一个threadId字段,第一次访问的时候threadId为空,JVM让其持有偏向锁,并将threadId设置为其线程id,再次进入的时候会先判断threadId是否与其线程id一直,如果一致可以直接使用此对象。如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数后,如果还没有正常获取到要使用的对象,此时会把轻量级锁升级为重量级锁,这个过程就构成了synchronized锁的升级

锁升级的目的:锁升级是为了减低锁带来的性能消耗。

线程B怎么知道线程A修改了变量

  1. volatile修饰变量
  2. Synchronized修饰修改遍历的方法
  3. wait/notify
  4. while查询

Synchronized、volatile、CAS比较

  1. Synchronized是悲观锁,属于抢占式,会引起其他线程阻塞
  2. volatile提供多线程共享变量可见性和禁止指令重排序优化
  3. CAS是基于冲突检测的乐观锁

Synchronized和Lock有什么区别

  • Synchronized是Java内置的关键字,在JVM层面,Lock是个Java类
  • Synchronized可以给类、方法、代码块加锁;Lock只能给代码块加锁
  • Sychronized不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;Lock需要自己加锁和释放锁,如果没有unlock()去释放会造成死锁
  • 通过Lock可以知道有没有成功获取锁,而Synchronized却无法办到;Lock是采用乐观锁CAS机制实现的

volatile

volatile关键字的作用

对于可见性,Java提供了volatile关键字来保证可见性和禁止指令重排。volatile提供happens-before保证,确保一个线程的修改对其他线程是可见的。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其它线程需要读取时,它会去内存中读取新值。

从实践角度而言,volatile重要作用就是和CAS结合,保证了原子性。 详细见java.util.concurrent.atomic包下的类

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

volatile变量和atomic变量有什么不同?

volatile可以确保先行关系,即写操作会发生在后续的读操作之前,但是不能保证原子性。例如用volatile修饰count遍历,那么count++操作就不是原子性的。
而AtomicInteger类提供的atomic方法可以让这种操作具有原子性。如getAndIncrement()方法会原子性的进行增量操作把当前值加一,其他数据类型和引用变量也可以进行相似操作。

Synchronized和volatile的区别是什么?

Synchronized表示只有一个线程能够获取作用对象的锁,执行代码,阻塞其他线程。
volatile表示变量在CPU的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的可见性;禁止指令重排序。
区别

  • volatile是变量修饰符;Synchronized可以修饰类、方法、遍历;
  • volatile仅能实现遍历的修改可见性,不能保证原子性;而Synchronized可以保证变量的修改可见性和原子性
  • volatile不会造成线程的阻塞;Synchronized可能会造成线程的阻塞
  • volatile标记的变量不会被编译器优化;Synchronized标记的变量可以被编译器优化
  • volatile是线程同步的轻量级实现,随意volatile性能肯定比Synchronized关键字好。但是volatile关键字只适用于变量,而Synchronized关键字可以修饰方法以及代码块。

Lock体系

Lock简介与初识AQS

Lock接口是什么?对比同步有什么优势?

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

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

整体上来说Lock是Synchronized的扩展版,Lock提供了无条件的、可轮询的(tryLock方法)、定时的、可中断的、可多条件队列的锁操作。

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

悲观锁:总是假设最坏的情况,每次拿数据的时候都认为别人会修改,所以每次拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统数据库用到的是这种锁机制,如行锁、表锁,读锁、写锁等,都是在操作之前先上锁。Synchronized关键字的实现也是悲观锁。

乐观锁:很乐观,每次去拿数据都认为别人不会修改,所以不会上锁。但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可使用版本号等机制控制ABA问题。乐观锁适用于多读的常见,这样可以提高吞吐量。Java下的java.util.concurrent.atomic包下面的原子变量就是使用乐观锁的一种实现方式CAS实现的

乐观锁的实现方式:

  1. 使用版本标识来确定读到的数据与提交的数据是否一次,提交后修改版本标识,不一致时可以采取丢失和再次尝试的策略。
  2. CAS操作包含三个操作数——需要读取的内存位置V,进行比较的预期原值A和拟写入的新值B。如果内存位置V和预期原值A相匹配,处理器自动将该位置更新为B。否则处理器不做任何操作。

CAS产生的问题?

  1. ABA问题
    • 加版本号
    • 加时间戳
    • atomic包里提供了一个类AtomicstampedReference来解决ABA问题
  2. 循环时间长开销大:
    -资源竞争验证的情况,CAS自旋概率会比较大。会浪费更多的CPU资源,效率低于Synchronized
  3. 只能保证一个共享变量的原子操作

AQS详解和源码分析

AQS原理分析

AQS全称为AbstractQueuedSynchronizer,这个类在java.util.concurrent.locks包下面。
AQS核心思想是,如果有请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即暂时获取不到锁的线程加入到队列中

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

在这里插入图片描述

AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排列工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。

AQS对资源的共享方式

  • Exclusive(独占):只要一个线程能执行,如ReentrantLock。又可以分为公平锁和非公平锁:
    • 公平锁:按照线程在队列中的排列顺序,先到者先拿到锁
    • 非公平锁:当线程要获取锁时,无视队列顺序直接取抢锁,谁抢到是谁的
  • Share(共享):多个线程可以同时执行,Semaphore/CountDownLatch

ReentrantLock实现原理与公平锁与非公平锁的区别

什么是可重入锁?

ReentrantLock重入锁,是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性。表示能够对共享资源能够重复加锁,即当前线程获取该锁后再次获取不会被阻塞。

Java关键字Synchronized隐式支持重入性,Synchronized通过获取自增,释放自减的方式实现重入。与此同时,ReentrantLock还支持公平锁和非公平锁两种方式。要想完全弄懂ReentrantLock需要明白:1. 重入性的实现原理;2.公平锁和非公平锁。

重入性的实现原理
想支持重入性,就要解决两个问题:1.在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功;2.由于锁会被获取n次,那么只有锁在被释放同样的n次后,该锁才算是完全释放成功

公平锁和非公平锁
何谓公平性,是针对获取锁而言的,如果一个锁是公平的,那么获取锁的顺序就应该符合请求上的绝对时间顺序,满足FIFO。

读写锁ReentrantReadWriteLock源码分析

使用ReentrantLock某些时候有局限性,本身是为了防止线程A在写数据,线程B读数据造成数据不一致。但这样,线程C和线程D也在读数据,读数据是不会改变数据的,没必要加锁,但还是加锁了降低了程序的性能。因此产生了读写锁ReadWriteLock

ReadWriteLock是一个读写锁接口,读写锁是用来提升并发程序性能的锁分离技术,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写分离。读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。

读写锁有三个重要特性:

  1. 公平选择性:支持非公平(默认)和公平获取锁,吞吐量还是非公平优于公平
  2. 重进入:读锁和写锁都支持线程重进入
  3. 锁降级:遵循获取写锁、获取读锁,再释放写锁的次序,写锁可以降级称为读锁。

并发容器

并发容器之ConcurrentHashMap详解

之前写的博客里有分析:面试之ConcurrentHashMap原理

并发容器之ThreadLocal详解

ThreadLocal是一个本地线程副本变量工具,在每个线程中都创建了一个ThreadLocalMap对象,简单说就是以空间换时间的做法。每个线程可以访问自己的内部ThreadLocalMap对象内的value。通过这种方式,避免资源在多线程间共享。

应用场景:为每个线程分配一个JDBC连接Connection,这样可以保证每个线程都在各自的Connection上进行数据库操作;还有Session管理等问题。

ThreadLocal内存泄漏分析与解决方案

并发容器之BlockingQueue详解

阻塞队列是一个支持两个附加操作的队列。

这两个附加操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。

阻塞队列通常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

JDK7提供了7个阻塞对垒。分别是:

  • ArrayBlockingQueue:数组构成的有界阻塞队列
  • LinkedBlockingQueue:链表构成的有界阻塞队列
  • PriorityBlockingQueue:支持优先级的无界阻塞队列
  • DealyQueue:使用优先级队列实现的阻塞队列
  • SynchronousQueue:一个不存储元素的阻塞队列
  • LinkedTransferQueue:一个由链表结构组成的无界阻塞队列
  • LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列

Java5之前实现同步存取,使用的是普通集合。使用线程同步wait/notify/notifyAll/Synchronized关键字,实现消费者生产者模式。Java5后,可以用阻塞队列来实现,此方式大大减少了代码量,使得多线程编程更加容易,安全方面也有保障。

线程池

Executors类创建四种常见线程池

什么是线程池?有什么优点?

池化技术包括线程池、数据库连接池、HTTP连接池都是对这个思想的利用。池化技术主要是为了减少每次获取资源的消耗,提高对资源的利用率。

在面向对象编程中,创建和销毁线程都是很耗费时间的:创建一个对象需要获取内存资源或者其它更多资源。在Java中,虚拟机会试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源对象的创建和销毁。

优点

  • 降低资源消耗:重用存在的线程,减少对象创建销毁的开销
  • 提高响应速度:可以有效控制最大的并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免阻塞。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制创建,会消耗系统资源,降低系统的稳定性,使用线程可以进行统一分配,调优和监控。
  • 附加功能:提供定时执行、定期执行、单线程、并发数控制等功能

线程池的几种创建方式

Java5+中的Executor接口定义一个执行线程的工具。它的子类型即线程池接口是ExecutorService,工具类Executors提供了一些静态工厂方法,生成一些常用的线程池:

  1. newSingleThreadExecutor:创建一个单线程的线程池,相当于一个线程执行所有的任务。如果这个唯一的线程因为异常结束,会有一个新的线程来替代它,此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
  2. newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
  3. newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲的线程,当任务增加时,线程池可以新增线程来处理任务。该线程池不会对线程池大小做限制,线程池大小完全依赖于JVM能够创建的最大线程数。
  4. newScheduledThreadPool:创建一个大小无线的线程池。此线程池支持定时以及周期执行任务的需求。

线程池有哪些状态?

  • RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。
  • SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。
  • STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。
  • TIDYING:所有的任务都销毁了,workCount为0,线程池的状态转换为TIDYING状态时,会执行钩子方法terminated()
  • TERMINATED:terminated()方法结束后,线程池的状态就会变成这个。

在Java中Executor和Executors的区别?

  • Executors工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务需求
  • Executor接口对象能够执行我们的线程任务
  • ExcutorService接口继承了Executor接口并进行了扩展,提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值。
  • 使用ThreadPoolExecutor可以自定义线程池
  • Future表示异步计算的结果,提供了检查计算是否完成的方法,以等待计算的完成,并可以使用get()方法获取计算的结果。

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

  • 接收参数:exexutor只能执行Runnable类型的任务。submit()可以执行Runnable和Callable类型的任务
  • 返回值:submit()方法可以返回持有计算结果的Future对象,而executor()没有
  • 异常处理:submit()方便处理Exception

线程池之ThreadPoolExecutor详解

Executors和ThreadPoolExecutor创建线程池的区别

《阿里巴巴Java开发手册》中强制线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式。

Executors各个方法的弊端:

  • newFixedThreadPool和newSingleThreadExecutor
    • 主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM
  • newCachedThreadPool和newScheduledThreadPool
    • 主要问题是线程最大数Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM

ThreadPoolExecutor创建线程池方式只有一种,就是走它的构造函数,参数自己指定。

你知道怎么创建线程池吗?

一个是通过Executors工具类的静态方法,提供比较简便的构造几种常见线程池方法。另一种是使用ThreadPoolExecutor通过构造函数,参数自己指定。

ThreadPoolExecutor构造函数重要参数分析

ThreadPoolExecutor3个重要参数:

  • corePoolSize:核心线程数,定义了最小可以同时运行的线程数量
  • maximumPoolSize:线程池中允许存在的工作线程的最大数量
  • BlockingQueue:当新任务来的时候会判断当前运行的线程数量是否达到核心线程数,如果达到的话,任务就加入到队列中。

ThreadPoolExecutor其它常见参数:

  1. keepAliveTime:线程池中的线程数量大于corePoolSize的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,知道等待的时间超过了keepAliveTime才会被回收销毁;
  2. TimeUnit:keepAliveTime参数的时间单位
  3. ThreadFactory:为线程池提供创建新线程的线程工厂
  4. RejectedExecutionHandler:当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略.

RejectedExecutionHandler饱和策略

  • ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。
  • ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
  • ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉。
  • ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。

在这里插入图片描述

原子操作类

什么是原子操作?在Java Concurrency API中有哪些原子类

原子操作意为“不可被中断的一个或一系列操作”。
Java中可以通过锁和循环CAS方式来实现原子操作。

Java.util.concurrent这个包里面提供了一组原子类。其基本特性就是在多线程环境下,当有多个线程同时执行这些类包含的方法时,具有排他性。即当某个线程进入方法后,执行其中的指令不会被其他线程打断,别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择另一个线程进入。

原子类:AtomaicBoolean,AtomicInteger,AtomicLong,AtomicReference
原子数组:AtomicIntergerArray,AtomicLongArray,AtomicReferenceArray
原子属性更新器:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater

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

讲一下atomic原理

多个线程同时对单个变量进行操作时,具有排他性。即当多个线程同时对该变量的值进行更新的时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。

AtomicInteger类主要利用CAS(Compare And Swap)+volatile 和native方法来保证原子操作,从而避免synchronized的高开销,执行效率大为提升。

并发工具

并发工具值CountDownLatch与CyclicBarrier

CountDownLatch和CyclicBarrier都是用于控制并发的工具类,都可以理解成维护一个计数器,但是有不同的侧重点:

  • CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;而CylicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时运行;CountDownLatch强调一个线程等多个线程完成某件事情;CyclicBarrier是多个线程互等,大家都完成,再携手共进。
  • 调用CountDownLatch的countDown方法后,当前线程不会阻塞,会继续往下执行;而调用CyclicBarrier的await方法,会阻塞当前线程,知道CyclicBarrier指定的线程全部到达了指定点的时候,才能继续往下执行;
  • CountDownLatch是不能复用的,而CyclicBarrier是可以复用的。

并发工具之Semaphore与Exchanger

Semaphore
Seamaphore是一个信号量,作用是限制某段代码块的并发数。它有一个构造函数,传入int类型的n,代表某段代码最多只有n个线程访问。超过请等待,直到某个线程执行完毕,下一个线程再进入。n=1的时候相当于synchronized

Exchanger
Exchanger是一个用于线程间协作的工具类,用于两个线程间交换数据。它提供了一个交换的同步点,在这个同步点两个线程能够交换数据。交换数据是通过exhcnage方法来实现的,一个线程先执行echanger方法,那么它会同步等待另一个线程也执行exchange方法,这时候两个线程都达到了同步点,两个线程可以交换数据。

常用的并发工具类

  • Semaphore-允许多个线程同时访问
  • CountDownLatch 倒计时器
  • CylicBarrier 循环栅栏
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值