Java面试基础篇-并发多线程

什么是线程和进程?

进程

进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1–n个线程。(进程是资源分配的最小单位)

线程

线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位)

进程和线程的关系

线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的, 而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管 理和保护;而进程正相反。

线程状态

在这里插入图片描述
线程状态转换
在这里插入图片描述

线程创建方式

1.创建一个类继承Thread类,重写run()方法,将所要完成的任务代码写进run()方法中,创建对象,调用该对象的start()方法。

public class MyThread extends Thread {//继承Thread类

    @Override
    public void run() {
        //重写run方法
        System.out.println(" 线程运行了");
    }

    public static void main(String[] args) {
        new MyThread().start();//创建并启动线程
    }
}

2.实现Runnable接口
(1)创建一个类并实现Runnable接口
(2)重写run()方法,将所要完成的任务代码写进run()方法中
(3)创建实现Runnable接口的类的对象,将该对象当做Thread类的构造方法中的参数传进去
(4)使用Thread类的构造方法创建一个对象,并调用start()方法即可运行该线程

public class RunnableThread implements Runnable {//实现Runnable接口

    @Override
    public void run() {
        //重写run方法
        System.out.println("Runnable线程");
    }

    public static void main(String[] args) {
        //创建并启动线程
        RunnableThread myThread = new RunnableThread();
        Thread thread = new Thread(myThread);
        thread.start();
        //或者
        new Thread(new RunnableThread()).start();
    }
}

3.实现Callable接口
(1)创建一个类并实现Callable接口
(2)重写call()方法,将所要完成的任务的代码写进call()方法中,需要注意的是call()方法有返回值,并且可以抛出异常
(3)如果想要获取运行该线程后的返回值,需要创建Future接口的实现类的对象,即FutureTask类的对象,调用该对象的get()方法可获取call()方法的返回值
(4)使用Thread类的有参构造器创建对象,将FutureTask类的对象当做参数传进去,然后调用start()方法开启并运行该线程。

public class CallableThread implements Callable {//实现Runnable接口

    @Override
    public String call() {
        //重写call方法
        return "Callable线程";
    }

    public static void main(String[] args) {
        //执行Callable 方式,需要FutureTask 实现实现,用于接收运算结果
        FutureTask<String> futureTask = new FutureTask<String>(new CallableThread());
        new Thread(futureTask).start();
        //接收线程运算后的结果
        try {
            String sum = futureTask.get();
            System.out.println(sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

4.使用线程池创建
(1)使用Executors类中的newFixedThreadPool(int num)方法创建一个线程数量为num的线程池
(2)调用线程池中的execute()方法执行由实现Runnable接口创建的线程;调用submit()方法执行由实现Callable接口创建的线程。

public class ThreadPoolExecutorTest {

    public static void main(String[] args) {
        //创建线程池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        ThreadPool threadPool = new ThreadPool();
        for (int i = 0; i < 5; i++) {
            //为线程池分配任务
            executorService.submit(threadPool);
        }
        //关闭线程池
        executorService.shutdown();
    }
}

class ThreadPool implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}

什么是线程死锁?如何避免死锁?

多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
在这里插入图片描述
产生死锁必须具备以下四个条件:

  1. 互斥条件:该资源任意一个时刻只由一个线程占用。
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后
    才释放资源。
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

死锁代码示例:

public class DeadLockDemo {
    private static String A = "A";
    private static String B = "B";

    public static void main(String[] args) {
        deadLock();
    }

    private static void deadLock() {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (A) {
                    try {
                        Thread.currentThread().sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (B) {
                        System.out.println("1");
                    }
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (B) {
                    synchronized (A) {
                        System.out.println("2");
                    }
                }
            }
        });
        t1.start();
        t2.start();
    }
}

如何避免线程死锁?

  1. 破坏互斥条件 :这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资 源需要互斥访问)。
  2. 破坏请求与保持条件 :一次性申请所有的资源。
  3. 破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放
    它占有的资源。
  4. 破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破
    坏循环等待条件。

说说 sleep() 方法和 wait() 方法区别和共同点?

两者最主要的区别在于:sleep 方法没有释放锁,而 wait 方法释放了锁 。 两者都可以暂停线程的执行。
Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout)超时后线程会自动苏醒。

为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通 方法调用,还是在主线程里执行。

synchronized 关键字

synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰 的方法或者代码块在任意时刻只能有一个线程执行。
Java 6 之后 Java 官方对从 JVM 层面对synchronized 􏰁大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优 化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

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

synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能!

