初识并发编程(五) 初识 J.U.C

  1:基础

        在 Java 5.0 提供了 java.util.concurrent (简称JUC )包,在此包中增加了在并发编程中很常用的实用工具类,用于定义类似于线程的自定义子系统,包括线程池、异步 IO 和轻量级任务框架。提供可调的、灵活的线程池。还提供了设计用于多线程上下文中的 Collection 实现等。

2:AQS(AbstractQueuedSynchronizer)

        ​ AQS,在java.util.concurrent.locks包中,AbstractQueuedSynchronizer这个类是并发包中的核心,了解其他类之前,需要先弄清楚AQS。在JUC的很多类中都会存在一个内部类Sync,Sync都是继承自AbstractQueuedSynchronizer,相信不用说就能明白AQS有多重要。

AQS原理

        AQS就是一个同步器,要做的事情就相当于一个锁,所以就会有两个动作:一个是获取,一个是释放。获取释放的时候该有一个东西来记住他是被用还是没被用,这个东西就是一个状态。如果锁被获取了,也就是被用了,还有很多其他的要来获取锁,总不能给全部拒绝了,这时候就需要他们排队,这里就需要一个队列。这大概就清楚了AQS的主要构成了:

        获取和释放两个动作

        同步状态(原子操作)

        阻塞队列

2.1CountDownLatch

使用场景:并行计算....

springboot并发编程CountDownLatch配合异步线程池使用

​         同步辅助类,可以完成类似完成阻塞当前功能,一个/多个线程等待,等到其他线程完成CountDownLatch有一个计数器字段,可以根据需要减少它。然后可以用它来阻塞一个调用线程,直到它被计数到零。

        ​ 如果我们正在进行一些并行处理,我们可以使用与我们想要处理的多个线程相同的计数器值来实例化CountDownLatch。然后,我们可以在每个线程完成后调用countdown(),保证调用await()的依赖线程将阻塞,直到工作线程完成。

package com.mmall.concurrency.example.aqs;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * 认识 CountDownLatchExample
 */
@Slf4j
public class CountDownLatchExampleTime {
    private static int threadCount = 200;

    public static void main(String[] args) throws Exception{
        //实例化一个线程池
        ExecutorService exec = Executors.newCachedThreadPool();
        //
        final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
log.info("2233");
        for (int i = 0; i < threadCount; i++) {
            final int threadNum = i;
            exec.execute(() -> {
                try {
                    test(threadNum);
                } catch (Exception e) {
                    log.error("{}",e.getMessage());
                    e.printStackTrace();
                }finally {
                    countDownLatch.countDown();
                }
            });
        }

        //指定时间等待,当指定等待时间过后,如果exec.execute( 内容还没执行完,先跳过。执行log.info("finish++"); 同时exec.execute( 还是会继续执行
        countDownLatch.await(10, TimeUnit.MILLISECONDS);
//        countDownLatch.await();  不指定等待时间。等test(threadNum);执行完毕之后才会执行 log.info("finish++");
        //关闭线程池,节省资源   当前已经有的线程执行完,然后再关闭
        exec.shutdown();
        log.info("finish++");
    }

    public static void test(int i) throws Exception {
        Thread.sleep(10000);
        log.info("{}", i);
    }
}

 2.2 Semaphore(信号量)

 2.2.1:基础说明


​        Semaphore 通常我们叫它信号量, 可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。
​        可以把它简单的理解成我们停车场入口立着的那个显示屏,每有一辆车进入停车场显示屏就会显示剩余车位减1,每有一辆车从停车场出去,显示屏上显示的剩余车辆就会加1,当显示屏上的剩余车位为0时,停车场入口的栏杆就不会再打开,车辆就无法进入停车场了,直到有一辆车从停车场出去为止。

 2.2.2:使用场景


​        通常用于那些资源有明确访问数量限制的场景,常用于限流 (某个资源可被同时访问的个数)。
​        比如:数据库连接池,同时进行连接的线程有数量限制,连接不能超过一定的数量,当连接达到了限制数量后,后面的线程只能排队等前面的线程释放了数据库连接才能获得数据库连接。
​        比如:停车场场景,车位数量有限,同时只能容纳多少台车,车位满了之后只有等里面的车离开停车场外面的车才可以进入。

 2.2.3:Semaphore常用方法说明

