一.synchronized的功能扩展:重入锁(java.util.concurrent.locks.ReentrantLock)
重入锁可以完全替代synchronized关键字。在JDK 5.0的早期版本中,重入锁的性能远远好于synchronized,但从JDK 6.0开始,JDK在syn-chronized上做了大量的优化,使得两者的性能差距并不大。
01 public class ReenterLock implements Runnable{
02 public static ReentrantLock lock=new ReentrantLock();
03 public static int i=0;
04 @Override
05 public void run() {
06 for(int j=0;j<10000000;j++){
07 lock.lock();
08 try{
09 i++;
10 }finally{
11 lock.unlock();
12 }
13 }
14 }
15 public static void main(String[] args) throws InterruptedException {
16 ReenterLock tl=new ReenterLock();
17 Thread t1=new Thread(tl);
18 Thread t2=new Thread(tl);
19 t1.start();t2.start();
20 t1.join();t2.join();
21 System.out.println(i);
22 }
23 }
与synchronized相比,重入锁有着显示的操作过程。开发人员必须手动指定何时加锁,何时释放锁。也正因为这样,重入锁对逻辑控制的灵活性要远远好于synchronized。但值得注意的是,在退出临界区时,必须记得释放锁。
重入锁是可以反复进入的,这里的反复仅仅局限于一个线程。上述代码的第7~12行,可以写成下面的形式:
lock.lock();
lock.lock();
try{
i++;
}
finally{
lock.unlock();
lock.unlock();
}
重入锁还提供了一些高级功能。比如,重入锁可以提供中断处理的能力。
1.中断响应
对于synchronized来说,如果一个线程在等待锁,那么结果只有两种情况,要么它获得这把锁继续执行,要么它就保持等待。而使用重入锁,则提供另外一种可能,那就是线程可以被中断。也就是在等待锁的过程中,程序可以根据需要取消对锁的请求。
try {
lock.lockInterruptibly(); 申请锁(替换上面的 lock.lock();)
}catch{}
finally
{ if (lock.isHeldByCurrentThread()) {
lock1.unlock();
}
}
可通过 t2.interrupt(); 中断 进行通知 并捕获释放锁
2.锁申请等待限时
除了等待外部通知之外,要避免死锁还有一种方法,那就是限时等待。
我们可以使用tryLock()方法进行一次限时的等待
if(lock.tryLock(5, TimeUnit.SECONDS)) {}
else {}
tryLock()方法接收两个参数,一个表示等待时长,另外一个表示计时单位。
ReentrantLock.tryLock()方法也可以不带参数直接运行。在这种情况下,当前线程会尝试获得锁,如果锁并未被其他线程占用,则申请锁会成功,并立即返回true。如果锁被其他线程占用,则当前线程不会进行等待,而是立即返回false。这种模式不会引起线程等待,因此也不会产生死锁
3.公平锁
如果我们使用synchronized关键字进行锁控制,那么产生的锁就是非公平的。
而重入锁允许我们对其公平性进行设置。它有一个如下的构造函数:
public ReentrantLock(boolean fair)
当参数fair为true时,表示锁是公平的。公平锁看起来很优美,但是要实现公平锁必然要求系统维护一个有序队列,因此公平锁的实现成本比较高,性能相对也非常低下,因此,默认情况下,锁是非公平的。如果没有特别的需求,也不需要使用公平锁。公平锁和非公平锁在线程调度表现上也是非常不一样的。
对于非公平锁,根据系统的调度,一个线程会倾向于再次获取已经持有的锁,这种分配方式是高效的,但是无公平性可言。
对上面ReentrantLock的几个重要方法整理如下。
?lock():获得锁,如果锁已经被占用,则等待。
?lockInterruptibly():获得锁,但优先响应中断。
?tryLock():尝试获得锁,如果成功,返回true,失败返回false。该方法不等待,立即返回。
?tryLock(long time, TimeUnit unit):在给定时间内尝试获得锁。
?unlock():释放锁。
就重入锁的实现来看,它主要集中在Java层面。在重入锁的实现中,主要包含三个要素:
第一,是原子状态。原子状态使用CAS操作(在第4章进行详细讨论)来存储当前锁的状态,判断锁是否已经被别的线程持有。
第二,是等待队列。所有没有请求到锁的线程,会进入等待队列进行等待。待有线程释放锁后,系统就能从等待队列中唤醒一个线程,继续工作。
第三,是阻塞原语park()和unpark(),用来挂起和恢复线程。没有得到锁的线程将会被挂起。有关park()和unpark()的详细介绍,可以参考3.1.7线程阻塞工具类:LockSupport。
重入锁的好搭档:Condition条件
它和wait()和notify()方法的作用是大致相同的。但是wait()和notify()方法是和synchro-nized关键字合作使用的,而Condtion是与重入锁相关联的。通过Lock接口(重入锁就实现了这一接口)的Condition newCondition()方法可以生成一个与当前重入锁绑定的Condition实例。利用Condition对象,我们就可以让线程在合适的时间等待,或者在某一个特定的时刻得到通知,继续执行。
Condition接口提供的基本方法如下:
void await() throws InterruptedException;
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
void signal();
void signalAll();
?await()方法会使当前线程等待,同时释放当前锁,当其他线程中使用signal()或者sig-nalAll()方法时,线程会重新获得锁并继续执行。或者当线程被中断时,也能跳出等待。这和Object.wait()方法很相似。
?awaitUninterruptibly()方法与await()方法基本相同,但是它并不会在等待过程中响应中断。
?singal()方法用于唤醒一个在等待中的线程。相对的singalAll()方法会唤醒所有在等待中的线程。这和Obejct.notify()方法
在JDK内部,重入锁和Condition对象被广泛地使用,以ArrayBlockingQueue为例,它的put()方法实现如下:
二、允许多个线程同时访问:信号量(Semaphore)
信号量为多线程协作提供了更为强大的控制方法。广义上说,信号量是对锁的扩展。无论是内部锁synchronized还是重入锁ReentrantLock,一次都只允许一个线程访问一个资源,而信号量却可以指定多个线程,同时访问某一个资源。
信号量主要提供了以下构造函数:
public Semaphore(int permits)
public Semaphore(int permits, boolean fair) //第二个参数可以指定是否公平
主要方法有:
public void acquire()
public void acquireUninterruptibly()
public boolean tryAcquire()
public boolean tryAcquire(long timeout, TimeUnit unit)
public void release()
三、ReadWriteLock读写锁
private static ReentrantReadWriteLock readWriteLock=newReentrantReadWriteLock();
private static Lock readLock = readWriteLock.readLock();
private static Lock writeLock = readWriteLock.writeLock();
?读-读不互斥:读读之间不阻塞。
?读-写互斥:读阻塞写,写也会阻塞读。
?写-写互斥:写写阻塞。如果在系统中,读操作次数远远大于写操作,则读写锁就可以发挥最大的功效,提升系统的性能。
四、倒计时器:CountDownLatch
对于倒计时器,一种典型的场景就是火箭发射。在火箭发射前,为了保证万无一失,往往还要进行各项设备、仪器的检查。只有等所有的检查完毕后,引擎才能点火。这种场景就非常适合使用CountDownLatch。它可以使得点火线程等待所有检查线程全部完工后,再执行。
构造函数:
public CountDownLatch(int count)
CountDownLatch end = new CountDownLatch(10);//创建实例 计数数量为10。这表示需要有10个线程完成任务,等待在CountDownLatch上的线程才能继续执行。
end.countDown(); //通知Count-DownLatch,一个线程已经完成了任务,倒计时器可以减1啦。
end.await(); //等待 要求主线程等待所有10个检查任务全部完成。待10个任务全部完成后,主线程才能继续执行。
五、循环栅栏:CyclicBarrier
CyclicBarrier是另外一种多线程并发控制实用工具。和CountDownLatch非常类似,它也可以实现线程间的计数等待,但它的功能比CountDown-Latch更加复杂且强大。
前面Cyclic意为循环,也就是说这个计数器可以反复使用。比如,假设我们将计数器设置为10,那么凑齐第一批10个线程后,计数器就会归零,然后接着凑齐下一批10个线程,这就是循环栅栏内在的含义。
CyclicBarrier的使用场景也很丰富。比如,司令下达命令,要求10个士兵一起去完成一项任务。这时,就会要求10个士兵先集合报道,接着,一起雄赳赳气昂昂地去执行任务。当10个士兵把自己手头的任务都执行完成了,那么司令才能对外宣布,任务完成!
比CountDownLatch略微强大一些,CyclicBarrier可以接收一个参数作为barrierAc-tion。所谓barrierAction就是当计数器一次计数完成后,系统会执行的动作。如下构造函数,其中,parties表示计数总数,也就是参与的线程总数。
public CyclicBarrier(int parties, Runnable barrierAction)
CyclicBarrier cyclic = new CyclicBarrier(N, new BarrierRun(flag, N));
cyclic.await();
CyclicBarrier.await()方法可能会抛出两个异常。
一个是InterruptedException,也就是在等待过程中,线程被中断,应该说这是一个非常通用的异常。大部分迫使线程等待的方法都可能会抛出这个异常,使得线程在等待时依然可以响应外部紧急事件。
另外一个异常则是CyclicBarrier特有的BrokenBarrierException。一旦遇到这个异常,则表示当前的CyclicBarrier已经破损了,可能系统已经没有办法等待所有线程到齐了。如果继续等待,可能就是徒劳无功的,因此,还是就地散货,打道回府吧。
六、线程阻塞工具类:LockSupport
LockSupport是一个非常方便实用的线程阻塞工具,它可以在线程内任意位置让线程阻塞。和Thread.suspend()相比,它弥补了由于resume()在前发生,导致线程无法继续执行的情况。和Ob-ject.wait()相比,它不需要先获得某个对象的锁,也不会抛出InterruptedException异常。LockSupport的静态方法park()可以阻塞当前线程,类似的还有parkNanos()、parkUntil()等方法。它们实现了一个限时的等待。
LockSupport.park(); //阻塞线程t1
LockSupport.unpark(t1); //释放阻塞
当然,我们依然无法保证unpark()方法发生在park()方法之后。但是执行这段代码,你会发现,它自始至终都可以正常的结束,不会因为park()方法而导致线程永久性的挂起。
这是因为LockSupport类使用类似信号量的机制。它为每一个线程准备了一个许可,如果许可可用,那么park()函数会立即返回,并且消费这个许可(也就是将许可变为不可用),如果许可不可用,就会阻塞。而unpark()则使得一个许可变为可用(但是和信号量不同的是,许可不能累加,你不可能拥有超过一个许可,它永远只有一个)。这个特点使得:即使unpark()操作发生在park()之前,它也可以使下一次的park()操作立即返回。这也就是上述代码可顺利结束的主要原因。
同时,处于park()挂起状态的线程不会像sus-pend()那样还给出一个令人费解的Runnable的状态。它会非常明确地给出一个WAITING状态,甚至还会标注是park()引起的
此外,如果你使用park(Object)函数,还可以为当前线程设置一个阻塞对象。这个阻塞对象会出现在线程Dump中。这样在分析问题时,就更加方便了。在堆栈信息中,我们可以看出当前线程等待的对象。
除了有定时阻塞的功能外,LockSup-port.park()还能支持中断影响。但是和其他接收中断的函数很不一样,LockSupport.park()不会抛出InterruptedException异常。它只是会默默的返回,但是我们可以从Thread.interrupted()等方法获得中断标记。