【多线程】JUC的常见类,Callable接口,ReentranLock,Semaphore,CountDownLatch

简介: 【多线程】JUC的常见类,Callable接口,ReentranLock,Semaphore,CountDownLatch

JUC:java.util.concurrent

一、Callable 接⼝

接口 方法
Callable call,带有返回值
Runnable run,void
所以创建一个线程,希望它给你返回一个结果,那么使用 Callable 更加方便一些

比如,创建一个线程,让这个线程计算:1+2+3+4+…+1000=?

//使用Runnable接口
public class Demo {  
    private static int result;  
  
    public static void main(String[] args) throws InterruptedException {  
        Thread t = new Thread(() -> {  
            int sum = 0;  
            for (int i = 0; i < 1000; i++) {  
                sum += i;  
            }        
        });        
        t.start();  
        t.join();  
        
        System.out.println(“result=” + result);  
    }
}
  • 当前代码可以解决问题,但不够优雅,必须得引入一个成员变量 result

相比之下,Collable 就可以解决上述问题:

import java.util.concurrent.Callable;  
import java.util.concurrent.ExecutionException;  
import java.util.concurrent.FutureTask;  
  
public class Demo3 {  
    public static void main(String[] args) throws ExecutionException, InterruptedException {  
        Callable<Integer> callable = new Callable<Integer>() {  
            @Override  
            public Integer call() throws Exception {  
                int sum = 0;  
                for (int i = 0; i < 1000; i++) {  
                    sum += i;  
                }                
                return sum;  
            }        
        };        
        FutureTask<Integer> futureTask = new FutureTask<>(callable);  
        Thread t = new Thread(futureTask); //Thread  
        t.start();  
        //后续需要通过 futureTask 来拿到最终的结果  
        System.out.println(futureTask.get());  
    }
}
  • Thread 不能直接传入 callable 作为参数,需要传入实例化的 Callable 对象 callable 实例化一个 FutureTask 对象 futureTask,再将这个 futureTask 对象创建出线程
  • 并且后面要拿到 sum 的值,也需要通过 futureTask
  • futureTaskget操作是带有阻塞功能的
  • 当前 t 线程还没执行完,get 就会阻塞
  • 直到 t 线程执行完之后,get 才会返回

  • 比如说你去吃麻辣烫,夹好菜后,服务员会给你一个“号码牌”,方便你后续取餐。这里通过 FutureTask 实例化出的对象 futureTask 就相当于是号码牌,你通过 Callable 实例出的 callable 对象就相当于是你夹的菜。你是通过你夹的菜(callable)拿到号码牌(futureTask),最后你想要拿到菜,也得通过号码牌(futureTask)拿到

创建线程的方式:

  1. 直接继承 Thread
  2. 使用 Runnable
  3. 使用 Callable
  4. 使用 lambda
  5. 使用线程池

二、ReentrantLock

  • 可重入锁
  • synchronized 只是 Java 提供的一种加锁的方式
  • ReentrantLock 属于经典风格的锁,通过 lockunlock 方法来完成加锁和解锁
import java.util.concurrent.locks.ReentrantLock;
public class Demo {  
    private static int count = 0;  
  
    public static void main(String[] args) throws InterruptedException {  
        ReentrantLock locker = new ReentrantLock();  
  
        Thread t1 = new Thread(() -> {  
            for (int i = 0; i < 50000; i++) {  
                locker.lock();  
                count++;  
                locker.unlock();  
            }        
        });        
        Thread t2 = new Thread(() -> {  
            for (int i = 0; i < 50000; i++) {  
                locker.lock();  
                count++;  
                locker.unlock();  
            }        
        });        
        t1.start();  
        t2.start();  
        t1.join();  
        t2.join();  
        System.out.println("count=" + count);  
    }
}

synchronized 和 ReentrantLock 的差异

