Java 多线程底层原理与实现

Java 多线程底层原理与实现

  • 一、各种锁的概念
    • 1.乐观锁与悲观锁
    • 2.共享锁/独占锁
    • 3.可重入锁
    • 4.公平锁和非公平锁
    • 5.无锁、偏向锁、轻量级锁、重量级锁
    • 6.自旋锁
    • 7.互斥锁和读写锁
  • 二、线程的实现方式
    • 1. 继承Thread 类
    • 2. 实现Runnable 接口
    • 3. 实现Callable 接口,配合FutureTask
    • 4. 使用线程池
  • 三、线程池详解
    • 1. 为什么使用线程池
    • 2. 线程池核心参数与工作原理
      • 核心参数
      • 阻塞队列
      • 拒绝策略
      • 添加线程流程
    • 3. Java 中实现的线程池
      • newCachedThreadPool
      • newFixedThreadPool
      • newSingleThreadExecutor
      • newScheduledThreadPool
    • 4. 监控线程池
      • 直接使用线程池参数
      • 扩展线程池
  • 四、线程基本概念与方法
    • 1. 线程的基本概念
      • 线程的状态
    • 2. 多线程常用方法
  • 五、线程通信方式
    • 1. 使用Synchronized
    • 2. 使用ReentrantLock
  • 六、理论与源码解析
    • 1. AQS
    • 2. CAS
    • 3. ReentrantLock 和 Synchronized 的区别
    • 4. notify 和 notifyAll的区别
    • 5. volatile 关键字有什么特点

一、各种锁的概念

1.乐观锁与悲观锁

乐观锁

每次读取数据的时候都认为数据没有被修改过,读取数据的时候不加锁 , 但是在更新的时候会去对比一下原来的值,看有没有被别人更改过。适用于读多写少的场景

juc中atomic 使用的就是乐观锁,即CAS

悲观锁

每次读取数据的时候都认为数据已经被修改过,读取数据的时候也会加锁。别人想要拿到数据就要等待锁。适合写操作比较多的场景

synchronized 实现也是悲观锁

2.共享锁/独占锁

独占锁

独占锁指锁一次只能被一个线程占有

ReentrantLock 就是独占锁

共享锁

锁可以被多个线程持有

ReadWriteLock 中 Read 共享, write独占

3.可重入锁

如果当前线程持有obj对象的锁,而内部代码块中还需要获取obj锁,直接放行的方式就是可重入锁。

synchronized 和 ReentrantLock都是可重入锁

实现方式为为锁对象设置一个计数器和占有他的线程,每次获取锁的时候计数器加一,释放锁的时候计数器减一,当计数器为0的时候释放锁。

4.公平锁和非公平锁

公平锁

所有尝试获取锁的线程都会加入锁的等待队列,每次唤醒队列中的第一个线程。

常见于AQS

非公平锁

抢占锁时候判断锁是否被占有,没被占有直接抢占锁,如果被占有就加入等待队列

ReentrantLock使用非公平锁

非公平锁的性能比公平锁好,因为线程有机会不阻塞直接获得锁,公平锁需要唤醒阻塞队列中的线程,所以公平锁的CPU开销会比较大。

5.无锁、偏向锁、轻量级锁、重量级锁

偏向锁

仅有一个线程在使用锁,没有竞争线程,就是偏向锁,一旦有其他线程产生竞争,锁升级为轻量级锁

轻量级锁

当前有两个线程,一个持有锁,另一个会自旋等待锁;当再有一个线程(3个以上)同时竞争锁的时候,锁升级为重量级锁

重量级锁

其他线程试图获取锁的时候都会进入阻塞队列,只有当前线程释放锁的时候才会唤醒线程。

6.自旋锁

获取不到锁就一直循环试图获取锁。

7.互斥锁和读写锁

synchronized、ReentrantLock属于互斥锁;(都是独占锁)

ReadWriteLock 属于读写锁(读为共享锁,写为独占锁)

二、线程的实现方式

Java 中线程的实现方式主要有四种,利用Spring 注解@Asyn 也可以实现,这里不做详细讨论。它们分别是:

  • 继承Thread类,重写run 方法
  • 实现Runnable 接口,重写run 方法
  • 实现Callable 接口,重写call()方法,创建FutureTask 对象,指定Callable 对象
  • 利用线程池

1. 继承Thread 类

class TestThread extends Thread {
    @Override
    public void run() {
        // do something ....
    }
}

