并发编程

266 阅读13分钟

一、java线程池

1. java常见的线程池使用。都在什么场景下

ThreadPoolExecutor

参数:

corePoolSize: 线程池核心线程数最大值、
maximumPoolSize: 线程池最大线程数大小
keepAliveTime: 非核心线程空闲的存活时间
unit:线程空闲存活时间单位
workQueue: 存在任务的阻塞队列
threadFactory: 创建线程的工厂, 可以给创建的线程设置名字,方便排查问题
handler:拒绝策略

为什么不推荐使用Executors创建线程池

  • newFixedThreadPool newSingleThreadExecutor都是用的LinkedBlockingQueue,允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而引起OOM异常

  • CachedThreadPool 最大线程数Integer.MAX_VALUE,可能会创建大量的线程,引起OOM异常

2. 线程池流程原理

  • 提交一个任务,线程池里存活的核心线程数小于线程数corePoolSize时,线程池会创建一个核心线程去处理提交的任务。

  • 如果线程池核心线程数已满,即线程数已经等于corePoolSize,一个新提交的任务,会被放进任务队列workQueue排队等待执行。

  • 当线程池里面存活的线程数已经等于corePoolSize了,并且任务队列workQueue也满,判断线程数是否达到maximumPoolSize,即最大线程数是否已满,如果没到达,创建一个非核心线程执行提交的任务。

  • 如果当前的线程数达到了maximumPoolSize,还有新的任务过来的话,直接采用拒绝策略处理

为什么要用阻塞队列,直接创建非核心线程不行吗?

  • 线程池创建线程需要获取mainlock这个全局锁,影响并发效率,阻塞队列可以很好的缓冲。
  • 如果新任务的到达速率超过了线程池的处理速率,那么新到来的请求将累加起来,这样的话将耗尽资源。

3. 拒绝策略

  • AbortPolicy (直接抛出一个异常,默认)
  • DiscardPolicy(直接丢弃任务)
  • DiscardOldestPolicy(丢弃队列中最老的任务,当前的任务继续提交给线程池)
  • CallerRunsPolicy (交给线程池调用所有的线程进行处理)

4. 线程池异常处理

  1. try catch捕获异常

  2. 结合Future对象的get方法接受抛出的异常,进行处理

  3. 重写ThreadPoolExecutor#afterExcute方法

5. 线程池工作队列

  • ArrayBlockingQueue : 数组实现的有界阻塞队列
  • LinkedBlockingQueue: 链表实现的有界队列,可以设置容量 newFixedThreadPool
  • DelayQueue : 延迟队列
  • PriorityBlockingQueue: 优先级队列
  • SynchronousQueue:同步队列,每一个插入操作必须等另一个移除,吞吐量通常要高于LinkedBlockingQuene , newCachedThreadPool用这个

6. 参数设置

CPU密集:核心线程数 cpu数+1 (1是可以理解为备份的线程好)

IO密集:核心线程数 2*CPU数

美团动态线程池

image.png

image.png

7. 实际遇到的问题

线程池打满

方法论:

  1. apollo动态可配置

  2. 监控记录,可以实时看到当前活跃线程数及队列排队情况

  3. 拒绝报警和日志记录

二、并发设计模式

三、什么是多线程的上下文切换

多线程会共同使用一台机器上的CPU,而当线程数大于给程序分配的CPU数量时,为了让各个线程都有执行的机会,就需要轮转使用CPU。不同线程切换使用CPU叫做上下文切换

四、悲观锁、乐观锁、可重入锁、读写锁、公平锁、非公平锁

悲观锁: 行锁、表锁、读锁、写锁、synchronized

乐观锁: 使用版本标记、CAS 实现

可重入锁: 定义: 线程可以进入任何一个它已经拥有的锁所同步着的代码块。 ReentrantLock的可重入功能基于AQS的同步状态:state juejin.cn/post/708820…

读写锁 juejin.cn/post/710279…