双重校验锁实现对象单例

public class Singleton {
    // 双重检测
    private static volatile Singleton singleton;// 防止指令重排

    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton =  new Singleton();
                }
            }
        }
        return singleton;
    }
}

singleton = new Singleton();
这段代码其实是分为三步执行:

  1. 为 singleton 分配内存空间
  2. 初始化 singleton
  3. 将 singleton 指向分配的内存地址
    使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。

谈谈 synchronized和ReentrantLock 的区别

区别:
1等待可中断;
2可实现公平锁;
3可实现选择性通知(锁可以绑定多个条件)

volatile关键字

volatile 关键字的主要作用就是保证变量的可⻅性然后还有一个作用是防止指令重排序。

volatile的特性

  • 可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
  • 原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。

volatile写-读的内存语义

写语义:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内 存。
读语义:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

volatile内存语义的实现

在指令序列中插入内存屏障来 禁止特定类型的处理器重排序。
内存屏障指令分类
在这里插入图片描述

并发编程的三个重要特性

  • 原子性 : 一个的操作或者多次操作,要么所有的操作全部都得到执行并且不会收到任何因素的 干扰而中断,要么所有的操作都执行,要么都不执行。synchronized 可以保证代码片段的原 子性。
  • 可⻅性 :当一个变量对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新 值。volatile 关键字可以保证共享变量的可⻅性。
  • 有序性 :代码在执行的过程中的先后顺序,Java 在编译器以及运行期间的优化,代码的执行顺 序未必就是编写代码时候的顺序。volatile 关键字可以禁止指令进行重排序优化。

happens-before规则

  • 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
  • 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的 读。
  • 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
  • start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的
    ThreadB.start()操作happens-before于线程B中的任意操作。
  • join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作
    happens-before于线程A从ThreadB.join()操作成功返回。

ThreadLocal

ThreadLocal原理

 
public class Thread implements Runnable { 
//与此线程有关的ThreadLocal值。由ThreadLocal类维护 ThreadLocal.ThreadLocalMap 
threadLocals = null;
//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护 
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
......
}

从上面 Thread 类 源代码可以看出 Thread 类中有一个 threadLocals 和 一个 inheritableThreadLocals 变量,它们都是 ThreadLocalMap 类型的变量,我们可以把 ThreadLocalMap 理解为 ThreadLocal 类实现的定制化的 HashMap 。默认情况下这两个变量都是null,只有当前线程调用 ThreadLocal 类的 set 或 get 方法时才创建它们,实际上调用这两个 方法的时候,我们调用的是 ThreadLocalMap 类对应的 get() 、 set() 方法。

ThreadLocal 内存泄露问题

ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果
ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被 清理掉。这样一来,ThreadLocalMap 中就会出现key为null的Entry。假如我们不做任何措施的话, value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种 情况,在调用 set() 、 get() 、 remove() 方法的时候,会清理掉 key 为 null 的记录。使用完ThreadLocal方法后 最好手动调用remove()方法

线程池

使用线程池的好处

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

实现Runnable接口和Callable接口的区别

Runnable 接口不会返回结果或抛出检查异常,但是Callable 接口可 以。所以,如果任务不需要返回结果或抛出异常推荐使用 Runnable 接口,这样代码看起来会更加简 洁。

执行execute()方法和submit()方法的区别

1、使用submit提交子任务,一定要获取返回值Future,通过get方法获取可能出现的异常,并且可以进行捕获(推荐)
2、使用execute执行子任务,异常可以被抛出,但是主线程不能捕获子任务线程中的异常
3、使用submit提交子任务,只是提交,不获取返回值future,异常会被封装在子线程内部,不会抛出,主线程也无法捕获。

ThreadPoolExecutor 类分析

ThreadPoolExecutor 3 个最重要的参数:

  • corePoolSize : 核心线程数线程数定义了最小可以同时运行的线程数量。
  • maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数 量变为最大线程数。
  • workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到 的话,新任务就会被存放在队列中。

ThreadPoolExecutor 其他常⻅参数:

  1. keepAliveTime :当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任 务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了
    keepAliveTime 才会被回收销毁;
  2. unit : keepAliveTime 参数的时间单位。
  3. threadFactory :executor 创建新线程的时候会用到。 4. handler :饱和策略。关于饱和策略下面单独介绍一下。