acquire()  
获取一个令牌,在获取到令牌、或者被其他线程调用中断之前线程一直处于阻塞状态。

	acquire(int permits)  
	获取一个令牌,在获取到令牌、或者被其他线程调用中断、或超时之前线程一直处于阻塞状态。
    
	acquireUninterruptibly() 
	获取一个令牌,在获取到令牌之前线程一直处于阻塞状态(忽略中断)。
    
	tryAcquire()
	尝试获得令牌,返回获取令牌成功或失败,不阻塞线程。

	tryAcquire(long timeout, TimeUnit unit)
	尝试获得令牌,在超时时间内循环尝试获取,直到尝试获取成功或超时返回,不阻塞线程。

release()
释放一个令牌,唤醒一个获取令牌不成功的阻塞线程。

hasQueuedThreads()
等待队列里是否还存在等待线程。

getQueueLength()
获取等待队列里阻塞的线程数。

drainPermits()
清空令牌把可用令牌数置为0,返回清空令牌的数量。

availablePermits()
返回可用的令牌数量。

Demo 1:

import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

/**
 * 认识 Semaphore
 */
@Slf4j
public class SemaphoreExample {
    private static int threadCount = 200;

    public static void main(String[] args) throws Exception {
        //实例化一个线程池
        ExecutorService exec = Executors.newCachedThreadPool();
        //实例化一个信号量  每次访问限制为3
        Semaphore sp = new Semaphore(3);
        for (int i = 0; i < threadCount; i++) {
            final int threadNum = i;
            exec.execute(() -> {
                try {
                    //获取许可
                    sp.acquire();
//                    sp.acquire(3);   //一次就获取3个许可,由于上面Semaphore sp=new Semaphore(3);  所以只能一个个线程执行
                    test(threadNum);
                    //释放许可
                    sp.release();
//                    sp.release(3);//一次性释放三个许可
                } catch (Exception e) {
                    log.error("{}", e.getMessage());
                    e.printStackTrace();
                }
            });
        }
 
        //关闭线程池,节省资源   当前已经有的线程执行完,然后再关闭 
        exec.shutdown();
        log.info("finish");
    }

    public static void test(int i) throws Exception {
        log.info("{}", i);
        Thread.sleep(1000);
    }
}

Demo 2:

import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.*;

/**
 * 认识 Semaphore tryAcquire
 */
@Slf4j
public class SemaphoreExampleTry {
    private static int threadCount = 200;

    public static void main(String[] args) throws Exception {
        //实例化一个线程池
        ExecutorService exec = Executors.newCachedThreadPool();
        //
        //实例化一个信号量  每次访问限制为3
        Semaphore sp = new Semaphore(3);
        for (int i = 0; i < threadCount; i++) {
            final int threadNum = i;
            exec.execute(() -> {
                try {
                    //尝试获取许可,没有就直接跳过   当前允许的并发是3,超过3就丢弃
                    if (sp.tryAcquire(50000,TimeUnit.MILLISECONDS)) {
//                    sp.acquire(3);   //一次就获取3个许可,由于上面Semaphore sp=new Semaphore(3);  所以只能一个个线程执行
                        test(threadNum);
                        //释放许可
                        sp.release();
                    }
                } catch (Exception e) {
                    log.error("{}", e.getMessage());
                    e.printStackTrace();
                }
            });
        }

        //关闭线程池,节省资源   当前已经有的线程执行完,然后再关闭
        exec.shutdown();
        log.info("finish");
    }

    public static void test(int i) throws Exception {

        log.info("{}", i);
        Thread.sleep(1000);
    }


}

1.3 CyclicBarrier

        同步辅助类,允许一组线程相互等待,直到到达某个公共 点。

        多个线程相互等待,只有都准备好了才能各自继续往下执行

        释放线程后继续使用

Demo :

