JUC(java.util.concurrent)的常见类(多线程编程常用类)

简介: JUC(java.util.concurrent)的常见类(多线程编程常用类)

Callable接口

这个东西可以类比于之前见过的Runnable接口.两者的区别在于Runnable关注执行过程,不关注执行结果.Callable关注执行结果,它之中的call方法(类比于run方法)返回值就是线程执行任务的结果.Callable<V>里面的V期望线程的入口方法里,返回值是啥类型,此处的泛型参数就是啥类型.

Callable优势

示例:创建线程计算1+2+...+1000,使用Runnable版本

public class ThreadDemo7 {
    private static int sum = 0;
 
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                int result = 0;
                for(int i = 0; i <= 1000; i++) {
                    result += i;
                }
                sum = result;
            }
        });
        t.start();
        t.join();
 
        //主线程获取到计算结果
        //此处要想获取到结果,就需要专门搞一个成员变量保存上述的计算结果
        System.out.println("sum =" + sum);
    }
}

这么做虽然能够解决问题,但是代码不是很优雅,这时我们就希望依靠返回值来直接保存计算结果,

这就用到了Callable接口,使用流程如下:

1.创建一个匿名内部类,实现Callable接口.Callable带有泛型参数.泛型参数表示返回值的类型

2.重写Callable的call方法,完成累加的过程,直接通过返回值返回计算结果

3.把callable示例用FutureTask包装一下.

4.创建线程,线程的构造方法传入FutureTask.此时新线程就会执行FutureTask内部的Callable的call方法,完成计算.计算结果就放进了FutureTask对象中.

5.在主线程中调用futureTask.get()能够阻塞等待新线程计算完毕.并获取FutureTask中的结果.

代码如下:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadDemo8 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int result = 0;
                for(int i = 0; i <= 1000; i++) {
                    result += i;
                }
                return result;
            }
        };
 
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread t = new Thread(futureTask);
        t.start();
 
        //接下来这个代码也不需要join,使用futureTask获取到结果.
        //get()方法具有阻塞功能.如果线程不执行完毕,get就会阻塞
        //等到线程执行完了,return的结果,就会被get返回回来
        System.out.println(futureTask.get());
    }
}

可以看到,使用Callable和FutureTask之后,代码简化了很多,也不必手动写线程同步代码了.

理解Callable

Callable通常需要搭配FutureTask来使用.FutureTask用来保存Callable的返回结果.因为Callable往往是在另一个线程中执行的,什么时候执行完并不确定.

FutureTask就可以负责这个等待结果出来的工作.

理解FutureTask

FutureTask即未来的任务,既然这个任务是在未来执行完毕,最终取结果时就需要一张凭证.

可以想象成去吃麻辣烫.当餐点好后,后厨就开始做了.同时前台会给你一张小票.这个小票就是FutureTask.后面我们可以随时凭这张小票去查看自己的这份麻辣烫做出来没.

总结:创建线程的方式:1.继承Thread(包含匿名内部类).2.实现Runnable(包含匿名内部类).

3.基于lambda. 4.基于Callable. 5.基于线程池.

ReentrantLock

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

ReentrantLock也是可重入锁."Reentrant"这个单词的原意就是"可重入".

ReentrantLock的用法:

lock():加锁,如果获取不到锁就死等.

trylock(超时时间):加锁,如果获取不到锁,等待一定时间之后就放弃加锁.(此处通过trylock提供了更多的可操作空间)

unlock():解锁

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

ReentrantLock和synchronized的区别

通过上述解释,我们不免发现ReentrantLock和Synchronized非常相像,下面来说一说他们的区别:

1.synchronized是一个关键字,是JVM内部实现的(大概率是基于C++实现).ReentrantLock是标准库中的一个类,在JVM外实现的(基于Java实现).

2.synchronized使用时不需要手动释放锁.ReentrantLock使用时需要手动释放.使用起来更灵活,但是也容易遗漏unlock.

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

