Java-10.2

CyclicBarrier
用来控制多个线程互相等待,只有当多个线程都到达时,这些线程才会继续执行。
和 CountdownLatch 相似,都是通过维护计数器来实现的。线程执行 await() 方法之后计数器会减 1,并进行等待,直到计数器为 0,所有调用 await() 方法而在等待的线程才能继续执行。
CyclicBarrier 和 CountdownLatch 的一个区别是,CyclicBarrier 的计数器通过调用 reset() 方法可以循环使用,所以它才叫做循环屏障。
CyclicBarrier 有两个构造函数,其中 parties 指示计数器的初始值,barrierAction 在所有线程都到达屏障的时候会执行一次。

public CyclicBarrier(int parties, Runnable barrierAction) {
    if (parties <= 0) throw new IllegalArgumentException();
    this.parties = parties;
    this.count = parties;
    this.barrierCommand = barrierAction;
}

public CyclicBarrier(int parties) {
    this(parties, null);
}

在这里插入图片描述

public class CyclicBarrierExample {

    public static void main(String[] args) {
        final int totalThread = 10;
        CyclicBarrier cyclicBarrier = new CyclicBarrier(totalThread);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < totalThread; i++) {
            executorService.execute(() -> {
                System.out.print("before..");
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
                System.out.print("after..");
            });
        }
        executorService.shutdown();
    }
}
before..before..before..before..before..before..before..before..before..before..after..after..after..after..after..after..after..after..after..after..

Semaphore
Semaphore 类似于操作系统中的信号量,可以控制对互斥资源的访问线程数。
以下代码模拟了对某个服务的并发请求,每次只能有 3 个客户端同时访问,请求总数为 10。

public class SemaphoreExample {

    public static void main(String[] args) {
        final int clientCount = 3;
        final int totalRequestCount = 10;
        Semaphore semaphore = new Semaphore(clientCount);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < totalRequestCount; i++) {
            executorService.execute(()->{
                try {
                    semaphore.acquire();
                    System.out.print(semaphore.availablePermits() + " ");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                }
            });
        }
        executorService.shutdown();
    }
}
1 0 1 0 1 2 2 2 2 1 

结果不唯一!

补充:

  1. Semaphore是什么,能做什么?
    Semaphore 是 synchronized 的加强版,作用是控制线程的并发数量。就这一点而言,单纯的synchronized 关键字是实现不了的。
    直接看例子吧,这个例子包含3个类,一个是线程类,一个是 Semaphore 关键代码类,一个类是主main方法类:
    线程类MyThread:
package test_semaphore_1;

public class MyThread extends Thread{

	private SemaphoreService service;

    public MyThread(String name, SemaphoreService service) {
        super();
        this.setName(name);
        this.service = service;
    }

    @Override
    public void run() {
        this.service.doSomething();
    }
}

SemaPhore关键代码类:

package test_semaphore_1;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.Semaphore;

public class SemaphoreService {

	private static SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");

    private Semaphore semaphore = new Semaphore(1);// 同步关键类,构造方法传入的数字是多少,则同一个时刻,只运行多少个进程同时运行制定代码

    public void doSomething() {
        try {
            /**
             * 在 semaphore.acquire() 和 semaphore.release()之间的代码,同一时刻只允许制定个数的线程进入,
             * 因为semaphore的构造方法是1,则同一时刻只允许一个线程进入,其他线程只能等待。
             * */
            semaphore.acquire();
            System.out.println(Thread.currentThread().getName() + ":doSomething start-" + getFormatTimeStr());
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + ":doSomething end-" + getFormatTimeStr());
            semaphore.release();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static String getFormatTimeStr() {
        return sf.format(new Date());
    }
}

main方法测试类:

package test_semaphore_1;

public class SemaphoreTest {

