java并发编程学习:用 Semaphore (信号量)控制并发资源

简介: 并发编程这方面以前关注得比较少,恶补一下,推荐一个好的网站:并发编程网 - ifeve.com,上面全是各种大牛原创或编译的并发编程文章。 今天先来学习Semaphore(信号量),字面上看,根本不知道这东西是干啥的,借用 并发工具类(三)控制并发线程数的Semaphore一文中的交通红绿信号灯的...

并发编程这方面以前关注得比较少,恶补一下,推荐一个好的网站:并发编程网 - ifeve.com,上面全是各种大牛原创或编译的并发编程文章。

今天先来学习Semaphore(信号量),字面上看,根本不知道这东西是干啥的,借用 并发工具类(三)控制并发线程数的Semaphore一文中的交通红绿信号灯的例子来理解一下:

一条4车道的主干道,假设100米长,每辆车假设占用的长度为10米(考虑到前后车距),也就是说这条道上满负载运行的话,最多只能容纳4*(100/10)=40辆车,如果有120辆车要通过的话(为简单起见,一波40辆,分成3波),就必须要红绿信号灯来调度了,对于最前面的一波来讲,它们看到的是绿灯,允许通过,第一波全进入道路后,红绿灯变成红色,表示后面的2波,要停下来等候第1波车辆全通过,然后红绿灯才会变成绿色,让第2波通过,如此运转下去....

 

这跟多线程并发有啥关系呢?Semaphore就是红绿信号灯,3波车辆就是3个并发的线程,而主干道就是多个线程要并发访问的公用资源,由于资源有限,所以必须通过Semaphore来控制线程对资源的访问,否则就变成资源竞争,严重的话会导致死锁等问题。

 

下面用一个示例演示,假设有N个并发线程都要打印文件,但是打印机只有1台,先来一个打印队列类:

package yjmyzz.lesson01;

import java.util.concurrent.Semaphore;

public class PrintQueue {

    private final Semaphore semaphore;

    public PrintQueue() {
        semaphore = new Semaphore(1);//限定了共享资源只能有1个(相当于只有一把钥匙)
    }

    public void printJob(Object document) {
        try {
            semaphore.acquire();//取得对共享资源的访问权(即拿到了钥匙))

            long duration = (long) (1 + Math.random() * 10);
            System.out.printf("%s: PrintQueue: Printing a Job during %d seconds\n", Thread.currentThread().getName(), duration);
            Thread.sleep(duration);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            semaphore.release();//钥匙用完了,要还回去,这样其它线程才能继续有序的拿到钥匙,访问资源
        }
    }
}

由于是在多线程环境中,真正运行的作业处理,得继承自Runnable(或Callable)

package yjmyzz.lesson01;

public class Job implements Runnable {

    private PrintQueue printQueue;

    public Job(PrintQueue printQueue) {
        this.printQueue = printQueue;
    }

    public void run() {
        System.out.printf("%s: Going to print a job\n", Thread.currentThread().getName());
        printQueue.printJob(new Object());
        System.out.printf("%s: The document has been printed\n", Thread.currentThread().getName());
    }
}

好了,测试一把:

package yjmyzz.lesson01;

public class Main {
    public static void main(String args[]) {

        PrintQueue printQueue = new PrintQueue();

        int threadCount = 3;

        Thread thread[] = new Thread[threadCount];
        for (int i = 0; i < threadCount; i++) {
            thread[i] = new Thread(new Job(printQueue), "Thread" + i);
        }

        for (int i = 0; i < threadCount; i++) {
            thread[i].start();
        }
    }
}

输出:

Thread0: Going to print a job
Thread2: Going to print a job
Thread1: Going to print a job
Thread0: PrintQueue: Printing a Job during 7 seconds
Thread0: The document has been printed
Thread2: PrintQueue: Printing a Job during 5 seconds
Thread2: The document has been printed
Thread1: PrintQueue: Printing a Job during 1 seconds
Thread1: The document has been printed

