【JavaEE】JUC(Java.util.concurrent)常见类

简介: 【JavaEE】JUC(Java.util.concurrent)常见类

前言

经过前面文章的学习我们大致了解了如何实现多线程编程和解决多线程编程中遇到的线程不安全问题,java.util.concurrent 是我们多线程编程的一个常用包,那么今天我将为大家分享 java.util.concurrent 包下的其他几种常见的类。


ReentrantLock

ReentrantLock 是可重入互斥锁,跟 synchronized 定位是类似的,都是用来保证线程安全的,但是 ReentrantLock 在某些方面相较于 synchronized 有突出的优势。


ReentrantLock 的加锁方式分为两种:

lock():如果获取不到锁,线程就会进入阻塞等待状态

tryLock():如果获取不到锁,就会放弃加锁,而不是进入阻塞等待状态

使用 ReentrantLock 的时候需要手动 unLock() 解锁,如果忘记了手动解锁这个操作,将会带来比较严重的后果。


所以为了解决有些时候忘记手动解锁的情况,往往需要借助 try-finally 来进行解锁操作。

ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
    //working
}finally {
    lock.unlock();
}

ReentranLock 与 synchronized 相比的优势



ReentrantLock 在加锁的时候,有两种方式,lock() 和tryLock()。使用 lock() 的时候,如果未获取到锁就会进入阻塞等待状态,而使用 tryLock() 的话,如果没有获取到锁就会放弃获取,而不是进入阻塞等待


ReentrantLock 提供了公平锁的实现(默认情况下是非公平锁)


ReentranLock 提供了更强大的等待通知机制,搭配了Condition类,实现等待通知,可以指定唤醒某个线程

如何选择使用哪个锁?

  • 锁竞争不激烈的时候, 使用 synchronized, 效率更高, 自动释放更方便.
  • 锁竞争激烈的时候, 使用 ReentrantLock, 搭配 trylock 更灵活控制加锁的行为, 而不是死等.
  • 如果需要使用公平锁, 使用 ReentrantLock.


通常情况下还是建议使用 synchronized,虽然 ReentrantLock 在某些方面具有优势,但是使用起来较麻烦,并且Java程序员在 synchronized 里面做了很多的优化。


原子类

原子类内部都是使用 CAS 操作实现的,因为 CAS 操作时原子性的,不需要进行加锁操作,所以性能要比加锁好很多。

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


原子类常见方法:

addAndGet(int delta); —— i += delta;

decrementAndGet(); —— --i;

getAndDecrement(); —— i–;

incrementAndGet(); —— ++i;

getAndIncrement(); —— i++;

CAS 操作我在前面详细讲解过,大家想要了解的话可以去看看。CAS(Compare And Swap)操作

线程池

线程池是为了解决因线程频繁创建和销毁而造成的资源浪费问题。

使用线程池需要使用到 ExecutorServiceExecutors 两个类。

ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(new Runnable() {
  @Override
  public void run() {
    System.out.println("hello");
  }
});
  • ExecutorService 表示一个线程池实例.
  • Executors 是一个工厂类, 能够创建出几种不同风格的线程池.
  • ExecutorService 的 submit 方法能够向线程池中提交若干个任务

Executors 创建线程池的几种方式


newFixedThreadPool: 创建固定线程数的线程池

newCachedThreadPool: 创建线程数目动态增长的线程池.

newSingleThreadExecutor: 创建只包含单个线程的线程池.

newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer。

这里面也涉及到了工厂模式,大家可以去看看这篇文章了解一下【JavaEE】多线程案例-线程池

信号量

信号量(Semaphore)是一种实现任务间通信的机制,可以用于实现任务之间的同步或临界资源的互斥访问。它通常被用于协助一组相互竞争的任务来访问临界资源。


信号量是一个非负整数,获取信号量的任务会将该整数减1。当信号量为0时,所有试图获取该信号量的任务都将处于阻塞状态。信号量的值代表积累下来的释放信号量操作的次数。


申请资源的操作被称为 P 操作,释放资源的操作被称为 V 操作。


信号量可以分为二值信号量和计数信号量两种。二值信号量只有一个消息队列,队列有两种状态:空或满。而计数信号量可以看做长度大于1的消息队列,用于计数。信号量的计数值表示还有多少个事件未被处理。当某个事件发生时,任务或中断会释放一个信号量(将信号量计数值加1);当需要处理某个事件时,任务或中断会取走一个信号量(将信号量计数值减1)。


加锁和解锁的操作就可以看成是二值信号量的操作,当加锁的时候信号量就为0,释放锁的时候信号量就为1.


在Java代码中,信号量相关的操作被封装在 Semaphore 类中,acquire() 方法表示申请资源,release() 方法表示释放资源。

public class Test2 {
    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(4);
        semaphore.acquire();
        System.out.println("获取资源");
        semaphore.acquire();
        System.out.println("获取资源");
        semaphore.acquire();
        System.out.println("获取资源");
        semaphore.acquire();
        System.out.println("获取资源");
        semaphore.acquire();
        System.out.println("获取资源");
        semaphore.release();
        System.out.println("释放资源");
    }
}

当申请的资源量大于总的资源量的时候,线程就会进入阻塞等待状态,直到其他线程释放掉部分信号量。

CountDownLatch

CountDownLatch是Java中的一个同步工具类,用来协调多个线程之间的同步。它允许一个或多个线程等待,直到在其他线程中执行的一组操作完成。


CountDownLatch通过一个计数器来实现这个功能,计数器的初始值通常设置为需要等待完成的线程数量。每当一个线程完成了自己的任务,计数器的值就会减1。当计数器的值达到0时,表示所有线程都已经完成任务,此时在CountDownLatch上等待的线程就可以恢复执行。


