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