从输出上看,线程0打印完成后,线程2才开始打印,然后才是线程1,没有出现一哄而上,抢占打印机的情况。这样可能没啥感觉,我们把PrintQueue如果去掉Semaphore的部分,变成下面这样:

package yjmyzz.lesson01;

public class PrintQueue {

    //private final Semaphore semaphore;

    public PrintQueue() {
        //semaphore = new Semaphore(1);//限定了共享资源只能有1个(相当于只有一把钥匙)
    }

    public void printJob(Object document) {
        try {
            //semaphore.acquire();//取得对共享资源的访问权(即拿到了钥匙))
            long duration = (long) (1 + Math.random() * 10);
            System.out.printf("%s: PrintQueue: Printing a Job during %d seconds\n", Thread.currentThread().getName(), duration);
            Thread.sleep(duration);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //semaphore.release();//钥匙用完了,要还回去,这样其它线程才能继续有序的拿到钥匙,访问资源
        }
    }
}

这回的输出:

Thread0: Going to print a job
Thread2: Going to print a job
Thread1: Going to print a job
Thread2: PrintQueue: Printing a Job during 4 seconds
Thread1: PrintQueue: Printing a Job during 8 seconds
Thread0: PrintQueue: Printing a Job during 1 seconds
Thread0: The document has been printed
Thread2: The document has been printed
Thread1: The document has been printed

可以发现,3个线程全都一拥而上,同时开始打印,也不管打印机是否空闲,实际应用中,这样必然出问题。

 

好的,继续,突然有一天,公司有钱了,又买了2台打印机,这样就有3台打印机了,这时候怎么办呢?简单的把PrintQueue构造器中的

    public PrintQueue() {
        semaphore = new Semaphore(3);
    }

就行了吗?仔细想想,就会发现问题,代码中并没有哪里能告诉线程哪个打印机正在打印,哪个打印机当前空闲,所以仍然有可能出现N个线程(N<=3)同时抢一台打印机的情况(即:如果把控制权当成钥匙的话,相当于有可能3个人各领取到了1把钥匙,但是这3把钥匙是相同的,3个人都看中了同一个箱子,都要用手中的钥匙去抢着开箱)。

所以得改进一下:

package yjmyzz.lesson02;