CountDownLatch可以用来确保某些活动在其他活动完成之前不会继续执行。例如,可以确保某个计算在其需要的所有资源都被初始化之后才继续执行,或者确保某个服务在其依赖的所有其他服务都已经启动之后才启动。


在Java中,可以使用CountDownLatch的countDown()方法来对计数器做减操作,就是告诉CountDownLatch我这个当前的任务完成了,使用await()方法等待计数器达到0。所有调用await()方法的线程都会被阻塞,直到计数器达到0或者等待线程被中断或者超时。

public class Demo2 {
    private static int count;
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(10);
        for(int i = 0; i < 10; i++) {
            Thread t = new Thread(() -> {
                try {
                    Thread.sleep((long)(Math.random() * 4000));
                    System.out.println(++count + "号完成比赛");
                    countDownLatch.countDown();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
            t.start();
        }
        countDownLatch.await();
        System.out.println("结束比赛");
    }
}

只有调用了10次 countDown() 方法之后,await() 方法才会结束等待,继续执行后面的代码。


需要注意的是,CountDownLatch是一次性的,一旦计数器的值达到0,就不能再次使用。如果需要多次使用类似的功能,可以考虑使用CyclicBarrier等其他同步工具类。


相关面试题

1) 线程同步的方式有哪些?

synchronized, ReentrantLock, Semaphore 等都可以用于线程同步.

2) 为什么有了 synchronized 还需要 juc 下的 lock?

以 juc 的 ReentrantLock 为例,


synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活,

synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃.

synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个 true 开启公平锁模式.

synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是一个随机等待的线程. ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程.

3) AtomicInteger 的实现原理是什么?


基于 CAS 机制. 伪代码如下:

class AtomicInteger {
  private int value;
  public int getAndIncrement() {
    int oldValue = value;
    while ( CAS(value, oldValue, oldValue+1) != true) {
      oldValue = value;
    }
    return oldValue;
  }
}

4) 信号量听说过么?之前都用在过哪些场景下?


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


使用信号量可以实现 “共享锁”, 比如某个资源允许 3 个线程同时使用, 那么就可以使用 P 操作作为加锁, V 操作作为解锁, 前三个线程的 P 操作都能顺利返回, 后续线程再进行 P 操作就会阻塞等待,直到前面的线程执行了 V 操作

相关文章
|
1天前
|
Java
【JAVA基础篇教学】第五篇:Java面向对象编程:类、对象、继承、多态
【JAVA基础篇教学】第五篇:Java面向对象编程:类、对象、继承、多态
|
1天前
|
存储 安全 Java
Java容器类List、ArrayList、Vector及map、HashTable、HashMap
Java容器类List、ArrayList、Vector及map、HashTable、HashMap
|
1天前
|
Java 编译器 开发者
Java一分钟之-继承:复用与扩展类的特性
【5月更文挑战第9天】本文探讨了Java中的继承机制,通过实例展示了如何使用`extends`创建子类继承父类的属性和方法。文章列举了常见问题和易错点,如构造器调用、方法覆盖、访问权限和类型转换,并提供了解决方案。建议深入理解继承原理,谨慎设计类结构,利用抽象类和接口以提高代码复用和扩展性。正确应用继承能构建更清晰、灵活的代码结构,提升面向对象设计能力。
8 0
|
2天前
|
Java
【Java多线程】面试常考 —— JUC(java.util.concurrent) 的常见类
【Java多线程】面试常考 —— JUC(java.util.concurrent) 的常见类
10 0
|
2天前
|
Java API 调度
【Java多线程】Thread类的基本用法
【Java多线程】Thread类的基本用法
5 0
|
2天前
|
SQL Java 数据库连接
JDBC Java标准库提供的一些api(类+方法) 统一各种数据库提供的api
JDBC Java标准库提供的一些api(类+方法) 统一各种数据库提供的api
8 0
|
2天前
|
Java
Java一分钟之-类与对象:面向对象编程入门
【5月更文挑战第8天】本文为Java面向对象编程的入门指南,介绍了类与对象的基础概念、常见问题及规避策略。文章通过代码示例展示了如何定义类,包括访问修饰符的适当使用、构造器的设计以及方法的封装。同时,讨论了对象创建与使用时可能遇到的内存泄漏、空指针异常和数据不一致等问题,并提供了相应的解决建议。学习OOP需注重理论与实践相结合,不断编写和优化代码。
25 1
|
4天前
|
Java 开发者
在Java中,接口和超类在多态性中扮演着重要的角色
Java中的接口和超类支持多态性,接口作为规范,允许多继承和回调机制;超类提供基类,实现代码重用和方法重写,两者共同促进代码的灵活性和可维护性。
25 10
|
4天前
|
Java
Java并发Futures和Callables类
Java程序`TestThread`演示了如何在多线程环境中使用`Futures`和`Callables`。它创建了一个单线程`ExecutorService`,然后提交两个`FactorialService`任务,分别计算10和20的阶乘。每个任务返回一个`Future`对象,通过`get`方法获取结果,该方法会阻塞直到计算完成。计算过程中模拟延迟以展示异步执行。最终,打印出10!和20!的结果。
|
SQL 缓存 监控
Java JUC 简介
本系列文章旨在介绍 Java 并发相关的知识,本文作为开篇主要介绍了 JDK 中常用的并发库(JUC)的使用方式, 后续的文章中我会自上而下地剖析了 JUC 中各个部门的实现原理,从直接下级框架 AbstractQueuedSynchronizer 也就是大家常说的 AQS,再到其中使用的 CAS, Wait,Park,最后到操作系统层面的 Mutex,Condition,希望通过这篇文章,大家能够对整个 Java 并发有一个清晰全面的认识,而且把这些内容串在一起你会发现它们本质上都是相通的。