ThreadPoolExecutor 饱和策略定义: 如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,
ThreadPoolTaskExecutor 定义一些策略:
ThreadPoolExecutor.AbortPolicy :抛出 RejectedExecutionException 来拒绝新任 务的处理。
ThreadPoolExecutor.CallerRunsPolicy :调用执行自己的线程运行任务。您不会任务请 求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加 队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可 以选择这个策略。
ThreadPoolExecutor.DiscardPolicy: 不处理新任务,直接丢弃掉。 ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。

线程池原理在这里插入图片描述

AQS

AQS 原理概览

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

对于非可重入锁状态不是0则去阻塞;
对于可重入锁如果是0则执行,非0则判断当前线程是否是获取到这个锁的线程,是的话把state状态+1,比如重入5次,那么state=5。而在释放锁的时候,同样需要释放5次直到state=0其他线程才有资格获得锁。

AQS 对资源的共享方式

AQS定义两种资源共享方式
Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁: 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的。

Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、 CountDownLatch、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。

AQS底层使用了模板方法模式

同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):

  1. 使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对 于共享资源state的获取和释放)
  2. 将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。

公平锁与分公平锁

(1):公平锁指在分配锁前检查是否有线程在排队等待获取该锁,优先分配排队时间最长的线程,非公平直接尝试获取锁
(2):公平锁需多维护一个锁线程队列,效率低;默认非公平

ReentrantLock

CAS+AQS队列来实现
(1):先通过CAS尝试获取锁, 如果此时已经有线程占据了锁,那就加入AQS队列并且被挂起;
(2):当锁被释放之后, 排在队首的线程会被唤醒CAS再次尝试获取锁,
(3):如果是非公平锁, 同时还有另一个线程进来尝试获取可能会让这个线程抢到锁;
(4):如果是公平锁, 会排到队尾,由队首的线程获取到锁。

ReentrantReadWriteLock

读 写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状 态,使得该状态的设计成为读写锁实现的关键。高16位表示读,低16位表示写。

写锁的获取与释放
写锁是一个支持重进入的排它锁。如果当前线程已经获取了写锁,则增加写状态。如果当 前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程, 则当前线程进入等待状态。
读锁的获取与释放
读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问 (或者写状态为0)时,读锁总会被成功地获取,而所做的也只是(线程安全的)增加读状态。如 果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已被其他线程 获取,则进入等待状态。

StampedLock

ReentrantReadWriteLock使得多个读线程同时持有读锁(只要写锁未被占用),而写锁是独占的。
StampedLock类,在JDK1.8时引入,是对读写锁ReentrantReadWriteLock的增强,该类提供了一些功能,优化了读锁、写锁的访问,同时使读写锁之间可以互相转换,更细粒度控制并发。

Java中的并发工具类

CountDownLatch

CountDownLatch是等待其他线程执行到某一个点的时候,在继续执行逻辑(子线程不会被阻塞,会继续执行),只能被使用一次。

原理

CountDownLatch内部使用了AQS锁,前面已经讲述过AQS的内部结构,其实内部有一个state字段,通过该字段来控制锁的操作。CountDownLatch是如何控制多个线程执行都执行结束?其实CountDownLatch内部是将state作为计数器来使用,比如我们初始化时,state计数器为3,同时开启三个线程当有一个线程执行成功,每当有一个线程执行完成后就将state值减少1,直到减少到为0时,说明所有线程已经执行完毕。

public class CountDownLatch {
    /**
     * 同步控制,
     * 使用 AQS的state来表示计数。
     */
    private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;
        // 初始化state值(也就是需要等待几个线程完成任务)
        Sync(int count) {
            setState(count);
        }
        // 获取state值。
        int getCount() {
            return getState();
        }
        // 获得锁。
        protected int tryAcquireShared(int acquires) {
            // 这里判断如果state=0的时候才能获得锁,反之获取不到将当前线程放入到队列中阻塞。
            // 这里是关键点。
            return (getState() == 0) ? 1 : -1;
        }

        protected boolean tryReleaseShared(int releases) {
            // state进行减少,当state减少为0时,阻塞线程才能进行处理。
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
    }
    // 锁对象。
    private final Sync sync;

    /**
     * 初始化同步锁对象。
     */
    public CountDownLatch(int count) { 
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }

    /**
     * 导致当前线程等待直到闩锁倒计时到零,除非线程是被中断。如果当前计数为零,则此方法立即返回。如果当前计数大于零,
     * 则当前线程将被禁用以进行线程调度并处于休眠状态,直到发生以下两种情况:
     * 1.计数达到零。
     * 2.如果当前线程被中断。
     */
    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

