• 22-08-29 西安 JUC(02)线程安全集合类、 juc强大的辅助类


    如何理解线程不安全

    集合类就相当于一个资源类,并发的多个线程同时操作同一资源类对象时 调用它的成员方法修改成员属性值 如果调用的方法没有加锁 会出现并发的数据安全问题

    线程不安全集合

    List:ArrayListSet: HashSetMap:HashMap

    线程不安全List

    1、ArrayList演示线程不安全

    UUID回顾

    1. String str1 = UUID.randomUUID().toString();//e251ab3e-fa96-478c-b32f-1028db9b396f
    2. String str2 = UUID.randomUUID().toString().replace("-","");//8f76be380f1346a1b8406ca233129b0e
    3. System.out.println(str1);
    4. System.out.println(str2);

    按照8-4-4-4-12的顺序进行分隔。加上中间的横杆,UUID有36个字符。

    ArrayList线程不安全演示

    1. List list = new ArrayList<>();
    2. //并发多个线程操作资源类对象
    3. for (int i = 0;i < 30; i++) {
    4. new Thread(()->{
    5. list.add(UUID.randomUUID().toString().substring(0,8));
    6. System.out.println(list);//list.toString()
    7. },i+"").start();
    8. }

    控制台运行报错:并发修改异常

    报错分析:

    并发的多个线程同时更新集合中的数据,但是集合的方法没有加锁,导致并发的数据安全问题

    1. public boolean add(E e) {
    2. ensureCapacityInternal(size + 1); // Increments modCount!!
    3. elementData[size++] = e;
    4. return true;
    5. }

    深层次分析:

    ArrayList:
        toSting方法执行时 会调用next方法 获取集合中的每一个元素  拼接集合的元素的字符串输出


    next方法中 调用了checkForComodification 检查当前方法执行时元素的个数 和 现在集合中元素的个数是否一样。不一样 则抛出并发更新异常 “ConcurrentModificationException”

    1. //以下为ArrayList源码部分
    2. final void checkForComodification() {
    3. if (modCount != expectedModCount)
    4. throw new ConcurrentModificationException();
    5. }

    但是ArrayList使用多的原因:
        1、不加锁 性能高
        2、以后Controller或者Servlet的方法  多线程并发访问时,每个方法执行时基本上不会再内部开子线程,一般也不会更新Controller或者Servlet定义的成员变量

    每个线程调用的方法形成了一个独立的线程栈 ,不同线程栈中的方法互相独立。

    每个方法中的局部变量操作时没有并发线程安全问题的    如果这个方法 使用的是 同一个对象的成员属性 是有并发数据安全问题


    2、Vector 保证并发的线程安全

    1. //线程安全的List集合类
    2. Vector list = new Vector();
    3. //并发多个线程操作资源类对象
    4. for (int i = 0;i < 30; i++) {
    5. new Thread(()->{
    6. list.add(UUID.randomUUID().toString().substring(0,8));
    7. System.out.println(list);//list.toString()
    8. },i+"").start();
    9. }

    Vector中基本所有的方法都添加了synchronized关键字保证线程安全

    包括:CRUD和toString以及iterator()获取迭代器的方法

     Vector 性能较差:内存消耗比较大,适合增量较大的写操作


    3、SynchronizedList 装饰者模式

    是Collections工具类提供的可以将线程不安全的List对象转为(装饰)线程安全的SynchronizedList对象

    1. //线程安全的List集合类
    2. List list = Collections.synchronizedList(new ArrayList<>());
    3. //并发多个线程操作资源类对象
    4. for (int i = 0;i < 30; i++) {
    5. new Thread(()->{
    6. list.add(UUID.randomUUID().toString().substring(0,8));
    7. System.out.println(list);//list.toString()
    8. },i+"").start();
    9. }

    SynchronizedList 基本上所有的方法 都通过synchronized(this)同步代码块加锁,代码块内的方法体通过创建对象时接收的不安全的List对象相同的方法来实现 【装饰者模式】

    SynchronizedList        ,迭代遍历性能要高于Vector


    SynchronizedList 使用场景:适合并发写多 读稍少     使用较多


    4、CopyOnWriteArrayList 写时复制

    CopyOnWrite容器(简称COW容器)即写时复制的容器。

    通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器

    这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器

    写操作:不直接修改数据,先拷贝数据的副本,修改副本数据,改成功后使用副本替换原数据

    1. //线程安全的List集合类
    2. CopyOnWriteArrayList list = new CopyOnWriteArrayList();
    3. //并发多个线程操作资源类对象
    4. for (int i = 0;i < 30; i++) {
    5. new Thread(()->{
    6. list.add(UUID.randomUUID().toString().substring(0,8));
    7. System.out.println(list);//list.toString()
    8. },i+"").start();
    9. }

    不会有竞争关系的原理: 每个线程并发时 都是修改自己拷贝的副本数据,多个线程不会修改同一个数据

    CopyOnWriteArrayList为了保证同时只有一个线程写数据 ;写/更新的方法使用了ReentrantLock加锁。

    1. 在向CopyOnWriteArrayList添加元素时,会复制⼀个新的数组,写操作在新数组上进⾏,读操作在原数组上
    2. 进⾏,写操作结束之后会把原数组指向新数组

    所有的读的方法均未加锁,读取数据时读取的是当前的集合的数据

    cow优缺点:

    优点:get/迭代器方法 性能高,未加锁 

    1. CopyOnWriteArrayList允许在写操作时来读取数据,⼤⼤提⾼了读的性能,
    2. 因此适合读多写少的应⽤场景

    缺点

    1. 内存占用问题。写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存

    2. 数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。

    CopyOnWriteArrayList 使用场景:适合读多写少场景,比如白名单、黑名单。  尽量少用


    5、线程安全的List性能测试

    1. /*
    2. 线程安全的List的性能测试
    3. 先并发写入20w的数据,并发随机读取20w次数据各自的时间
    4. */
    5. public static void main(String[] args) {
    6. List list1 = new Vector();
    7. //spring-core核心包中提供了StopWatch的工具类 可以帮助我们统计方法执行的时间
    8. StopWatch stopWatch = new StopWatch();
    9. stopWatch.start("vector:write");//一次统计开始
    10. IntStream.rangeClosed(1,200000)
    11. .parallel() //并行
    12. .forEach(i->{
    13. list1.add(i);
    14. });
    15. stopWatch.stop();//一次统计的结束
    16. stopWatch.start("vector:read");//一次统计开始
    17. IntStream.rangeClosed(1,200000)
    18. .parallel() //并行
    19. .forEach(i->{
    20. list1.get(new Random().nextInt(200000));
    21. });
    22. stopWatch.stop();//一次统计的结束
    23. //======================
    24. List list2 = Collections.synchronizedList(new ArrayList<>());
    25. stopWatch.start("synchronizedList:write");//一次统计开始
    26. IntStream.rangeClosed(1,200000)
    27. .parallel() //并行
    28. .forEach(i->{
    29. list2.add(i);
    30. });
    31. stopWatch.stop();//一次统计的结束
    32. stopWatch.start("synchronizedList:read");//一次统计开始
    33. IntStream.rangeClosed(1,200000)
    34. .parallel() //并行
    35. .forEach(i->{
    36. list2.get(new Random().nextInt(200000));
    37. });
    38. stopWatch.stop();//一次统计的结束
    39. //==========
    40. List list3 = new CopyOnWriteArrayList();
    41. stopWatch.start("CopyOnWriteArrayList:write");//一次统计开始
    42. IntStream.rangeClosed(1,200000)
    43. .parallel() //并行
    44. .forEach(i->{
    45. list3.add(i);
    46. });
    47. stopWatch.stop();//一次统计的结束
    48. stopWatch.start("CopyOnWriteArrayList:read");//一次统计开始
    49. IntStream.rangeClosed(1,200000)
    50. .parallel() //并行
    51. .forEach(i->{
    52. list3.get(new Random().nextInt(200000));
    53. });
    54. stopWatch.stop();//一次统计的结束
    55. //获取统计结果输出
    56. System.out.println(stopWatch.prettyPrint());
    57. }

    获取统计结果输出:看得出来时间基本都是cow在写的时候用掉的 


    线程不安全Set

    1、HashSet演示线程不安全

    1. //HashSet线程不安全测试: ConcurrentModificationException
    2. Set set = new HashSet();
    3. //并发多个线程操作资源类对象
    4. for (int i = 0; i < 30; i++) {
    5. new Thread(()->{
    6. set.add(UUID.randomUUID().toString().substring(0,8));
    7. System.out.println(set);//set.toString()
    8. },i+"").start();
    9. }


    2、synchronizedSet 同步代码块加锁

    通过同步代码块加锁,迭代器没有加锁

    1. Set set = Collections.synchronizedSet(new HashSet<>());
    2. //并发多个线程操作资源类对象
    3. for (int i = 0; i < 30; i++) {
    4. new Thread(()->{
    5. set.add(UUID.randomUUID().toString().substring(0,8));
    6. System.out.println(set);//set.toString()
    7. },i+"").start();
    8. }
    9. SynchronizedSet 原理和SynchronizedList基本一样

      除了迭代器相关的方法 没有加锁,其他的方法都通过synchronized同步代码块加锁

      SynchronizedSet 优先使用


      3、CopyOnWriteArraySet 

      使用cow技术实现的并发安全的set

      1. CopyOnWriteArraySet set = new CopyOnWriteArraySet();
      2. //并发多个线程操作资源类对象
      3. for (int i = 0; i < 30; i++) {
      4. new Thread(()->{
      5. set.add(UUID.randomUUID().toString().substring(0,8));
      6. System.out.println(set);//set.toString()
      7. },i+"").start();
      8. }

      线程不安全Map

      1、HashMap演示线程不安全

      1. Map map = new HashMap();//ConcurrentModificationException
      2. //并发多个线程操作资源类对象
      3. for (int i = 0; i < 30; i++) {
      4. String s = i+"";
      5. new Thread(()->{
      6. map.put(s , UUID.randomUUID().toString().substring(0,8));
      7. System.out.println(map);//map.toString()
      8. },i+"").start();
      9. }


      2、HashTable保证线程安全

      1. //线程安全
      2. //Hashtable:和vector类似,所有的方法都通过synchronized在方法上加锁了
      3. Map map = new Hashtable();
      4. //并发多个线程操作资源类对象
      5. for (int i = 0; i < 30; i++) {
      6. String s = i+"";
      7. new Thread(()->{
      8. map.put(s , UUID.randomUUID().toString().substring(0,8));
      9. System.out.println(map);//map.toString()
      10. },i+"").start();
      11. }

      HashTable 所有的方法都加锁了


      3、SynchronizedMap

      1. //线程安全
      2. //SynchronizedMap: 和synchornizedList/set 类似, 所有的方法都通过同步代码块加锁了
      3. Map map = Collections.synchronizedMap(new HashMap<>());
      4. //并发多个线程操作资源类对象
      5. for (int i = 0; i < 30; i++) {
      6. String s = i + "";
      7. new Thread(() -> {
      8. map.put(s, UUID.randomUUID().toString().substring(0, 8));
      9. System.out.println(map);//map.toString()
      10. }, i + "").start();
      11. }

      SynchronizedMap: 所有的方法都通过同步代码块加锁,包括迭代器也加锁了


      4、ConcurrentHashMap 分段加锁

      采用分段加锁 技术加锁,效率高、并发线程安全。

      分段锁,就是将数据分段,对每⼀段数据分配⼀把锁。当⼀个线程占⽤锁访问其中⼀个段数据的时候,其他段的数据也能被其他线程访问
      在并发环境下将实现更⾼的吞吐量,⽽在单线程环境下只损失⾮常⼩的性能。
      1. //线程安全
      2. //ConcurrentHashMap: 分段加锁保证数据安全的同时可以提高CRUD的效率
      3. Map map = new ConcurrentHashMap();
      4. //并发多个线程操作资源类对象
      5. for (int i = 0; i < 30; i++) {
      6. String s = i + "";
      7. new Thread(() -> {
      8. map.put(s, UUID.randomUUID().toString().substring(0, 8));
      9. System.out.println(map);//map.toString()
      10. }, i + "").start();
      11. }

      ConcurrentHashMap: 开发中优先使用

      mysql的innodb存储引擎支持行级锁 和分段加锁技术类似

      分段加锁的原理:只对 Map的 tab中  某个索引位置顶点元素加锁

      ⼀个 ConcurrentHashMap ⾥包含⼀个 Segment 数组, Segment 的结构和 HashMap类似,是⼀种数组和链表结构。
      ⼀个Segment ⾥包含⼀个 HashEntry 数组,每个HashEntry是⼀个链表结构 的元素, 每个Segment 守护着⼀个 HashEntry 数组⾥的元素,当对HashEntry数组的数据进⾏修改时,必须⾸先获得它对应的 Segment 锁。
      1. /** Implementation for put and putIfAbsent */
      2. final V putVal(K key, V value, boolean onlyIfAbsent) {//方法未加锁
      3. if (key == null || value == null) throw new NullPointerException();
      4. int hash = spread(key.hashCode());//计算key的hashcode值
      5. int binCount = 0;
      6. for (Node[] tab = table;;) {//死循环 循环内的代码只有执行了break或者return或者抛出异常循环才会结束 tab 接收了table 也就是kv值构成的Node节点的数组
      7. Node f; int n, i, fh;
      8. if (tab == null || (n = tab.length) == 0)//判断Map的Node节点的数组是否为空
      9. tab = initTable();//初始化Map的Node节点数组 后面没有结束循环 所以继续执行下一次循环 通过n接收Node节点数组的长度
      10. else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//使用存入数据的key的hash和数组的长度-1 &运算 得到当前key在tab中应该存储的索引 最后从tab中根据索引获取元素
      11. //如果此行执行 代表key在tab中的索引位置没有存入元素
      12. //将key和value创建为Node对象 存入到tab的索引为i的位置(cas:compare and swap 利用c操作系统中硬件级别支持的函数保证线程安全)
      13. if (casTabAt(tab, i, null,
      14. new Node(hash, key, value, null)))
      15. break; // no lock when adding to empty bin
      16. }
      17. else if ((fh = f.hash) == MOVED)//Map数据正在迁移 不管
      18. tab = helpTransfer(tab, f);
      19. else {// 代表key在tab中要存入的索引位置存在元素
      20. V oldVal = null;
      21. //分段加锁的原理:只对 Map的 tab中 某个索引位置顶点元素加锁
      22. synchronized (f) {//f代表key在tab中要存储的位置的Node节点对象 使用同步代码块加锁
      23. if (tabAt(tab, i) == f) {//判断锁定的对象是否正确
      24. if (fh >= 0) {//将新增的k-v创建为node对象 添加到f的最后的下一个节点中
      25. binCount = 1;
      26. for (Node e = f;; ++binCount) {
      27. K ek;
      28. if (e.hash == hash &&
      29. ((ek = e.key) == key ||
      30. (ek != null && key.equals(ek)))) {
      31. oldVal = e.val;
      32. if (!onlyIfAbsent)
      33. e.val = value;
      34. break;
      35. }
      36. Node pred = e;
      37. if ((e = e.next) == null) {
      38. pred.next = new Node(hash, key,
      39. value, null);
      40. break;
      41. }
      42. }
      43. }
      44. else if (f instanceof TreeBin) {
      45. Node p;
      46. binCount = 2;
      47. if ((p = ((TreeBin)f).putTreeVal(hash, key,
      48. value)) != null) {
      49. oldVal = p.val;
      50. if (!onlyIfAbsent)
      51. p.val = value;
      52. }
      53. }
      54. }
      55. }
      56. if (binCount != 0) {
      57. if (binCount >= TREEIFY_THRESHOLD)
      58. treeifyBin(tab, i);
      59. if (oldVal != null)
      60. return oldVal;
      61. break;
      62. }
      63. }
      64. }
      65. addCount(1L, binCount);
      66. return null;
      67. }

      并发容器和同步容器

      1、并发容器

      并发容器:不直接加锁的通过其他方式保证并发线程安全的容器类。在jdk5.0引入了concurrent包,其中提供了很多并发容器

      ConcurrentHashMap:  分段加锁     
            内部采用Segment结构,进行两次Hash进行定位,写时只对Segment加锁

      CopyOnWriteArrayList/CopyOnWriteSet:写时复制技术

            写时复制一份新的容器,在新的上面修改,然后把引用指向新的。只能实现数据的最终一致性,非实时一致的;代替List,适用于读操作为主的情况


      2、同步容器

      同步容器可以简单地理解为通过synchronized来实现同步的容器。

      同步容器会导致多个线程中对容器方法调用的串行执行,降低并发性,因为它们都是以容器自身对象为锁。在并发下进行迭代的读和写时并不是线程安全的

      Vector、Stack、HashTable、Collections类的静态工厂方法创建的类(如Collections.synchronizedList)

      小总结:

      线程不安全使用ArrayList,线程安全使用synchronizedList

      线程不安全使用HashMap,线程安全 使用ConcurrentHashMap


      juc强大的辅助类

      juc的辅助类只能在单体应用中起作用,分布式集群启动的应用会失效

      将来Redis提供了Redisson,它提供了juc辅助类 对应的 分布式的辅助类

      使用场景:多个线程间需要协作通信时使用,不需要我们自己考虑线程间如何通信

      --------------------

      1、CountDownLatch:倒计数器

      作用:可以控制倒计数后执行特定的操作,但是只能进行一轮的倒计数

      常用方法:

      public CountDownLatch(int count)   //实例化一个倒计数器,count指定初始计数
      public void countDown()   // 每调用一次,计数减一
      public void await()   //等待,当计数减到0时,阻塞线程并行执行

      count值只能被设置⼀次,CountDownLatch没有提供任何机制去重新设置这个计数值。

      场景例如:在手机上安装一个应用程序,假如需要5个子进程检查服务授权,那么主进程会维护一个计数器,初始计数就是5。用户每同意一个授权该计数器减1,当计数减为0时,主进程才启动,否则就只有阻塞等待了。

      1. CountDownLatch cdl = new CountDownLatch(5);
      2. for (int i = 1; i <=5; i++) {
      3. new Thread(()->{
      4. System.out.println(Thread.currentThread().getName()+" 检查授权服务...");
      5. int anInt = new Random().nextInt(5);
      6. try {
      7. TimeUnit.SECONDS.sleep(anInt);
      8. } catch (InterruptedException e) {
      9. e.printStackTrace();
      10. }
      11. System.out.println(Thread.currentThread().getName()+" 检查完毕,"+"用时:"+anInt+"秒");
      12. cdl.countDown();//让countDownLatch的 count值-1
      13. },"子进程-"+i).start();
      14. }
      15. cdl.await();//阻塞当前线程 直到 cdl设置的count的值变为0才停止阻塞
      16. //主进程才启动
      17. System.out.println("主进程: 授权成功");

      场景二:
      玩游戏的时候,在游戏真正开始之前,⼀般会等待⼀些前置任务完成, ⽐如“ 加载地图数据 加载⼈物模型 加载背景⾳乐 等等。只有当所有的东⻄都加载完成后,玩家才能真正进⼊游戏。

      面试:CountDownLatch 与 join 方法的区别

      调用一个子线程的 join()方法后,该线程会一直被阻塞直到该线程运行完毕

      -----------

      CountDownLatch 使用计数器允许子线程运行完毕或者运行中时候递减计数,也就是 CountDownLatch 可以在子线程运行任何时候让 await 方法返回而不一定必须等到线程结束;另外使用线程池来管理线程时候一般都是直接添加 Runnable 到线程池这时候就没有办法在调用线程的 join 方法了,countDownLatch 相比 Join 方法让我们对线程同步有更灵活的控制。


      2、CyclicBarrier:循环栅栏

      意思就是一个可循环利用的屏障。该命令只在每个屏障点运行一次。可以看成多组倒计时器的组合

      常用方法

      1.CyclicBarrier(int parties, Runnable barrierAction) 创建一个CyclicBarrier实例,

      1. parties指定参与相互等待的线程数,
      2. barrierAction一个可选的Runnable命令,
      3. 该命令只在每个屏障点运行一次,可以在执行后续业务之前共享状态。
      4. 该操作由最后一个进入屏障点的线程执行。

      2.CyclicBarrier(int parties) 创建一个CyclicBarrier实例,parties指定参与相互等待的线程数。

      3.await() 设置栅栏,创建CyclicBarrier对象时执行的数量线程数都执行到此行时栅栏才会放行

      案例: 3个玩家玩游戏,所有人通过同一关后 才可以开始下一关 一共4关

      1. //参数1:参与者数量 参数2:每次所有参与者到达栅栏时的回调任务
      2. //回调任务谁执行:最后达到栅栏的子线程执行回调任务
      3. CyclicBarrier cb = new CyclicBarrier(3, () -> {
      4. System.out.println(Thread.currentThread().getName() + " 恭喜所有人过关.....");
      5. });
      6. for (int i = 1; i <= 3; i++) {
      7. new Thread(() -> {
      8. try {
      9. System.out.println(Thread.currentThread().getName() + "正在准备中");
      10. TimeUnit.SECONDS.sleep(new Random().nextInt(3));
      11. System.out.println(Thread.currentThread().getName() + "准备完毕");
      12. //所有玩家都准备好之后 开始第一关游戏
      13. cb.await();//设置栅栏 创建CyclicBarrier对象时执行的数量线程数都执行到此行时栅栏才会放行
      14. System.out.println(Thread.currentThread().getName() + "开始第一关");
      15. TimeUnit.SECONDS.sleep(new Random().nextInt(5));
      16. System.out.println(Thread.currentThread().getName() + "第一关通关");
      17. //所有玩家都通过第一关游戏才开始下一关
      18. cb.await();
      19. System.out.println(Thread.currentThread().getName() + "开始第2关");
      20. TimeUnit.SECONDS.sleep(new Random().nextInt(5));
      21. System.out.println(Thread.currentThread().getName() + "第2关通关");
      22. cb.await();
      23. System.out.println(Thread.currentThread().getName() + "开始第3关");
      24. TimeUnit.SECONDS.sleep(new Random().nextInt(5));
      25. System.out.println(Thread.currentThread().getName() + "第3关通关");
      26. cb.await();
      27. System.out.println(Thread.currentThread().getName() + "开始最后1关");
      28. TimeUnit.SECONDS.sleep(new Random().nextInt(5));
      29. System.out.println(Thread.currentThread().getName() + "最后1关通关");
      30. cb.await();
      31. } catch (Exception e) {
      32. e.printStackTrace();
      33. }
      34. }, "玩家: " + i).start();
      35. }

      控制台打印

      面试:CyclicBarrier和CountDownLatch的区别?

      CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置,可以使用多次,所以CyclicBarrier能够处理更为复杂的场景;CountDownLatch允许一个或多个线程等待一组事件的产生,而CyclicBarrier用于等待其他线程运行到栅栏位置


      3、Semaphore:信号量

      作用:控制资源数量,不能超过资源的数量,被使用的资源释放后可以被复用

      给定一个资源数目有限的资源池,假设资源数目为N,每一个线程均可获取一个资源,但是当资源分配完毕时,后来线程需要阻塞等待,直到前面已持有资源的线程释放资源之后才能继续

      常用方法

      public Semaphore(int permits) // 构造方法,permits指资源数目(信号量),默认情况下,是⾮公平的

      public void acquire() throws InterruptedException // 占用资源,当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量减1),要么一直等下去,直到有线程释放信号量,或超时。
      public void release() // (释放)实际上会将信号量的值加1,然后唤醒等待的线程。

      信号量主要用于两个目的:

      1. 多个共享资源的互斥使用。

      2. 用于并发线程数的控制。保护一个关键部分不要一次输入超过N个线程。

      Semaphore往往⽤于资源有限的场景中,去限制线程的数量。案例:停车场抢车位,10个车抢三个车位。使用Semaphore限制同时只能有3个线程在⼯作

      1. //构造器参数表示资源数量 为3个车位
      2. Semaphore s = new Semaphore(3);
      3. for (int i = 0; i < 10; i++) {
      4. new Thread(()->{
      5. try {
      6. System.out.println(Thread.currentThread().getName()+" 开始抢车位....");
      7. //如果抢到车位
      8. s.acquire();//尝试获取资源 获取到停止阻塞 否则一直阻塞直到获取资源成功
      9. s.tryAcquire(timeout,TimeUnit) 尝试获取资源 超过指定时间则中断
      10. System.out.println(Thread.currentThread().getName()+" 抢到车位....正在停车");
      11. int anInt = new Random().nextInt(8);
      12. TimeUnit.SECONDS.sleep(anInt);
      13. //释放停车位
      14. s.release();//释放资源
      15. System.out.println(Thread.currentThread().getName()+" 离开车位..."+"总计停车时长为"+anInt+"秒");
      16. } catch (InterruptedException e) {
      17. e.printStackTrace();
      18. }
      19. },"车辆:"+i).start();
      20. }

    10. 相关阅读:
      web前端期末大作业 ——电影主题介绍 你好,李焕英 ——html+css+javascript网页设计实例
      04-树6 Complete Binary Search Tree
      Linux CentOS 8(DNS的配置与管理)
      vue3的基本使用(超详细)
      p101的spring练习之用户列表展示2 ——for循环遍历
      WEB核心【请求转发(阶段重点)】第六章
      (附源码)小程序 智能养老系统平台 毕业设计 170900
      2014-2020年国有大型商业银行和全国股份制商业银行绿色信贷数据
      Web 部署
      【Redis】缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存击穿、缓存降级
    11. 原文地址:https://blog.csdn.net/m0_56799642/article/details/126585324