JUC并发编程详解

JUC => java.util.concurrent,这个包里放的东西都是和多线程相关的!

一、Callable接口

Callable是一个interface,相当于把线程封装了一个 “返回值”,方便程序猿借助多线程的方式计算结果。

需要用到FutureTask类:
在这里插入图片描述

代码示例: 创建线程计算 1 + 2 + 3 + … + 1000,使用 Callable 版本

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Demo {
    // 创建线程, 通过线程来计算 1 + 2 + 3 + ... + 1000
    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;
            }
        };

        FutureTask<Integer> futureTask = new FutureTask<>(callable);

        // 创建线程, 来执行上述任务.
        // Thread 的构造方法, 不能直接传 callable, 还需要一个中间的类.
        Thread t = new Thread(futureTask);
        t.start();

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

若使用Thread类,需要一个辅助类,还需要使用一系列的加锁和 wait notify 操作,代码复杂、容易出错!
因此如果当前多线程完成的任务希望带上结果,使用 Callable 就比较好!

目前为止:
在这里插入图片描述

二、ReentrantLock类

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

ReentrantLock 也是可重入锁,“Reentrant” 这个单词的意思就是 “可重入”。

ReentrantLock 的用法:

  • lock():加锁,如果获取不到锁就死等
  • tryLock(超时时间):加锁,如果获取不到锁,等待一定的时间之后就放弃加锁
  • unlock():解锁

代码:

import java.util.concurrent.locks.ReentrantLock;

public class Demo30 {
    public static void main(String[] args) {
        ReentrantLock locker = new ReentrantLock(true);

        try {
            // 加锁
            locker.lock();
        } finally {
            // 解锁
            locker.unlock();
        }

    }
}

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

常见面试题:
synchronized 和 ReentrantLock 的区别?

区别 = 缺点 + 优势! 参考以上内容~~

三、原子类

原子类内部用的是 CAS 实现,所以性能要比加锁实现 i++ 高很多。原子类有以下几个:
(Java里已经封装好了,可以直接来使用~)
在这里插入图片描述

代码:
以 AtomicInteger 举例,常见方法有:

addAndGet(int delta);   i += delta;
decrementAndGet(); --i;
getAndDecrement(); i--;
incrementAndGet(); ++i;
getAndIncrement(); i++;
import java.util.concurrent.atomic.AtomicInteger;

public class Demo31 {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger count = new AtomicInteger(0);
//                // 相当于 ++count
//                count.incrementAndGet();
//                // 相当于 count--
//                count.getAndDecrement();
//                // 相当于 --count
//                count.decrementAndGet();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                // 相当于 count++
                count.getAndIncrement();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                // 相当于 count++
                count.getAndIncrement();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        // get 获取到内部的值.
        System.out.println(count.get());
    }
}

四、线程池

线程池 (通俗易懂),博客链接~~

五、信号量Semaphore

信号量,用来表示 “可用资源的个数”,本质上就是一个计数器。

理解信号量
可以把信号量想象成是停车场的展示牌:当前有车位100个,表示有100个可用资源.。
当有车开进去的时候,就相当于申请一个可用资源,可用车位就 -1 (这个称为信号量的 P 操作)
当有车开出来的时候,就相当于释放一个可用资源,可用车位就 +1 (这个称为信号量的 V 操作)
如果计数器的值已经为 0 了,还尝试申请资源,就会阻塞等待,直到有其他线程释放资源!!!

基于信号量也是可以实现生产者消费者模型的~~

信号量可以视为是一个更广义的锁;锁就是一个特殊的信号量 (可用资源只有1的信号量)!
把互斥锁也看作是计数为1的信号量 (取值只有1和0,也叫做二元信号量)

Java标准库提供了Semaphore这个类,其实就是把操作系统提供的信号量封装了一下~~

当需求中有多个可用资源的时候,就要记得使用信号量!!!(目的也是为了控制线程安全~~)

代码示例:

import java.util.concurrent.Semaphore;

public class Demo32 {
    public static void main(String[] args) throws InterruptedException {
        // 构造的时候需要指定初始值, 计数器的初始值. 表示有几个可用资源
        Semaphore semaphore = new Semaphore(4);
        // 这是 P 操作, 申请资源, 计数器 - 1
        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 操作");
        // 这是 V 操作, 释放资源, 计数器 + 1
        semaphore.release();
    }
}

信号量这个概念是荷兰数学家迪杰斯特拉提出来的,"求图的最短路径"就是他提出的!
P、V 就是荷兰语中的申请和释放的首字母~~


例题: 编写代码实现两个线程增加同一个变量 (使用 Semphore 来控制线程安全)

import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;

public class ThreadDemo5 {
    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(1);
        AtomicInteger count = new AtomicInteger();

        Thread t1 = new Thread(() -> {
            try {
                semaphore.acquire();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for (int i = 0; i < 100; i++) {
                count.getAndIncrement();
            }
            semaphore.release();
        });

        Thread t2 = new Thread(() -> {
            try {
                semaphore.acquire();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for (int i = 0; i < 100; i++) {
                count.getAndIncrement();
            }
            semaphore.release();
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

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

六、闭锁CountDownLatch

类似于一个跑步比赛:
在这里插入图片描述
在这里插入图片描述
代码:

import java.util.concurrent.CountDownLatch;

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        // 有 10 个选手参加了比赛
        CountDownLatch countDownLatch = new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {
            // 创建 10 个线程来执行一批任务.
            Thread t = new Thread(() -> {
                System.out.println("选手出发! " + Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("选手到达! " + Thread.currentThread().getName());
                // 撞线
                countDownLatch.countDown();
            });
            t.start();
        }

        // await 是进行阻塞等待. 会等到所有的选手都撞线之后, 才解除阻塞
        countDownLatch.await();
        System.out.println("比赛结束!");
    }
}

七、线程安全的集合类

原来的集合类,大部分都不是线程安全的。

Vector, Stack, HashTable, 是线程安全的(不建议用), 其他的集合类不是线程安全的!

7.1 多线程环境使用 ArrayList

1)自己使用同步机制 (synchronized 或者 ReentrantLock)

2)Collections.synchronizedList(new ArrayList);

synchronizedList 是标准库提供的一个基于 synchronized 进行线程同步的 List,
synchronizedList 的关键操作上都带有 synchronized
(套了一层壳,壳上加锁了)

3)使用 CopyOnWriteArrayList

CopyOnWrite容器即写时拷贝的容器。

  • 当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,
  • 添加完元素之后,再将原容器的引用指向新的容器。

这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。
所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
优点:
在读多写少的场景下,性能很高,不需要加锁竞争
缺点:
1.占用内存较多
2.新写的数据不能被第一时间读取到

在这里插入图片描述

这种写时拷贝的思想很多地方都会用到!一个典型的,显卡给显示器渲染画面,也是类似的操作~~

7.2 多线程环境使用队列

1)ArrayBlockingQueue

基于数组实现的阻塞队列

2)LinkedBlockingQueue

基于链表实现的阻塞队列

3)PriorityBlockingQueue

基于堆实现的带优先级的阻塞队列

4)TransferQueue

最多只包含一个元素的阻塞队列

7.3 多线程环境使用哈希表

HashMap 本身不是线程安全的。
在多线程环境下使用哈希表可以使用:

  • Hashtable (不推荐使用,是属于无脑给各种方法加synchronized)
  • ConcurrentHashMap (推荐使用!背后做了很多的优化策略~~)

HashTable是官方明确提出不建议使用的!上古时期就有了,当时设计得不是很好,确实现在就没啥优势~~

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yyhgo_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值