【Java|多线程与高并发】JUC中常用的类和接口

简介: JUC是Java并发编程中的一个重要模块,全称为Java Util Concurrent(Java并发工具包),它提供了一组用于多线程编程的工具类和框架,帮助开发者更方便地编写线程安全的并发代码。

1. JUC是什么

JUC是Java并发编程中的一个重要模块,全称为Java Util Concurrent(Java并发工具包),它提供了一组用于多线程编程的工具类和框架,帮助开发者更方便地编写线程安全的并发代码。


本文主要介绍Java Util Concurrent下的一些常用接口和类


2. Callable接口

Callable接口类似于Runnable. 有一点区别就是Runable描述的任务没有返回值,而Callable接口是带有返回值的


示例:


Callable<返回值类型> callable = new Callable<Integer>() {
    @Override
    public 返回值类型 call() throws Exception {
       // 执行的任务      
    }
};

Callable接口定义了一个call()方法,因此在创建实例的时要实现这个方法. 该方法在任务执行完成后返回一个结果,并且可以抛出异常。


与Runnable不同,Callable描述的任务不能直接传给线程去执行. 因此需要借助FutureTask<T>这个类


FutureTask<返回值类型> futureTask = new FutureTask<>(callable);

获取上述任务的返回值可以使用 FuturTask提供的get方法.


示例:

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int ret = 0;
                for (int i = 1; i <= 10; i++) {
                    ret += i;
                }
                return ret;
            }
        };
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread t1 = new Thread(futureTask);
        t1.start();
        System.out.println(futureTask.get());
    }

运行结果:


e0e796a155344deebe004341c51ddd2d.png

3. ReentrantLock

ReentrantLock:ReentrantLock是Lock接口的一个实现类,它实现了Lock接口的所有方法。ReentrantLock支持重入性,也就是说同一个线程可以多次获取同一个锁,而不会产生死锁。这种特性使得ReentrantLock可以用于更复杂的线程同步场景。


在ReentrantLock中,有三个十分重要的方法:


1.lock():加锁

2.unlock():解锁.

3.tryLock(): 用于尝试获取锁,如果锁是可用的,就立即获取并返回true,如果锁不可用,就立即返回false,而不会阻塞当前线程。还可以指定获取锁的最大等待时间.

与synchronized不同,它的加锁和解锁操作时分开的,需要自己去添加.


这也可能会导致如果在加锁之后,代码出现异常,则有可能执行不到unlock方法.这也是ReentranLock的一个小弊端.但我们可以通过使用try finally来避免.


tryLock方法有两个版本:


cba26b7b082d4720b05cd60fccec45ee.png


无参的tryLock()方法用于尝试获取锁,如果锁是可用的,就立即获取并返回true,如果锁不可用,就立即返回false,而不会阻塞当前线程。


而另一个版本的tryLock()方法,可以指定超时时间来尝试获取锁


在实际开发中, 使用这种"死等的策略"往往要慎重,tryLock()让我们面对这种情况有更多的选择


ReentrantLock可以实现公平锁. 默认是非公平的.


但当我们创建实例时,传入参数true时.就变成公平锁了


ReentrantLock reentrantLock = new ReentrantLock(true);

synchronize搭配wait/notify方法来实现线程的等待通知的,唤醒的线程是随机的


ReentrantLock搭配Condition类实现线程等待通知的.可以指定线程来进行唤醒


synchronized是Java中的关键字,底层是JVM实现的(C++)


ReentranLock 是标准库的一个类,底层是基于Java实现的


4. 原子类

原子类是为了解决多线程环境下的竞态条件(Race Condition)和数据不一致的问题。在多线程环境下,如果多个线程同时对一个共享变量进行读取和写入操作,可能会导致数据的不一致性,从而产生错误的结果。


原子类是基于CAS实现的


Java提供了多种原子类,常用的原子类有以下四个:


1.AtomicInteger:用于对int类型的变量进行原子操作。

2.AtomicLong:用于对long类型的变量进行原子操作。

3.AtomicBoolean:用于对boolean类型的变量进行原子操作。

4.AtomicReference:用于对引用类型的变量进行原子操作。

接下来使用原子类AtomicInteger来实现两个线程针对同一个变量自增50000次的操作.


因为是类的实例对象,我们不能直接对类的实例对象进行++操作. 只能借助类提供的一些方法


AtomicInteger的一些方法:

