乐趣区

关于java:Java同步组件之CyclicBarrierReentrantLock

[TOC]

Java 同步组件详情

  • CountDownLatch : 是闭锁, 通过一个计数来保障线程是否始终阻塞
  • Semaphore: 管制同一时间, 并发线程数量
  • CyclicBarrier: 字面意思是回环栅栏, 通过它能够实现让一组线程期待至某个状态之后再全副同时执行。
  • ReentrantLock: 是一个重入锁, 一个线程取得了锁之后依然能够重复加锁, 不会呈现本人阻塞本人的状况。
  • Condition: 配合ReentrantLock, 实现期待 / 告诉模型
  • FutureTask:FutureTask 实现了接口 Future,同 Future 一样,代表异步计算的后果。

CyclicBarrier介绍

CycliBarrier是一个同步辅助类, 它容许一组线程互相期待, 直到达到某个公共的屏障点 (common barrier point), 也称之为栅栏点。通过它能够多个线程之间的互相期待, 只有当每个线程都准备就绪后, 能力各自实现后续的操作。它和CountDownLatch 有类似的中央, 都是通过计数器实现。当某个线程调用 await() 办法之后, 该线程就进去了期待状态, 计数器执行的是加一操作, 当计数器达到初始值, 后面调用 await() 的线程会被唤醒, 继续执行前面的操作。因为 CyclicBarrier 在期待线程开释后, 能够被重用, 所以被称为循环屏障。

CountDownLatch 比拟

相同点

  • 都是同步辅助类
  • 应用计数器实现

不同点

  • CyclicBarrier容许一个或多个线程, 期待其它一组线程实现操作, 再继续执行。
  • CyclicBarrier容许一组线程之间互相期待, 达到一个共同点, 再继续执行。
  • CountDownLatch不能被复用。
  • CyclicBarrier适宜更简单的业务场景, 如计算产生谬误, 通过重置计数器, 并让线程从新执行。
  • CyclicBarrier还提供其它有用的办法, 比方 getNumberWaiting 办法能够取得 CyclicBarrier 阻塞的线程数量,isBroken办法用来晓得阻塞的线程是否被中断。

CountDownLatchCyclicBarrier 的场景比拟

CyclicBarrier : 好比一扇门,默认状况下敞开状态,堵住了线程执行的路线,直到所有线程都就位,门才关上,让所有线程一起通过。

CyclicBarrier能够用于多线程计算数据,最初合并计算结果的利用场景。比方咱们用一个 Excel 保留了用户所有银行流水,每个 Sheet 保留一个帐户近一年的每笔银行流水,当初须要统计用户的日均银行流水,先用多线程解决每个 sheet 里的银行流水,都执行完之后,失去每个 sheet 的日均银行流水,最初,再用 barrierAction 用这些线程的计算结果,计算出整个 Excel 的日均银行流水。

CountDownLatch : 监考老师发下去试卷,而后坐在讲台旁边玩着手机期待着学生答题,有的学生提前交了试卷,并约起打球了,等到最初一个学生交卷了,老师开始整顿试卷,贴封条

代码演示

package com.rumenz.task.CyclicBarrier;

import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;


public class CyclicBarrierExample {public static void main(String[] args) {CyclicBarrier cyclicBarrier=new CyclicBarrier(2,new Runnable(){
            @Override
            public void run() {System.out.println("汇总计算 ----");
            }
        });

        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(()->{

            try{System.out.println("计算昨天的数据");
                Thread.sleep(5000);
                cyclicBarrier.await();}catch (Exception e){e.printStackTrace();
            }

        });

        executorService.execute(()->{
            try{System.out.println("计算明天的数据");
                Thread.sleep(3000);
                cyclicBarrier.await();}catch (Exception e){e.printStackTrace();
            }

        });
        executorService.shutdown();}
}
// 计算昨天的数据
// 计算明天的数据
// 汇总计算 ----

ReentrantLock可重入锁

JAVA中的锁分两类:synchronized关键字与 J.U.C 所提供的锁。J.U.C的外围锁是 ReentrantLock, 实质上都是lockunlock的操作。

ReentrantLock(可重入锁)和 synchronized 的区别

可重入性:ReentrantLock 字面意思即为再进入锁,称为可重入锁,其实 synchronize 所应用的锁也是能够重入的,两者对于这个区别不大,它们都是同一个线程进入一次,锁的计数器进行自增,要等到锁的计数器降落为零时,能力开释锁

