面试问JUC(java.util.concurrent)的常见类你能答出来几句?

1.Callable接口

Callable 是一个 interface . 相当于把线程封装了一个 “返回值”. 方便程序猿借助多线程的方式计算结果.通过下面两个代码实例,可以清晰的看到Callable的优势:

现在要求创建线程计算 1 + 2 + 3 + … + 1000的结果。

在没有Callable的情况下,我们想要在直接通过一个线程来获取到计算结果是不可能的,因为这种情况下,线程的执行不返回任何结果。Runnable接口的run方法定义了线程的执行逻辑,但该方法没有返回值。此时想要实现题目要求,就必须再线程中对一个全局变量进行修改,并且配合线程通信方法来实现:

public class Demo1 {
    
    
    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 中的结果.
	static class MyCallable implements Callable<Integer> {
    
    
        @Override
        public Integer call() throws Exception {
    
    
            int result = 0;
            for (int i = 1; i <= 1000; i++) {
    
    
                result += i;
            }
            return result;
        }
    }
    public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
        Callable<Integer> myCallable = new MyCallable();
        FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
        Thread t = new Thread(futureTask);
        t.start();
        int result = futureTask.get();
        System.out.println(result);
    }

理解Callable

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

2.Callable 通常需要搭配 FutureTask 来使用. FutureTask 用来保存 Callable 的返回结果. 因为Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定.

3.FutureTask 就可以负责这个等待结果出来的工作。future部分源码:

	private volatile int state;
    private static final int NEW          = 0;
    private static final int COMPLETING   = 1;
    private static final int NORMAL       = 2;
    private static final int EXCEPTIONAL  = 3;
    private static final int CANCELLED    = 4;
    private static final int INTERRUPTING = 5;
    private static final int INTERRUPTED  = 6;
	private Object outcome; // non-volatile, protected by state reads/writes
    /** The underlying callable; nulled out after running */
    private Callable<V> callable;
    
	public FutureTask(Callable<V> callable) {
    
    
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }
    public V get() throws InterruptedException, ExecutionException {
    
    
        int s = state;
        if (s <= COMPLETING)
            s = awaitDone(false, 0L);
        return report(s);
    }
    private V report(int s) throws ExecutionException {
    
    
        Object x = outcome;
        if (s == NORMAL)
            return (V)x;
        if (s >= CANCELLED)
            throw new CancellationException();
        throw new ExecutionException((Throwable)x);
    }

2. ReentrantLock

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

ReentrantLock 的用法:

  • lock(): 加锁, 如果获取不到锁就死等.
  • trylock(超时时间): 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁.
  • unlock(): 解锁
ReentrantLock lock = new ReentrantLock();
-----------------------------------------
lock.lock();
try {
    
    
// working
} finally {
    
    
lock.unlock()
}

ReentrantLock 和 synchronized 的区别:

  1. synchronized 是一个关键字, 是 JVM 内部实现的(大概率是基于 C++ 实现). ReentrantLock 是标准库的一个类, 在 JVM 外实现的(基于 Java 实现).
  2. synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活,但是也容易遗漏 unlock.
  3. synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃.
  4. synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个 true 开启公平锁模式.
ReentrantLock reentrantLock = new ReentrantLock(true);
// ReentrantLock 的构造方法
public ReentrantLock(boolean fair) {
    
    
sync = fair ? new FairSync() : new NonfairSync();
}
  1. 更强大的唤醒机制. synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是一个随机等待的线程. ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程.
class SharedResource {
    
    
    private final Lock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();
    private int value;

    public void produce(int newValue) {
    
    
        lock.lock();
        try {
    
    
            value = newValue;
            System.out.println("Producing: " + value);
            condition.signal(); // 唤醒等待的线程
        } finally {
    
    
            lock.unlock();
        }
    }

    public int consume() {
    
    
        lock.lock();
        try {
    
    
            while (value == 0) {
    
    
                try {
    
    
                    condition.await(); // 等待,直到有数据被生产
                } catch (InterruptedException e) {
    
    
                    Thread.currentThread().interrupt();
                }
            }
            int consumedValue = value;
            value = 0;
            System.out.println("Consuming: " + consumedValue);
            return consumedValue;
        } finally {
    
    
            lock.unlock();
        }
    }
}

