【多线程】面试官:如何利用线程工具,防止多线程同时操作一个资源?

简介: 通过前面的学习,知道了线程的利与弊,正确的使用多线程,会尽最大的可能去压榨我们系统的资源,从而提高效率,但是如果不合理使用线程,可能会造成副作用,给系统带来更大的压力,进一步的思考,如何才能防止多线程操作一个资源?

前言

大家好,我是小郭,通过前面的学习,知道了线程的利与弊,正确的使用多线程,会尽最大的可能去压榨我们系统的资源,从而提高效率,但是如果不合理使用线程,可能会造成副作用,给系统带来更大的压力,进一步的思考,如何才能防止多线程操作一个资源?

有什么办法防止多线程同时操作一个资源

  1. volatile
  2. 原子类
  3. Synchronized和Lock
  4. Semaphore

我们主要来学习下 Semaphore

Semaphore是什么?

Semaphore被翻译为计数信号量,通常使用进行并发线程数量的限制,保证多个线程能够合理的使用资源。用大白话理解就是理解为红路灯。

官方的翻译:

计数信号量。 从概念上讲,信号量维护一组许可证。 如有必要,每个acquire块都将阻塞直到获得许可为止,然后再获取它。 每个release添加一个许可证,从而有可能释放阻塞的获取者。 但是,没有使用实际的许可对象。 Semaphore只是保持可用数量的计数并采取相应措施。

信号灯模型

  1. 计数器,初始化计数器数量
  2. 等待队列,用于排队,未拿到许可证的线程放入队列,拿到许可证,则移除队列

主要方法和核心参数

核心参数

  • int permits 允许并发线程数量
  • boolean fair 是否公平锁

构造方法

//默认是非公平锁,只要传入并发线程数量 
public Semaphore(int permits) { 
    sync = new NonfairSync(permits); 
} 
public Semaphore(int permits, boolean fair) { 
    sync = fair ? new FairSync(permits) : new NonfairSync(permits); 
}

构造方法中,主要做的就是初始化计数器,设置公平锁,第一个构造方法是默认的非公平锁,因为当等待队列中的线程有了空位,设置为非公平锁才能都来争抢提高效率,但是可能会造成某些线程饿死。 所以根据场景,我们进行公平锁的选择。

核心方法

从此信号量获取许可,先休眠,直到获得可用线程或者被中断

void acquire() throws InterruptedException

中断继续

void acquireUninterruptibly() 

从此信号量获取设定线程数许可,先休眠,直到获得可用线程或者被中断

void acquire(int permits) 

尝试获取许可,如果能够获取成功则立即返回true,否则,则返回false

boolean tryAcquire() 

和上面一样,设置了等待最长时间

boolean tryAcquire(long timeout, TimeUnit unit) 

释放许可

void release() 

返回当前可用的许可证数

int availablePermits() 

等待许可证数

int getQueueLength() 

返回正在等待线程的合集

Collection<Thread> getQueuedThreads()

原理

实现的核心还是AQS的共享模式

Sync extends AbstractQueuedSynchronizer

acquire()

//以共享模式获取,如果中断则中止.
public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    //1. 通过首先检查中断状态,中断返回异常
    if (Thread.interrupted())
        throw new InterruptedException();
    // 2. 以共享模式获取,获取到了锁,接下去,执行,没有就排队
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}
//非公平模式共享锁获取
protected int tryAcquireShared(int acquires) {
    for (;;) {
        //判断当前节点在同步队列中是否有前驱节点的判断,获取不到返回-1
        if (hasQueuedPredecessors())
            return -1;
        //Semaphore用AQS的state变量的值代表可用许可数    
        int available = getState();
        int remaining = available - acquires;
        //如果剩余许可数小于0或者CAS将当前可用许可数设置为剩余许可数成功,则返回成功许可数
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}
private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
        //加入等待队列
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        //自旋过程中的退出条件是是当前节点的前驱节点是头结点并且tryAcquireShared(arg)
        //返回值大于等于0即能成功获得同步状态
        for (;;) {
            //获取前驱节点
            final Node p = node.predecessor();
            if (p == head) {
                //争夺锁
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    //先把 head 给占了,然后唤醒队列中其他的线程
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        //失败的话,取消状态清除该节点
        if (failed)
            cancelAcquire(node);
    }
}
//设置head的值,完成初始化工作
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
        //利用CAS修改头部数据
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

实践

没有进行控制的代码

public static void main(String[] args) {
    ExecutorService executorService = Executors.newFixedThreadPool(5);
    IntStream.range(0,5).forEach(i -> executorService.submit(() ->{
        try {
            System.out.println(Thread.currentThread().getName() + "gogogo");
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + "正在操作");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "释放操作");
    }));
    executorService.shutdown();
}

输出结果

pool-1-thread-1gogogo
pool-1-thread-3gogogo
pool-1-thread-4gogogo
pool-1-thread-2gogogo
pool-1-thread-5gogogo
pool-1-thread-1正在操作
pool-1-thread-5正在操作
pool-1-thread-5释放操作
pool-1-thread-2正在操作
pool-1-thread-2释放操作
pool-1-thread-3正在操作
pool-1-thread-3释放操作
pool-1-thread-4正在操作
pool-1-thread-4释放操作
pool-1-thread-1释放操作

进行改造使用我们的并发工具Semaphore

输出结果