image.png

公平锁与非公平锁

ReentrantLock默认无参构造函数使用的是非公平锁,有参构造函数可指定使用公平锁

对于公平锁:是指在获取锁之前会检查队列中有没有线程在等待,如果有的话就不会去获取锁,而是会入队列。
那么对于非公平锁,显然就是在获取锁之前不会去检查队列中有没有线程在等待,而是直接去获取锁。如果锁没有线程占用,则队列中被唤醒的线程和新来的线程会同时竞争锁。此时,队列中被唤醒的线程并不一定能优先获得锁,所以是非公平的。

ReentrantLock可重入锁、公平锁、非公平锁实现原理

juejin.cn/post/708820…

五、volatile

JMM内存模型

juejin.cn/post/684490…

不同线程对共享变量操作的可见性,也就是说一个线程修改了变量,当修改写完内存时,另一个线程立马能看到最新的值。

三大特性:

  1. 保证可见性
  2. 不保证原子性
  3. 禁止指令重排

volatile能保证原子性么? 为什么?

不能保证原子性

volatile可见性原理

juejin.cn/post/712785…

image.png

volatile和synchronized的区别

  1. volatile只能修饰实例变量和类变量,synchronized能修饰方法和代码块
  2. volatile不能保证原子性,synchronized能保证原子性
  3. volatile不会阻塞,synchronized会有阻塞,影响性能
  4. volatile是轻量级锁,synchronized是重量级锁
  5. 都保证了可见性和有序性

六、synchronized

锁方法,锁对象,锁代码块

特性:

  1. 有序性: 加上synchronized后,依然会发生重排序,只不过,我们有同步代码块,可以保证只有一个线程执行同步代码快中的代码,从而保证有序性
  2. 可见性 可见性参考上文
  3. 原子性:
  4. 可重入性: 锁对象的时候有个计数器,会记录下线程获取锁的次数,在执行完对应的代码块后,计数器-1,直到计数器清零,就释放锁了。
  5. 不可中断性: 一个线程获取锁之后,另外一个线程阻塞或者等待状态,前一个不释放,后一个会一直阻塞或者等待,不可以被中断。

原理:

synchronized修饰的方法或者代码块,编译完成后前后出现monitorenter和monitorexit两个指定,其实这两个指定就是对monitor计数器操作的。enter用来+1 , exit用来-1直到0.

juejin.cn/post/684490…

锁升级

原来synchronized是重量级锁(获取锁需要用户态和内核态的切换,消耗性能),后来做了优化

无锁->偏向锁->轻量级锁->重量级锁

juejin.cn/post/709791…

image.png

juejin.cn/post/707253…

image.png

  • 加锁的时候发现只有一个线程,直接上锁,不存在竞争关系,这就是偏向锁。
  • 这时候第二个线程来了,偏向锁升级到轻量级锁,通过自旋+CAS来抢锁
  • 在偏向锁的基础上如果出现了重度竞争就会直接升级成重量级锁

synchronized与lock的区别

  1. synchronized是关键字,lock是一个接口,有丰富的API
  2. synchronized不可中断,lock可以中断
  3. synchronized会自动释放锁,lock需要手动释放
  4. lock可以知道线程有没有拿到锁,synchronized不知道
  5. synchronized是非公平锁,reentrantLock支持公平锁和非公平锁。

Lock基本原理

获取锁:还记得之前AQS中的int类型的state值,这里就是通过CAS(乐观锁)去修改state的值。

image.png

释放锁:就是对AQS中的状态值State进行修改。同时更新下一个链表中的线程等待节点

mp.weixin.qq.com/s/ktTOXAOxQ…

reentrantLock

juejin.cn/post/702305…

七、什么是阻塞队列?常见的阻塞队列分哪些?

BlockingQueue是Queue的子接口,作为线程同步的工具。
  1. 当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据加入队列。
  2. 当队列中填满数据的情况下,生产端的所有线程都会自动阻塞(挂起),直到队列中有空的位置,线程被自动唤醒