public class Demo2 {
    
    
        public static void main(String[] args) {
    
    
            SharedResource sharedResource = new SharedResource();

            // 生产者线程
            Thread producerThread = new Thread(() -> {
    
    
                sharedResource.produce(42);
            });

            // 消费者线程
            Thread consumerThread = new Thread(() -> {
    
    
                int value = sharedResource.consume();
            });

            producerThread.start();
            consumerThread.start();
        }
    }

3. 原子类(java.util.concurrent.atomic)

原子类内部用的是 CAS 实现,所以性能要比加锁实现 i++ 高很多。原子类有以下几个

AtomicBoolean
AtomicInteger
AtomicIntegerArray
AtomicLong
AtomicReference
AtomicStampedReference

这些原子类是非常有用的,因为它们可以确保在多线程环境中进行原子操作,避免了竞态条件和数据竞争。这些操作是线程安全的,不需要显式的同步措施。例如,你可以使用AtomicInteger来实现计数器,多个线程可以同时递增计数器的值而不会发生竞态条件。常用方法有:

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

以下是一个示例,演示了如何使用AtomicInteger:

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicExample {
    
    
    public static void main(String[] args) {
    
    
        AtomicInteger counter = new AtomicInteger(0);

        // 多个线程同时递增计数器的值
        Runnable task = () -> {
    
    
            for (int i = 0; i < 1000; i++) {
    
    
                counter.incrementAndGet();//++i
            }
        };

        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);
        
        //同时启动两个线程对原子类的实力进行自增
        thread1.start();
        thread2.start();

        try {
    
    
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }

        System.out.println("Counter: " + counter.get()); // 应该输出 2000
    }
}

4. 线程池

线程池在前面的博文有详细的讲解:线程池博文

5. 信号量 Semaphore

java.util.concurrent.Semaphore 是 Java 并发编程中的一个重要工具,用于控制并发访问资源的数量。它提供了一种计数信号机制,允许你控制同时访问某个共享资源的线程数。

Semaphore 维护了一个计数器,该计数器表示允许同时访问资源的线程数量。当线程要访问资源时,它必须先获取信号量,如果信号量计数大于零,则线程可以访问资源,同时信号量计数减一;如果计数为零则线程必须等待,直到有其他线程释放资源,增加信号量计数。

Semaphore 的核心方法包括:

acquire(): 当线程想要获得一个信号量时,可以调用这个方法。如果信号量计数大于零,线程将成功获得信号量,计数减一;否则,线程将阻塞,直到有信号
           量可用。((这个称为信号量的 P 操作))

release(): 当线程使用完资源后,应该调用 release() 方法来释放信号量,这将使信号量计数加一,表示资源可供其他线程使用。((这个称为信号量的 
           V操作))

6. CountDownLatch

CountDownLatch 的主要思想是,一个线程(通常是主线程)等待其他一组线程执行完特定任务,然后再继续执行。它通过一个计数器来实现,该计数器初始化为一个正整数,每个线程在完成任务时会将计数器减一,当计数器的值达到零时,等待的线程将被唤醒继续执行。

举个例子

同时等待 N 个任务执行结束.—>好像跑步比赛,10个选手依次就位,哨声响才同时出发;所有选手都通过终点,才能公布成绩。

理解:

  1. 构造 CountDownLatch 实例, 初始化 10 表示有 10 个任务需要完成.
  2. 每个任务执行完毕, 都调用 latch.countDown() . 在 CountDownLatch 内部的计数器同时自减.
  3. 主线程中使用 latch.await(); 阻塞等待所有任务执行完毕. 相当于计数器为 0 了.
public class Demo3 {
    
    
    public static void main(String[] args) {
    
    
        int taskCount = 10;
        CountDownLatch latch = new CountDownLatch(taskCount);

        Runnable task = () -> {
    
    
            // 模拟任务的执行
            System.out.println("任务正在执行。");
            // 任务完成后减少计数器
            latch.countDown();
        };

        // 启动 10 个线程执行任务
        for (int i = 0; i < taskCount; i++) {
    
    
            Thread thread = new Thread(task);
            thread.start();
        }

        try {
    
    
            // 等待所有任务完成
            latch.await();
            System.out.println("所有任务已完成。主线程可以继续执行。");
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

猜你喜欢

转载自blog.csdn.net/qq_45875349/article/details/133932094
今日推荐