美文网首页Java程序员面试复习手册
Java并发知识点快速复习手册(上)

Java并发知识点快速复习手册(上)

作者: 蛮三刀酱 | 来源:发表于2019-01-31 22:24 被阅读0次

    前言

    参考

    线程状态转换

    在这里插入图片描述

    新建(New)

    创建后尚未启动。

    可运行(Runnable)

    可能正在运行,也可能正在等待 CPU 时间片。

    包含了操作系统线程状态中的 Running 和 Ready。

    阻塞(Blocking)

    等待获取一个排它锁,如果其线程释放了锁就会结束此状态。

    无限期等待(Waiting)

    等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。

    进入方法 退出方法
    没有设置 Timeout 参数的 Object.wait() 方法 Object.notify() / Object.notifyAll()
    没有设置 Timeout 参数的 Thread.join() 方法 被调用的线程执行完毕
    LockSupport.park() 方法 -

    限期等待(Timed Waiting)

    无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。

    调用 Thread.sleep() 方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。

    调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述。

    睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态。

    阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁

    而等待是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入。

    进入方法 退出方法
    Thread.sleep() 方法 时间结束
    设置了 Timeout 参数的 Object.wait() 方法 时间结束 / Object.notify() / Object.notifyAll()
    设置了 Timeout 参数的 Thread.join() 方法 时间结束 / 被调用的线程执行完毕
    LockSupport.parkNanos() 方法 -
    LockSupport.parkUntil() 方法 -

    死亡(Terminated)

    可以是线程结束任务之后自己结束,或者产生了异常而结束。

    使用线程

    有三种使用线程的方法:

    • 实现 Runnable 接口
    • 实现 Callable 接口
    • 继承 Thread 类

    实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。

    实现 Runnable 接口

    需要实现 run() 方法。

    通过 Thread 调用 start() 方法来启动线程。

    public class MyRunnable implements Runnable {
        public void run() {
            // ...
        }
    }
    
    public static void main(String[] args) {
        MyRunnable instance = new MyRunnable();
        Thread thread = new Thread(instance);
        thread.start();
    }
    

    实现 Callable 接口

    Callable就是Runnable的扩展。

    与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。

    public class MyCallable implements Callable<Integer> {
        public Integer call() {
            return 123;
        }
    }
    
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable mc = new MyCallable();
        FutureTask<Integer> ft = new FutureTask<>(mc);
        Thread thread = new Thread(ft);
        thread.start();
        System.out.println(ft.get());
    }
    

    继承 Thread 类

    同样也是需要实现 run() 方法,并且最后也是调用 start() 方法来启动线程。

    public class MyThread extends Thread {
        public void run() {
            // ...
        }
    }
    
    public static void main(String[] args) {
        MyThread mt = new MyThread();
        mt.start();
    }
    

    其他方法

    严格说不能算方法,只能算实现方式:

    • 匿名内部类
    • 线程池

    实现接口 VS 继承 Thread

    实现接口会更好一些,因为:

    • Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;
    • 类可能只要求可执行就行,继承整个 Thread 类开销过大。
    • 代码可以被多线程共享,数据独立,很容易实现资源共享

    start和run有什么区别?

    详细解释:https://blog.csdn.net/lai_li/article/details/53070141?locationNum=13&fps=1

    start方法:

    • 通过该方法启动线程的同时也创建了一个线程,真正实现了多线程。无需等待run()方法中的代码执行完毕,就可以接着执行下面的代码

    • 此时start()的这个线程处于就绪状态,当得到CPU的时间片后就会执行其中的run()方法。这个run()方法包含了要执行的这个线程的内容,run()方法运行结束,此线程也就终止了。

    run方法:

    • 通过run方法启动线程其实就是调用一个类中的方法,当作普通的方法的方式调用。并没有创建一个线程,程序中依旧只有一个主线程,必须等到run()方法里面的代码执行完毕,才会继续执行下面的代码,这样就没有达到写线程的目的。

    线程代码示例

    package cn.thread.test;
     
    /*
     * 设计4个线程,其中两个线程每次对j增加1,另外两个线程对j每次减少1。写出程序。
     */
    public class ThreadTest1 {
     
        private int j;
        
        public static void main(String[] args) {
            ThreadTest1 tt = new ThreadTest1();
            Inc inc = tt.new Inc();
            Dec dec = tt.new Dec();
            
            
            Thread t1 = new Thread(inc);
            Thread t2 = new Thread(dec);
            Thread t3 = new Thread(inc);
            Thread t4 = new Thread(dec);
            t1.start();
            t2.start();
            t3.start();
            t4.start();
            
        }
        
        private synchronized void inc() {
            j++;
            System.out.println(Thread.currentThread().getName()+"inc:"+j);
        }
        
        private synchronized void dec() {
            j--;
            System.out.println(Thread.currentThread().getName()+"dec:"+j);
        }
        
        class Inc implements Runnable {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    inc();
                }
            }
        }
        
        class Dec extends Thread {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    dec();
                }
            }
        }
    }
    

    基础线程机制

    Executor线程池

    https://segmentfault.com/a/1190000014741369#articleHeader3

    Executor 管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。异步是指多个任务的执行互不干扰,不需要进行同步操作。

    • 当前线程池大小 :表示线程池中实际工作者线程的数量;

    • 最大线程池大小 (maxinumPoolSize):表示线程池中允许存在的工作者线程的数量上限;

    • 核心线程大小 (corePoolSize ):表示一个不大于最大线程池大小的工作者线程数量上限。

    如果运行的线程少于 corePoolSize,则 Executor 始终首选添加新的线程,而不进行排队;

    如果运行的线程等于或者多于 corePoolSize,则 Executor 始终首选将请求加入队列,而不是添加新线程;

    如果无法将请求加入队列,即队列已经满了,则创建新的线程,除非创建此线程超出 maxinumPoolSize, 在这种情况下,任务将被拒绝。

    不用线程池的弊端

    • 线程生命周期的开销非常高。每个线程都有自己的生命周期,创建和销毁线程所花费的时间和资源可能比处理客户端的任务花费的时间和资源更多,并且还会有某些空闲线程也会占用资源。
    • 程序的稳定性和健壮性会下降,每个请求开一个线程。如果受到了恶意攻击或者请求过多(内存不足),程序很容易就奔溃掉了。

    ThreadPoolExecutor类

    实现了Executor接口,是用的最多的线程池,下面是已经默认实现的三种:

    • newCachedThreadPool:一个任务创建一个线程;

    非常有弹性的线程池,对于新的任务,如果此时线程池里没有空闲线程,线程池会毫不犹豫的创建一条新的线程去处理这个任务。

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
            executorService.execute(new MyRunnable());
        }
        executorService.shutdown();
    }
    
    • newFixedThreadPool:所有任务只能使用固定大小的线程;

    一个固定线程数的线程池,它将返回一个corePoolSize和maximumPoolSize相等的线程池。

    • SingleThreadExecutor:相当于大小为 1 的 FixedThreadPool。

    ThreadPoolExecutor提供了shutdown()和shutdownNow()两个方法来关闭线程池

    区别:

    • 调用shutdown()后,线程池状态立刻变为SHUTDOWN,而调用shutdownNow(),线程池状态立刻变为STOP。
    • shutdown()等待任务执行完才中断线程,而shutdownNow()不等任务执行完就中断了线程。

    ScheduledThreadPoolExecutor类

    相当于提供了延迟和周期执行功能的ThreadPoolExecutor类

    Daemon 守护线程

    守护线程是程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分。

    当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程。

    main() 属于非守护线程,垃圾回收是守护线程。

    使用 setDaemon() 方法将一个线程设置为守护线程。

    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable());
        thread.setDaemon(true);
    }
    
    • 使用守护线程不要访问共享资源(数据库、文件等),因为它可能会在任何时候就挂掉了。
    • 守护线程中产生的新线程也是守护线程

    sleep()

    Thread.sleep(millisec) 方法会休眠当前正在执行的线程,millisec 单位为毫秒。

    sleep() 可能会抛出 InterruptedException,因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。

    public void run() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    

    yield()

    对静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。

    public void run() {
        Thread.yield();
    }
    

    中断

    一个线程执行完毕之后会自动结束,如果在运行过程中发生异常也会提前结束。

    现在已经没有强制线程终止的方法了!

    Stop方法太暴力了,不安全,所以被设置过时了。

    interrupt():报出InterruptedException

    https://segmentfault.com/a/1190000014463417#articleHeader9

    要注意的是:interrupt不会真正停止一个线程,它仅仅是给这个线程发了一个信号告诉它,它应该要结束了(明白这一点非常重要!)

    调用interrupt()并不是要真正终止掉当前线程,仅仅是设置了一个中断标志。这个中断标志可以给我们用来判断什么时候该干什么活!什么时候中断由我们自己来决定,这样就可以安全地终止线程了!

    通过调用一个线程的 interrupt() 来中断该线程,可以中断处于:

    • 阻塞
    • 限期等待
    • 无限期等待状态

    那么就会抛出 InterruptedException,从而提前结束该线程。

    但是不能中断 I/O 阻塞和 synchronized 锁阻塞。

    对于以下代码,在 main() 中启动一个线程之后再中断它,由于线程中调用了 Thread.sleep() 方法,因此会抛出一个 InterruptedException,从而提前结束线程,不执行之后的语句。

    public class InterruptExample {
    
        private static class MyThread1 extends Thread {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                    System.out.println("Thread run");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new MyThread1();
        thread1.start();
        thread1.interrupt();
        System.out.println("Main run");
    }
    
    Main run
    java.lang.InterruptedException: sleep interrupted
        at java.lang.Thread.sleep(Native Method)
        at InterruptExample.lambda$main$0(InterruptExample.java:5)
        at InterruptExample$$Lambda$1/713338599.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:745)
    

    interrupted()和isInterrupted()

    interrupt线程中断还有另外两个方法(检查该线程是否被中断):

    • 静态方法interrupted()-->会清除中断标志位
    • 实例方法isInterrupted()-->不会清除中断标志位

    如果一个线程的 run() 方法执行一个无限循环(不属于阻塞、限期等待、非限期等待),例如while(True),并且没有执行 sleep() 等会抛出 InterruptedException 的操作,那么调用线程的 interrupt() 方法就无法使线程提前结束

    然而,

    但是调用 interrupt() 方法会设置线程的中断标记,此时调用 interrupted() 方法会返回 true。因此可以在循环体中使用 interrupted() 方法来判断线程是否处于中断状态,从而提前结束线程。

    Thread t1 = new Thread( new Runnable(){
        public void run(){
            // 若未发生中断,就正常执行任务
            while(!Thread.currentThread.isInterrupted()){
                // 正常任务代码……
            }
            // 中断的处理代码……
            doSomething();
        }
    } ).start();
    

    Executor线程池的中断操作

    • 调用 Executor 的 shutdown() 方法会等待线程都执行完毕之后再关闭
    • 但是如果调用的是 shutdownNow() 方法,则相当于调用每个线程的 interrupt() 方法。

    以下使用 Lambda 创建线程,相当于创建了一个匿名内部线程。

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(() -> {
            try {
                Thread.sleep(2000);
                System.out.println("Thread run");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        executorService.shutdownNow();
        System.out.println("Main run");
    }
    
    Main run
    java.lang.InterruptedException: sleep interrupted
        at java.lang.Thread.sleep(Native Method)
        at ExecutorInterruptExample.lambda$main$0(ExecutorInterruptExample.java:9)
        at ExecutorInterruptExample$$Lambda$1/1160460865.run(Unknown Source)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
        at java.lang.Thread.run(Thread.java:745)
    

    如果只想中断 Executor 中的一个线程,可以通过使用 submit() 方法来提交一个线程,它会返回一个 Future<?> 对象,通过调用该对象的 cancel(true) 方法就可以中断线程。

    Future<?> future = executorService.submit(() -> {
        // ..
    });
    future.cancel(true);
    

    互斥同步

    • JVM 实现的 synchronized
    • JDK 实现的 ReentrantLock。

    可重入与不可重入锁

    https://blog.csdn.net/u012545728/article/details/80843595

    不可重入锁

    所谓不可重入锁,即若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞。

    public class Count{
        Lock lock = new Lock();
        public void print(){
            lock.lock();
            doAdd();
            lock.unlock();
        }
        public void doAdd(){
            lock.lock();
            //do something
            lock.unlock();
        }
    }
    

    可重入锁

    所谓可重入,意味着线程可以进入它已经拥有的锁的同步代码块儿

    我们设计两个线程调用print()方法,第一个线程调用print()方法获取锁,进入lock()方法,由于初始lockedBy是null,所以不会进入while而挂起当前线程,而是是增量lockedCount并记录lockBy为第一个线程。接着第一个线程进入doAdd()方法,由于同一进程,所以不会进入while而挂起,接着增量lockedCount,当第二个线程尝试lock,由于isLocked=true,所以他不会获取该锁,直到第一个线程调用两次unlock()将lockCount递减为0,才将标记为isLocked设置为false。

    可重入锁的概念和设计思想大体如此,Java中的可重入锁ReentrantLock设计思路也是这样

    synchronized和ReentrantLock都是可重入锁

    synchronized

    1. 同步一个代码块

    public void func () {
        synchronized (this) {
            // ...
        }
    }
    

    它只作用于同一个对象,如果调用两个不同对象上的同步代码块,就不会进行同步。

    2. 同步一个方法

    public synchronized void func () {
        // ...
    }
    

    它和同步代码块一样,只作用于同一个对象。

    3. 同步一个类

    public void func() {
        synchronized (SynchronizedExample.class) {
            // ...
        }
    }
    

    作用于整个类,也就是说两个线程调用同一个类的不同对象上的这种同步语句,也需要进行同步。

    4. 同步一个静态方法

    public synchronized static void fun() {
        // ...
    }
    

    作用于整个类。

    释放锁的时机

    • 当方法(代码块)执行完毕后会自动释放锁,不需要做任何的操作。

    • 当一个线程执行的代码出现异常时,其所持有的锁会自动释放。

    Lock

    有ReentrantLock和ReentrantReadWriteLock,后者分为读锁和写锁,读锁允许并发访问共享资源。

    public class LockExample {
    
        private Lock lock = new ReentrantLock();
    
        public void func() {
            lock.lock();
            try {
                for (int i = 0; i < 10; i++) {
                    System.out.print(i + " ");
                }
            } finally {
                lock.unlock(); // 确保释放锁,从而避免发生死锁。
            }
        }
    }
    
    public static void main(String[] args) {
        LockExample lockExample = new LockExample();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(() -> lockExample.func());
        executorService.execute(() -> lockExample.func());
    }
    
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
    

    ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁,相比于 synchronized,它多了以下高级功能:

    1. 等待可中断

    当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行时间非常长的同步块很有帮助。

    2. 可实现公平锁

    公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。

    • synchronized 中的锁是非公平的
    • ReentrantLock 默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。

    3. 锁绑定多个条件

    • synchronized 中,锁对象的 wait() 和 notify() 或 notifyAll() 方法可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁
    • 而 ReentrantLock 则无须这样做,只需要多次调用 newCondition() 方法即可。

    ReentrantReadWriteLock

    我们知道synchronized内置锁和ReentrantLock都是互斥锁(一次只能有一个线程进入到临界区(被锁定的区域)

    ReentrantReadWriteLock优点:

    • 读取数据的时候,可以多个线程同时进入到到临界区(被锁定的区域)
    • 在写数据的时候,无论是读线程还是写线程都是互斥的
    • 如果读的线程比写的线程要多很多的话,那可以考虑使用它。它使用state的变量高16位是读锁,低16位是写锁
    • 写锁可以降级为读锁,读锁不能升级为写锁

    synchronized 和 ReentrantLock 比较

    1. 锁的实现

    synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。

    2. 性能

    新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。

    3. 等待可中断

    ReentrantLock 可中断,而 synchronized 不行。

    4. 公平锁

    • 公平锁能保证:老的线程(234)排队使用锁,新线程仍然排队使用锁(2345)。
    • 非公平锁保证:老的线程(234)排队使用锁;但是无法保证新线程5抢占已经在排队的线程的锁(正好在1释放锁的时候抢占到了锁,没有进入排队队列)

    synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。

    5. 锁绑定多个条件

    一个 ReentrantLock 可以同时绑定多个 Condition 对象。

    使用选择

    除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。

    • synchronized好用,简单,性能不差
    • 没有使用到Lock显式锁的特性就不要使用Lock锁了。
    • synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。
    • 并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为JVM 会确保锁的释放

    线程之间的协作

    当多个线程可以一起工作去解决某个问题时,如果某些部分必须在其它部分之前完成,那么就需要对线程进行协调。

    join()

    在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待, 直到目标线程结束。

    对于以下代码,虽然 b 线程先启动,但是因为在 b 线程中调用了 a 线程的 join() 方法,因此 b 线程会等待 a 线程结束才继续执行,因此最后能够保证 a 线程的输出先与 b 线程的输出。

    wait() notify() notifyAll()

    调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。

    它们都属于 Object 的一部分,而不属于 Thread。

    只能用在同步方法或者同步控制块中使用,否则会在运行时抛出 IllegalMonitorStateExeception。

    使用 wait() 挂起期间,线程会释放锁。这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程,造成死锁。

    wait() 和 sleep() 的区别

    1. wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法;
    2. wait() 会释放锁,sleep() 不会。

    await() signal() signalAll()

    java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。

    相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。

    使用 Lock 来获取一个 Condition 对象。

    public class AwaitSignalExample {
        private Lock lock = new ReentrantLock();
        private Condition condition = lock.newCondition();
    
        public void before() {
            lock.lock();
            try {
                System.out.println("before");
                condition.signalAll();
            } finally {
                lock.unlock();
            }
        }
    
        public void after() {
            lock.lock();
            try {
                condition.await();
                System.out.println("after");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
    
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        AwaitSignalExample example = new AwaitSignalExample();
        executorService.execute(() -> example.after());
        executorService.execute(() -> example.before());
    }
    
    before
    after
    

    J.U.C - AQS

    从整体来看,concurrent包的实现示意图如下:

    在这里插入图片描述

    https://segmentfault.com/a/1190000014595928

    java.util.concurrent(J.U.C)大大提高了并发性能,AQS 被认为是 J.U.C 的核心。

    AbstractQueuedSynchronizer简称为AQS:AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,我们Lock之类的两个常见的锁都是基于它来实现的。

    • AQS可以给我们实现锁的框架
    • 内部实现的关键是:先进先出的队列、state状态
    • 定义了内部类ConditionObject
    • 拥有两种线程模式:
      • 独占模式
      • 共享模式
    • 在LOCK包中的相关锁(常用的有ReentrantLock、 ReadWriteLock)都是基于AQS来构建
    • 一般我们叫AQS为同步器

    AQS实现特点

    • 同步状态
      • 使用volatile修饰实现线程可见性
      • 修改state状态值时使用CAS算法来实现
    • 先进先出队列

    CountdownLatch

    维护了一个计数器 cnt,每次调用 countDown() 方法会让计数器的值减 1,减到 0 的时候,那些因为调用 await() 方法而在等待的线程就会被唤醒。

    使用说明:

    • count初始化CountDownLatch,然后需要等待的线程调用await方法。await方法会一直受阻塞直到count=0。
    • 而其它线程完成自己的操作后,调用countDown()使计数器count减1。
    • 当count减到0时,所有在等待的线程均会被释放

    说白了就是通过count变量来控制等待,如果count值为0了(其他线程的任务都完成了),那就可以继续执行。

    在这里插入图片描述
    public class CountdownLatchExample {
        public static void main(String[] args) throws InterruptedException {
            final int totalThread = 10;
            CountDownLatch countDownLatch = new CountDownLatch(totalThread);
            ExecutorService executorService = Executors.newCachedThreadPool();
            for (int i = 0; i < totalThread; i++) {
                executorService.execute(() -> {
                    System.out.print("run..");
                    countDownLatch.countDown();
                });
            }
            countDownLatch.await();
            System.out.println("end");
            executorService.shutdown();
        }
    }
    
    run..run..run..run..run..run..run..run..run..run..end
    

    CyclicBarrier

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

    和 CountdownLatch相似,都是通过维护计数器来实现的。但是它的计数器是递增的,每次执行 await() 方法之后,计数器会加 1,直到计数器的值和设置的值相等,等待的所有线程才会继续执行。

    CyclicBarrier可以被重用(对比于CountDownLatch是不能重用的),CyclicBarrier 的计数器通过调用 reset() 方法可以循环使用,所以它才叫做循环屏障。

    在这里插入图片描述
    public CyclicBarrier(int parties, Runnable barrierAction) {
        if (parties <= 0) throw new IllegalArgumentException();
        this.parties = parties;
        this.count = parties;
        this.barrierCommand = barrierAction;
    }
    
    public CyclicBarrier(int parties) {
        this(parties, null);
    }
    
    public class CyclicBarrierExample {
        public static void main(String[] args) {
            final int totalThread = 10;
            CyclicBarrier cyclicBarrier = new CyclicBarrier(totalThread);
            ExecutorService executorService = Executors.newCachedThreadPool();
            for (int i = 0; i < totalThread; i++) {
                executorService.execute(() -> {
                    System.out.print("before..");
                    try {
                        cyclicBarrier.await();
                    } catch (InterruptedException | BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                    System.out.print("after..");
                });
            }
            executorService.shutdown();
        }
    }
    before..before..before..before..before..before..before..before..before..before..after..after..after..after..after..after..after..after..after..after..
    

    Semaphore

    Semaphore 就是操作系统中的信号量,可以控制对互斥资源的访问线程数。

    • 当调用acquire()方法时,会消费一个许可证。如果没有许可证了,会阻塞起来
    • 当调用release()方法时,会添加一个许可证。
    • 这些"许可证"的个数其实就是一个count变量罢了~
    在这里插入图片描述
    public class SemaphoreExample {
        public static void main(String[] args) {
            final int clientCount = 3;
            final int totalRequestCount = 10;
            Semaphore semaphore = new Semaphore(clientCount);
            ExecutorService executorService = Executors.newCachedThreadPool();
            for (int i = 0; i < totalRequestCount; i++) {
                executorService.execute(()->{
                    try {
                        semaphore.acquire();
                        System.out.print(semaphore.availablePermits() + " ");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        semaphore.release();
                    }
                });
            }
            executorService.shutdown();
        }
    }
    

    J.U.C - 其它组件

    FutureTask

    在介绍 Callable 时我们知道它可以有返回值,返回值通过 Future<V> 进行封装。

    FutureTask 实现了 RunnableFuture 接口,该接口继承自 Runnable 和 Future<V> 接口,这使得 FutureTask 既可以当做一个任务执行,也可以有返回值。

    public class FutureTask<V> implements RunnableFuture<V>
    
    public interface RunnableFuture<V> extends Runnable, Future<V>
    

    当一个计算任务需要执行很长时间,那么就可以用 FutureTask 来封装这个任务,用一个线程去执行该任务,然后其它线程继续执行其它任务。当需要该任务的计算结果时,再通过 FutureTask 的 get() 方法获取。

    public class FutureTaskExample {
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            FutureTask<Integer> futureTask = new FutureTask<Integer>(new Callable<Integer>() {
                @Override
                public Integer call() throws Exception {
                    int result = 0;
                    for (int i = 0; i < 100; i++) {
                        Thread.sleep(10);
                        result += i;
                    }
                    return result;
                }
            });
    
            Thread computeThread = new Thread(futureTask);
            computeThread.start();
    
            Thread otherThread = new Thread(() -> {
                System.out.println("other task is running...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            otherThread.start();
            System.out.println(futureTask.get());
        }
    }
    
    other task is running...
    4950
    

    BlockingQueue

    java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:

    • FIFO 队列 :LinkedBlockingQueue、ArrayListBlockingQueue(固定长度)
    • 优先级队列(每个元素都有优先级) :PriorityBlockingQueue

    提供了阻塞的 take() 和 put() 方法:如果队列为空 take() 将阻塞,直到队列中有内容;如果队列为满 put() 将阻塞,指到队列有空闲位置。

    使用 BlockingQueue 实现生产者消费者问题

    public class ProducerConsumer {
    
        private static BlockingQueue<String> queue = new ArrayBlockingQueue<>(5);
    
        private static class Producer extends Thread {
            @Override
            public void run() {
                try {
                    queue.put("product");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.print("produce..");
            }
        }
    
        private static class Consumer extends Thread {
    
            @Override
            public void run() {
                try {
                    String product = queue.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.print("consume..");
            }
        }
    }
    
    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            Producer producer = new Producer();
            producer.start();
        }
        for (int i = 0; i < 5; i++) {
            Consumer consumer = new Consumer();
            consumer.start();
        }
        for (int i = 0; i < 3; i++) {
            Producer producer = new Producer();
            producer.start();
        }
    }
    
    produce..produce..consume..consume..produce..consume..produce..consume..produce..consume..
    

    ArrayBlockingQueue, LinkedBlockingQueue, ConcurrentLinkedQueue

    都是线程安全的,不然叫什么并发类呢

    ArrayBlockingQueue, LinkedBlockingQueue 继承自 BlockingQueue, 他们的特点就是 Blocking, Blocking 特有的方法就是 take() 和 put(), 这两个方法是阻塞方法, 每当队列容量满的时候, put() 方法就会进入wait, 直到队列空出来, 而每当队列为空时, take() 就会进入等待, 直到队列有元素可以 take()

    ArrayBlockingQueue, LinkedBlockingQueue 区别在于:

    链表和数组性质决定的

    • ArrayBlockingQueue 必须指定容量,
    • 公平读取: ArrayBlockingQueue可以指定 fair 变量, 如果 fair 为 true, 则会保持 take() 或者 put() 操作时线程的 block 顺序, 先 block 的线程先 take() 或 put(), fair 由内部变量 ReentrantLock 保证

    ConcurrentLinkedQueue 通过 CAS 操作实现了无锁的 poll() 和 offer(),

    • 他的容量是动态的,

    • 由于无锁, 所以在 poll() 或者 offer() 的时候 head 与 tail 可能会改变,所以它会持续的判断 head 与 tail 是否改变来保证操作正确性, 如果改变, 则会重新选择 head 与 tail.

    • 而由于无锁的特性, 他的元素更新与 size 变量更新无法做到原子 (实际上它没有 size 变量), 所以他的 size() 是通过遍历 queue 来获得的, 在效率上是 O(n), 而且无法保证准确性, 因为遍历的时候有可能 queue size 发生了改变。

    ForkJoin

    除了ScheduledThreadPoolExecutor和ThreadPoolExecutor类线程池以外,还有一个是JDK1.7新增的线程池:ForkJoinPool线程池

    主要用于并行计算中,和 MapReduce 原理类似,都是把大的计算任务拆分成多个小任务并行计算。

    public class ForkJoinExample extends RecursiveTask<Integer> {
        private final int threhold = 5;
        private int first;
        private int last;
    
        public ForkJoinExample(int first, int last) {
            this.first = first;
            this.last = last;
        }
    
        @Override
        protected Integer compute() {
            int result = 0;
            if (last - first <= threhold) {
                // 任务足够小则直接计算
                for (int i = first; i <= last; i++) {
                    result += i;
                }
            } else {
                // 拆分成小任务
                int middle = first + (last - first) / 2;
                ForkJoinExample leftTask = new ForkJoinExample(first, middle);
                ForkJoinExample rightTask = new ForkJoinExample(middle + 1, last);
                leftTask.fork();
                rightTask.fork();
                result = leftTask.join() + rightTask.join();
            }
            return result;
        }
    }
    
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ForkJoinExample example = new ForkJoinExample(1, 10000);
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        Future result = forkJoinPool.submit(example);
        System.out.println(result.get());
    }
    

    ForkJoin 使用 ForkJoinPool 来启动,它是一个特殊的线程池,线程数量取决于 CPU 核数。

    public class ForkJoinPool extends AbstractExecutorService
    

    ForkJoinPool 实现了工作窃取算法来提高 CPU 的利用率。每个线程都维护了一个双端队列,用来存储需要执行的任务。工作窃取算法允许空闲的线程从其它线程的双端队列中窃取一个任务来执行。窃取的任务必须是最晚的任务,避免和队列所属线程发生竞争。例如下图中,Thread2 从 Thread1 的队列中拿出最晚的 Task1 任务,Thread1 会拿出 Task2 来执行,这样就避免发生竞争。但是如果队列中只有一个任务时还是会发生竞争。

    在这里插入图片描述

    相关文章

      网友评论

        本文标题:Java并发知识点快速复习手册(上)

        本文链接:https://www.haomeiwen.com/subject/lralsqtx.html