	public static void main(String args[]) {
        SemaphoreService service = new SemaphoreService();
        for (int i = 0; i < 10; i++) {
            MyThread t = new MyThread("thread" + (i + 1), service);
            t.start();// 这里使用 t.run() 也可以运行,但是不是并发执行了 
        }
    }
}

结果:

thread1:doSomething start-2020-10-02 10:27:05.506
thread1:doSomething end-2020-10-02 10:27:07.507
thread2:doSomething start-2020-10-02 10:27:07.507
thread2:doSomething end-2020-10-02 10:27:09.508
thread3:doSomething start-2020-10-02 10:27:09.508
thread3:doSomething end-2020-10-02 10:27:11.509
thread4:doSomething start-2020-10-02 10:27:11.509
thread4:doSomething end-2020-10-02 10:27:13.509
thread5:doSomething start-2020-10-02 10:27:13.509
thread5:doSomething end-2020-10-02 10:27:15.509
thread6:doSomething start-2020-10-02 10:27:15.509
thread6:doSomething end-2020-10-02 10:27:17.510
thread7:doSomething start-2020-10-02 10:27:17.510
thread7:doSomething end-2020-10-02 10:27:19.510
thread8:doSomething start-2020-10-02 10:27:19.510
thread8:doSomething end-2020-10-02 10:27:21.511
thread9:doSomething start-2020-10-02 10:27:21.511
thread9:doSomething end-2020-10-02 10:27:23.512
thread10:doSomething start-2020-10-02 10:27:23.512
thread10:doSomething end-2020-10-02 10:27:25.513

实践证明,确实是同一个时刻只有一个线程能访问,那如果把 Semaphore 的构造方法入参改成 2 呢,修改 SemaphoreService.java 文件:

package test_semaphore_1;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.Semaphore;

public class SemaphoreService {

	private static SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");

    private Semaphore semaphore = new Semaphore(2);// 同步关键类,构造方法传入的数字是多少,则同一个时刻,只运行多少个进程同时运行制定代码

    public void doSomething() {
        try {
            /**
             * 在 semaphore.acquire() 和 semaphore.release()之间的代码,同一时刻只允许制定个数的线程进入,
             * 因为semaphore的构造方法是1,则同一时刻只允许一个线程进入,其他线程只能等待。
             * */
            semaphore.acquire();
            System.out.println(Thread.currentThread().getName() + ":doSomething start-" + getFormatTimeStr());
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + ":doSomething end-" + getFormatTimeStr());
            semaphore.release();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static String getFormatTimeStr() {
        return sf.format(new Date());
    }
}

结果:

thread2:doSomething start-2020-10-02 10:34:13.796
thread1:doSomething start-2020-10-02 10:34:13.796
thread1:doSomething end-2020-10-02 10:34:15.797
thread2:doSomething end-2020-10-02 10:34:15.797
thread3:doSomething start-2020-10-02 10:34:15.797
thread4:doSomething start-2020-10-02 10:34:15.797
thread3:doSomething end-2020-10-02 10:34:17.798
thread4:doSomething end-2020-10-02 10:34:17.798
thread5:doSomething start-2020-10-02 10:34:17.798
thread6:doSomething start-2020-10-02 10:34:17.798
thread6:doSomething end-2020-10-02 10:34:19.798
thread5:doSomething end-2020-10-02 10:34:19.798
thread7:doSomething start-2020-10-02 10:34:19.798
thread8:doSomething start-2020-10-02 10:34:19.798
thread7:doSomething end-2020-10-02 10:34:21.798
thread8:doSomething end-2020-10-02 10:34:21.798
thread9:doSomething start-2020-10-02 10:34:21.798
thread10:doSomething start-2020-10-02 10:34:21.798
thread9:doSomething end-2020-10-02 10:34:23.799
thread10:doSomething end-2020-10-02 10:34:23.799
  1. 方法 acquire( int permits ) 参数作用,及动态添加 permits 许可数量 
    acquire( int permits ) 中的参数是什么意思呢?可以这么理解, new Semaphore(6) 表示初始化了 6个通路, semaphore.acquire(2) 表示每次线程进入将会占用2个通路,semaphore.release(2) 运行时表示归还2个通路。没有通路,则线程就无法进入代码块。
    而上面的代码中,semaphore.acquire() + semaphore.release() 在运行的时候,其实和 semaphore.acquire(1) + semaphore.release(1) 效果是一样的。  
    上代码:
    线程类MyThread:
package test_semaphore_2;

public class MyThread extends Thread{

	private SemaphoreService2 service;

    public MyThread(String name, SemaphoreService2 service) {
        super();
        this.setName(name);
        this.service = service;
    }

    @Override
    public void run() {
        this.service.doSomething();
    }
}

SemaPhore关键代码类:

package test_semaphore_2;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.Semaphore;

public class SemaphoreService2 {

	private static SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");