锁的实现:synchronized 依赖于 JVM 实现无奈理解底层源码,而 ReentrantLock 基于 JDK 实现。通过浏览源码,区别就相似于操作系统管制实现与用户应用代码实现。

性能区别:在 synchronized 优化以前,性能比 ReentrantLock 差很多,但自从 synchronize 引入了偏差锁、轻量级锁(自选锁)后,也就是自循锁后,两者性能差不多(JDK1.6 当前,为了缩小取得锁和开释锁所带来的性能耗费,进步性能,引入了“轻量级锁”和“偏差锁”)。在两种场景下都能够应用,官网更举荐应用 synchronized,因为写法更容易。synchronized 的优化其实是借鉴了 ReentrantLock 中的 CAS 技术,都是试图在用户态就把加锁问题解决,防止进入内核态的线程阻塞。

ReentrantLocksynchronized 的性能区别

  • synchronized更加便当, 它由编译器保障加锁与开释。ReentrantLock须要手动申明和开释锁, 所以为了防止遗记手动开释锁造成死锁, 所以最好在 finally 中申明开释锁。
  • ReentrantLock的锁粒度更细更灵便。

ReentrantLock特有性能

  • ReentrantLock能够指定为偏心或者非偏心,synchronized是能是非偏心锁。(偏心锁的意思就是先期待的锁先取得锁)
  • 提供一个 Condition 类, 它能够分组唤醒须要唤醒的线程。不像 synchronized 要么随机唤醒一个, 要么全副唤醒。
  • 提供可能中断期待锁的线程的机制,通过 lock.lockInterruptibly()实现,这种机制 ReentrantLock 是一种自选锁,通过循环调用 CAS 操作来实现加锁。性能比拟好的起因是防止了进入内核态的阻塞状态。想进方法防止线程进入内核阻塞状态,是咱们剖析和了解锁设计的要害

如果满足 ReentrantLock 三个独有的性能,那么必须应用 ReentrantLock。其余状况下能够依据性能、业务场景等等来抉择 synchronized 还是 ReentrantLock

synchronized的应用场景

 synchronized 能做的,ReentrantLock 都能做;而 ReentrantLock 能做的,而 synchronized 却不肯定做得了。性能方面,ReentrantLock 不比 synchronized 差

  • J.U.C 包中的锁定类是用于高级状况和高级用户的工具,除非说你对 Lock 的高级个性有特地分明的理解以及有明确的须要,或这有明确的证据表明同步曾经成为可伸缩性的瓶颈的时候,否则咱们还是持续应用 synchronized
  • 相比拟这些高级的锁定类,synchronized 还是有一些劣势的,比方 synchronized 不可能遗记开释锁。在退出 synchronized 块时,JVM 会主动开释锁,会很容易遗记要应用 finally 开释锁,这对程序十分无害。
  • 还有当 JVM 应用 synchronized 治理锁定申请和开释时,JVM 在生成线程转储时可能包含锁定信息,这些信息对调试十分有价值,它们能够标识死锁以及其余异样行为的起源。而 Lock 类常识一般的类,JVM 不晓得哪个线程具备 Lock 对象,而且简直每个开发人员都是比拟相熟 synchronized

代码演示

package com.rumenz.task;

import com.google.common.net.InternetDomainName;

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.Lock;
import java.util.concurrent.locks.ReentrantLock;


public class ReentrantLockExample {
    private static Integer clientTotal=5000;
    private static Integer threadTotal=200;
    public static Integer count=0;
    // 申明锁的实例, 调用构造方法,默认生成一个不偏心的锁 
    private final static Lock lock=new ReentrantLock();

    public static void main(String[] args) throws Exception{ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore=new Semaphore(threadTotal);
        final CountDownLatch countDownLatch=new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {executorService.execute(()->{
                   try{semaphore.acquire();
                       update();
                       semaphore.release();}catch (Exception e){e.printStackTrace();
                   }
            countDownLatch.countDown();});
        }
        countDownLatch.await();
        executorService.shutdown();
        System.out.println("count:"+count);

    }

    private static void update() {lock.lock();
        try{count++;}finally {lock.unlock();
        }

    }
}
//count:5000

ReentrantLock罕用办法

