logo头像
Snippet 博客主题

JDK并发包

1.同步控制

1.1重入锁

重入锁使用java.util.concurrent.locks.ReentrantLock类来实现的,它完全可以替代synchronized关键字。与synchronized相比,重入锁有着显式的操作过程,开发人员必须手动指定何时加锁,何时释放锁。所以,重入锁对逻辑控制的灵活性要远远好于synchronized。重入锁对于一个线程是可以反复进入的,也就是说,一个线程可以多次获得同一把锁,只不过需要注意的是,如果同一个线程多次获得锁,那么在释放锁的时候,也必须释放相同的次数。

重入锁的功能:

①中断响应

在等待重入锁的过程中,程序可以根据需要取消对锁的请求。过程就是,如果一个线程正在等待锁,那么它依然可以收到一个通知,被告知无须再等待,可以停止工作了。这种情况对死锁是有一定帮助的。

在对锁的请求时,使用lockInterruptibly()方法,这是一个可以对锁中断进行响应的锁申请动作,即在等待锁的过程中,可以调用interrupt()方法,线程就会响应并中断对锁的等待。

②锁申请等待限时

通过调用tryLock()方法设置线程申请锁的等待时间,即限时等待,过了这个等待时间,让线程自动放弃,这也是一种避免死锁的方式。

tryLock()方法接收两个参数,一个表示等待时长,另外一个表示计时单位。当然,tryLock()方法也可以不接收参数直接运行。这种情况下,当前线程会尝试获得锁,如果锁并未被其它线程占用,则申请锁会成功,并立即返回true;如果锁被其它线程占用,则当前线程不会进行等待,而是立即返回false。这种模式下也不会一起线程等待,因此也不会产生死锁。

③公平锁

在大多数情况下,锁的申请是非公平的。也就是说,系统只是会从这个锁的等待队列中随机挑选一个,因此不能保证其公平性。而公平锁,则不是这样,它会按照时间的先后顺序,保证先到者先得,后到者后得。公平锁的一大特点是:它不会产生饥饿现象。

可以在创建重入锁,new ReentrantLock(true)的时候,在构造方法中传入一个true,来标识这个重入锁是公平的。要实现公平锁必然要求系统维护一个有序队列,因此公平锁的实现成本比较高,性能也相对非常低下。故在默认情况下,锁是非公平的。

ReentrantLock几个重要的方法:

①lock():获得锁,如果锁已经被占用,则等待;

②lockInterruptibly():获得锁,但优先响应中断;

③tryLock():尝试获得锁,如果成功,返回true,失败返回false。该方法不等待,立即返回;

④tryLock(long time, TimeUnit unit):在给定时间内尝试获得锁;

⑤unlock():释放锁。

在重入锁的实现中,主要包含三个要素:

①是原子状态。原子状态使用CAS操作来存储当前锁的状态,判断是否已经被别的线程持有;

②是等待队列。所有没有请求到锁的线程,会进入等待队列进行等待。待有线程释放锁后,系统就能从等待队列中唤醒一个线程,继续工作;

③是阻塞原语park()和unpark(),用来挂起和恢复线程,没有得到锁的线程将会被挂起。

1.2Condition条件

Condition对象的作用其实和wait()和notify()方法的作用是大致相同的,只不过wait()和notify()是配合synchronized关键字使用的,而Condition是配合重入锁使用的。通过ReentrantLock接口的Condition newCondition()方法可以生成一个与当前重入锁绑定的Condition实例。利用Condition对象,我们就可以让线程在合适的时间等待,或者在某一个特定的时刻得到通知,继续执行。

Condition方法简介:

①await()方法会使当前线程等待,同时释放当前锁,当其他线程中使用signal()或者signalAll()方式时,线程就会重新获得锁并继续执行。或者当前线程被中断时,也能跳出来等待;

②awaitUninterruptibly()方法与await()方法基本相同,但是它并不会在等待过程中响应中断;

③signal()方法用于唤醒一个在等待中的线程,相对的signalAll()方法会唤醒所有在等待中的线程。

和Object.wait()和Object.notify()方法一样,当线程使用Condition.await()时,要求线程持有相关的重入锁,在Condition.await()调用后,这个线程会释放这把锁。同理,在调用Condition.signal()方法时,也要求线程先获得相关的锁。

1.3信号量(Semaphore)

信号量是为多线程协作提供了更为强大的控制方法。广义上说,信号量是对锁的扩展。无论是内部锁synchronized还是重入锁ReentrantLock,一次都只允许一个线程访问同一个资源,而信号量却可以指定多个线程,同时访问某一个资源。

在构造信号量对象时,必须要指定信号量的准入数,即同时能申请多少个许可。当每个线程每次只申请一个许可时,这就相当于指定了同时有多少个线程可以访问某一个资源。Semaphore的构造方法还有一个参数,可以指定当前信号量是否是公平的。

Semaphore方法简介:

①acquire()方法尝试获得一个准入的许可,若无法获得,则线程会等待,直到有线程释放一个许可或者当前线程被中断;

②acquireUninterruptibly()方法和acquire()方法类似,但不响应中断;

③tryAcquire()方法尝试获得一个许可,如果成功返回true,失败则返回false,它不会进行等待,立即返回;

④tryAcquire(long timeout, TimeUnit unit)方法尝试获得一个许可,如果成功返回true,失败则等待timeout时长,直到获得许可返回true,或者超时返回false;

⑤release()用于在线程访问资源结束后,释放一个许可,以使其他等待许可的线程可以进行资源访问。

1.4ReadWriteLock读写锁

ReadWriteLock是JDK5提供的读写分离锁,读写分离锁可以有效地帮助减少锁竞争,以提升系统性能。读写锁允许多个线程同时读,但是写写操作和读写操作间依然是需要相互等待和持有锁的。读写锁的约束情况如下表所示:

非阻塞 阻塞
阻塞 阻塞

①读-读不互斥:读读之间不阻塞;

②读-写互斥:读阻塞写,写也会阻塞读;

③写-写互斥:写写阻塞。

1.5倒计时器:CountDownLatch

CountDownLatch通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。

1.6循环栅栏:CyclicBarrier

CyclicBarrier可以理解为循环栅栏,它比CountDownLatch略微强大一些,CyclicBarrier可以接收一个参数作为barrierAction,所谓barrierAction就是当计数器一次计数完成后,系统会执行的动作。

CyclicBarrier.await()方法可能会抛出两个异常,一个是InterruptedException,也就是在等待过程中,线程被中断,应该说这是一个非常通用的异常。大部分迫使线程等待的方法都可能抛出这个异常,使得线程在等待时依然可以响应外部紧急事件。另外一个异常则是CyclicBarrier特有的BrokenBarrierException。一旦遇上这个异常,则表示当前的CyclicBarrier已经被破坏了,可能系统已经没有办法等待所有线程到齐了。

1.7线程阻塞工具类:LockSupport

LockSupport是一个非常方便实用的线程阻塞工具,它可以在线程内任意位置让线程阻塞。和Thread.suspend()相比,它弥补了由于resume()在前发生,导致线程无法继续执行的情况。

LockSupport类使用类似信号量的机制,它为每一个线程准备了一个许可,如果许可可用,那么park()方法会立即返回,并且消费这个许可(也就是把许可变为不可用),如果许可不可用,就会阻塞。而unpark()则使得一个许可变为可用(但是和信号量不同的是,许可不能累加,你不可能拥有超过一个许可,它永远只有一个)。所以即使unpark()操作发生在park()之前,它也可以使下一次的park()操作立即返回。

支付宝打赏 微信打赏

赞赏是不耍流氓的鼓励