八、callable、runnable、future

Callable接口类似于Runnable,被线程执行后可以返回值,这个返回值可以被Future拿到。也就是说Future可以拿到异步执行任务的返回值。

Callable产生结果,Future拿到异步执行的结果。

九、线程与进程

进程是操作系统分配资源的最小单元,线程是操作系统调度的最小单元。

进程是指一个内存中的应用程序,每个进程都有自己独立的一块内存空间,一个进程可以启动多个线程。 每个线程并行执行不同的任务。

线程的生命周期

初始、运行、阻塞、等待、超时等待、终止

线程中断

线程在运行过程中被其他线程打断。与stop的区别:stop是强制终止线程,线程中断是给目标线程发送一个中断信号,如果目标线程没有收到中断信号并结束线程,线程则不会终止。

创建线程的方式

  • 通过扩展Thread类创建多线程
  • 通过实现Runnable接口创建多线程
  • 实现Callable接口,通过FutureTask创建多线程
  • 使用Executor框架创建多线程

线程死锁怎么产生?怎么避免?

四个条件:

  • 互斥:(不可避免)
  • 请求与保持:一个进程因请求资源而阻塞时,对已获得的资源保持不放(不释放锁)--一次性申请所有资源
  • 不剥夺:进程已获得的资源,在未使用之前,不能强行剥夺(抢夺资源)--主动释放占有资源
  • 循环等待:若干进程之间形成一种头尾相接的循环等待的资源关闭(死循环)---按顺申请资源

死锁的问题定位