AtomicInteger atomicInteger = new AtomicInteger();
// atomicInteger++
atomicInteger.getAndIncrement();
// ++atomicInteger
atomicInteger.incrementAndGet();
// atomicInteger--
atomicInteger.getAndDecrement();
// --atomicInteger
atomicInteger.decrementAndGet();
public class Demo23 {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger atomicInteger = new AtomicInteger();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                atomicInteger.getAndIncrement();
            }
        });
        Thread t2 = new Thread(() ->{
            for (int i = 0; i < 50000; i++) {
                atomicInteger.getAndIncrement();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(atomicInteger);
    }
}

运行结果:


fd85cd57bcbc46b7b300f87d51fbe222.png

如果不用原子类,就需要使用synchronized来实现.


5. 线程池

线程池在我之前的文章中详细介绍过,这里就不再这里进行赘述了. 感兴趣的小伙伴可以看这篇文章: 【Java|多线程与高并发】线程池详解


6. 信号量

信号量(Semaphore)维护了一个许可计数器,表示可用的许可数量。当一个线程需要访问共享资源时,它必须先获取一个许可,如果许可数量为0,则线程将被阻塞,直到有可用的许可。当线程使用完共享资源后,它必须释放许可,以便其他线程可以获取许可并访问资源。


信号量的许可数量可以在创建信号量实例时进行设置

// 设置信号量的许可数量为 5
Semaphore semaphore = new Semaphore(5);

信号量中提供了两个主要操作:P(等待)和V(释放)。


P操作: 会尝试获取一个信号量的许可,如果许可数量不为0,则可以成功获取许可并继续执行;如果许可数量为0,则线程将被阻塞,直到有其他线程释放许可为止。


V操作: 会释放一个信号量的许可,使得其他被阻塞的线程可以获取许可并继续执行。


P操作对应的方法为acquire()


V操作对应的方法为release()


例如:

public class Demo24 {
    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(5);
        semaphore.acquire();
        semaphore.acquire();
        semaphore.acquire();
        semaphore.acquire();
        semaphore.acquire();
        System.out.println("此时信号量的许可数量为0");
        semaphore.acquire();
        semaphore.release();
    }
}

运行结果:

c81089912b6747aa9747acf35de28d51.png



信号量可以通过控制许可的数量,可以限制同时访问共享资源的线程数量,从而避免竞争条件和数据不一致性。


7. CoutDownLatch

CountDownLatch(倒计时门闩)是Java并发编程中的一种同步工具,用于等待一组线程完成某个任务。


通过CountDownLatch的构造方法,指定等待线程的数量(计数器).

// 设置等待线程的数量为 5
CountDownLatch countDownLatch = new CountDownLatch(5);


当一个线程完成了自己的任务后,可以调用CountDownLatch的countDown()方法将计数器减1。其他线程可以通过调用CountDownLatch的await()方法来等待计数器变为0。


示例:

public class Demo25 {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(5);
        for (int i = 0;i < 5;i++){
            Thread t = new Thread(() ->{
                System.out.println(Thread.currentThread().getName()+" 执行任务");
                countDownLatch.countDown();
            });
            t.start();
        }
        countDownLatch.await();
    }
}

指定CoutDownLatch等待线程的数量为5,并创建5个线程. 线程执行完后执行countDown()方法. 并调用await()等待计数器变为0.


运行结果:


c338eb363b2046c4a58aac1ef0ff8564.png



如果计数器的初始值大于等于等待的线程数量,会进入阻塞等待状态。


更改计数器的值为6,运行结果:


44f3957a69b546f384880d059e92eb1d.png


为了避免上述情况,可以使用await的一个重载版本来设置最大等待时间


ec33ffad1e58458aadbdd19a3cb551cf.png


8. 线程安全的集合类


1.Hashtable和ConcurrentHashMap:线程安全的哈希表实现,支持高并发的读写操作。


2.CopyOnWriteArrayList:线程安全的动态数组实现,适用于读多写少的场景


3.CopyOnWriteArraySet:线程安全的集合实现,基于CopyOnWriteArrayList,适用于读多写少的场景。


4.ConcurrentLinkedQueue:线程安全的无界队列实现,支持高并发的入队和出队操作。


5.BlockingQueue接口的实现类有: ArrayBlockingQueue、LinkedBlockingQueue、LinkedTransferQueue等,用于实现线程安全的阻塞队列。


6.ConcurrentSkipListMap:线程安全的跳表实现的有序映射表,支持高并发的读写操作。


7.ConcurrentSkipListSet:线程安全的跳表实现的有序集合,支持高并发的读写操作。


对于Hashtable和ConcurrentHashMap:

Hashtable并不建议使用. 它是用synchronized修饰方法.相当于对this进行加锁. 一个哈希表只有一个锁.

推荐使用ConcurrentHashMap. 这个类背后做了很多优化策略.


ConcurrentHashMap是给每个哈希桶进行加锁.


当两个线程访问同一个哈希桶,才会有冲突. 如果不是同一个哈希桶,就没有锁冲突.因此大大降级了锁冲突的概率


ConcurrentHashMap只给写操作加锁,读操作不加锁.


当多个线程同时进行写操作才会有锁冲突,同时进行读操作并不会有锁冲突. 当有的线程在写,有的线程在读.也不存在线程安全问题. ConcurrentHashMap保证读到的数据不会是写了一半的,要么是写之前的,要么就是写之后的.


ConcurrentHashMap充分使用了CAS的特性. 内部有很多使用到CAS的地方,而不是直接加锁


ConcurrentHashMap对扩容操作进行了特殊优化.


