Java多线程常见面试题-第二节:JUC(java.util.concurrent)

Posted 快乐江湖

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java多线程常见面试题-第二节:JUC(java.util.concurrent)相关的知识,希望对你有一定的参考价值。

文章目录

JUC:JUC是java.util.concurrent包的简称,目的就是为了更好的支持高并发任务。让开发者进行多线程编程时减少竞争条件和死锁的问题

一:Callable接口

Callable接口Callable接口类似于Runnable,也即它的实例也可以像Runnable一样传给线程去执行,但是Runnable不返回结果,也不会抛出受查异常,而Callable与此相反

下面使用"1 + 2 + 3 +…+ 1000"这样一个例子来展示使用Callable和不使用Callable时代码的区别

①不使用Callable 可以看到,这种实现逻辑需要借助一个辅助类,还需要使用一系列加锁、wait等操作,比较繁琐。具体实现方式如下(不唯一)

  • 创建一个类Result,包含一个sum表示最终结果,一个lock表示线程锁对象
  • main方法内先创建Result实例,然后创建一个线程thread,在线程内部计算"1 + 2 + 3 +…+ 1000"
  • 主线程同时使用wait等待线程thread计算结束
  • 当线程thread计算完毕之后,通过notify唤醒主线程,接着主线程打印结果
public class TestDemo 
    static class Result
        public int sum = 0;
        public final Object lock = new Object();
    

    public static void main(String[] args) throws InterruptedException 
        Result result = new Result();

        Thread thread = 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();
                
            
        ;
        thread.start();

        synchronized (result.lock)
            while(result.sum == 0)
                result.lock.wait();
            
        

        System.out.println("sum:" + result.sum);

    


②使用Callable 可以看到,在使用CallableFutureTask之后,代码简化了许多,并且不用手动写线程同步代码了。具体实现方式如下

  • 创建一个匿名内部类实现Callable接口。Callable带有泛型参数,它表示返回值的类型
  • 重写Callable中的 call方法,完成计算并直接通过return返回计算结果
  • 由于Thread的构造方法中不能直接传入Callable,所以还需要用FutureTaskCallable的实例给包装一下
  • 创建线程,在其构造方法中传入FutureTask。此时,线程就会执行FutureTask内部的Callablecall方法,完成计算后结果就被放入到了FutureTask对象中
  • main方法中调用futureTask.get()后就会阻塞等到线程计算完毕,并获取到FutureTask中的结果
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class TestDemo2 
    public static void main(String[] args) throws ExecutionException, InterruptedException 
        // 使用Callable来定义一个任务
        Callable<Integer> callable = new Callable<Integer>() 
            @Override
            public Integer call() throws Exception 
                int sum = 0;
                for(int i = 1; i <= 1000; i++)
                    sum += i;
                
                return sum;
            
        ;

        // Thread构造方法不能直接传入Callable,所以需要借助一个中间类
        FutureTask<Integer> futureTask = new FutureTask<>(callable);

        // 创建线程执行任务
        Thread thread = new Thread(futureTask);
        thread.start();

        // 获取线程计算结果
        // get方法会阻塞,直到call方法计算完毕
        System.out.println(futureTask.get());
    


二:ReentrantLock

ReentrantLock:和Synchronized类似,也是可重入锁,用来实现互斥效果,保证线程安全,相较于Synchronized来说更加灵活,也具有更多的方法。具体来说ReentrantLock有三个用法

  • lock():加锁(如果获取不到就会死等)
  • trylock(超时时间):加锁(如果获取不到锁,在等待一段时间后就会放弃加锁)
  • unlock():解锁
ReentrantLock lock = new ReentrantLock();

lock.lock();
try
	//working....
finally
	lock.unlock(); // 千万不要忘记解锁

ReentrantLockSynchronized区别如下

  • Synchronized是一个关键字,是JVM内部实现的;ReentrantLock标准库的一类,是在JVM外实现的
  • Synchronized使用时不需要手动释放锁;ReentrantLock使用时需要手动释放,使用起来更加灵活,但也容易忘记解锁
  • Synchronized在申请锁失败时会死等;ReentrantLock可以通过trylock的方式等待一段时间就放弃
  • Synchronized是非公平锁,ReentrantLock默认为非公平锁,可以通过构造方法传入一个true开启公平锁模式
  • ReentrantLock具有更加强大的唤醒机制
    • Synchronized是通过Objectwait/notify来实现的,每次随机唤醒一个等待的线程
    • ReentrantLock搭配Condition类实现,可以精确控制唤醒某个指定的线程

