• 多线程(4) — 同步控制


      关键字Synchronize是最简单的控制方法,决定了一个线程是否可以访问临界区资源。同时,Object.wait()方法和Object.notify()方法起到了线程等待和通知的作用。下面介绍重入锁:

      1. 重入锁

      重入锁使用java.util.concurrent.locks.ReentrantLock类来实现。重入锁有着显示的操作过程,必须手动指定何时加锁何时释放锁,在对逻辑灵活性控制方面优于Synchronize关键字。退出临界区时要释放锁,不然其他线程无法访问临界区。下面是个简单的例子:

    public class ReenLockDemo implements Runnable{
        public static ReentrantLock lock = new ReentrantLock();
        public static int i = 0;
        @Override
        public void run() {
            for(int j=0;j<100000;j++){
                lock.lock();
                try {
                    i++;
                }finally{
                    lock.unlock();
                }
            }
        }
        public static void main(String[] args) throws InterruptedException {
            ReenLockDemo t = new ReenLockDemo();
            Thread t1 = new Thread(t);
            Thread t2 = new Thread(t);
            t1.start();t2.start();
            t1.join();t2.join();
            System.out.println(i);
        }
    }

      重入锁有以下处理能力

      • 中断响应

    相对于Synchronize关键字来说,重入锁提供了另外一种可能,就是线程可以被中断,也就是在等待锁的过程中,程序可以根据需要取消对锁的请求,这对解决死锁问题有很大帮助。这时候请求锁就不用lock.lock()方法了,而是用lock.lockInterruptibly()方法,也就是说如果线程当遇到interrupt方法时会放弃对锁的请求,并释放资源。

      • 锁申请等待限时

    也许是因为死锁,也许是因为饥饿,线程总是得不到锁,如果我们需要等待一个时长,让线程自动放弃的话,就可以使用tryLock()方法。这个方法如果不传参数的话,直接就去请求锁,请求到了就返回true请求不到就返回false。还有一种就是传入参数tryLock(long timeout, TimeUnit unit),第一个参数是时长,第二个参数是时间单位,这个方法就是等待相应时长后如果仍然没有取得锁就会放弃锁。这个就很容易解决死锁的问题了。

      • 公平锁

    在Synchronize关键字中,线程是一起在请求锁,然后系统会随机选一个线程获得锁,这样就不能保证线程获得锁是公平的。而公平锁就是老老实实排队,先到先得,这样就避免了饥饿现象。使用就是在构造函数中定义这是个公平锁。

    public ReentrantLock(boolean fair)

    当fair是true时,表示锁是公平的。为了保证内部的有序,公平锁内部必然会维护一个有序队列,这样成本高,性能也会低下,一般情况下不建议使用公平锁。

    总结一下 ReentrantLock重入锁的几个方法:

    lock() 获得锁,如果锁被占用,则等待
    lockInterruptibly() 获得锁,如果锁被占,会等待,但会优先相应中断
    tryLock() 尝试获得锁,如果成功返回true,否则返回false,直接返回,不进行等待
    tryLock(long time,TimeUnit unit) 在给定的时间内尝试获得锁,如果成功返回true,否则返回false
    unlock() 释放锁

      2. 重入锁的好搭档Condition
        通过lock.newCondition()这个方法可以获得与lock关联的一个Condition,利用Condition对象可以让线程在合适的时间等待,或者在某一个特定的时刻得到通知,继续执行。下面介绍几个方法:

    await()方法 使当前线程等待,同时释放当前锁,当其他线程中使用signal()方法或signalAll()方法时,线程会重新获得锁并继续执行。或者当线程被中断时,也能跳出等待,和Object.wait()方法有点相似。
    awaitUninterrupttibly()方法 与await()方法基本相同,但是它不会在等待过程中响应中断。
    singal() 用于唤醒一个在等待中的线程,singalAll()方法会唤醒所有在等待中的线程。这和Object.notify()方法类似。
    package nc.vo.test;
    
    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class ReeterLockCondition implements Runnable {
     public static ReentrantLock lock = new ReentrantLock();
     public static Condition condition = lock.newCondition();
     @Override
     public void run() {
      try {
       this.lock.lock();
       this.condition.await();
       System.out.println("Thread is going on");
      } catch (InterruptedException e) {
       e.printStackTrace();
      }finally{
       this.lock.unlock();
      }
      
     }
     public static void main(String[] args) throws InterruptedException{
      ReeterLockCondition t = new ReeterLockCondition();
      Thread t1 = new Thread(t);
      t1.start();
      Thread.sleep(2000);
      lock.lock();
      condition.signal();
      lock.unlock();
     }
    }

        Condition.await()方法使用时,要求线程持有重入锁,在这个方法调用之后,这个线程会释放这把锁。同理Condition.signal()方法调用时,也要求线程获得相关锁,调用之后会从当前Condition对象的等待队列中唤醒一个线程。一旦线程被唤醒,会重新尝试获得与之绑定的重入锁,一旦成功获取,就可以继续执行了。因此,singal()方法调用后,一般需要释放相关锁,让给被唤醒的线程,让它继续执行。

     3. 允许多线程同时访问:信号量--Semaphore

    信号量是对锁的扩展,无论内部锁Synchronize还是重入锁ReentrantLock,一次只允许一个线程访问,而信号量却可以指定多个线程,同时访问一资源。信号量主要提供以下构造函数:

    public Semaphore(int permits)
    public Semaphore(int permits,boolean fair) //第二个参数指定是否公平

      构造函数中必须指定信号量的准入数,即同时能申请多少个许可,也就是指定同时可以有多少线程可访问同一个资源。其方法有如下几个:

    acquire()方法 尝试获得准入许可,若无法获得,则线程会等待,直到有线程释放许可,或当前线程中断
    acquireUninterruptibly()方法 与acquire()方法类似,但是不响应中断
    tryAcquire()方法 尝试获得许可,成功返回true否则返回false,不会等待,立即返回
    tryAcquire(long timeout, TimeUnit unit)方法 尝试获得许可,成功返回true否则返回false,可等待指定时长
    release()方法 用于线程访问资源结束以后释放一个许可

      4. ReadWriteLock  读写锁

      读写分离锁可以有效地帮助减少锁竞争,提升系统性能。由于读与读之间不对数据进行操作,不会破坏数据的完整性,因此读与读之间进行阻塞等待的话是不合理的,只有读与写或者写与写之间才需要线程进行阻塞。如果系统中读的次数远大于写,则读写锁可以发挥最大的功效了,提升系统性能。

    import java.util.Random;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    import java.util.concurrent.locks.ReentrantReadWriteLock;
    
    public class ReadWriteLockDemo {
    
        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 int value;
        public Object handleRead(Lock lock) throws InterruptedException{
            try {
                lock.lock();
                Thread.sleep(1000);
                return value;
            }finally{
                lock.unlock();
            }
        }
        
        public void handleWrite(Lock lock,int index) throws InterruptedException{
            try {
                lock.lock();
                Thread.sleep(1000);
                value = index;
            }finally{
                lock.unlock();
            }
        }
        
        public static void main(String[] args) {
            final ReadWriteLockDemo demo = new ReadWriteLockDemo();
            Runnable readRunnable = new Runnable(){
                
                @Override
                public void run() {
                    try {
                        demo.handleRead(readLock);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                
            };
            Runnable writeRunnable = new Runnable(){
    
                @Override
                public void run() {
                    try {
                        demo.handleWrite(writeLock,new Random().nextInt());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    
                }
                
            };
            
            for (int i = 0; i < 18; i++) {
                new Thread(readRunnable).start();
            }
            
            for (int i = 0; i < 18; i++) {
                new Thread(writeRunnable).start();
            }
        }
    }

      5. 倒计数器:CountDownLatch

        这个工具通常用来控制线程等待,让一个线程等待直到倒计数结束再开始执行。典型的例子就是发射火箭,为了保证万无一失,还要对各项设备、仪器进行检查,所有检查完毕后才能点火发射。点火线程必须等待所有的检查线程全部完毕后才能执行。且看下面这个代码例子:

    import java.util.Random;
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class CountDownLatchDemo implements Runnable {
        static final CountDownLatch end = new CountDownLatch(10);
        static final CountDownLatchDemo demo = new CountDownLatchDemo();
        
        @Override
        public void run() {
            try {
                //模拟检查任务
                Thread.sleep(new Random().nextInt());
                System.out.println("check complete!");
                end.countDown();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
        public static void main(String[] args) throws InterruptedException {
            ExecutorService exec = Executors.newFixedThreadPool(10);
            for (int i = 0; i < 10; i++) {
                exec.submit(demo);
            }
            //等待检查
            end.await();
            //发射火箭
            System.out.println("Fire");
            exec.shutdown();
        }
    }

      上述代码生成一个CountDownLatch实例,计数数量是10,表示需要10个线程完成任务以后CountDownLatch上的线程才能继续执行。countdown()方法就是通知CountDownLatch一个线程已经完成任务了,计数器减1。await()方法要求主线程等待所有检查任务全部完成,待10个任务全部完成后主线程才能继续执行。

    6. 循环栅栏  CyclicBarrier

      是另外一种多线程并发控制工具,与CountDownLatch类似,它也可以实现线程间的计算等待,但功能比CountDownLatch要强大,可以循环进行计数,也就是说这个计数器可以反复使用。例如一拨10个线程,等待了10个线程后,再等待下一拨10个线程,如此往复,这就是循环的意思。其构造函数如下:

    public CyclicBarrier(int parties, Runnable barrierAction)//第一个参数表示计数的总数也就是参与线程总数,第二个参数是一次计数完成后会执行的业务操作

      下面以士兵10个一排集合为例:

     1 import java.util.Random;
     2 import java.util.concurrent.BrokenBarrierException;
     3 import java.util.concurrent.CyclicBarrier;
     4 
     5 public class CyclicBarrierDemo {
     6 
     7     public static class Soldier implements Runnable{
     8         private String soldier;
     9         private final CyclicBarrier cyclic;
    10         public Soldier(CyclicBarrier cyclic,String name){
    11             this.cyclic = cyclic;
    12             this.soldier = name;
    13         }
    14         @Override
    15         public void run() {
    16             try {
    17                 cyclic.await();
    18             } catch (InterruptedException e) {
    19                 e.printStackTrace();
    20             } catch (BrokenBarrierException e) {
    21                 e.printStackTrace();
    22             }
    23         }
    24         public void work(){
    25             try {
    26                 Thread.sleep(Math.abs(new Random().nextInt()%10000));
    27             } catch (InterruptedException e) {
    28                 e.printStackTrace();
    29             }
    30             System.out.println(this.soldier+":任务完成");
    31         }
    32     }
    33     public static class BarrierRun implements Runnable{
    34         boolean flag;
    35         int N;
    36         public BarrierRun(boolean flag,int N){
    37             this.flag = flag;
    38             this.N = N;
    39         }
    40         @Override
    41         public void run() {
    42             if(flag){
    43                 System.out.println("司令:[士兵"+N+"个,任务完成!");
    44             }else{
    45                 System.out.println("司令:[士兵"+N+"个,集合完毕!");
    46                 this.flag = true;
    47             }
    48         }
    49     }
    50     
    51     public static void main(String[] args) {
    52         final int N = 10;
    53         Thread[] allSoldier = new Thread[N];
    54         boolean flag = false;
    55         CyclicBarrier cyclic = new CyclicBarrier(N,new BarrierRun(flag,N));
    56         System.out.println("集合队伍!");
    57         for (int i = 0; i < N; i++) {
    58             System.out.println("士兵"+i+"报道!");
    59             allSoldier[i] = new Thread(new Soldier(cyclic,"士兵"+i));
    60             allSoldier[i].start();
    61         }
    62     }
    63 }

      计数器设置为10,计数达到指标时BarrierRun的run()方法。每个士兵都会执行Solider的run()方法。集合完毕意味着一次计数完成,再次调用await()方法时会进行下次计数。CyclicBarrier.await()方法会抛出两个异常,一个是InterruptedException,也就是在等待中线程被中断。另外一个异常是BrokenBarrierException,表示当前的CyclicBarrier已经破损,可能系统已经没有办法等待所有线程齐了,一旦异常可以避免其他过多的不必要的等待。

    7.线程阻塞工具类:LockSupport

      这个工具可以在线程内任意位置让线程阻塞,与Thread.suspend()方法相比,它弥补了由于resume()方法导致线程无法继续执行的情况。和Object.wait()方法相比,它不需要先获得某个对象的锁,也不会抛出InterruptedException异常。LockSupport的静态方法park()可以阻塞当前线程,类似的还有parkNanos()、parkUntil()等方法。他们实现了一个限时的等待。

      LockSupport类使用了类似信号量的机制,为每个线程准备了一个许可,如果许可可用park()方法立即返回,并且消费这个许可,如果不可用,就会阻塞,而unpark()方法是的一个许可变为可用,即使unpark()方法在park()方法之前执行,也可以使下一次的park()操作立即返回、这也是park()方法不会挂起的原因。

      park()方法可以被中断,但是中断后默默返回,不会抛出异常,但是可以在Thread.interruptedI()方法获得中断标记。

     

  • 相关阅读:
    验证email的正则表达式
    时间管理的小技巧
    如何在项目中进行畅快的沟通
    为什么你总成为不了架构师?
    我的时间管理Color My Time
    《重来》值得你多看几遍
    程序员如何成为一位出色的项目经理?
    大学毕业后拉开差距的真正原因
    [精华] FreeBSD-FAQ集锦(一)
    awk 常用信息
  • 原文地址:https://www.cnblogs.com/wangyongwen/p/11223069.html
Copyright © 2020-2023  润新知