    /**
     * 等待计数器清零或被中断,等待一段时间后如果还是没有
     */
    public boolean await(long timeout, TimeUnit unit)
        throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }

    /**
     * 使当前线程等待直到闩锁倒计时到零,除非线程被中断或指定的等待时间已过。
     */
    public void countDown() {
        sync.releaseShared(1);
    }

    /**
     * 返回state值。
     */
    public long getCount() {
        return sync.getCount();
    }
}

CyclicBarrier

CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一 组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会 开门,所有被屏障拦截的线程才会继续运行。

CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数 量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。

原理


//非定时等待
public int await() throws InterruptedException, BrokenBarrierException {
  try {
    return dowait(false, 0L);
  } catch (TimeoutException toe) {
    throw new Error(toe);
  }
}

//定时等待
public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException {
  return dowait(true, unit.toNanos(timeout));
}

//核心等待方法
private int dowait(boolean timed, long nanos) throws InterruptedException, BrokenBarrierException, TimeoutException {
  final ReentrantLock lock = this.lock;
  lock.lock();
  try {
    final Generation g = generation;
    //检查当前栅栏是否被打翻
    if (g.broken) {
      throw new BrokenBarrierException();
    }
    //检查当前线程是否被中断
    if (Thread.interrupted()) {
      //如果当前线程被中断会做以下三件事
      //1.打翻当前栅栏
      //2.唤醒拦截的所有线程
      //3.抛出中断异常
      breakBarrier();
      throw new InterruptedException();
    }
    //每次都将计数器的值减1
    int index = --count;
    //计数器的值减为0则需唤醒所有线程并转换到下一代
    if (index == 0) {
      boolean ranAction = false;
      try {
        //唤醒所有线程前先执行指定的任务
        final Runnable command = barrierCommand;
        if (command != null) {
          command.run();
        }
        ranAction = true;
        //唤醒所有线程并转到下一代
        nextGeneration();
        return 0;
      } finally {
        //确保在任务未成功执行时能将所有线程唤醒
        if (!ranAction) {
          breakBarrier();
        }
      }
    }

//如果计数器不为0则执行此循环
for (;;) {
  try {
    //根据传入的参数来决定是定时等待还是非定时等待
    if (!timed) {
      trip.await();
    }else if (nanos > 0L) {
      nanos = trip.awaitNanos(nanos);
    }
  } catch (InterruptedException ie) {
    //若当前线程在等待期间被中断则打翻栅栏唤醒其他线程
    if (g == generation && ! g.broken) {
      breakBarrier();
      throw ie;
    } else {
      //若在捕获中断异常前已经完成在栅栏上的等待, 则直接调用中断操作
      Thread.currentThread().interrupt();
    }
  }
  //如果线程因为打翻栅栏操作而被唤醒则抛出异常
  if (g.broken) {
    throw new BrokenBarrierException();
  }
  //如果线程因为换代操作而被唤醒则返回计数器的值
  if (g != generation) {
    return index;
  }
  //如果线程因为时间到了而被唤醒则打翻栅栏并抛出异常
  if (timed && nanos <= 0L) {
    breakBarrier();
    throw new TimeoutException();
  }
}
​```

  } finally {
    lock.unlock();
  }
}


Semaphore

Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。

原理

Semaphore可以用于做流量控制,特别是公用资源有限的应用场景,比如数据库连接。假 如有一个需求,要读取几万个文件的数据,因为都是IO密集型任务,我们可以启动几十个线程 并发地读取,但是如果读到内存后,还需要存储到数据库中,而数据库的连接数只有10个,这 时我们必须控制只有10个线程同时获取数据库连接保存数据,否则会报错无法获取数据库连 接。这个时候,就可以使用Semaphore来做流量控制。

/**
     *  获取1个许可
     */
    public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }
    /**
     * 共享模式下获取许可,获取成功则返回,失败则加入阻塞队列,挂起线程
     * @param arg
     * @throws InterruptedException
     */
    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        //尝试获取许可,arg为获取许可个数,当可用许可数减当前许可数结果小于0,则创建一个节点加入阻塞队列,挂起当前线程。
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }
/**
     * 1、创建节点,加入阻塞队列,
     * 2、重双向链表的head,tail节点关系,清空无效节点
     * 3、挂起当前节点线程
     * @param arg
     * @throws InterruptedException
     */
    private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        //创建节点加入阻塞队列
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            for (;;) {
                //获得当前节点pre节点
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);//返回锁的state
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                //重组双向链表,清空无效节点,挂起当前线程
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

Exchanger

Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。

posted @ 2021-05-16 21:02  ByteX  阅读(16)  评论(0)    收藏  举报