文章目录
一、重入锁
1、重入锁简介
重入锁是用于线程间协同工作的一种机制,可以完全替代synchronized关键字,在java中为java.util.concurrent.locks包下的ReentrantLock类。之所以叫重入锁,是因为该锁可以反复获取多次,在释放锁的时候也必须释放相同次数。
与synchronized相比,重入锁必须在程序中指出何时加锁,何时释放锁,因此对程序的逻辑控制灵活性要远远好于synchronized。
public class ReentrantLockTest implements Runnable{
public static ReentrantLock lock = new ReentrantLock();
public static int i = 0;
@Override
public void run(){
for(int j=0;j<10000000;j++){
// 获取锁,只有获取到了才会执行后续代码
lock.lock();
try{
i++;
}finally{
// 释放锁
lock.unlock();
}
}
}
public static void main(String[] args)throws InterruptedException{
ReentrantLockTest lockTest = new ReentrantLockTest();
Thread t1 = new Thread(lockTest);
Thread t2 = new Thread(lockTest);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
2、中断响应
使用synchronized时,一个线程要么获得锁继续执行,要么就会一直等待。而使用重入锁,则可以根据需要取消对锁的请求,也可以有效避免死锁。
public class IntRunable implements Runnable{
public static ReentrantLock lock1 = new ReentrantLock();
public static ReentrantLock lock2 = new ReentrantLock();
int lock;
public IntRunable(int lock){
this.lock = lock;
}
@Override
public void run(){
try{
if(lock == 1){
// 获取锁lock1并设置为可响应中断的
lock1.lockInterruptibly();
try{
Thread.sleep(500);
} catch(InterruptedException e){
e.printStackTrace();
}
// 获取锁lock2并设置为可响应中断的
lock2.lockInterruptibly();
} else {
// 获取锁lock2并设置为可响应中断的
lock2.lockInterruptibly();
try{
Thread.sleep(500);
} catch(InterruptedException e){
e.printStackTrace();
}
// 获取锁lock1并设置为可响应中断的
lock1.lockInterruptibly();
}
} catch(InterruptedException e){
e.printStackTrace();
}finally{
if(lock1.isHeldByCurrentThread()){
// 如果被中断了,就释放锁
lock1.unlock();
}
if(lock2.isHeldByCurrentThread()){
lock2.unlock();
}
System.out.println(Thread.currentThread().getId()+":线程退出");
}
}
public static void main(String[] args)throws InterruptedException{
IntRunable intRunableA = new IntRunable(1);
IntRunable intRunableB = new IntRunable(2);
Thread tA = new Thread(intRunableA);
Thread tB = new Thread(intRunableB);
tA.start();
tB.start();
Thread.sleep(1000);
// 中断tB,打破tA、tB之间的死锁
tB.interrupt();
}
}
线程tA和tB启动后,tA先占用lock1,休眠0.5秒后再去尝试占用lock2,,tB则先抢占了lock2,休眠0.5秒后再去尝试占用lock1,因此两者之间就会形成互相等待的死锁。但由于主线程中对tB线程进行中断,故tB会放弃对lock1的申请,同时释放已获得lock2。tA线程就可以顺利得到lock2而继续执行下去了。
3、限时等待
除了等待外部通知外,避免死锁的另一个方法就是限时等待。也就是说给线程获取锁的时候加个时间限制,当超过这个时间后,线程仍未获取到锁,就放弃。重入锁的tryLock() 方法可进行一次限时的等待。tryLock()方法接收两个参数,一个等待时长,一个时间单位。ReentrantLock.tryLock()方法也可以不带参数直接运行。在这种情况下,当前线程会尝试获得锁,如果锁并未被其他线程占用,则申请锁会成功,并立即返回true。如果锁被其他线程占用,则当前线程不会进行等待,而是立即返回false。
public class LimitTimeRunnable implements Runnable{
public static ReentrantLock lock = new ReentrantLock();
@Override
public void run(){
try{
// 尝试获取锁,5秒内没获取到就放弃
if(lock.tryLock(5, TimeUnit.SECONDS){
Thread.sleep(6000);
}else{
System.out.println(Thread.currentThread().getName() + "获取锁失败");
}
}catch(InterruptedException e){
e.printStackTrace();
}finally{
if(lock.isHeldByCurrentThread()){
lock.unlock();
}
}
}
public static void main(String[] args){
LimitTimeRunnable ltRunnable = new LimitTimeRunnable();
Thread t1 = new Thread(ltRunnable, "t1");
Thread t2 = new Thread(ltRunnable, "t2");
t1.start();
t2.start();
}
}
执行结果:
在本例中,两个线程会同时尝试去获取同一把锁,并且获取到后sleep6秒,所以未获取到锁的线程在等待5秒后就会自动放弃,从而获取失败。
4、公平锁
在大多数情况下,锁的获取都是非公平的,也就是说并不是按线程的先来后到给线程分配锁,系统只是会从这个锁的等待队列中随机挑选一个,后申请锁的线程可能会先获取到锁,并且更倾向于将锁分配给已经持有锁的线程。而公平的锁会按照时间的先后顺序,保证先到先得,因此公平锁的一大特点是它不会产生饥饿现象。
synchronized关键字产生的锁就是非公平的,而重入锁允许对其公平性进行设置。ReentrantLock有这样一个构造函数:
public ReentrantLock(boolean fair);
当参数fair为true时,就表示将重入锁设置为公平的。公平锁虽保障了公平,但也由此需要维护一个有序队列,实现成本比较高,性能相对也非常低下。所以默认情况下,锁是非公平的。
5、重入锁实现原理
在重入锁的实现中,主要包含三个要素:
- 使用CAS(Compare And Swap,比较交换)来存储当前锁的状态,判断锁是否已经被别的线程持有;
- 所有申请锁的线程在没有获取到锁时,会进入等待队列进行等待,直到有线程释放锁后,系统就从等待队列中选择一个线程;
- 使用阻塞原语park()和unpark(),用来挂起和恢复线程,没有得到锁的线程将会被挂起。
二、Condition
Condition是与重入锁相关联的,通过Lock接口的newCondition()方法可以生成一个与当前重入锁相关联的COndition实例。Condition对象可以在某一个时刻通过其await()方法使线程等待或通过signal()方法通知其他线程继续执行,与Object.wait()和Object.notify()类似。具体来说:
- await() 方法会使当前线程等待,同时释放当前锁,直到其他线程中使用了signal()或signalAll()方法时,才可能重新获取锁并继续执行;
- awaitUninterruptibly() 方法与await()方法基本相同,但是不会在等待过程中响应中断;
- signal() 方法用于唤醒一个在等待中的线程,signalAll() 方法则用于唤醒所有等待中的线程,不会释放锁,需要手动释放。
public class ConditionRunnable implements Runnable{
public static ReentrantLock lock = new ReentrantLock();
public static Condition condition = lock.newCondition();
@Override
public void run(){
try{
lock.lock();
// 使当前线程等待
condition.await();
System.out.println("线程被唤醒,继续执行");
}catch(InterruptedException e){
e.printStackTrace();
}finally{
lock.unlock();
}
}
public static void main(String[] args)throws InterruptedException{
ConditionRunnable r = new ConditionRunnable();
Thread t1 = new Thread(r);
t1.start();
Thread.sleep(2000);
lock.lock();
// 唤醒线程
condition.signal();
lock.unlock();
}
}
与Object.wait()和notify()方法一样,当线程使用Condition.await()和signal()时,线程需持有相关的重入锁。
三、信号量Semaphore
无论是内部锁synchronized还是重入锁ReentrantLock,一次都只允许一个线程访问一个资源,而信号量可以指定多个线程同时访问某一个资源。
public Semaphore(int permits)
public Semaphore(int permits,boolean fair)
其中permits参数用于指定一次有多少个线程可以访问资源,fair参数指定锁是否是公平的。Semaphore的常用方法有:
// 尝试获取一个准入的许可,若无法获取,则一直等待
public void acquire()
// 与acquire()类似,但不响应中断
public void acquireUninterruptibly()
// 尝试获取一个许可,如果成功返回true,失败false,不会进行等待
public boolean tryAcquire()
// 在指定时间内尝试获取一个许可
public boolean tryAcquire(long timout, TimeUnit unit)
// 释放一个许可
public void release()
下面举例演示Semaphore的使用:
public class SemaphoreRunnable implements Runnable{
// 每次允许5个线程同时访问资源执行
final Semaphore semp = new Semaphore(5);
@Override
public void run(){
try{
// 获取执行许可
semp.acquire();
// 模拟耗时操作
Thread.sleep(2000);
System.out.println(Thread.currentThread().getId() + "线程执行完毕");
// 释放许可
semp.release();
}catch(InterruptedException e){
e.printStackTrace();
}
}
public static void main(String[] args){
// 创建含有20个线程的线程池
ExecutorService exe = Executors.newFixedThreadPool(20);
final SemaphoreRunnable r = new SemaphoreRunnable();
for(int i=0;i<20;i++){
exe.submit(r);
}
}
}
执行结果:
可以看到,每一次输出是5个线程的执行结果。
四、读写锁ReadWriteLock
读写锁可以通过将读操作和写操作分离,减少锁竞争,进而有效的提高系统性能。因为如果使用重入锁或者内部锁,不管是读还是写操作,都需要先等待获取锁,但读操作并不会破坏数据的完整性,这种等待就显得没有意义。
总的来说,使用读写锁可以只对写操作进行加锁,而使读操作真正的并行执行。
public class ReadWriteLockExample{
// 实例化一个重入锁,与读写锁进行对比
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();
System.out.println("读操作:" + value);
Thread.sleep(1000);
return value;
}finally{
lock.unlock();
}
}
// 写操作
public void handleWrite(Lock lock,int index) throws InterruptedException{
try{
lock.lock();
System.out.println("写操作:" + index);
Thread.sleep(1000);
value = index;
}finally{
lock.unlock();
}
}
public static void main(String[] args){
final ReadWriteLockExample example = new ReadWriteLockExample();
Runnable readRunnable = new Runnable(){
@Override
public void run(){
try{
// 通过在此处切换具体的锁,可以发现读写锁的执行时间会比重入锁短得多
example.handleRead(readLock);
// 或者 example.handleRead(lock);
}catch(InterruptedException e){
e.printStackTrace();
}
}
};
Runnable writeRunnable = new Runnable(){
@Override
public void run(){
try{
example.handleWrite(writeLock,new Random().nextInt());
// 或者 example.handleWrite(lock,new Random().nextInt());
}catch(InterruptedException e){
e.printStackTrace();
}
}
};
// 模拟18个读线程
for(int i=0;i<18;i++){
new Thread(readRunnable).start();
}
// 模拟2个写线程
for(int i=18;i<20;i++){
new Thread(writeRunnable).start();
}
}
}
五、CountDownLatch
Count Down在英文中意为倒计数,Latch为阀门的意思,CountDownLatch可以让一个线程等待,直到给定的倒计数结束,再开始执行。在创建CountDownLatch对象时,必需传递一个整数,这个整数就是计数个数。
public class CountDownLatchRunnable implements Runnable{
static final CountDownLatch latch = new CountDownLatch(10);
static final CountDownLatchRunnable r = new CountDownLatchRunnable();
@Override
public void run(){
try{
Thread.sleep(new Random().nextInt(10)*1000);
System.out.println(Thread.currentThread().getId() +"号线程任务完成");
// 完成任务,使倒计数减一
latch.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(r);
}
// 使主线程等到倒计数倒计完毕后才能再继续执行
latch.await();
System.out.println("所有准备任务已完成,继续运行主线程");
exec.shutdown();
}
}
执行结果:
六、CyclicBarrier
CyclicBarrier中文意为循环栅栏,和CountDownLatch类似,也可以实现线程间的计数等待,但它的功能比CountDownLatch更加复杂强大。比如,假设我们将CyclicBarrier计数器设置为10,那么凑齐第一批10个线程后,计数器就会归零,然后接着凑齐下一批10个线程,这就是循环栅栏内在的含义。
CyclicBarrier可以接收一个参数作为barrierAction。所谓barrierAction就是当计数器一次计数完成后,系统会执行的动作。如我们现在对苹果进行装箱操作,每箱苹果6个,等有6个苹果送来后进行打包封装:
public class CyclicBarrierExample{
public static class Apple implements Runnable{
private int appleNo;
private final CyclicBarrier cyclic;
Apple(CyclicBarrier cyclic, int appleNo){
this.cyclic = cyclic;
this.appleNo = appleNo;
}
@Override
public void run(){
try{
// await()方法会使当前线程阻塞,直到cyclic计数器计数完毕
// 也就是说要求达到指定线程数时
cyclic.await();
putApple();
}catch(InterruptedException | BrokenBarrierException e){
e.printStackTrace();
}
}
void putApple(){
try{
// 模拟放入苹果的操作耗时
Thread.sleep(Math.abs(new Random().nextInt()%10000));
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println("放入" + appleNo +"号苹果");
}
}
public static class PackageApple implements Runnable{
int N;
public PackageApple(int N){
this.N = N;
}
@Override
public void run(){
System.out.println(N + "个苹果已全部送来,开始封装打包");
}
}
public static void main(String[] args){
final int N = 6;
Thread[] allApple = new Thread[N];
CyclicBarrier cyclic = new CyclicBarrier(N, new PackageApple(N));
for(int i=0;i<N;i++){
allApple[i] = new Thread(new Apple(cyclic,i));
allApple[i].start();
}
}
}