Java---JUC

1.callable接口

显然它也是一个函数式接口,这意味着我们可以通过Lambda表达式创建简化写法

 

对比Runnable接口:

1.call()方法多了一个可以抛出的异常,而Exception是所有异常的父类,因此可以抛出所有的异常,而run()方法只能在方法内部解决。

2.call()多了一个返回值。

我们在应用中介绍它的优势:

通过callable定义任务

1.定义一个类,实现callable接口

2.通过匿名内部类的方式

3.通过Lambda表达式的方式

        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                System.out.println("执行任务...");
                int sum = 0;
                //完成累加操作
                for (int i = 1; i <= 10; i++) {
                    sum +=i;
                }
                TimeUnit.SECONDS.sleep(1);
                System.out.println("运算结束..."+"sum = " + sum);
                return sum;
            }
        };

那么如何像Runnable一样搭配Thread?

        //搭配Future使用,获取Callable的执行结果
        FutureTask<Integer> futureTask  = new FutureTask<>(callable);

看源码知它继承自Runnable

如何获取callable的结果呢?(call()拥有返回值的好处)


注意此方法会一直阻塞等待到任务执行完毕

 注意此处本应抛出异常,如果在Run方法中就必须用try环绕

如果是其他业务异常的话就需要当场处理,但是call方法中就可以抛出 

面试题:Runnable与Callable的区别
1.Callable要实现call(),且有返回值,Runnable实现的run()但没有返回值

2. Callable的call()可以抛出异常,Runnable的run()不能抛出异常
3.Callable配合FutrueTask一起使用,通过futureTask.get()方法获取call()的结果
4.两都是描述线程任务的接口

面试题:创建线程有几种方式?
1.继承Thread类,实现run()方法
2.实现Runnable接口,并且实现run()方法
3.实现Callable接口,并且实现call()方法
4.通过创建线程池,并提交任务 

2.ReentrantLock

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

 

public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock();
        //加锁
        lock.lock();
        //被上锁的代码

        //释放锁
        lock.unlock();
        //尝试获取锁,如果获取到锁返回true,执行加锁的逻辑,如果获取不到锁返回false,执行不加锁 
          的逻辑
        lock.tryLock();
        //尝试加锁,并指定等待时间
        lock.tryLock(1, TimeUnit.SECONDS);

        //创建一个公平锁    获取锁的方式遵循先来后到
        ReentrantLock lock1 = new ReentrantLock(true);
        //创建一个读写锁
        ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

        //创建一个读锁
        ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();

        //创建一个写锁
        ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
    }

多个线程可以同时加读锁,也就是真正串行执行

1.当上面的读锁线程读共享变量时,下面的写锁线程就加写锁就要等待 ,因为写锁是排他锁,不能与其他锁共存。

2.只有当上面的线程全部完成读操作后,下面的写操作才可以执行。

3.加了写锁之后,其他线程也不能读了,必须等写操作完成,释放完写锁资源才能加读锁。

写锁与读锁不可共存

ReentrantLock与synchronized的区别 

1.

synchronized是一个关键字,在JVM内部实现(大概率是C++实现)

ReentrantLock是标准库中的一个类,在JVM外部实现(Java代码实现)

2.

synchronized使用时不需要手动释放锁资源

ReentrantLock需要手动释放,更加灵活,但也容易忘记释放锁资源

3.

synchronized申请锁资源失败后,死等

Reentrant Lock可以通过Try Lock等一段时间后放弃

4.

synchronized是一个非公平锁

Reentrant Lock默认是一个非公平锁,可以通过构造时传入一个true开启公平锁模式

5.

更强大的唤醒机制.

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

随机等待的线程.

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

的线程.

 

显然这个类底层是一个链表,并且内部装的是Thread线程,那么它的等待操作就是等待线程入队,唤醒就是出队操作,并且,从源码中并没有具体的链表长度,这意味着来链表尽可能大。

3.Semaphore

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

举一个例子理解:

停车场:
停车场外面通常会一个显示牌,牌子上会显示当前停车场中车位的可用个数进入停车场一辆车之个,个数就减1,出场一辆车之后个数 加1如果停车场的车位占满了,那么显示牌上就显示车位已满,这时外面的车就需要阻塞等待当一辆车出场之后,个数加1了,意味着释放了资源,外面等待的车就可以进场 