4.synchronized是非公平锁,ReentrantLock默认是非公平锁.可以通过一个构造方法传入一个true进入公平锁模式(原理:通过队列记录加锁线程的先后顺序).

//ReentrantLock的构造方法
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

5.搭配的等待通知机制是不同的

对于synchronize,搭配wait/notify

对于ReentrantLock,搭配Condition类,功能比wait,notify略强一些.

如何选择使用哪个锁

1.锁竞争不激烈时,使用synchronized,效率更高,自动释放更方便.

2.锁竞争激烈时,搭配trylock更灵活控制锁的行为,而不是死等

3.如果需要使用公平锁,使用ReentrantLock.

其实,一般情况下会使用synchronized即可.

信号量Semaphore

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

理解信号量(想象成一个更广义的锁)

可以把信号量想象成是停车场的展示牌:当前有车位100个.表示有100个可用资源.

当有车开进去的时候,就相当于申请(acquire)一个可用资源,可用车位就-1.(这称为信号量的P操作)

当有车开出来的时候,就相当于释放(release)一个可用资源,可用车位就+1.(这称为信号量的V操作)

如果计数器的值已经为0了,还尝试申请资源,就会堵塞等待,直到有其它线程释放资源.

Semaphore的PV操作中的加减计数器操作都是原子的,可以在多线程下直接使用.

所谓锁本质也是一种特殊的信号量.锁可以认为就是计数值为1的信号量,释放状态就是1,加锁状态就是0.对于这种非0即1的信号量.称为"二元信号量".

代码示例:

1.创建Semaphore示例,初始化为4,表示有4个可用资源.

2.acquire方法表示申请资源(P操作),release方法表示释放资源(V操作).

3.创建20个线程,每个线程都尝试申请资源,sleep一秒之后,释放资源,观察程序的执行效果.

import java.util.concurrent.Semaphore;
 
public class ThreadDemo9 {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(4);
 
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println("申请资源");
                    semaphore.acquire();
                    System.out.println("我获取到资源了");
                    Thread.sleep(1000);
                    System.out.println("我释放资源了");
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
 
        for(int i = 0; i < 10; i++) {
            Thread t = new Thread(runnable);
            t.start();
        }
    }
}

总结:如何保证线程安全

1.synchronized

2.Reentrantlock

3.CAS(原子类)