    private Semaphore semaphore = new Semaphore(6);//6表示有6个通路
    
    public void doSomething() {
        try {
            semaphore.acquire(2); // 2 表示进入此代码,就会消耗2个通路,2个通路从6个中扣除
            System.out.println(Thread.currentThread().getName() + ":doSomething start-" + getFormatTimeStr());
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + ":doSomething end-" + getFormatTimeStr());
            semaphore.release(2); // 释放占用的 2 个通路
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    public int availablePermits() {    // 查看可用通路数
        return semaphore.availablePermits();
    }
    
    public static String getFormatTimeStr() {
        return sf.format(new Date());
    }
}

main方法测试类:

package test_semaphore_2;

public class SemaphoreTest2 {

	public static void main(String args[]) {
        SemaphoreService2 service = new SemaphoreService2(); // 使用总 6 通路,每个线程占用2通路
        for (int i = 0; i < 10; i++) {
            MyThread t = new MyThread("thread" + (i + 1), service);
            t.start();// 这里使用 t.run() 也可以运行,但是不是并发执行了 
            System.out.println("可用通路数:" + service.availablePermits());
        }
    }
}

结果:

可用通路数:6
可用通路数:4
可用通路数:2
可用通路数:0
可用通路数:0
thread1:doSomething start-2020-10-02 10:46:15.142
thread3:doSomething start-2020-10-02 10:46:15.142
thread2:doSomething start-2020-10-02 10:46:15.142
可用通路数:0
可用通路数:0
可用通路数:0
可用通路数:0
可用通路数:0
thread1:doSomething end-2020-10-02 10:46:17.142
thread3:doSomething end-2020-10-02 10:46:17.142
thread2:doSomething end-2020-10-02 10:46:17.142
thread5:doSomething start-2020-10-02 10:46:17.142
thread4:doSomething start-2020-10-02 10:46:17.142
thread6:doSomething start-2020-10-02 10:46:17.142
thread6:doSomething end-2020-10-02 10:46:19.142
thread5:doSomething end-2020-10-02 10:46:19.142
thread4:doSomething end-2020-10-02 10:46:19.142
thread8:doSomething start-2020-10-02 10:46:19.142
thread7:doSomething start-2020-10-02 10:46:19.142
thread9:doSomething start-2020-10-02 10:46:19.142
thread8:doSomething end-2020-10-02 10:46:21.143
thread7:doSomething end-2020-10-02 10:46:21.143
thread9:doSomething end-2020-10-02 10:46:21.143
thread10:doSomething start-2020-10-02 10:46:21.143
thread10:doSomething end-2020-10-02 10:46:23.144

结果不唯一!

如果 acquire 的数量大于 release 的数量,则 通路迟早会被使用完,如果线程比较多,得不到后续运行,出现线程堆积内存,最终java进程崩掉;如果 acquire 的数量小于 release 的数量,就会出现并发执行的线程越来越多(换句话说,处理越来越快),最终也有可能出现问题。
比如,像上面的代码,SemaphoreService2.java 中 semaphore.release(2) 如果改成 semaphore.release(1) 则 就会出现有5个线程得不到运行堆积的情况,可以算一下:6-2-2-2+1+1+1=3,运行完一个回合后,还剩3个通路,3-2+1,第二回合,还剩2个通路,2-2+1=1,第3个回合,还剩一个通路,不足以运行任何一个线程。
把上面说的用代码实现一下,修改 SemaphoreService2.java 如下:

package test_semaphore_2;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.Semaphore;

public class SemaphoreService2 {

	private static SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");

    private Semaphore semaphore = new Semaphore(6);//6表示有6个通路
    