import java.util.concurrent.*;

/**
 * CyclicBarrier
 */
@Slf4j
public class CyclicBarrierExample {
    /**
     * 声明一个 CyclicBarrier,指定每次多少个线程 同步等待
     */
//    private static CyclicBarrier barrier = new CyclicBarrier(3);
  //添加执行代码,等都准备好之后,先执行这个
    private static CyclicBarrier barrier = new CyclicBarrier(3,()->{
        log.info("准备完毕之后会先执行这个");
    });

    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            final int numx = i;
            executor.execute(() -> {
                try {
                    race(numx);
                } catch (Exception e) {
                    log.error("E{}", e.getMessage());
                }
            });
        }
        executor.shutdown();

    }

    private static void race(int threadNum) throws Exception {
        Thread.sleep(1000);
        log.info("{} is ready", threadNum);
        //告诉barrier当前线程ok
//        barrier.await();
        //指定等待时间
        try {
          //指定等待时间必须用try包裹
            barrier.await(2000,TimeUnit.MICROSECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }

        log.info("{} continue", threadNum);
    }
}

2.4 ReentrantLock

2.4.1简介

​         ReentrantLock类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上)

        Reentrant 锁意味着什么呢?简单来说,它有一个与锁相关的获取计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,然后锁需要被释放两次才能获得真正释放。这模仿了 synchronized 的语义;如果线程进入由线程已经拥有的监控器保护的 synchronized 块,就允许线程继续进行,当线程退出第二个(或者后续) synchronized 块的时候,不释放锁,只有线程退出它进入的监控器保护的第一个 synchronized 块时,才释放锁.

2.4.2 Synchronized与ReentrantLock的性能区别

​         Synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。

​         Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作(Compare and Swap)。我们可以进一步研究ReentrantLock的源代码,会发现其中比较重要的获得锁的一个方法是compareAndSetState。这里其实就是调用的CPU提供的特殊指令。

2.4.3 Synchronized和lock用途区别

​         synchronized原语和ReentrantLock在一般情况下没有什么区别,但是在非常复杂的同步应用中,请考虑使用ReentrantLock,特别是遇到下面3种需求的时候。

                1.某个线程在等待一个锁的控制权的这段时间需要中断 [ lock.lockInterruptibly()]

                2.需要分开处理一些wait-notify,ReentrantLock里面的Condition应用,能够控制notify哪个线程

                3.具有公平锁功能,每个到来的线程都将排队等候。可指定是公平锁还是非公平锁

​         先说第一种情况,ReentrantLock的lock机制有2种,忽略中断锁和响应中断锁,这给我们带来了很大的灵活性。比如:如果A、B 2个线程去竞争锁,A线程得到了锁,B线程等待,但是A线程这个时候实在有太多事情要处理,就是一直不返回,B线程可能就会等不及了,想中断自己,不再等待这个锁了,转而处理其他事情。这个时候ReentrantLock就提供了2种机制,第一,B线程中断自己(或者别的线程中断它),但是ReentrantLock不去响应,继续让B线程等待,你再怎么中断,我全当耳边风(synchronized原语就是如此);第二,B线程中断自己(或者别的线程中断它),ReentrantLock处理了这个中断,并且不再等待这个锁的到来,完全放弃.

相似点:

        这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的(操作系统需要在用户态与内核态之间来回切换,代价很高,不过可以通过对锁优化进行改善)。

功能区别:

        这两种方式最大区别就是对于Synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成

​ 便利性:

        很明显Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。

​ 锁的细粒度和灵活度:

        很明显ReenTrantLock优于Synchronized

性能的区别:

        在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用synchronized,其实synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。

​ synchronized是Java内置的机制,是JVM层面的,而Lock则是接口,是JDK层面的

        尽管最初synchronized的性能效率比较差,但是随着版本的升级,synchronized已经变得原来越强大了,这也是为什么官方建议使用synchronized的原因.毕竟,他是一个关键字啊,这才是亲儿子,Lock,终归差了一点.

        synchronized:方便简洁,由编译器加锁和释放。只能是公平锁(先到先得 )。只有少量竞争者的时候推荐使用 (由于是自动释放,不会引发死锁)

        ReentrantLock:手动声明加锁和释放。 灵活度优于 synchronized。竞争者不少,线程增长趋势可预估情况下使用。ReentrantLock(使用不当,会引发死锁【没有执行unlock】)