tryLock():仅在调用时锁定未被另一个线程放弃的状况下才获取锁定。tryLock(long timeout, TimeUnit unit):如果锁定在给定的工夫内没有被另一个线程放弃且以后线程没有被中断,则获取这个锁定。lockInterruptbily():如果以后线程没有被中断的话,那么就获取锁定。如果中断了就抛出异样。isLocked():查问此锁定是否由任意线程放弃
isHeldByCurrentThread:查问以后线程是否放弃锁定状态。isFair:判断是不是偏心锁

ReentrantReadWriteLock 读写锁

public class ReentrantReadWriteLock
    implements ReadWriteLock, java.io.Serializable {
    /** 外部类提供的读锁 */
    private final ReentrantReadWriteLock.ReadLock readerLock;
    /** 外部类提供的读锁 */
    private final ReentrantReadWriteLock.WriteLock writerLock;
}

ReentrantReadWriteLock提供了 ReadLockWriteLock, 在没有任何读写锁时, 才能够获得写入锁。如果进行读取时, 可能有另外一个写入的申请, 为了放弃同步, 读取锁定。

ReentrantReadWriteLock写锁是互斥的, 也就是说, 读和读是不互斥的, 然而读和写, 写和读是互斥的。

在没有任何读写锁的时候才能够获得写入锁(乐观读取,容易写线程饥饿),也就是说如果始终存在读操作,那么写锁始终在期待没有读的状况呈现,这样我的写锁就永远也获取不到,就会造成期待获取写锁的线程饥饿。所以,此类不能乱用,在应用是肯定要把握其个性与实现形式。

ReentrantReadWriteLock 是 Lock 的另一种实现形式,咱们曾经晓得了 ReentrantLock 是一个排他锁,同一时间只容许一个线程拜访,而 ReentrantReadWriteLock 容许多个读线程同时拜访,但不容许写线程和读线程、写线程和写线程同时拜访。绝对于排他锁,进步了并发性。在理论利用中,大部分状况下对共享数据(如缓存)的拜访都是读操作远多于写操作,这时 ReentrantReadWriteLock 可能提供比排他锁更好的并发性和吞吐量。

ReentrantReadWriteLock代码演示

package com.rumenz.task.reentrant;

import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
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.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;


public class ReentrantLockExample {

    private static Integer clientTotal=5000;
    private static Integer threadTotal=200;
    private static LockMap map=new LockMap();

    public static void main(String[] args) throws Exception {ExecutorService executorService = Executors.newCachedThreadPool();
        final CountDownLatch countDownLatch=new CountDownLatch(clientTotal);
        final Semaphore semaphore=new Semaphore(threadTotal);
        for (int i = 0; i < 2500; i++) {
            final Integer m=i;
            executorService.execute(()->{
                try{semaphore.acquire();
                    map.put(m+"",m+"");
                    semaphore.release();}catch (Exception e){e.printStackTrace();
                }
                countDownLatch.countDown();});
        }
        for (int j = 0; j< 2500; j++) {
            final Integer n=j;
            executorService.execute(()->{executorService.execute(()->{
                    try{semaphore.acquire();
                        String s = map.get(n + "");
                        System.out.println("===="+s);
                        semaphore.release();}catch (Exception e){e.printStackTrace();
                    }
                    countDownLatch.countDown();});
            });

        }

        countDownLatch.await();

        executorService.shutdown();}

}
// 线程平安的一个 Map
class LockMap {private final Map<String,String> map=new TreeMap<>();
    // 申明读写锁
    private final ReentrantReadWriteLock reentrantReadWriteLock=new ReentrantReadWriteLock();
    // 取得读写锁中的读锁
    private final Lock rlock=reentrantReadWriteLock.readLock();
    // 取得读写锁中的写锁
    private final Lock wlock=reentrantReadWriteLock.writeLock();

    // 读取数据
    public String get(String key){rlock.lock();
        try{return map.get(key);
        }finally {rlock.unlock();
        }
    }
    // 写入数据
    public String put(String k,String v){wlock.lock();
        try{return map.put(k,v);
        }finally {wlock.unlock();
        }
    }
    // 读取数据
    public Set<String> getAllKeys(){rlock.lock();
        try {return map.keySet();
        }finally {rlock.unlock();
        }
    }
}

StampedLock介绍