2. 实现Runnable 接口

class TestThread implements Runnable {
    @Override
    public void run() {
        // do something ....
    }
}

3. 实现Callable 接口,配合FutureTask

// 创建Callable 对象
Callable<String> stringCallable = () -> {
    System.out.println("do something");
    Thread.sleep(2000);
    return "ok";
}; 
// 根据Callable 对象创建FutureTask 对象
FutureTask<String> stringFutureTask = new FutureTask<>(stringCallable);
// 创建线程并启动
new Thread(stringFutureTask).start();
// 阻塞当前线程直到FutureTask返回处理结果
String result = stringFutureTask.get();

4. 使用线程池

三、线程池详解

1. 为什么使用线程池

在开发过程中不建议使用直接使用继承Thread类或者直接实现Runnable 的方式来管理线程,当每接收一个请求创建一个线程,线程执行完毕再销毁的这种模式会引发如下事实:

  • 频繁创建线程耗费资源
  • 线程上下文切换问题
  • 可能引发资源耗尽的风险

使用线程池后,优点如下:

  • 加快响应时间
  • 增加吞吐量

但是线程池使用不当也会有一些风险,比如:

  • 死锁:线程池中的线程持有其他线程的锁
  • 资源不足:假如不断向无限线程池中添加任务就会导致资源不足。
  • 并发错误:wait 和 notify 使用不当
  • 请求过载:QPS 极高的情况下,不可能为每个请求都分配一个线程,分配可能导致请求过载。

2. 线程池核心参数与工作原理

核心参数

定义:

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

其中各个参数的含义如下:

  • corePoolSize:核心线程数目,核心线程没有最长存活时间,及时线程终止也不会被回收。
  • maximumPoolSize:线程池内 (核心线程+非核心线程)的数量
  • keepAliveTime:非核心线程的最大存活时间
  • unit:keepAliveTime的单位
  • workQueue:等待执行的任务队列
    • ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排
    • LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
    • SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
    • PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
  • threadFactory:线程工厂类
  • handler:拒绝策略

阻塞队列

  • ArrayBlockingQueue; //基于数组的先进先出队列,此队列创建时必须指定大小;
  • LinkedBlockingQueue; //基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;
  • SynchronousQueue; //这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。
  • SynchronousQueue ; // 这个队列不持有任务,而是会直接递交给线程池。如果没有空闲工作线程,则提交失败。

拒绝策略

  • ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
  • ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
  • ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
  • ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

添加线程流程

  1. 提交任务给线程池
  2. 判断核心线程数是否已满,如果没有满直接创建线程
  3. 如果核心线程已经满了,判断阻塞队列是否已经满了,如果阻塞队列没满,创建线程假如阻塞队列中。
  4. 如果此时阻塞队列也满了,判断线程池中所有的线程数目是否达到了线程池最大数目,如果达到最大数目执行拒绝策略。
  5. 如果没有超过最大线程数,创建线程并加入线程池,当有某线程的空闲时间超过keepAliveTime 的时候,该线程会销毁。

3. Java 中实现的线程池

Alibaba 开发手册明确规定不允许使用java 自带的线程池,现在对四种线程池做详细总结:

  • Executors. newFixedThreadPool
  • Executors. newCachedThreadPool
  • Executors. newSingleThreadExecutor
  • Executors. newScheduledThreadPool

newCachedThreadPool

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, 
                                      Integer.MAX_VALUE,
                                      60L, 
                                      TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

关注点:核心线程数为0,最大线程数为最大值,阻塞队列选用SynchronousQueue。

缺点:来了任务就执行,而且可容纳的最大线程数很大,过期时间很长,很容易OOM。

newFixedThreadPool

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, 
                                      nThreads,
                                      0L, 
                                      TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

关注点:核心线程数为n,最大线程数为n,阻塞队列选用LinkedBlockingQueue。

缺点:同时执行n个线程,但是队列采用LinkedBlockingQueue,大小默认为Integer最大值,所以和CachedPool一样容易爆内存。