//创建信号量变量
Semaphore semaphore = new Semaphore(5);

// 申请资源,如果有可用资源就申请,如果没有只有阻塞等待
semaphore.acquire();
                   
// 释放资源
semaphore.release();
        Semaphore semaphore = new Semaphore(5);
        // 定义线程的任务
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "开始申请资源...");
                try {
                    // 申请资源, 可用资源数减1
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + "====== 已经申请到资源 ======");
                    // 处理业务逻辑, 用休眠来模拟
                    TimeUnit.SECONDS.sleep(1);
                    // 释放资源,  可用资源数加1
                    semaphore.release();
                    System.out.println(Thread.currentThread().getName() + "***** 释放资源");

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        // 创建线程执行任务, 20 有20个线程需要执行任务
        for (int i = 0; i < 20; i++) {
            // 创建线程并指定任务
            Thread thread = new Thread(runnable);
            // 启动线程
            thread.start();
        }

通过信号量可以限制系统中并发执行的线程个数

4.CountDownLatch 

同时等待 N 个任务执⾏结束
//每个任务执⾏完毕, 都调⽤ latch.countDown() . 
//在 CountDownLatch 内部的计数器同时⾃减
count.countDown();
//主线程中使⽤ latch.await(); 阻塞等待所有任务执⾏完毕. 相当于计数器为 0 了
count.await();

CountDownLatch 可以等某个事情完成之后,(也就是等全部的线程执行完毕后)再去执行其他任务

应用场景:

比如可以把一个大的文件分成许多许多的小模块,对每个小模块分别进行下载,当全部的小模块下载好之后,将这些下载好的小模块合并成大的文件,这样就提高了下载速度 。

5.线程池

### Java JUC AQS 并发编程 抽象队列同步器 使用教程 源码解析 #### 什么是AQS? `AbstractQueuedSynchronizer`(简称AQS),作为Java并发包中的核心组件之一,提供了用于实现锁和其他同步器的基础框架。它不仅简化了锁和同步工具的创建过程,还提高了这些工具的工作效率[^2]。 #### 类图结构与工作原理 AQS的设计围绕着一个FIFO(先进先出)等待队列展开,该队列由多个节点组成,每个节点代表了一个正在等待获取资源的线程。每当有新的竞争者未能立即获得所需资源时,就会被构造成一个新的节点并加入到这个队列之中;而当现有持有者释放其持有的资源之后,则会从队头开始依次唤醒后续等待者去尝试占有资源[^5]。 #### 同步模式分类 为了适应不同场景下的需求,AQS支持两种主要类型的同步方式——独占式以及共享式: - **独占式**:一次只允许单个线程访问临界区,在这种情况下其他任何试图进入同一区域内的请求都将被迫挂起直到前序操作完成为止; - **共享式**:允许多个读取者同时存在而不互相干扰,只要不存在写入动作发生即可保持一致性和安全性[^3]。 #### 自定义同步器的关键接口 对于想要利用AQS来构建特定行为逻辑的新类型而言,开发者通常需要重载以下几个抽象方法以适配具体的应用环境: - `tryAcquire(int arg)` 和 `tryRelease(int arg)` - `tryAcquireShared(int arg)` 及 `tryReleaseShared(int arg)` - `isHeldExclusively()` 上述函数分别对应于独占/共享模式下对资源的操作控制流程,通过合理地覆盖它们可以轻松打造出满足业务特性要求的各种高级别同步原语[^1]。 ```java public class CustomSync extends AbstractQueuedSynchronizer { protected boolean tryAcquire(int acquires) { // 实现具体的独占式获取逻辑 return super.tryAcquire(acquires); } protected boolean tryRelease(int releases) { // 实现具体的独占式释放逻辑 return super.tryRelease(releases); } } ``` #### 队列管理机制详解 在实际运行过程中,AQS内部维护了一条双向链表形式的数据结构用来存储各个待处理的任务单元。每当新成员到来之时便会调用`enqueue()`方法将其追加至末端位置上形成完整的链条关系网状链接,并且借助CAS指令保证整个插入过程的安全可靠性质不受外界因素影响破坏[^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值