synchronized Demo:

package com.mmall.concurrency.example.aqs.lock;

import com.alibaba.fastjson.JSON; 
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

/**
 * 线程安全
 */
@Slf4j 
public class SynchronizedExample {

    /**
     * 请求总数
     */
    public static int clientTotal = 5000;
    /**
     * 同时并发执行的线程数
     */
    public static int threadTotal = 200;
    public static int count = 0;


    /**
     * Semaphore:   允许并发的数量  [Semaphore 是 synchronized 的加强版,作用是控制线程的并发数量。]
     *  1):在进行操作的时候,需要先acquire获取到许可,才可以继续执行任务, 如果获取失败,则进入阻塞;处理完成之后需要release释放许可。
     *  2): acquire与release之间的关系:在实现中不包含真正的许可对象,并且Semaphore也不会将许可与线程关联起来,
     * 因此在一个线程中获得的许可可以在另一个线程中释放。可以将acquire操作视为是消费一个许可,而release操作是创建一个许可,Semaphore并不受限于它在创建时的初始许可数量。
     * 也就是说acquire与release并没有强制的一对一关系,release一次就相当于新增一个许可,许可的数量可能会由于没有与acquire操作一对一而导致超出初始化时设置的许可个数。
     */


    /**
     * CountDownLatch:  CountDownLatch是一种java.util.concurrent包下一个同步工具类,它允许一个或多个线程等待直到在其他线程中一组操作执行完成。
     * 1、CountDownLatch countDownLatch = new CountDownLatch(N); //构造对象时候 需要传入参数N
     * 2、countDownLatch.await()  能够阻塞线程 直到调用N次end.countDown() 方法才释放线程
     * 3、countDownLatch.countDown() 可以在多个线程中调用  计算调用次数是所有线程调用次数的总和
     *
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {
        //定义一个线程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        //定义信号量  允许并发的数量  [Semaphore 是 synchronized 的加强版,作用是控制线程的并发数量。]
        final Semaphore semaphore = new Semaphore(threadTotal);
        //定义计数器  CountDownLatch是一种java.util.concurrent包下一个同步工具类,它允许一个或多个线程等待直到在其他线程中一组操作执行完成。
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            executorService.execute(() -> {
                try {
                    //引入型号量
                    // 在进行操作的时候,需要先acquire获取到许可,才可以继续执行任务,
                    // 如果获取失败,则进入阻塞;处理完成之后需要release释放许可。
                    semaphore.acquire();
                    //核心操作
                    add();
                    //释放进程
                    semaphore.release();
                } catch (Exception e) {
                    log.error("Error message:{} JOSNE:{}", e.getMessage(), JSON.toJSONString(e));
                    e.printStackTrace();
                }
                countDownLatch.countDown();
            });
        }

        countDownLatch.await();
        //关闭线程池
        executorService.shutdown();
        //此时返回的 count结果不确定,此是一个不安全的线程
        log.info("count {}", count);
    }

    private synchronized static void add() {
        count++;
    }
}

ReentrantLock Demo

package com.mmall.concurrency.example.aqs.lock;

import com.alibaba.fastjson.JSON;
import com.mmall.concurrency.annoations.NotThreadSafe; 
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 线程安全
 */
@Slf4j 
public class ReentrantLockExample {

    /**
     * 请求总数
     */
    public static int clientTotal = 5000;
    /**
     * 同时并发执行的线程数
     */
    public static int threadTotal = 200;
    public static int count = 0;
    /**
     * 声明一个ReentrantLock 锁
     */
    public static ReentrantLock lock = new ReentrantLock();