ReentrantLockSynchronized如何选择

  • 锁竞争不激烈的时候使用Synchronized:效率会更高,自动释放也方便
  • 锁竞争激烈的时候使用ReentrantLock:可以搭配trylock更灵活地控制加锁行为,而不至于死等
  • 如果需要公平锁则使用ReentrantLock

三:原子类

原子类:原子类内部通过CAS实现(前文已讲过),所以性能要比加锁实现i++高很多,主要有以下几个

  • AtomicBoolean
  • AtomicInteger
  • AtomicIntegerArray
  • AtomicLong
  • AtomicReference
  • AtomicStampedReference

AtomicBoolean为例,涉及方法有

  • addAndGet(int delta)i += delta
  • decrementAndGet()--i
  • getAndDecrement()i--
  • incrementAndGet()++i
  • getAndIncrement()i++
public class TestDemo3 
    public static void main(String[] args) throws InterruptedException 
        AtomicInteger count = new AtomicInteger(0);

        Thread thread1= new Thread()
            @Override
            public void run()
                for(int i = 0; i < 50000; i++)
                    // 相当于count++
                    count.getAndIncrement();
                
            
        ;
        Thread thread2= new Thread()
            @Override
            public void run()
                for(int i = 0; i < 50000; i++)
                    // 相当于count++
                    count.getAndIncrement();
                
            
        ;
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();

        System.out.println("count:" + count.get());
    

四:信号量Semaphore

(1)什么是信号量

信号量:本质就是一个变量(分为整形和记录型两种),表示系统中某种资源的数量。控制信号量有两种原子操作:

  • P操作(wait(S)原语):这个操作会把信号量减去1,相减后如果信号量<0则表示资源已经被占用,进程需要阻塞;相减后如果信号量 ≥ 0 \\ge0 0,则表明还有资源可以使用,进程可以正常执行
  • V操作(signal(S)原语):这个操作会把信号量加上1,相加后如果信号量 ≤ 0 \\le0 0,则表明当前有阻塞中的进程,于是会把该进程唤醒;相加后如果信号量>0,则表明当前没有阻塞中的进程

(2)java.util.concurrent.Semaphore

java.util.concurrent.Semaphore:Java将信号量有关的系统接口进行封装,也即java.util.concurrent.Semaphore中,其中

  • seamphore.acuire()对应的是P操作
  • seamphore.release()对应的是V操作

如下申请4个资源,然后连续进行4次P操作后程序阻塞

import java.util.concurrent.Semaphore;

public class TestDemo4 
    public static void main(String[] args) throws InterruptedException 
        // 4个可用资源
        Semaphore semaphore = new Semaphore(4);
        // 连续申请4个资源
        semaphore.acquire();
        System.out.println("P操作");
        semaphore.acquire();
        System.out.println("P操作");
        semaphore.acquire();
        System.out.println("P操作");
        semaphore.acquire();
        System.out.println("P操作");
        // 此时资源数目已空, 申请操作将被阻塞
        semaphore.acquire();
        System.out.println("P操作");

        semaphore.release();
        System.out.println("V操作");
        
    


五:CountDownLatch

CountDownLatch:用于同时等待N个任务执行结束。具体来说,每个任务执行完毕后,都会调用latch.countDown(),然后CountDownLatch内部的计数器就会减一,主线程中会使用latch.await(),用于阻塞等待所有任务执行完毕(此时计数器为0)

import java.util.concurrent.CountDownLatch;

public class TestDemo5 
    public static void main(String[] args) throws InterruptedException 
        // 10个任务
        CountDownLatch countDownLatch = new CountDownLatch(10);

        for(int i = 0; i < 10; i++)
            //创建10个线程分别执行这10个任务
            Thread thread = new Thread()
                @Override
                public void run()
                    System.out.println("任务开始" + Thread.currentThread().getName());
                    try 
                        Thread.sleep(1000);
                     catch (InterruptedException e) 
                        throw new RuntimeException(e);
                    
                    System.out.println("任务结束" + Thread.currentThread().getName());
                    // 任务完成
                    countDownLatch.countDown();
                
            ;
            thread.start();
        

        // 阻塞等待所有线程执行任务完毕
        countDownLatch.await();
        System.out.println("结束!");

    


以上是关于Java多线程常见面试题-第二节:JUC(java.util.concurrent)的主要内容,如果未能解决你的问题,请参考以下文章

Java并发指南18:JUC常见面试题及答案

敲黑板!Java多线程常见面试题!!

Java多线程常见面试题

Java 并发常见面试题总结(上)

Java多线程常见面试题-第三节:线程安全集合类和死锁

Java多线程常见面试题-第三节:线程安全集合类和死锁