    public void doSomething() {
        try {
            semaphore.acquire(2); // 2 表示进入此代码,就会消耗2个通路,2个通路从6个中扣除
            System.out.println(Thread.currentThread().getName() + ":doSomething start-" + getFormatTimeStr());
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + ":doSomething end-" + getFormatTimeStr());
            semaphore.release(1); // 释放占用的 2 个通路
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    public int availablePermits() {    // 查看可用通路数
        return semaphore.availablePermits();
    }
    
    public static String getFormatTimeStr() {
        return sf.format(new Date());
    }

在这里插入图片描述

  1. acquire 的不可中断实现
    仔细看一下上面的代码,semaphore.acquire() 和 semaphore.acquire(int permits) 是会抛出异常 InterruptedException 的,如果在 acquire 和 release 之间的代码是一个比较慢和复制的运算,如内存占用过多,或者栈深度很深等,jvm会中断这块代码。

如何才能不让 jvm 中断 代码执行呢?

答案是:使用 acquireUninterruptibly() 替换acquire()、使用 acquireUninterruptibly(int permits) 替换 acquire(int permits) 。acquireUninterruptibly 不会抛出 InterruptedException ,一个代码块一时执行不完,还会继续等待执行。

个人觉得,不要随便使用 acquireUninterruptibly ,因为 jvm 中断执行,是自身的一种自我保护机制,保证 java 进程的正常,除了特殊情况必须用 acquireUninterruptibly 外,都应该 使用 acquire ,同时,改进一下 SemaphoreService2 的 doSomething 方法,将 release 放到 finally 块 中,如下。

public void doSomething() {
        try {
            semaphore.acquire(2); // 2 表示进入此代码,就会消耗2个通路,2个通路从6个中扣除
            System.out.println(Thread.currentThread().getName() + ":doSomething start-" + getFormatTimeStr());
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + ":doSomething end-" + getFormatTimeStr());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            semaphore.release(2); // release 放到 finally 中
        }
    }
  1. 其他一些常有工具方法
    availablePermits() 方法在前面用过,表示返回 Semaphore 对象中的当前可用许可数,此方法通常用于调试,因为许可数量(通路)可能是实时在改变的。
    drainPermits() 方法可获取并返回立即可用的所有许可(通路)个数,并将可用许可置为0。
    getQueueLength() 获取等待许可的线程个数。
    hasQueuedThreads() 判断有没有线程在等待这个许可。
    getQueueLength() 和 hasQueuedThreads() 都是在判断当前有没有等待许可的线程信息时使用。

  2. 线程公平性
    上面用的 Semaphore 构造方法是 Semaphore semaphore = new Semaphore(int permits)
    其实,还有一个构造方法: Semaphore semaphore = new Semaphore(int permits , boolean isFair)
    isFair 的意思就是,是否公平,获得锁的顺序与线程启动顺序有关,就是公平,先启动的线程,先获得锁。isFair 不能100% 保证公平,只能是大概率公平。
    isFair 为 true,则表示公平,先启动的线程先获得锁。