private static Semaphore semaphore = new Semaphore(1,false);
public static void main(String[] args) {
    ExecutorService executorService = Executors.newFixedThreadPool(5);
    IntStream.range(0,5).forEach(i -> executorService.submit(() ->{
        try {
            semaphore.acquire();
            System.out.println(Thread.currentThread().getName() + "gogogo");
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + "正在操作");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        semaphore.release();
        System.out.println(Thread.currentThread().getName() + "释放操作");
    }));
    executorService.shutdown();
}

输出结果

pool-1-thread-1gogogo
pool-1-thread-1正在操作
pool-1-thread-1释放操作
pool-1-thread-2gogogo
pool-1-thread-2正在操作
pool-1-thread-2释放操作
pool-1-thread-3gogogo
pool-1-thread-3正在操作
pool-1-thread-3释放操作
pool-1-thread-4gogogo
pool-1-thread-4正在操作
pool-1-thread-4释放操作
pool-1-thread-5gogogo
pool-1-thread-5正在操作
pool-1-thread-5释放操作

我们可以很明显的看出,Semaphore进行了并发线程的控制,每次只一个线程,等待获取许可才能够往下执行。

在流量控制的场景,是我们的优先选择。

总结

  1. 初始化Semaphore的令牌数量
  2. 调用acquire()方法启动Semaphore,对进来的线程,进行令牌颁发,线程会阻塞在acquire()上
  3. 执行完回调函数后,执行release(),释放令牌
  4. 如果计数器的值小于等于0,就会自动唤醒等待的线程 Semaphore主要利用了队列和计数器,来完成对线程的控制,从而防止过多线程同时操作一个资源,在实战的项目中,我们也可以利用计数器的思路,进行各种池化资源的设计。
相关文章
|
3月前
|
安全 算法 Java
Java 多线程:线程安全与同步控制的深度解析
本文介绍了 Java 多线程开发的关键技术,涵盖线程的创建与启动、线程安全问题及其解决方案,包括 synchronized 关键字、原子类和线程间通信机制。通过示例代码讲解了多线程编程中的常见问题与优化方法,帮助开发者提升程序性能与稳定性。
145 0
|
3月前
|
数据采集 监控 调度
干货分享“用 多线程 爬取数据”:单线程 + 协程的效率反超 3 倍,这才是 Python 异步的正确打开方式
在 Python 爬虫中,多线程因 GIL 和切换开销效率低下,而协程通过用户态调度实现高并发,大幅提升爬取效率。本文详解协程原理、实战对比多线程性能,并提供最佳实践,助你掌握异步爬虫核心技术。
|
4月前
|
Java 数据挖掘 调度
Java 多线程创建零基础入门新手指南:从零开始全面学习多线程创建方法
本文从零基础角度出发,深入浅出地讲解Java多线程的创建方式。内容涵盖继承`Thread`类、实现`Runnable`接口、使用`Callable`和`Future`接口以及线程池的创建与管理等核心知识点。通过代码示例与应用场景分析,帮助读者理解每种方式的特点及适用场景,理论结合实践,轻松掌握Java多线程编程 essentials。
251 5
|
8月前
|
Java 程序员 开发者
Java社招面试题:一个线程运行时发生异常会怎样?
大家好,我是小米。今天分享一个经典的 Java 面试题:线程运行时发生异常,程序会怎样处理?此问题考察 Java 线程和异常处理机制的理解。线程发生异常,默认会导致线程终止,但可以通过 try-catch 捕获并处理,避免影响其他线程。未捕获的异常可通过 Thread.UncaughtExceptionHandler 处理。线程池中的异常会被自动处理,不影响任务执行。希望这篇文章能帮助你深入理解 Java 线程异常处理机制,为面试做好准备。如果你觉得有帮助,欢迎收藏、转发!
497 14
|
存储 Java
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
|
11月前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
11月前
|
存储 缓存 算法
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
本文介绍了多线程环境下的几个关键概念,包括时间片、超线程、上下文切换及其影响因素,以及线程调度的两种方式——抢占式调度和协同式调度。文章还讨论了减少上下文切换次数以提高多线程程序效率的方法,如无锁并发编程、使用CAS算法等,并提出了合理的线程数量配置策略,以平衡CPU利用率和线程切换开销。
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
|
11月前
|
存储 缓存 Java
大厂面试必看!Java基本数据类型和包装类的那些坑
本文介绍了Java中的基本数据类型和包装类,包括整数类型、浮点数类型、字符类型和布尔类型。详细讲解了每种类型的特性和应用场景,并探讨了包装类的引入原因、装箱与拆箱机制以及缓存机制。最后总结了面试中常见的相关考点,帮助读者更好地理解和应对面试中的问题。
264 4
|
12月前
|
算法 Java 数据中心
探讨面试常见问题雪花算法、时钟回拨问题,java中优雅的实现方式
【10月更文挑战第2天】在大数据量系统中,分布式ID生成是一个关键问题。为了保证在分布式环境下生成的ID唯一、有序且高效,业界提出了多种解决方案,其中雪花算法(Snowflake Algorithm)是一种广泛应用的分布式ID生成算法。本文将详细介绍雪花算法的原理、实现及其处理时钟回拨问题的方法,并提供Java代码示例。
1255 2
|
12月前
|
JSON 安全 前端开发
第二次面试总结 - 宏汉科技 - Java后端开发
本文是作者对宏汉科技Java后端开发岗位的第二次面试总结,面试结果不理想,主要原因是Java基础知识掌握不牢固,文章详细列出了面试中被问到的技术问题及答案,包括字符串相关函数、抽象类与接口的区别、Java创建线程池的方式、回调函数、函数式接口、反射以及Java中的集合等。
152 0