newSingleThreadExecutor

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService(
            new ThreadPoolExecutor( 1, 
                                    1,
                                    0L, 
                                    TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

关注点:核心线程数为1,最大线程数为1,阻塞队列选用LinkedBlockingQueue。

缺点:一个一个任务顺序执行,阻塞队列还是无限的(Integer最大值约等于无限),容易OOM。

newScheduledThreadPool

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

关注点:核心线程数为corePoolSize,最大线程数为最大值,阻塞队列选用DelayedWorkQueue。

缺点:允许最大线程数为Integer最大值,大量任务到来的时候创建这么多线程完全是不可取的。可能导致OOM。

4. 监控线程池

想要监控线程池状态有两种方案可以实现

直接使用线程池参数

  • taskCount:线程池任务总量。
  • completedTaskCount:已完成任务数量。
  • largestPoolSize:从创建开始到现在,线程池中最多同时持有多少个线程。
  • getPoolSize: 此时线程池的线程数量。

扩展线程池

继承线程池并重写下列方法:

  • beforeExecute
  • afterExecute
  • terminated

四、线程基本概念与方法

1. 线程的基本概念

线程的状态

  • 新建:新建线程对象,还没调用start
  • 就绪:随时可以获得CPU执行,只能转到运行状态。
  • 阻塞:运行状态通过wait 或者sleep进入。被唤醒后加入就绪态
  • 运行:就绪状态通过获得锁进入
  • 死亡:线程run 结束终止。

2. 多线程常用方法

方法名 解释
Object wait 等待Object 锁
Object notify 通知所有等待该锁的线程中的其中一个
Object notifyAll 通知所有等待该锁的线程
Thread join A线程中调用B线程的Join 方法后,A会立即阻塞,等待线程B执行完成后再唤醒进入就绪状态。
Thread yield 线程执行yield 方法后会放弃时间片,进入就绪队列,随时准备再次执行。但是不会释放锁。
Thread interrupt 中断线程,仅仅是给线程状态设置为中断,并不打断线程的运行
Thread isInterrupted 判断线程状态是否为中断状态
Thread setDaemon 设置当前线程为后台线程
Thread sleep 阻塞当前线程一段时间
Thread suspend 挂起线程,不释放锁,容易死锁
Thread stop 终止线程,释放锁。

五、线程通信方式

在Java 中实现多线程通信有多种方式(这里先使用两种,如果有其他想法可以评论下)

  • 使用synchronized 配合wait 和 notifyAll 使用
  • 使用ReentrantLock 配合 Condition 使用(JUC 包)
  • CountDownLatch 实现线程通信

这里通过一个示例来展示每种方式的用法, 新建两个线程交替打印1-100之间的数字,打印完以后主线程输出打印完毕

1. 使用Synchronized

使用Synchronized 来配合wait 和 notifyAll 来使用,其中

  • synchronized 来获得一个对象的锁
  • 使用notifyAll 通知其他等待此锁的对象可以竞争该锁。
  • 使用wait 表示等待该对象释放锁。

题解代码:

class TestThread implements Runnable {

    private static Object lock = new Object();
    // JUC 包中的类,初始设置一个值,使用countDownLatch.await()的时候会阻塞当前线程,直到初始值减小到0为止
    public static CountDownLatch countDownLatch = new CountDownLatch(2);
    private static volatile int increNum = 1;
    private static volatile int currentThreadNum = 0;
    private static final int MAX_NUM = 100;

    private volatile int threadNum;

    public TestThread(int threadNum) {
        this.threadNum = threadNum;
    }

    @Override
    public void run() {
        try {
            while (increNum < MAX_NUM) {
                synchronized (lock) { // 获得锁
                    while (this.threadNum == currentThreadNum) {
                        lock.wait(); // 交替打印实现,释放锁
                    }
                    System.out.println(String.format("Thread %d : %d" , this.threadNum , increNum));
                    ++ increNum;
                    currentThreadNum = this.threadNum;
                    lock.notifyAll(); // 通知所有线程可以抢锁了
                }
            }
            countDownLatch.countDown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class Main {
    public static void main(String[] args) throws InterruptedException {
        new Thread(new TestThread(1)).start();
        new Thread(new TestThread(2)).start();

        TestThread.countDownLatch.await();
        System.out.println("print over");
    }
}

2. 使用ReentrantLock

使用ReentrantLock 配合 Condition 实现线程通信,相关方法:

  • ReentrantLock 可以看成一把锁,谁获得锁谁可以访问资源。
  • Condition 也是一把锁,可以看成和ReentrantLock 相关联的锁,获得ReentrantLock 锁的线程可以操作Condition 这把锁,用来实现与其他线程的交互功能。

初始化

ReentrantLock lock = new ReentrantLock();
Condition lockCondition = lock.newCondition();

方法:

lock.lock();    // 尝试获得锁,获取不到阻塞当前线程,直到获得锁
lock.tryLock(); // 尝试获得锁一次,返回获取成功或者失败
lock.unlock();  // 释放锁,要在finally 中使用

lockCondition.signalAll();  // 通知所有等待此condition的线程
lockCondition.signal();     // 通知其他等待此condition 的一个线程,发出信号
lockCondition.await();      // 等待其他线程对此Condition 做signal 操作,否则阻塞当前线程,可以设置最长等待时间。
public class Main {
    public static void main(String[] args) throws InterruptedException {
        new Thread(new TestThread(1)).start();
        new Thread(new TestThread(2)).start();
        while (true) {
            try {
                TestThread.lock.lock();
                TestThread.lockCondition.await();
                System.out.println("print over");
                break;
            } finally {
                TestThread.lock.unlock();
            }
        }

    }
}
class TestThread implements Runnable {

    public static ReentrantLock lock = new ReentrantLock();
    public static Condition lockCondition = lock.newCondition();
    private static volatile int increNum = 1;
    private static volatile int currentThreadNum = 0;
    private static final int MAX_NUM = 100;

    private volatile int threadNum;

    public TestThread(int threadNum) {
        this.threadNum = threadNum;
    }

    @Override
    public void run() {
        while (increNum < MAX_NUM) {
            try {
                lock.lock();
                while (this.threadNum != currentThreadNum) {
                    System.out.println(String.format("Thread %d : %d", this.threadNum, increNum));
                    ++increNum;
                    currentThreadNum = this.threadNum;
                }
                if (increNum >= MAX_NUM) {
                    lockCondition.signalAll();
                    break;
                }
            } finally {
                lock.unlock();
            }
        }
    }
}

六、理论与源码解析

1. AQS

AQS是ReentrantLock的核心组件,内部设置state变量,持有锁线程,等待队列等信息。每次加锁给state+1(前提判断持有线程为当前线程),如果不是当前线程抢占锁则加入等待队列。state为0的时候再从队列中取出线程执行。

2. CAS

原理简介

CAS 包含如下三个重要属性

  • 内存地址V
  • 旧的预期值 A
  • 新的预期值 B

CAS 每次进行更新操作时,先从比较内存地址V中的数据和A是否相等,如果相等则更新内存地址V的值为B,否则什么都不做。

流程如下:

  1. 从V中获取值A
  2. 根据A计算目标值B
  3. 原子操作-----判断V中的值是否为A,如果为A则将V中的值改为B

存在的问题

  1. 自旋时间长,开销比较大
  2. 只能保证一个变量的原子操作
  3. 会出现ABA问题

ABA问题以及解决方案

ABA问题:

CAS的过程:

  1. 从V中获取值A
  2. 根据A计算目标值B
  3. 原子操作-----判断V中的值是否为A,如果为A则将V中的值改为B

因为只有第三步是原子操作,所以线程X获取值A,线程Y在此期间将值改为B,然后线程Z将值改为A,最后X线程执行第三步的时候对比值A还是原来的哪个值,所以会产生问题。

解决方案

  1. 加版本号
  2. 使用JUC中的 AtomicStampedReference
    • AtomicReference == AtomicInteger
    • AtomicReference 会给对象加一个时间戳

3. ReentrantLock 和 Synchronized 的区别

  • synchronized 发生异常时,会主动释放锁;
  • ReentrantLock 发生异常不会释放锁,容易造成死锁。
  • Lock是可中断锁

4. notify 和 notifyAll的区别

notify 只会唤醒一个线程,而notifyAll 会唤醒等待此Object 锁的所有线程。

5. volatile 关键字有什么特点

  1. 保证可见性

    直接从内存读取数据,而不是从寄存器中读取数据。

  2. 禁止指令重排序

    指令重排是jvm 优化代码的一种方式,但是再有volatile的地方会禁止指令重排,比如:

    int a = 1;
    int b = 1;
    a++;
    b++;
    volatile int c = a + b;
    a++;
    b++;
    

    这里前4行可能指令重排,比如先做b++在做a++;

    后两行数据也可能颠倒顺序,但不影响程序执行。

如果大家还有什么想补充的请再评论区告诉我

资源共享,共同进步!

你可能感兴趣的:(Web后端,java,多线程,并发编程)