实战Java高并发程序设计(三)JDK并发包
-
同步控制——重入锁
重入锁可以完全替代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();
,这也是为啥这叫冲入锁的原因,同一个线程可以多次获得锁,但是必须要多次释放该锁,否则其它线程无法进入该临界区。- 中断响应
ReentrantLock
的lockInterruptibly()
方法是一个可以对中断进行响应的锁申请动作,即在等待锁的过程中,可以响应中断。它对于处理死锁有一定的帮助。 -
锁申请等待限时
除了等待外部通知(例如给一个中断)之外,避免锁死还有另外一种方法,那就是限时等待。我们可以使用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(),用来挂起和恢复线程。没有得到锁的线程将会被挂起。 - 中断响应
-
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(); 信号量(Semaphore)
信号量是对锁的扩展,它能够制定多个线程同时访问某一个线程。申请信号量是用acquire()操作,离开临界区时,务必要使用release()释放信号量,否则会导致能进入临界区的线程越来越少,最后所有的线程均不可访问临界区。-
读写锁(ReadWriteLock)
在一个系统中,读-读不互斥、读-写互斥、写-写互斥,在读操作消耗远高于写消耗的情况下,读写分离能够有效地减少锁竞争,提升系统性能。我们可以通过以下方法来获得读锁(ReadLock)和写锁(WriteLock)。private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private static Lock readLock = readWriteLock.readLock();//获得读锁
private static Lock writeLock = readWriteLock.writeLock();//获得写锁 -
计数器(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);
}
} -
循环栅栏(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)能够指定等待的最晚时间。
   LockSupport使用了类似信号量的机制,它为每一个线程准备了一个许可,如果许可可用,那么park()方法就会立刻返回,否则就会阻塞。而unpark()方法则使得一个许可变为可用。
- 线程池
与进程相比,线程是一种轻量级的工具,但是其创建和关闭依然会花费时间。并且大量的线程会抢占内存资源,也会给GC带来很大压力。因此在实际项目中,线程的数量必须加以控制,盲目地创建线程可能会降低系统性能。