4.Semaphore (也可以用于实现生产者消费者模型:

定义两个信号量:一个用来表示队列中有多少个可以消费的元素sem1,另一个用于表示队列中有多少个可放置新元素的空间sem2.

生产:sem1.V(),sem2.P()

消费:sem1.P(),sem2.V()

CountDownLatch

同时等待N个任务执行结束.(多线程中执行一个任务,把大的任务分为几个部分,由每个线程分别执行).

就好像跑步比赛,10个选手依次就位,哨声响了才能同时出发;所有选手都通过终点,才能公布成绩.

1.构造CountDownLatch实例,初始化10表示有10个任务需要完成.

2.每个任务执行完毕,都调用latch.countDown().在CountDownLatch1内部的计数器同时自减

3.主线程中使用latch.await();阻塞等待所有任务执行完毕.相当于计数器为0了.

import java.util.Random;
import java.util.concurrent.CountDownLatch;
 
public class ThreadDemo10 {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(10);
        Runnable r = new Runnable() {
            @Override
            public void run() {
                Random random = new Random();
                int x = random.nextInt(5) + 1;
                try {
                    Thread.sleep(x * 1000);
                    latch.countDown();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        };
 
        for(int i = 0; i < 10; i++) {
            new Thread(r).start();
        }
 
        //必须等到所有线程全部结束
        latch.await();
        System.out.println("比赛结束");
    }
}

相关面试题

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.信号量听说过吗?都用于哪些场景下?

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

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

相关文章
|
4天前
|
安全 Java
java 中 i++ 到底是否线程安全?
本文通过实例探讨了 `i++` 在多线程环境下的线程安全性问题。首先,使用 100 个线程分别执行 10000 次 `i++` 操作,发现最终结果小于预期的 1000000,证明 `i++` 是线程不安全的。接着,介绍了两种解决方法:使用 `synchronized` 关键字加锁和使用 `AtomicInteger` 类。其中,`AtomicInteger` 通过 `CAS` 操作实现了高效的线程安全。最后,通过分析字节码和源码,解释了 `i++` 为何线程不安全以及 `AtomicInteger` 如何保证线程安全。
java 中 i++ 到底是否线程安全?
|
1天前
|
存储 安全 Java
java.util的Collections类
Collections 类位于 java.util 包下,提供了许多有用的对象和方法,来简化java中集合的创建、处理和多线程管理。掌握此类将非常有助于提升开发效率和维护代码的简洁性,同时对于程序的稳定性和安全性有大有帮助。
27 17
|
2天前
|
存储 安全 Java
如何保证 Java 类文件的安全性?
Java类文件的安全性可以通过多种方式保障,如使用数字签名验证类文件的完整性和来源,利用安全管理器和安全策略限制类文件的权限,以及通过加密技术保护类文件在传输过程中的安全。
|
5天前
|
缓存 Java 调度
Java中的多线程编程:从基础到实践
【10月更文挑战第24天】 本文旨在为读者提供一个关于Java多线程编程的全面指南。我们将从多线程的基本概念开始,逐步深入到Java中实现多线程的方法,包括继承Thread类、实现Runnable接口以及使用Executor框架。此外,我们还将探讨多线程编程中的常见问题和最佳实践,帮助读者在实际项目中更好地应用多线程技术。
11 3
|
5天前
|
缓存 安全 Java
Java中的多线程编程:从基础到实践
【10月更文挑战第24天】 本文将深入探讨Java中的多线程编程,包括其基本原理、实现方式以及常见问题。我们将从简单的线程创建开始,逐步深入了解线程的生命周期、同步机制、并发工具类等高级主题。通过实际案例和代码示例,帮助读者掌握多线程编程的核心概念和技术,提高程序的性能和可靠性。
8 2
|
5天前
|
Java
Java中的多线程编程:从基础到实践
本文深入探讨Java多线程编程,首先介绍多线程的基本概念和重要性,接着详细讲解如何在Java中创建和管理线程,最后通过实例演示多线程的实际应用。文章旨在帮助读者理解多线程的核心原理,掌握基本的多线程操作,并能够在实际项目中灵活运用多线程技术。
|
6天前
|
Java 程序员 开发者
Java编程中的异常处理艺术
【10月更文挑战第24天】在Java的世界里,代码就像一场精心编排的舞蹈,每一个动作都要精准无误。但就像最完美的舞者也可能踩错一个步伐一样,我们的程序偶尔也会遇到意外——这就是所谓的异常。本文将带你走进Java的异常处理机制,从基本的try-catch语句到高级的异常链追踪,让你学会如何优雅地处理这些不请自来的“客人”。
|
6天前
|
设计模式 SQL 安全
Java编程中的单例模式深入解析
【10月更文挑战第24天】在软件工程中,单例模式是设计模式的一种,它确保一个类只有一个实例,并提供一个全局访问点。本文将探讨如何在Java中使用单例模式,并分析其优缺点以及适用场景。
8 0
|
6天前
|
Java 开发者
Java中的多线程基础与应用
【10月更文挑战第24天】在Java的世界中,多线程是提高效率和实现并发处理的关键。本文将深入浅出地介绍如何在Java中创建和管理多线程,以及如何通过同步机制确保数据的安全性。我们将一起探索线程生命周期的奥秘,并通过实例学习如何优化多线程的性能。无论你是初学者还是有一定经验的开发者,这篇文章都将为你打开一扇通往高效编程的大门。
10 0
|
25天前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
38 1
C++ 多线程之初识多线程