  3. 方法 tryAcquire() 、 tryAcquire(int permits)、 tryAcquire(int permits , long timeout , TimeUint unit) 的使用:
    tryAcquire 方法,是 acquire 的扩展版,tryAcquire 作用是尝试得获取通路,如果未传参数,就是尝试获取一个通路,如果传了参数,就是尝试获取 permits 个 通路 、在指定时间 timeout 内 尝试 获取 permits 个通路。
    上代码试试看:
    3个类,线程类未变,以下是修改了的两个类:

package test_semaphore_3;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class SemaphoreService3 {

	private static SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");

    private Semaphore semaphore = new Semaphore(6,true);//6表示有6个通路
    
    public void doSomething() {
        try {
            if (semaphore.tryAcquire(2, 3, TimeUnit.SECONDS)) { // 在 3秒 内 尝试获取 2 个通路

                System.out.println(Thread.currentThread().getName() + ":doSomething start-" + getFormatTimeStr());
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName() + ":doSomething end-" + getFormatTimeStr()
                        + ",当前是否有进程等待:" + semaphore.hasQueuedThreads() + ",等待进程数:" + semaphore.getQueueLength());
                semaphore.release(2); // 释放占用的 2 个通路
            } else {
                System.out.println(Thread.currentThread().getName() + ":doSomething 没有获取到锁-准备退出-" + getFormatTimeStr());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    public int availablePermits() {    // 查看可用通路数
        return semaphore.availablePermits();
    }
    
    public static String getFormatTimeStr() {
        return sf.format(new Date());
    }
}
package test_semaphore_3;

public class SemaphoreTest3 {

	public static void main(String args[]) {
        SemaphoreService3 service = new SemaphoreService3(); // 使用总 6 通路,每个线程占用2通路
        for (int i = 0; i < 10; i++) {
            MyThread t = new MyThread("thread" + (i + 1), service);
            t.start();// 这里使用 t.run() 也可以运行,但是不是并发执行了 
        }
    }
}

结果:
在这里插入图片描述

  1. 多进路-多处理 vs 多进路-单处理
    在上面的代码中,我们之所以可以实现单处理,是因为在上面的所有线程都共有了同一个 Semaphore 来进行进程处理,那么如果 Semaphore 本身就是进程的一部分呢,会怎么样呢?
    比如,修改 第一个例子中的 SemaphoreTest 如下:
package com.cd.concurrent.semaphore;

public class SemaphoreTest {
    public static void main(String args[]) {
        for (int i = 0; i < 10; i++) {
            SemaphoreService service = new SemaphoreService();
            MyThread t = new MyThread("thread" + (i + 1), service);
            t.start();// 这里使用 t.run() 也可以运行,但是不是并发执行了 
        }
    }
}

结果:
在这里插入图片描述
如果 SemaphoreTest 类不进行修改,如何实现第一个例子 中的 单处理呢?
修改 SemaphoreService ,代码如下:

package test_semaphore_4;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.Semaphore;

public class SemaphoreService {

	private static SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");

    private Semaphore semaphore = new Semaphore(2);// 同步关键类,构造方法传入的数字是多少,则同一个时刻,只运行多少个进程同时运行制定代码

    public void doSomething() {
        try {
            /**
             * 在 semaphore.acquire() 和 semaphore.release()之间的代码,同一时刻只允许制定个数的线程进入,
             * 因为semaphore的构造方法是1,则同一时刻只允许一个线程进入,其他线程只能等待。
             * */
            semaphore.acquire();
            
            doSomethingMain(); // 将主要处理部分封装成一个方法
            
            semaphore.release();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    private static synchronized void doSomethingMain() throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + ":doSomething start-" + getFormatTimeStr());
        Thread.sleep(2000);
        System.out.println(Thread.currentThread().getName() + ":doSomething end-" + getFormatTimeStr());
    }

    public static String getFormatTimeStr() {
        return sf.format(new Date());
    }
}

注意:doSomethingMain() 方法必须是 static synchronized 的才行

结果:
在这里插入图片描述
这里,抛出一个问题,上面的代码,不用 synchronized 实现,而使用 ReentrantLock 来实现,按理说会更好的,原因如下:
synchronized 是 jvm 层面的实现,ReentrantLock 是 jdk 层面的实现,synchronized 的缺点如下:

  • 不能响应中断;
  • 同一时刻不管是读还是写都只能有一个线程对共享资源操作,其他线程只能等待
  • 锁的释放由虚拟机来完成,不用人工干预,不过此即使缺点也是优点,优点是不用担心会造成死锁,缺点是由可能获取到锁的线程阻塞之后其他线程会一直等待,性能不高。

而lock接口的提出就是为了完善synchronized的不完美的,首先lock是基于jdk层面实现的接口,和虚拟机层面不是一个概念;其次对于lock对象中的多个方法的调用,可以灵活控制对共享资源变量的操作,不管是读操作还是写操作
那么上面的代码如果使用 ReentrantLock 来实现,岂不是更好吗?好,修改 SemaphoreService:

package test_semaphore_5;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.ReentrantLock;

public class SemaphoreService {

	private static SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");

    private Semaphore semaphore = new Semaphore(2);// 同步关键类,构造方法传入的数字是多少,则同一个时刻,只运行多少个进程同时运行制定代码

    private ReentrantLock lock = new ReentrantLock();
    
    public void doSomething() {
        try {
            /**
             * 在 semaphore.acquire() 和 semaphore.release()之间的代码,同一时刻只允许制定个数的线程进入,
             * 因为semaphore的构造方法是1,则同一时刻只允许一个线程进入,其他线程只能等待。
             * */
            semaphore.acquire();

            lock.lock();
            doSomethingMain(); // 将主要处理部分封装成一个方法            

            semaphore.release();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    
    private void doSomethingMain() throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + ":doSomething start-" + getFormatTimeStr());
        Thread.sleep(2000);
        System.out.println(Thread.currentThread().getName() + ":doSomething end-" + getFormatTimeStr());
    }

    public static String getFormatTimeStr() {
        return sf.format(new Date());
    }
}

和预期的不一样呀,10个线程基本是同时执行了,那么问题出在哪里呢?
因为使用的不是同一个 SemaphoreService 对象实例,所有是多个锁分别加在了多个 SemaphoreService 实例中,就相当于没有

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值