Java 多线程底层原理与实现

本文深入探讨了Java多线程的各种锁的概念,包括乐观锁、悲观锁、可重入锁等,并介绍了线程的实现方式,如继承Thread、实现Runnable和Callable接口,以及线程池的使用。此外,还讲解了线程池的核心参数、工作原理及监控,以及线程的基本概念和通信方式。最后,分析了AQS、CAS、ReentrantLock与synchronized的区别等并发编程的关键知识点。

一、各种锁的概念

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. 多线程常用方法

方法名解释
Objectwait等待Object 锁
Objectnotify通知所有等待该锁的线程中的其中一个
ObjectnotifyAll通知所有等待该锁的线程
ThreadjoinA线程中调用B线程的Join 方法后,A会立即阻塞,等待线程B执行完成后再唤醒进入就绪状态。
Threadyield线程执行yield 方法后会放弃时间片,进入就绪队列,随时准备再次执行。但是不会释放锁。
Threadinterrupt中断线程,仅仅是给线程状态设置为中断,并不打断线程的运行
ThreadisInterrupted判断线程状态是否为中断状态
ThreadsetDaemon设置当前线程为后台线程
Threadsleep阻塞当前线程一段时间
Threadsuspend挂起线程,不释放锁,容易死锁
Threadstop终止线程,释放锁。

五、线程通信方式

在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++;

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

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

资源共享,共同进步!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值