【Java EE】JUC(java.util.concurrent) 的常见类

目录

  • 🌴Callable 接口
  • 🎍ReentrantLock
  • 🍀原子类
  • 🌳线程池
  • 🌲信号量 Semaphore
  • ☘️CountDownLatch、
  • ⭕相关面试题

    🌴Callable 接口

    Callable 是⼀个 interface . 相当于把线程封装了⼀个 “返回值”. ⽅便程序猿借助多线程的⽅式计算结

    果.

    **代码示例: **

    创建线程计算 1 + 2 + 3 + … + 1000, 不使⽤ Callable 版本。

    • 创建⼀个类 Result , 包含⼀个 sum 表⽰最终结果, lock 表⽰线程同步使⽤的锁对象.

    • main ⽅法中先创建 Result 实例, 然后创建⼀个线程 t. 在线程内部计算 1 + 2 + 3 + … + 1000.

    • 主线程同时使⽤ wait 等待线程 t 计算结束. (注意, 如果执⾏到 wait 之前, 线程 t 已经计算完了, 就不

    必等待了).

    • 当线程 t 计算完毕后, 通过 notify 唤醒主线程, 主线程再打印结果.

    public class Demo { static class Result { public int sum = 0;
            public Object lock = new Object();
        }
        public static void main(String[] args) throws InterruptedException { Result result = new Result();
            Thread t = new Thread() { @Override
                public void run() { int sum = 0;
                    for (int i = 1; i <= 1000; i++) { sum += i;
                    }
                    synchronized (result.lock) { result.sum = sum;
                        result.lock.notify();
                    }
                }
            };
            t.start();
            synchronized (result.lock) { while (result.sum == 0) { result.lock.wait();
                }
                System.out.println(result.sum);
            }
        }
    }
    

    可以看到, 上述代码需要⼀个辅助类 Result, 还需要使⽤⼀系列的加锁和 wait notify 操作, 代码复杂,

    容易出错.

    使⽤ Callable 版本代码示例

    • 创建⼀个匿名内部类, 实现 Callable 接⼝. Callable 带有泛型参数. 泛型参数表⽰返回值的类型.

    • 重写 Callable 的 call ⽅法, 完成累加的过程. 直接通过返回值返回计算结果.

    • 把 callable 实例使⽤ FutureTask 包装⼀下.

    • 创建线程, 线程的构造⽅法传⼊ FutureTask . 此时新线程就会执⾏ FutureTask 内部的 Callable 的

    call ⽅法, 完成计算. 计算结果就放到了 FutureTask 对象中.

    • 在主线程中调⽤ futureTask.get() 能够阻塞等待新线程计算完毕. 并获取到 FutureTask 中的

    结果.

    public class CallableTest { public static void main(String[] args) throws ExecutionException, InterruptedException { Callable callable = new Callable() { @Override
                public Integer call() throws Exception { int sum = 0;
                    for(int i = 1; i<=1000; i++){ sum+=i;
                    }
                    return sum;
                }
            };
            FutureTask futuretask = new FutureTask<>(callable);
            Thread t = new Thread(futuretask);
            t.start();
            int result = futuretask.get();
            System.out.println(result);
        }
    }
    

    可以看到, 使⽤ Callable 和 FutureTask 之后, 代码简化了很多, 也不必⼿动写线程同步代码了.

    理解 Callable

    Callable 和 Runnable 相对, 都是描述⼀个 “任务”. Callable 描述的是带有返回值的任务, Runnable

    描述的是不带返回值的任务.

    Callable 通常需要搭配 FutureTask 来使⽤. FutureTask ⽤来保存 Callable 的返回结果. 因为

    Callable 往往是在另⼀个线程中执⾏的, 啥时候执⾏完并不确定.

    FutureTask 就可以负责这个等待结果出来的⼯作.

    理解 FutureTask

    想象去吃⿇辣烫. 当餐点好后, 后厨就开始做了. 同时前台会给你⼀张 “⼩票” . 这个⼩票就是

    FutureTask. 后⾯我们可以随时凭这张⼩票去查看⾃⼰的这份⿇辣烫做出来了没.

    🎍ReentrantLock

    可重⼊互斥锁. 和 synchronized 定位类似, 都是⽤来实现互斥效果, 保证线程安全.

    ReentrantLock 也是可重⼊锁. “Reentrant” 这个单词的原意就是 “可重⼊”

    ReentrantLock 的⽤法:

    • lock(): 加锁, 如果获取不到锁就死等.

    • trylock(超时时间): 加锁, 如果获取不到锁, 等待⼀定的时间之后就放弃加锁.

    • unlock(): 解锁

    ReentrantLock lock = new ReentrantLock(); 
    -----------------------------------------
    lock.lock(); 
    try { // working 
    } finally { lock.unlock() 
    }
    

    ReentrantLock 和 synchronized 的区别:

    • synchronized 是⼀个关键字, 是 JVM 内部实现的(⼤概率是基于 C++ 实现). ReentrantLock 是标准

    库的⼀个类, 在 JVM 外实现的(基于 Java 实现).

    • synchronized 使⽤时不需要⼿动释放锁. ReentrantLock 使⽤时需要⼿动释放. 使⽤起来更灵活, 但

    是也容易遗漏 unlock.

    • synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的⽅式等待⼀段时间就放

    弃.

    • synchronized 是⾮公平锁, ReentrantLock 默认是⾮公平锁. 可以通过构造⽅法传⼊⼀个 true 开启

    公平锁模式.

    // ReentrantLock 的构造⽅法
    public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync();
    }
    

    • 更强⼤的唤醒机制. synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是⼀个

    随机等待的线程. ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定

    的线程.

    如何选择使⽤哪个锁?

    • 锁竞争不激烈的时候, 使⽤ synchronized, 效率更⾼, ⾃动释放更⽅便.

    • 锁竞争激烈的时候, 使⽤ ReentrantLock, 搭配 trylock 更灵活控制加锁的⾏为, ⽽不是死等.

    • 如果需要使⽤公平锁, 使⽤ ReentrantLock

    🍀原子类

    原⼦类内部⽤的是 CAS 实现,所以性能要⽐加锁实现 i++ ⾼很多。原⼦类有以下⼏个

    • AtomicBoolean

    • AtomicInteger

    • AtomicIntegerArray

    • AtomicLong

    • AtomicReference

    • AtomicStampedReference

    以 AtomicInteger 举例,常⻅⽅法有

    addAndGet(int delta); i += delta;
    decrementAndGet(); --i;
    getAndDecrement(); i--;
    incrementAndGet(); ++i;
    getAndIncrement(); i++;
    

    使用示例:

    public class AtomicTest { static AtomicInteger count = new AtomicInteger();
        public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(()->{
                for (int i = 0; i < 5000; i++) {
                    count.getAndIncrement();
                }
            });
            Thread t2 = new Thread(()->{
                for (int i = 0; i < 5000; i++) {
                    count.getAndIncrement();
                }
            });
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println(count.get());
        }
    }
    

    🌳线程池

    虽然创建销毁线程⽐创建销毁进程更轻量, 但是在频繁创建销毁线程的时候还是会⽐较低效.

    线程池就是为了解决这个问题. 如果某个线程不再使⽤了, 并不是真正把线程释放, ⽽是放到⼀个 “池

    ⼦” 中, 下次如果需要⽤到线程就直接从池⼦中取, 不必通过系统来创建了.

    ExecutorService 和 Executors

    关于线程池这部分大家可以看博主之前的线程池详解

    🌲信号量 Semaphore

    信号量, ⽤来表⽰ “可⽤资源的个数”. 本质上就是⼀个计数器

    理解信号量

    可以把信号量想象成是停⻋场的展⽰牌: 当前有⻋位 100 个. 表⽰有 100 个可⽤资源.

    当有⻋开进去的时候, 就相当于申请⼀个可⽤资源, 可⽤⻋位就 -1 (这个称为信号量的 P 操作)

    当有⻋开出来的时候, 就相当于释放⼀个可⽤资源, 可⽤⻋位就 +1 (这个称为信号量的 V 操作)

    如果计数器的值已经为 0 了, 还尝试申请资源, 就会阻塞等待, 直到有其他线程释放资源.

    Semaphore 的 PV 操作中的加减计数器操作都是原⼦的, 可以在多线程环境下直接使⽤.

    代码⽰例

    • 创建 Semaphore ⽰例, 初始化为 4, 表⽰有 4 个可⽤资源.

    • acquire ⽅法表⽰申请资源(P操作), release ⽅法表⽰释放资源(V操作)

    • 创建 20 个线程, 每个线程都尝试申请资源, sleep 1秒之后, 释放资源. 观察程序的执⾏效果.

    public class Test { public static void main(String[] args) { Semaphore semaphore = new Semaphore(4);
            Runnable runnable = new Runnable() { @Override
                public void run() { try { System.out.println("申请资源");
                        semaphore.acquire();
                        System.out.println("我获取到资源了");
                        Thread.sleep(1000);
                        System.out.println("我释放资源了");
                        semaphore.release();
                    } catch (InterruptedException e) { e.printStackTrace();
                    }
                }
            };
            for (int i = 0; i < 20; i++) { Thread t = new Thread(runnable);
                t.start();
            }
        }
    

    ☘️CountDownLatch、

    同时等待 N 个任务执⾏结束.

    好像跑步⽐赛,10个选⼿依次就位,哨声响才同时出发;所有选⼿都通过终点,才能公布成绩。

    • 构造 CountDownLatch 实例, 初始化 10 表⽰有 10 个任务需要完成.

    • 每个任务执⾏完毕, 都调⽤ latch.countDown() . 在 CountDownLatch 内部的计数器同时⾃

    减.

    • 主线程中使⽤ latch.await(); 阻塞等待所有任务执⾏完毕. 相当于计数器为 0 了.

    public class Demo { public static void main(String[] args) throws Exception { CountDownLatch latch = new CountDownLatch(10);
      Runnable r = new Runable() { @Override
       public void run() { try { Thread.sleep(Math.random() * 10000);
         latch.countDown();
        } catch (Exception e) { e.printStackTrace();
        }
       }
      };
      for (int i = 0; i < 10; i++) { new Thread(r).start();
      }
      // 必须等到 10 ⼈全部回来
      latch.await();
      System.out.println("⽐赛结束");
     }
    }
    

    ⭕相关面试题

    1. 线程同步的⽅式有哪些?

    synchronized, ReentrantLock, Semaphore 等都可以⽤于线程同步

    1. 为什么有了 synchronized 还需要 juc 下的 lock?

    以 juc 的 ReentrantLock 为例,

    • synchronized 使⽤时不需要⼿动释放锁. ReentrantLock 使⽤时需要⼿动释放. 使⽤起来更灵活,

    • synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的⽅式等待⼀段时间就放

    弃.

    • synchronized 是⾮公平锁, ReentrantLock 默认是⾮公平锁. 可以通过构造⽅法传⼊⼀个 true 开启

    公平锁模式.

    • synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是⼀个随机等待的线程.

    ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程.

    1. AtomicInteger 的实现原理是什么?

    基于 CAS 机制. 伪代码如下:

    class AtomicInteger { private int value;
     public int getAndIncrement() { int oldValue = value;
     while ( CAS(value, oldValue, oldValue+1) != true) { oldValue = value;
     }
     return oldValue;
     }
    }
    

    执⾏过程参考 “CAS详解与应用” 博客.

    1. 信号量听说过么?之前都⽤在过哪些场景下?

    信号量, ⽤来表⽰ “可⽤资源的个数”. 本质上就是⼀个计数器.

    比特就业课

    使⽤信号量可以实现 “共享锁”, ⽐如某个资源允许 3 个线程同时使⽤, 那么就可以使⽤ P 操作作为加

    锁, V 操作作为解锁, 前三个线程的 P 操作都能顺利返回, 后续线程再进⾏ P 操作就会阻塞等待, 直到前

    ⾯的线程执⾏了 V 操作.

    1. 解释⼀下 ThreadPoolExecutor 构造⽅法的参数的含义

    参考博主的 线程池详解 博客