    /**
     * Semaphore:   允许并发的数量  [Semaphore 是 synchronized 的加强版,作用是控制线程的并发数量。]
     *  1):在进行操作的时候,需要先acquire获取到许可,才可以继续执行任务, 如果获取失败,则进入阻塞;处理完成之后需要release释放许可。
     *  2): acquire与release之间的关系:在实现中不包含真正的许可对象,并且Semaphore也不会将许可与线程关联起来,
     * 因此在一个线程中获得的许可可以在另一个线程中释放。可以将acquire操作视为是消费一个许可,而release操作是创建一个许可,Semaphore并不受限于它在创建时的初始许可数量。
     * 也就是说acquire与release并没有强制的一对一关系,release一次就相当于新增一个许可,许可的数量可能会由于没有与acquire操作一对一而导致超出初始化时设置的许可个数。
     */


    /**
     * CountDownLatch:  CountDownLatch是一种java.util.concurrent包下一个同步工具类,它允许一个或多个线程等待直到在其他线程中一组操作执行完成。
     * 1、CountDownLatch countDownLatch = new CountDownLatch(N); //构造对象时候 需要传入参数N
     * 2、countDownLatch.await()  能够阻塞线程 直到调用N次end.countDown() 方法才释放线程
     * 3、countDownLatch.countDown() 可以在多个线程中调用  计算调用次数是所有线程调用次数的总和
     *
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {
        //定义一个线程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        //定义信号量  允许并发的数量  [Semaphore 是 synchronized 的加强版,作用是控制线程的并发数量。]
        final Semaphore semaphore = new Semaphore(threadTotal);
        //定义计数器  CountDownLatch是一种java.util.concurrent包下一个同步工具类,它允许一个或多个线程等待直到在其他线程中一组操作执行完成。
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            executorService.execute(() -> {
                try {
                    //引入型号量
                    // 在进行操作的时候,需要先acquire获取到许可,才可以继续执行任务,
                    // 如果获取失败,则进入阻塞;处理完成之后需要release释放许可。
                    semaphore.acquire();
                    //核心操作
                    add();
                    //释放进程
                    semaphore.release();
                } catch (Exception e) {
                    log.error("Error message:{} JOSNE:{}", e.getMessage(), JSON.toJSONString(e));
                    e.printStackTrace();
                }
                countDownLatch.countDown();
            });
        }

        countDownLatch.await();
        //关闭线程池
        executorService.shutdown();
        //此时返回的 count结果不确定,此是一个不安全的线程
        log.info("count {}", count);
    }

    private static void add() {
        //加锁
        lock.lock();
        try {
            count++;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //解锁
            lock.unlock();

        }
    }
}

2.4.4 ReentrantLock中的lock()、lockInterruptibly()、tryLock()说明

        lock()  阻塞式地获取锁,只有在获取到锁后才处理interrupt信息。
        lockInterruptibly() 阻塞式地获取锁,立即处理interrupt信息,并抛出异常。
        tryLock()  尝试获取一次锁,不管成功失败,都立即返回true、false。
        tryLock(long timeout, TimeUnit unit)在timeout时间内阻塞式地获取锁,成功返回true,超时返回false,同时立即处理interrupt信息,并抛出异常 。

 

package com.mmall.concurrency.example.aqs.lock;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

@Slf4j
public class ReentrantLockAllExample {
    private static ReentrantLock sync = new ReentrantLock();

    public static void lock() {
        try {
            sync.lock();
            log.info("sync.lock");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            sync.unlock();
            log.info("sync.lock.unlock");

        }
    }
    public static void tryLocktime() {
        try {
            sync.tryLock(1, TimeUnit.MILLISECONDS);
            log.info("sync.tryLock.time");

        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            sync.unlock();
            log.info("sync.tryLock.time.unlock");

        }
    }
    public static void tryLock() {
        try {
            sync.tryLock();
            log.info("sync.tryLock");

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            sync.unlock();
            log.info("sync.tryLock.unlock");
        }
    }

    public static void main(String[] args) {
        lock();
        tryLock();
        tryLocktime();
    }
}

初识并发编程(五) 初识 J.U.C_第1张图片

 

你可能感兴趣的:(高并发,基础技术,J.U.C,高并发)