import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class PrintQueue {

    private boolean freePrinters[];//用来存放打印机的状态,true表示空闲,false表示正在打印

    private Lock lockPrinters;//增加了锁,保证多个线程,只能获取得锁,才能查询哪台打印机空闲的

    private final Semaphore semaphore;


    public PrintQueue() {
        int printerNum = 3;//假设有3台打印机
        semaphore = new Semaphore(printerNum);
        freePrinters = new boolean[printerNum];

        for (int i = 0; i < printerNum; i++) {
            freePrinters[i] = true;//初始化时,默认所有打印机都空闲
        }
        lockPrinters = new ReentrantLock();
    }


    private int getPrinter() {
        int ret = -1;
        try {
            lockPrinters.lock();//先加锁,保证1次只能有1个线程来获取空闲的打印机
            for (int i = 0; i < freePrinters.length; i++) {
                //遍历所有打印机的状态,发现有第1个空闲的打印机后,领取号码,
                // 并设置该打印机为繁忙状态(因为马上就要用它)
                if (freePrinters[i]) {
                    ret = i;
                    freePrinters[i] = false;
                    break;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //最后别忘记了解锁,这样后面的线程才能上来领号
            lockPrinters.unlock();
        }
        return ret;
    }

    public void printJob(Object document) {
        try {
            semaphore.acquire();

            int assignedPrinter = getPrinter();//领号
            long duration = (long) (1 + Math.random() * 10);
            System.out.printf("%s: PrintQueue: Printing a Job in Printer%d during %d seconds\n", Thread.currentThread().getName(),
                    assignedPrinter, duration);
            Thread.sleep(duration);
            freePrinters[assignedPrinter] = true;//打印完以后,将该打印机重新恢复为空闲状态

        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            semaphore.release();
        }
    }
}

测试一下,这回把线程数增加到5,输出结果类似下面这样:

Thread0: Going to print a job
Thread4: Going to print a job
Thread3: Going to print a job
Thread2: Going to print a job
Thread1: Going to print a job
Thread4: PrintQueue: Printing a Job in Printer1 during 7 seconds
Thread0: PrintQueue: Printing a Job in Printer0 during 4 seconds
Thread3: PrintQueue: Printing a Job in Printer2 during 8 seconds
Thread0: The document has been printed
Thread2: PrintQueue: Printing a Job in Printer0 during 1 seconds
Thread2: The document has been printed
Thread4: The document has been printed
Thread1: PrintQueue: Printing a Job in Printer0 during 1 seconds
Thread3: The document has been printed
Thread1: The document has been printed

从输出结果可以看出,一次最多只能有3个线程使用这3台打印机,而且每个线程使用的打印机互不冲突,打印完成后,空闲的打印机会给其它线程继续使用,继续折腾,如果把getPrinter()中加锁的部分去掉,即:

    private int getPrinter() {
        int ret = -1;
        try {
            //lockPrinters.lock();//先加锁,保证1次只能有1个线程来获取空闲的打印机
            for (int i = 0; i < freePrinters.length; i++) {
                //遍历所有打印机的状态,发现有第1个空闲的打印机后,领取号码,
                // 并设置该打印机为繁忙状态(因为马上就要用它)
                if (freePrinters[i]) {
                    ret = i;
                    freePrinters[i] = false;
                    break;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //最后别忘记了解锁,这样后面的线程才能上来领号
            //lockPrinters.unlock();
        }
        return ret;
    }

再跑一下,结果如何,为了放大冲突,这回开到15个线程来抢3台打印机,输出如下:

Thread0: Going to print a job
Thread14: Going to print a job
Thread13: Going to print a job
Thread12: Going to print a job
Thread11: Going to print a job
Thread10: Going to print a job
Thread9: Going to print a job
Thread8: Going to print a job
Thread7: Going to print a job
Thread6: Going to print a job
Thread5: Going to print a job
Thread4: Going to print a job
Thread3: Going to print a job
Thread2: Going to print a job
Thread1: Going to print a job
Thread0: PrintQueue: Printing a Job in Printer0 during 29 seconds
Thread14: PrintQueue: Printing a Job in Printer0 during 92 seconds
Thread13: PrintQueue: Printing a Job in Printer1 during 66 seconds
Thread0: The document has been printed
Thread12: PrintQueue: Printing a Job in Printer0 during 86 seconds
Thread13: The document has been printed
Thread11: PrintQueue: Printing a Job in Printer1 during 1 seconds
Thread11: The document has been printed
Thread10: PrintQueue: Printing a Job in Printer1 during 58 seconds
Thread14: The document has been printed
Thread9: PrintQueue: Printing a Job in Printer0 during 92 seconds
Thread12: The document has been printed
Thread8: PrintQueue: Printing a Job in Printer0 during 59 seconds
Thread10: The document has been printed
Thread7: PrintQueue: Printing a Job in Printer1 during 51 seconds
Thread8: The document has been printed
Thread6: PrintQueue: Printing a Job in Printer0 during 33 seconds
Thread7: The document has been printed
Thread5: PrintQueue: Printing a Job in Printer1 during 2 seconds
Thread9: The document has been printed
Thread3: PrintQueue: Printing a Job in Printer1 during 85 seconds
Thread4: PrintQueue: Printing a Job in Printer0 during 61 seconds
Thread5: The document has been printed
Thread6: The document has been printed
Thread2: PrintQueue: Printing a Job in Printer0 during 66 seconds
Thread4: The document has been printed
Thread1: PrintQueue: Printing a Job in Printer0 during 9 seconds
Thread1: The document has been printed
Thread3: The document has been printed
Thread2: The document has been printed

注意红色的部分:Thread0与Thread14同时分配到了Printer0上了,出现了多个线程同时抢一个资源的情况。

 


参考文章:

http://ifeve.com/thread-synchronization-utilities-2/

http://ifeve.com/thread-synchronization-utilities-3/

http://ifeve.com/concurrency-semaphore/

目录
相关文章
|
6月前
|
Java
如何在Java中进行多线程编程
Java多线程编程常用方式包括:继承Thread类、实现Runnable接口、Callable接口(可返回结果)及使用线程池。推荐线程池以提升性能,避免频繁创建线程。结合同步与通信机制,可有效管理并发任务。
261 6
|
6月前
|
IDE Java 编译器
java编程最基础学习
Java入门需掌握:环境搭建、基础语法、面向对象、数组集合与异常处理。通过实践编写简单程序,逐步深入学习,打牢编程基础。
354 1
|
6月前
|
存储 Oracle Java
java零基础学习者入门课程
本课程为Java零基础入门教程,涵盖环境搭建、变量、运算符、条件循环、数组及面向对象基础,每讲配示例代码与实践建议,助你循序渐进掌握核心知识,轻松迈入Java编程世界。
522 0
|
6月前
|
负载均衡 Java API
grpc-java 架构学习指南
本指南系统解析 grpc-java 架构,涵盖分层设计、核心流程与源码结构,结合实战路径与调试技巧,助你从入门到精通,掌握高性能 RPC 开发精髓。
593 8
|
11月前
|
机器学习/深度学习 消息中间件 存储
【高薪程序员必看】万字长文拆解Java并发编程!(9-2):并发工具-线程池
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发编程中的强力并发工具-线程池,废话不多说让我们直接开始。
396 0
|
安全 Java 调度
解锁Java并发编程高阶技能:深入剖析无锁CAS机制、揭秘魔法类Unsafe、精通原子包Atomic,打造高效并发应用
【8月更文挑战第4天】在Java并发编程中,无锁编程以高性能和低延迟应对高并发挑战。核心在于无锁CAS(Compare-And-Swap)机制,它基于硬件支持,确保原子性更新;Unsafe类提供底层内存操作,实现CAS;原子包java.util.concurrent.atomic封装了CAS操作,简化并发编程。通过`AtomicInteger`示例,展现了线程安全的自增操作,突显了这些技术在构建高效并发程序中的关键作用。
258 1
|
11月前
|
缓存 安全 Java
【高薪程序员必看】万字长文拆解Java并发编程!(3-1):并发共享问题的解决与分析
活锁:多个线程相互影响对方退出同步代码块的条件而导致线程一直运行的情况。例如,线程1的退出条件是count=5,而线程2和线程3在其代码块中不断地是count进行自增自减的操作,导致线程1永远运行。内存一致性问题:由于JIT即时编译器对缓存的优化和指令重排等造成的内存可见性和有序性问题,可以通过synchronized,volatile,并发集合类等机制来解决。这里的线程安全是指,多个线程调用它们同一个实例的方法时,是线程安全的,但仅仅能保证当前调用的方法是线程安全的,不同方法之间是线程不安全的。
187 0
|
11月前
|
Java 程序员
【高薪程序员必看】万字长文拆解Java并发编程!(3-2):并发共享问题的解决与分析
wait方法和notify方法都是Object类的方法:让当前获取锁的线程进入waiting状态,并进入waitlist队列:让当前获取锁的线程进入waiting状态,并进入waitlist队列,等待n秒后自动唤醒:在waitlist队列中挑一个线程唤醒:唤醒所有在waitlist队列中的线程它们都是之间协作的手段,只有拥有对象锁的线程才能调用这些方法,否则会出现IllegalMonitorStateException异常park方法和unpark方法是LockSupport类中的方法。
193 0
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####