侧边栏壁纸
博主头像
快乐江湖的博客博主等级

更多内容请点击CSDN关注“快乐江湖”

  • 累计撰写 127 篇文章
  • 累计创建 33 个标签
  • 累计收到 2 条评论

目 录CONTENT

文章目录

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

快乐江湖
2023-07-17 / 0 评论 / 0 点赞 / 6 阅读 / 23325 字

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

    }
}

0

评论区