public class DeadLockDemo {
    //两个锁对象
    private static Object obj1 = new Object();
    private static Object obj2 = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            while (true) {
                synchronized (obj1) {
                    System.out.println(Thread.currentThread().getName() + "获得obj1锁");
                    try {
                        //睡3秒让另一个线程先获得obj2锁
                        TimeUnit.SECONDS.sleep(3);
                        //此时另一个线程已经持有obj2锁,这里请求获取obj2锁就会发生死锁
                        synchronized (obj2) {
                            System.out.println(Thread.currentThread().getName() + "获得obj2锁");
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

        new Thread(() -> {
            while (true) {
                synchronized (obj2) {
                    System.out.println(Thread.currentThread().getName() + "获得obj2锁");
                    try {
                        //睡3秒让另一个线程先获得obj1锁
                        TimeUnit.SECONDS.sleep(3);
                        synchronized (obj1) {
                            System.out.println(Thread.currentThread().getName() + "获得obj1锁");
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }
}
复制代码

主要是两个命令配合起来使用,定位死锁。

jps指令:jps -l可以查看运行的Java进程。 jstack指令:jstack pid可以查看某个Java进程的堆栈信息,同时分析出死锁。

image.png

image.png

线程run和start的区别

  • start是启动线程,轮到该线程执行,会自动调用run
  • 直接调run方法无法达到启动多线程的目的,相当于主线程线性执行Thread对象的run方法。
  • 一个线程对start方法只能调用一次。run方法没有限制

十、什么是CAS?CAS存在什么问题?

Compare And Swap, 比较和交换。 是乐观锁的实现形式。能在不使用锁的情况下实现多线程之间的变量交换。ReentrantLock 内部的 AQS 和原子类内部都使用了 CAS。

涉及到的三个数: 内存里的数A,期望的数B,要更新的数U。当A==B时,更新U。否则继续重试到成功更新

存在的问题:

  1. ABA问题
  2. 循环重试时间长开销大
  3. 只能保证一个共享变量的原子性

十一、 什么是AQS

AbstractQueuedSynchronizer,抽象队列同步器。定义一套多线程访问共享资源的同步器框架。许多并发工具都的实现都依赖于它,如ReentrantLock

image.png

当前线程获取同步状态失败时,同步器会将当前线程以及等待状态(独占或共享)构造成为一个节点并将其加入同步队列并进行自旋,当同步状态释放时,会把首节中的后继节点对应的线程唤醒,使其再次尝试获取同步状态。

十二、wait()和sleep()

相同点:都是将当前线程暂停,把机会交给其他线程

不同点:

  1. wait()的Object超类中的方法,sleep()是线程Thread类的方法
  2. wait()会释放锁,sleep不会释放锁
  3. 唤醒方式不同,wait需要notify或者notifyAll、中断、达到指定时间来唤醒。sleep达到指定时间就会被唤醒
  4. 调用obj.wait()需要先获取对象的锁,Thread.sleep()不用

image.png

十三、wait()、notify()、notifyAll()

wait: 是线程进入阻塞等待状态,并释放锁

notify:用来唤醒等待的线程

notifyAll: 唤醒等待池的所有线程,竞争锁,如果竞争不成功,还是回去等待池

三个线程轮流打印ABC10次

多个方法: juejin.cn/post/684490…

juejin.cn/post/694027…

十四、ThreadLocal

1. 为什么要用TheadLocal

并发场景下,多个线程同时修改公共变量,可能出现线程安全问题。为了解决这个问题,可以使用synchronized或者Lock,给访问资源上锁,保证代码原子性。但在高并发场景下,多线程竞争一把锁,会有大量的锁等待,影响性能。ThreadLocal提供了一种空间换时间的新思路。

核心思想:共享变量在每个线程都有一个副本,每个线程操作自己的副本,对另外的线程没有影响。

2. ThreadLocal基本原理

参考

juejin.cn/post/709775…


public class ThreadLocal<T> {
    
     public T get() {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取当前线程的成员变量ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //根据threadLocal对象从map中获取Entry对象
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                //获取保存的数据
                T result = (T)e.value;
                return result;
            }
        }
        //初始化数据
        return setInitialValue();
    }
    
    private T setInitialValue() {
        //获取要初始化的数据
        T value = initialValue();
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取当前线程的成员变量ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        //如果map不为空
        if (map != null)
            //将初始值设置到map中,key是this,即threadLocal对象,value是初始值
            map.set(this, value);
        else
           //如果map为空,则需要创建新的map对象
            createMap(t, value);
        return value;
    }
    
    public void set(T value) {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取当前线程的成员变量ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        //如果map不为空
        if (map != null)
            //将值设置到map中,key是this,即threadLocal对象,value是传入的value值
            map.set(this, value);
        else
           //如果map为空,则需要创建新的map对象
            createMap(t, value);
    }
    
     static class ThreadLocalMap {
        ...
     }
    
}

ThreadLocal的get、set方法和setInitialValue方法都是操作的ThreadLocalMap的数据。


static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
   }
   
   
   private Entry[] table;
}

ThreadLocal整体结构

ThreadLocal整体结构.png

3. 为什么要用ThreadLocal做key

ThreadLocalMap为什么要用ThreadLocal做key,而不是用Thread做key?

image.png

4. 为什么Entry的key设计成弱引用

我们都知道ThreadLocal变量对ThreadLocal对象是有强引用存在的。

即使ThreadLocal变量生命周期完了,设置成null了,但由于key对ThreadLocal还是强引用。

此时,如果执行该代码的线程使用了线程池,一直长期存在,不会被销毁。

就会存在这样的强引用链:Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> key -> ThreadLocal对象。

那么,ThreadLocal对象和ThreadLocalMap都将不会被GC回收,于是产生了内存泄露问题。

为了解决这个问题,JDK的开发者们把Entry的key设计成了弱引用

5. ThreadLocal的用途

  1. 在spring事务中,保证一个线程下,一个事务的多个操作拿到的是一个Connection
  2. 在hiberate中管理session
  3. 在JDK8之前,为了解决SimpleDateFormat的线程安全问题
  4. 获取当前登录用户上下文
  5. 临时保存权限问题
  6. 使用MDC保存日志信息