实际开发中,大多数情况下,使用 synchronized 即可,但 ReentrantLock 相比于 synchronized 还是有一些差异的

  1. synchronized 属于关键字(底层是通过 JVMC++代码实现的)
    ReentrantLock 测试标准库提供的类,通过 Java 代码实现的
  2. synchronized 通过代码块控制加锁解锁
    ReentrantLock 通过调用 lock/unlock 方法来完成,unlock 可能会遗漏,要确保能执行到 unlock(通常会把 unlock 放在 finally 中)
  3. ReentrantLock提供了tryLock这样的加锁风格(重点
  • 前面介绍的加锁,都是发现锁被别人占用了,就阻塞等待
  • tryLock 在加锁失败的时候,不会阻塞,而是直接返回,并且通过返回值来反馈是加锁成功还是失败
  • 坚持不一定成功,但是放弃了一定轻松~(摆烂态度)
  1. ReentrantLock还提供了公平锁的实现(重点
  • 它默认是非公平的,但可以在构造方法中,将参数设为 true,将其设为公平锁
  1. ReentrantLock还提供了功能更强的“等待通知机制”
  • 基于 Condition 类,功能要比 wait/notify 更强一些

三、信号量 Semaphore

也称为“信号灯”(开船的水手,旗语)

你去车库停车,如何知道是否还有空位?

现在的停车场,一般的入口处,就会有一个“电子牌”,会显示有多少个空闲车位

  • 有车开进去了,上述的计数 -1
  • 有车开出来了,上述的计数 +1
  • 如果计数为 0 了,说明没空位了

这里的“电子牌”就像是一个“信号量”,信号量是一个计数器,通过计数器衡量“可用资源”的个数

  • 申请资源(acquire):让计数器 -1(“P”操作
  • 释放资源(release):让计数器 +1(“V”操作
  • 如果计数器为 0 了,继续申请,就会出现阻塞
    所以信号量的操作也称为“PV 操作

操作系统本身提供了信号量实现,JVM 把操作系统的信号量封装了一下,我们直接使用就可以了

import java.util.concurrent.Semaphore;  
  
public class Demo5 {  
    public static void main(String[] args) throws InterruptedException {  
        //参数为可用资源的个数,计数器的初始值  
        Semaphore semaphore = new Semaphore(3);  
  
        semaphore.acquire();  
        System.out.println("申请一个资源1");  
        semaphore.acquire();  
        System.out.println("申请一个资源2");  
        semaphore.acquire();  
        System.out.println("申请一个资源3");  
        semaphore.acquire();  
        System.out.println("申请一个资源4");  
    }
}
//运行结果
申请一个资源1
申请一个资源2
申请一个资源3
  • 初始化的信号量为 3
  • 申请了三个资源后,没空位了,计数器为 0 了,就堵塞住了

若在这里面释放一次资源,就可以将资源 4 申请进去:

import java.util.concurrent.Semaphore;  
  
public class Demo5 {  
    public static void main(String[] args) throws InterruptedException {  
        //参数为可用资源的个数,计数器的初始值  
        Semaphore semaphore = new Semaphore(3);  
  
        semaphore.acquire();  
        System.out.println("申请一个资源1");  
        semaphore.acquire();  
        System.out.println("申请一个资源2");  
        semaphore.acquire();  
        System.out.println("申请一个资源3");  
        semaphore.release();  
        System.out.println("释放一个资源");  
        semaphore.acquire();  
        System.out.println("申请一个资源4");  
    }
}
//运行结果
申请一个资源1
申请一个资源2
申请一个资源3
释放一个资源
申请一个资源4

平替加锁解锁

  • 在需要加锁的时候,可以设置一个信号量,初始化一个资源
  • 谁要用的时候就申请这个资源,用完之后再释放
  • 这样的话,申请到唯一资源的线程执行操作的时候,就不会有其他的线程进行操作了
import java.util.concurrent.Semaphore;  
  
public class Demo4 {  
    private static int count = 0;  
  
    public static void main(String[] args) throws InterruptedException {  
        Semaphore semaphore = new Semaphore(1);  
        Thread t1 = new Thread(() -> {  
            for (int i = 0; i < 50000; i++) {  
                try {  
                    semaphore.acquire();  
                } catch (InterruptedException e) {  
                    throw new RuntimeException(e);  
                }                
                count++;  
                semaphore.release();  
            }        
        });        
        Thread t2 = new Thread(() -> {  
            for (int i = 0; i < 50000; i++) {  
                try {  
                    semaphore.acquire();  
                } catch (InterruptedException e) {  
                    throw new RuntimeException(e);  
                }                
                count++;  
                semaphore.release();  
            }        
        });        
        t1.start();  
        t2.start();  
        t1.join();  
        t2.join();  
        System.out.println("count=" + count);  
    }
}
  • 这种值为 1 的信号量,就相当一个锁,资源要么是 1,要么是 0,所以也称为“二元信号量

四、CountDownLatch

“锁存器”

很多时候,需要把一个大的任务,拆成多个小的任务,通过多线程/线程池来执行。如何衡量,所有的任务都执行完毕了?

  • 比如“多线程下载”
  • 浏览器的下载,一般是单线程的,下载速度是有限的(一秒 2-3 MB)
  • 但是可以通过多线程的方式提高下载速度
    就可以使用专门的下载工具,通过多个线程,和服务器建立多个网络连接(服务器进行网速限制都是针对一个连接做出的限制),那如果我创建 10-20 个线程,那么下载的总速度就能大幅度提高
  • 多个线程进行一起操作,每个线程下载一部分,所有线程下载完毕再进行拼装
import java.util.concurrent.CountDownLatch;  
import java.util.concurrent.ExecutorService;  
import java.util.concurrent.Executors;  
  
public class Demo6 {  
    public static void main(String[] args) throws InterruptedException {  
        ExecutorService executorService = Executors.newFixedThreadPool(4);  
  
        //构造方法的个数,就是拆分出来的任务数量  
        CountDownLatch countDownLatch = new CountDownLatch(20);  
  
        for (int i = 0; i < 20; i++) {  
            int id = i;  
            executorService.submit(() -> {  
                System.out.println("下载任务" + id + "开始执行");  
                try {  
                    Thread.sleep(3000);  
                } catch (InterruptedException e) {  
                    throw new RuntimeException(e);  
                }                
                System.out.println("下载任务" + id + "结束执行");  
                //完毕 over!  
                countDownLatch.countDown();  
            });        
        }        
        // 当 countDownLatch 收到了 20 个“完成”,所有的任务就都完成了  
        // await => all wait  
        // await 这个词是计算机术语,“等待所有”  
        countDownLatch.await();  
  
        System.out.println("所有任务都完成");  
    }
}
  • CountDownLatch 一般都是结合线程池进行使用
  • 借助 CountDownLatch 就能衡量出当前任务是否整体执行结束

上面这些再实际开发中用的布套多,但面试可能问到,特别是“ReentrantLock”和“Semaphore”

五、相关面试题




相关文章
|
13天前
|
安全 Java API
java如何请求接口然后终止某个线程
通过本文的介绍,您应该能够理解如何在Java中请求接口并根据返回结果终止某个线程。合理使用标志位或 `interrupt`方法可以确保线程的安全终止,而处理好网络请求中的各种异常情况,可以提高程序的稳定性和可靠性。
44 6
|
1月前
|
Java
java线程接口
Thread的构造方法创建对象的时候传入了Runnable接口的对象 ,Runnable接口对象重写run方法相当于指定线程任务,创建线程的时候绑定了该线程对象要干的任务。 Runnable的对象称之为:线程任务对象 不是线程对象 必须要交给Thread线程对象。 通过Thread的构造方法, 就可以把任务对象Runnable,绑定到Thread对象中, 将来执行start方法,就会自动执行Runable实现类对象中的run里面的内容。
42 1
|
1月前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
48 4
|
1月前
|
安全 Java
在 Java 中使用实现 Runnable 接口的方式创建线程
【10月更文挑战第22天】通过以上内容的介绍,相信你已经对在 Java 中如何使用实现 Runnable 接口的方式创建线程有了更深入的了解。在实际应用中,需要根据具体的需求和场景,合理选择线程创建方式,并注意线程安全、同步、通信等相关问题,以确保程序的正确性和稳定性。
121 11
|
2月前
|
Java
在Java多线程编程中,实现Runnable接口通常优于继承Thread类
【10月更文挑战第20天】在Java多线程编程中,实现Runnable接口通常优于继承Thread类。原因包括:1) Java只支持单继承,实现接口不受此限制;2) Runnable接口便于代码复用和线程池管理;3) 分离任务与线程,提高灵活性。因此,实现Runnable接口是更佳选择。
57 2
|
2月前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
41 2
|
1月前
|
Java
为什么一般采用实现Runnable接口创建线程?
因为使用实现Runnable接口的同时我们也能够继承其他类,并且可以拥有多个实现类,那么我们在拥有了Runable方法的同时也可以使用父类的方法;而在Java中,一个类只能继承一个父类,那么在继承了Thread类后我们就不能再继承其他类了。
28 0
|
7月前
|
存储 Java
高并发编程之多线程锁和Callable&Future 接口
高并发编程之多线程锁和Callable&Future 接口
89 1
|
4月前
|
并行计算 Java 大数据
Callable和Future
Callable和Future
|
7月前
|
Java
Java并发编程:理解并使用Future和Callable接口
【2月更文挑战第25天】 在Java中,多线程编程是一个重要的概念,它允许我们同时执行多个任务。然而,有时候我们需要等待一个或多个线程完成,然后才能继续执行其他任务。这就需要使用到Future和Callable接口。本文将深入探讨这两个接口的用法,以及它们如何帮助我们更好地管理多线程。