实战Java高并发程序设计(三)JDK并发包

时间:2022-04-25 23:50:45

实战Java高并发程序设计(三)JDK并发包


  1. 同步控制——重入锁
       重入锁可以完全替代synchronized关键字。其使用方法如下:

    public ReentrantLock lock = new Reentrantlock();
    public void run(){
    lock.lock();
    lock.lock();
    try{
    do something...
    }finally{//为了保证该线程执行完临界区代码后能释放锁,将unlock放在finally中
    lock.unlock();
    lock.unlock();
    }
    }

       由于其通过人工进行lock和unlock,因此比synchronized更好控制临界区。
       注意,这段代码有两个lock.lock();,这也是为啥这叫冲入锁的原因,同一个线程可以多次获得锁,但是必须要多次释放该锁,否则其它线程无法进入该临界区。

    • 中断响应
         ReentrantLocklockInterruptibly()方法是一个可以对中断进行响应的锁申请动作,即在等待锁的过程中,可以响应中断。它对于处理死锁有一定的帮助。
    • 锁申请等待限时
         除了等待外部通知(例如给一个中断)之外,避免锁死还有另外一种方法,那就是限时等待。我们可以使用tryLock()方法进行一次限时等待。

      public static ReentrantLock lock = new ReentrantLock();
      public void run(){
      try{
      if(lock.tryLock(5,TimeUnit.SECONDS)){
      Thread.sleep(6000);
      }else{
      System.out.println("get lock failed");
      }
      }catch(InterruptedException e){
      e.printStackTrance();
      }finally{
      if(lock.isHeldByCurrentThread())//lockInterruptibly()与tryLock()一样,在释放前要判断当前线程是否获得该锁资源。
      lock.unlock();
      }
      }

         如果tryLock()方法没有携带任何参数,那么默认不进行等待,这样也不会发生死锁。

    • 公平锁
         它会按照时间先后,保证先到着优先获得该锁,而不考虑其优先级。它的最大特点是,不会产生饥饿现象。而synchronized关键字产生的锁就是非公平的。
         重入锁有一下构造函数:

      public ReentrantLock(boolean fair)

         当fair为true时,表示公平锁。要注意,实现公平锁需要系统维护一个有序队列,因此公平锁的性能相对较低。在非公平锁的情况下,根据系统的调度,一个线程会倾向于再次获得已经持有的锁,即在多个具有相同优先级的线程连续抢占同一把锁时,很容易发生同一个线程连续获得该锁的情况,这种分配方式无疑是高效的,但不公平。

       在重入锁的实现中,主要包含三个要素:
       第一是原子状态。原子状态使用CAS操作来存储当前锁的状态,判断锁是已经被被的线程持有。
       第二是等待队列。所有没有请求到锁的线程,会进入等待队列进行等待。待有线程释放锁后,系统就能从等待队里中唤醒一个线程,继续工作。
       第三是阻塞原语park()和unpark(),用来挂起和恢复线程。没有得到锁的线程将会被挂起。

  2. Condition条件
       wait()和notify()方法是和synchronized关键字合作使用的,而Condition的await()和signal()是与重入锁相关联的。
       当线程使用Condition.await()时,要求线程持有相关的重入锁,在Condition.await()调用后,这个线程会释放这把锁。同理,在Condition.signal()方法调用时,也要求线程先获得相关的锁。在signal()方法调用后,系统会从当前Condition对象的等待队列中,唤醒一个线程。一旦线程被唤醒,它会重新尝试获得与之绑定的重入锁,一旦成功获取,就可以继续执行了。因此,在signal()方法调用之后,还需要释放先关的锁

    public ReentrantLock lock = new ReentrantLock();
    public Condition condition = lock.newCondition();
  3. 信号量(Semaphore)
       信号量是对锁的扩展,它能够制定多个线程同时访问某一个线程。申请信号量是用acquire()操作,离开临界区时,务必要使用release()释放信号量,否则会导致能进入临界区的线程越来越少,最后所有的线程均不可访问临界区。

  4. 读写锁(ReadWriteLock)
       在一个系统中,读-读不互斥、读-写互斥、写-写互斥,在读操作消耗远高于写消耗的情况下,读写分离能够有效地减少锁竞争,提升系统性能。我们可以通过以下方法来获得读锁(ReadLock)和写锁(WriteLock)。

    private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private static Lock readLock = readWriteLock.readLock();//获得读锁
    private static Lock writeLock = readWriteLock.writeLock();//获得写锁
  5. 计数器(CountDownLatch)
    这个工具通常用来控制线程等待,它可以让某一个线程等待,直到计数结束再执行。比如有4个线程跑4个任务A、B、C、D,D任务需要ABC都完成之后才能执行,此时就能够使用CountDownLatch。一下例子输出4中模拟读写锁的总耗时。

    public class test {

    private static Lock lock = new ReentrantLock();
    private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private static Lock readLock = readWriteLock.readLock();
    private static Lock writeLock = readWriteLock.writeLock();
    private static int value = 0;
    private static CountDownLatch latch = new CountDownLatch(40);

    public void read(Lock lock){
    try{
    lock.lock();
    Thread.sleep(1000);
    System.out.println("read:"+value);
    }
    catch (InterruptedException e) {
    e.printStackTrace();
    }finally{
    lock.unlock();
    }
    }

    public void write(Lock lock,int val){
    try{
    lock.lock();
    value = val;
    System.out.println("write:"+value);
    }finally{
    lock.unlock();
    }
    }

    public static void main(String[] args) throws InterruptedException {
    final test t = new test();
    Runnable readRunnable = ()->{
    t.read(readLock);
    latch.countDown();//计数器减一,也代表完成了一个任务
    };

    Runnable writeRunnable = ()->{
    t.write(writeLock, new Random().nextInt(100));
    latch.countDown();//计数器减一,也代表完成了一个任务
    };
    long beg = new Date().getTime();
    for(int i = 0 ;i<20;i++){
    Thread th = new Thread(readRunnable);
    th.start();
    }

    for(int j = 0 ; j< 20 ;j++){
    Thread th = new Thread(writeRunnable);
    th.start();
    }


    latch.await();//如果计数器没到0,则阻塞;当计数器到0时,则继续执行。
    System.out.println(System.currentTimeMillis()-beg);
    }
    }
  6. 循环栅栏(CyclicBarrier)
       CyclicBarrier是CountDownLatch的加强版,它能够循环计数,每次计数完成之后,会执行指定的方法。其构造函数如下:

    public CyclicBarrier(int parties, Runnable barrierAction)

   其中parties用来指定线程数量,barrierAction用来指定每次计数完成之后,执行的函数。注意:这个函数由这轮计数,最后一个到来的线程执行。
7. 线程阻塞工具类(LockSupport)
   LockSupport是一个非常方便的线程阻塞工具,它可以在线程内任意位置让线程阻塞。但是它不像suspend那样会导致多个线程死锁,也不像wait和notify那样需要先获得某个对象锁。

> LockSupport.park()方法可以阻塞当前线程
> LockSupport.parkNanos(long nanos)能够实现一个限时等待。
> LockSupport.parkUntil(long deadline)能够指定等待的最晚时间。

&ensp; &ensp;LockSupport使用了类似信号量的机制,它为每一个线程准备了一个许可,如果许可可用,那么park()方法就会立刻返回,否则就会阻塞。而unpark()方法则使得一个许可变为可用。
  1. 线程池
       与进程相比,线程是一种轻量级的工具,但是其创建和关闭依然会花费时间。并且大量的线程会抢占内存资源,也会给GC带来很大压力。因此在实际项目中,线程的数量必须加以控制,盲目地创建线程可能会降低系统性能。