多线程(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()方法获得中断标记。

 

posted @ 2019-07-23 20:28  扁豆一号  阅读(402)  评论(0编辑  收藏  举报