在 JDK1.8 中,新增 StampedLock,它是 ReentrantReadWriteLock 的增强版,是为了解决 ReentrantReadWriteLock 的一些有余。正因为 ReentrantReadWriteLock 呈现了读和写是互斥的状况,须要优化,因而就呈现了 StampedLock!

它管制锁有三种模式(写、读、乐观读)。一个 StempedLock 的状态是由版本和模式两个局部组成。锁获取办法返回一个数字作为票据(stamp),他用相应的锁状态示意并管制相干的拜访。数字 0 示意没有写锁被锁写访问,在读锁上分为乐观锁和乐观锁。

乐观读:如果读的操作很多写的很少,咱们能够乐观的认为读的操作与写的操作同时产生的状况很少,因而不乐观的应用齐全的读取锁定。程序能够查看读取材料之后是否受到写入材料的变更,再采取之后的措施。

它的思维是读写锁中读不仅不阻塞读,同时也不应该阻塞写。在读的时候如果产生了写,则该当重读而不是在读的时候间接阻塞写。应用 StampedLock 就能够实现一种无障碍操作,即读写之间不会阻塞对方,然而写和写之间还是阻塞的

StampedLock源码中的一个案例

package com.rumenz.task.stampedLock;
import java.util.concurrent.locks.StampedLock;

class Point {
    private double x, y;
    // 锁实例
    private final StampedLock sl = new StampedLock();
    // 排它锁 - 写锁(writeLock)void move(double deltaX, double deltaY) {long stamp = sl.writeLock();
        try {
            x += deltaX;
            y += deltaY;
        } finally {sl.unlockWrite(stamp);
        }
    }

    // 乐观读锁(tryOptimisticRead)double distanceFromOrigin() {
        // 尝试获取乐观读锁(1)long stamp = sl.tryOptimisticRead();
        // 将全副变量拷贝到办法体栈内(2)将两个字段读入本地局部变量
        double currentX = x, currentY = y;
        // 查看在(1)获取到读锁票据后,锁有没被其余写线程排它性抢占(3)if (!sl.validate(stamp)) {
            // 如果被抢占则获取一个共享读锁(乐观获取)(4)stamp = sl.readLock();
            try {currentX = x; // 将两个字段读入本地局部变量(5)
                currentY = y; // 将两个字段读入本地局部变量(5)
            } finally {
                // 开释共享读锁(6)sl.unlockRead(stamp);
            }
        }
        // 返回计算结果(7)return Math.sqrt(currentX * currentX + currentY * currentY);
    }

    // 应用乐观锁获取读锁,并尝试转换为写锁
    void moveIfAtOrigin(double newX, double newY) {
        // 这里能够应用乐观读锁替换(1)long stamp = sl.readLock();
        try {
            // 如果以后点在原点则挪动(2)while (x == 0.0 && y == 0.0) {
                // 尝试将获取的读锁降级为写锁(3)long ws = sl.tryConvertToWriteLock(stamp);
                // 降级胜利,则更新票据,并设置坐标值,而后退出循环(4)if (ws != 0L) { // 这是确认转为写锁是否胜利
                    stamp = ws; // 如果胜利 替换票据
                    x = newX; // 进行状态扭转
                    y = newY; // 进行状态扭转
                    break;
                }
                else {
                    // 读锁降级写锁失败则开释读锁,显示获取独占写锁,而后循环重试(5)sl.unlockRead(stamp); // 咱们显式开释读锁
                    stamp = sl.writeLock(); // 显式间接进行写锁 而后再通过循环再试}
            }
        } finally {sl.unlock(stamp); // 开释读锁或写锁(6)
        }
    }
}

总结

  • synchronized 是在 JVM 层面上实现的,岂但能够通过一些监控工具监控 synchronized 的锁定,而且在代码执行时出现异常,JVM 会主动开释锁定;
  • ReentrantLock、ReentrantReadWriteLock,、StampedLock 都是对象层面的锁定,要保障锁定肯定会被开释,就必须将 unLock()放到 finally{}中;
  • StampedLock 对吞吐量有微小的改良,特地是在读线程越来越多的场景下;
  • StampedLock 有一个简单的 API,对于加锁操作,很容易误用其余办法;
  • 当只有大量竞争者的时候,synchronized 是一个很好的通用的锁实现;
  • 当线程增长可能预估,ReentrantLock 是一个很好的通用的锁实现;

关注微信公众号:【入门小站】, 解锁更多知识点

退出移动版