在扩容过程中,旧的哈希表和新的哈希表会同时存在一段时间.每次进行哈希表操作的操作,都会把旧的哈希表中的元素搬运一部分,直到搬运完成. 避免了扩容时间过长,造成卡顿的情况


HashMap,Hashtable和ConcurrentHashMap的区别,这也是一个常见面试题.


回答这个问题. 可以从线程安全方面,HashMap是线程不安全的.Hashtable和ConcurrentHashMap是线程安全的,然后回答Hashtable和ConcurrentHashMap的区别. ConcurrentHashMap与Hashtable相比做了哪些改进等.


CopyOnWriteArrayList适用于读多写少的场景.

一般情况下,如果有的线程在进行写作操(修改),优点线程在读,很可能会读到修改了一半的数据.因此CopyOnWriteArrayList为了解决这个问题,就会把原来的数据复制一份,写操作就会在这个拷贝的数据上进行


但如果数据特别多/修改特别频繁,就不适合使用了


感谢你的观看!希望这篇文章能帮到你!

专栏: 《从零开始的Java学习之旅》在不断更新中,欢迎订阅!

“愿与君共勉,携手共进!”



相关文章
|
1天前
|
消息中间件 Java Linux
2024年最全BATJ真题突击:Java基础+JVM+分布式高并发+网络编程+Linux(1),2024年最新意外的惊喜
2024年最全BATJ真题突击:Java基础+JVM+分布式高并发+网络编程+Linux(1),2024年最新意外的惊喜
|
1天前
|
安全 Java 程序员
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第20天】本文将深入探讨Java并发编程的核心概念,包括线程安全和性能优化。我们将详细解析synchronized关键字、ReentrantLock类以及java.util.concurrent包中的高级工具类,如Semaphore、CountDownLatch和CyclicBarrier等。通过实例演示如何使用这些工具来提高多线程程序的性能和可靠性。
|
1天前
|
安全 算法 Java
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第20天】 在多核处理器日益普及的今天,并发编程成为了软件开发中不可忽视的重要话题。Java语言提供了丰富的并发工具和机制来帮助开发者构建高效且线程安全的应用程序。本文将探讨Java并发的核心概念,包括线程同步、锁机制、以及如何通过这些工具实现性能优化。我们将透过实例分析,揭示并发编程中的常见问题,并展示如何利用现代Java API来解决这些问题。
|
1天前
|
安全 Java 开发者
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第20天】在Java并发编程中,线程安全和性能优化是两个关键要素。本文将深入探讨Java并发编程的基本概念、线程安全的实现方法以及性能优化技巧。通过分析同步机制、锁优化、无锁数据结构和并发工具类的使用,我们将了解如何在保证线程安全的前提下,提高程序的性能。
|
1天前
|
安全 Java 编译器
Java一分钟之——泛型方法与泛型接口
【5月更文挑战第20天】Java泛型提供编译时类型安全检查,提升代码重用和灵活性。本文探讨泛型方法和接口的核心概念、常见问题和避免策略。泛型方法允许处理多种数据类型,而泛型接口需在实现时指定具体类型。注意类型擦除、误用原始类型和泛型边界的理解。通过明确指定类型参数、利用通配符和理解类型擦除来避免问题。泛型接口要精确指定类型参数,适度约束,利用默认方法。示例代码展示了泛型方法和接口的使用。
26 1
Java一分钟之——泛型方法与泛型接口
|
1天前
|
安全 算法 Java
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第20天】 在Java开发中,正确处理并发问题对于确保应用的稳定性和提高性能至关重要。本文将深入探讨Java并发编程的核心概念——线程安全,以及如何通过各种技术和策略实现它,同时保持甚至提升系统性能。我们将分析并发问题的根源,包括共享资源的竞争条件、死锁以及线程活性问题,并探索解决方案如同步机制、锁优化、无锁数据结构和并发工具类等。文章旨在为开发者提供一个清晰的指南,帮助他们在编写多线程应用时做出明智的决策,确保应用的高效和稳定运行。
|
1天前
|
安全 Java
Java中的多线程编程:概念、实现及性能优化
【5月更文挑战第20天】在计算机科学中,多线程是一种允许程序同时执行多个任务的技术。Java作为一种广泛使用的编程语言,提供了对多线程编程的支持。本文将介绍Java中多线程的基本概念、实现方法以及性能优化策略,帮助读者更好地理解和应用多线程技术。
|
2天前
|
并行计算 Java API
Java 8中的接口默认方法和静态方法以及并行数组
【5月更文挑战第19天】Java 8引入了许多新特性,其中包括接口的默认方法和静态方法,以及并行数组的能力。这些特性增强了Java的面向对象编程模型和数组处理能力。让我们深入了解它们的概念和实践。
20 2
|
7月前
|
设计模式 缓存 Java
Java多线程线程池:提升应用性能的终极利器
Java多线程线程池:提升应用性能的终极利器
575 0
|
6天前
|
Java 数据库
【Java多线程】对线程池的理解并模拟实现线程池
【Java多线程】对线程池的理解并模拟实现线程池
18 1

热门文章

最新文章