Java并发编程之ReentrantLock

目录

一、概述

1.1、ReentrantLock与synchronized比较

1.2、公平锁的执行过程

1.3、部分方法

二、源码解析

2.1、公平锁与非公平锁

2.2、lock与lockInterruptibly

 三、示例

3.1、公平锁

3.2、非公平锁

3.3、可中断锁

3.4、tryLock锁


一、概述

在Java5之前,Java多线程中可以使用synchronized隐式锁实现线程之间同步互斥。Java5中提供了Lock类(显示锁)也可以实现线程间的同步,而且在使用上更加方便。本文主要研究ReentrantLock的使用。

ReentrantLock是一个独占的、可重入的、公平&非公平的锁。

实现原理:AQS+CAS+park/unpark+自旋

1.1、ReentrantLock与synchronized比较

1)ReentrantLock和synchronized都是独占锁,只允许线程互斥的访问临界区。但是实现上两者不同:synchronized加锁解锁的过程是隐式的,用户不用手动操作,优点是操作简单,但显得不够灵活。一般并发场景使用synchronized的就够了;ReentrantLock需要手动加锁和解锁,且解锁的操作尽量要放在finally代码块中,保证线程正确释放锁。ReentrantLock操作较为复杂,但是因为可以手动控制加锁和解锁过程,在复杂的并发场景中能派上用场。

2)ReentrantLock和synchronized都是可重入的。synchronized因为可重入因此可以放在被递归执行的方法上,且不用担心线程最后能否正确释放锁;而ReentrantLock在重入时要却确保重复获取锁的次数必须和重复释放锁的次数一样,否则可能导致其他线程无法获得该锁。

3)和synchronized一样,默认的ReentrantLock实现是非公平锁,因为相比公平锁,非公平锁性能更好。当然公平锁能防止饥饿,某些情况下也很有用。在创建ReentrantLock的时候通过传进参数true创建公平锁,如果传入的是false或没传参数则创建的是非公平锁。

4)当使用synchronized实现锁时,阻塞在锁上的线程除非获得锁否则将一直等待下去,也就是说这种无限等待获取锁的行为无法被中断。而ReentrantLock给我们提供了一个可以响应中断的获取锁的方法lockInterruptibly()。该方法可以用来解决死锁问题。

5)ReentrantLock还给我们提供了获取锁限时等待的方法tryLock(),可以选择传入时间参数,表示等待指定的时间,无参则表示立即返回锁申请的结果:true表示获取锁成功,false表示获取锁失败。我们可以使用该方法配合失败重试机制来更好的解决死锁问题。

6)使用synchronized结合Object上的wait和notify方法可以实现线程间的等待通知机制。ReentrantLock结合Condition接口同样可以实现这个功能。而且相比前者使用起来更清晰也更简单。

1.2、公平锁的执行过程

1.3、部分方法

1、lock() 获得锁

2、lockInterruptibly()获得可中断锁

3、tryLock()只有在调用时其他线程没有持有锁的情况下才获取锁

4、tryLock(long timeout, TimeUnit unit)如果在给定的等待时间内没有其他线程持有锁,则获取该锁。  

5、unlock()释放锁

二、源码解析

2.1、公平锁与非公平锁

公平锁部分源码:

final void lock() {
    acquire(1);
}

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }   

    ......
    return false;
}

非公平锁部分源码:

final void lock() {

   // 第一次尝试争抢锁
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}


final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {

        // 第二次尝试挣抢锁,不判断等待队列是否有线程等待获得锁
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    ......
    return false;
}               

可以看出,非公平锁在加锁过程中会进行两次锁的争抢,它不管AQS等待队列中存不存在等待锁的线程,都会尝试去持有锁。

2.2、lock与lockInterruptibly

它们的不同点是,在等待获取锁的过程中,对其中断后的处理方式不同。

lock部分源码

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

static void selfInterrupt() {
    Thread.currentThread().interrupt();
}    


final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            ......
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

lockInterruptibly部分源码

public final void acquireInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (!tryAcquire(arg))
        doAcquireInterruptibly(arg);
}

private void doAcquireInterruptibly(int arg)
    throws InterruptedException {
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            ......
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}   

 三、示例

3.1、公平锁

package demo8;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockTest {

    static Lock lock = new ReentrantLock(true);

    public static void main(String[] args) throws InterruptedException {

        for(int i=0;i<5;i++){
            new Thread(new ThreadDemo(i)).start();
        }

    }

    static class ThreadDemo implements Runnable {
        Integer id;

        public ThreadDemo(Integer id) {
            this.id = id;
        }

        @Override

      public void run() {
            try {
                TimeUnit.MILLISECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for(int i=0;i<2;i++){
                lock.lock();
                System.out.println("获得锁的线程:"+id);
                lock.unlock();
            }
        }
    }
}

执行结果:

获得锁的线程:3
获得锁的线程:4
获得锁的线程:1
获得锁的线程:2
获得锁的线程:0
获得锁的线程:3
获得锁的线程:4
获得锁的线程:1
获得锁的线程:2
获得锁的线程:0

3.2、非公平锁

package demo8;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockTest {

    static Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {

        for(int i=0;i<5;i++){
            new Thread(new ThreadDemo(i)).start();
        }

    }

    static class ThreadDemo implements Runnable {
        Integer id;

        public ThreadDemo(Integer id) {
            this.id = id;
        }

        @Override

      public void run() {
            try {
                TimeUnit.MILLISECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for(int i=0;i<2;i++){
                lock.lock();
                System.out.println("获得锁的线程:"+id);
                lock.unlock();
            }
        }
    }
}

执行结果:

获得锁的线程:0
获得锁的线程:0
获得锁的线程:4
获得锁的线程:4
获得锁的线程:2
获得锁的线程:2
获得锁的线程:3
获得锁的线程:3
获得锁的线程:1
获得锁的线程:1

说明:公平锁和非公平锁该如何选择

  • 如果申请获取锁的线程足够多,那么可能会造成某些线程长时间得不到锁,这就是非公平锁的“饥饿”问题。
  • 大部分情况下我们使用非公平锁,因为其性能比公平锁好很多。但是公平锁能够避免线程饥饿,某些情况下也很有用。

3.3、可中断锁

package com.flychuer.demo3;

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockTest {

	static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread t1 = new Thread() {
            public void run() {
                try {
                    lock.lockInterruptibly();
                } catch (InterruptedException e1) {
                    e1.printStackTrace();
                }
                try {
                	// 无限循环,一直占用锁
                    while (true) {  
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                } finally {
                    lock.unlock();
                }
            }
        };
        t1.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
        
        
        Thread t2 = new Thread() {
            public void run() {
                try {
                	
                	// 因线程t1一直占用锁,t2一直无法获得锁
                    lock.lockInterruptibly();
                    while (true) {
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                } catch (InterruptedException e1) {
                    e1.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        };
        t2.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
        
        Thread t3 = new Thread() {
            public void run() {
                System.out.println("准备中断t2");
                // 中断t2
                t2.interrupt();
                System.out.println("中断t2");
            }
        };
        t3.start();
    }

}

 执行结果:

准备中断t2
中断t2
java.lang.InterruptedException
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(Unknown Source)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(Unknown Source)
    at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(Unknown Source)
    at com.flychuer.demo3.CountDownLatchTest$2.run(CountDownLatchTest.java:39)
Exception in thread "Thread-1" java.lang.IllegalMonitorStateException
    at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(Unknown Source)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(Unknown Source)
    at java.util.concurrent.locks.ReentrantLock.unlock(Unknown Source)
    at com.flychuer.demo3.CountDownLatchTest$2.run(CountDownLatchTest.java:50)

从执行结果可以看出,在线程t2无限等待锁的过程中,被线程t3中断了,lockInterruptibly()则直接抛出中断异常,由上层调用者区去处理中断。

将lockInterruptibly()换成lock()后,再执行以下代码

package com.flychuer.demo3;

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockTest {

	static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread t1 = new Thread() {
            public void run() {
                lock.lock();
                try {
                	// 无限循环,一直占用锁
                    while (true) {  
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                } finally {
                    lock.unlock();
                }
            }
        };
        t1.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
        
        
        Thread t2 = new Thread() {
            public void run() {
                try {
                	
                	// 因线程t1一直占用锁,t2一直无法获得锁
                    lock.lock();
                    while (true) {
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                } finally {
                    lock.unlock();
                }
            }
        };
        t2.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
        
        Thread t3 = new Thread() {
            public void run() {
                System.out.println("准备中断t2");
                // 中断t2
                t2.interrupt();
                System.out.println("中断t2");
            }
        };
        t3.start();
    }

}

执行结果如下,并且程序一直执行,因为线程t1是死循环,一直不释放锁,所以t2被中断也不会立即抛出异常,只有等t2获得锁后才会抛出异常。

准备中断t2
中断t2

结论:ReentrantLock的中断和非中断加锁模式的区别在于:线程尝试获取锁操作失败后,在等待过程中,如果该线程被其他线程中断了,它是如何响应中断请求的。lock方法会忽略中断请求,继续获取锁直到成功;而lockInterruptibly则直接抛出中断异常来立即响应中断,由上层调用者处理中断。

那么,为什么要分为这两种模式呢?这两种加锁方式分别适用于什么场合呢?根据它们的实现语义来理解,我认为lock()适用于锁获取操作不受中断影响的情况,此时可以忽略中断请求正常执行加锁操作,因为该操作仅仅记录了中断状态(通过Thread.currentThread().interrupt()操作,只是恢复了中断状态为true,并没有对中断进行响应)。如果要求被中断线程不能参与锁的竞争操作,则此时应该使用lockInterruptibly方法,一旦检测到中断请求,立即返回不再参与锁的竞争并且取消锁获取操作(即finally中的cancelAcquire操作)。

3.4、tryLock锁

tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。

package com.flychuer.demo3;

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockTest1 {

	static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread t1 = new Thread() {
            public void run() {
                if(lock.tryLock()) {
                	try {
                		System.out.println("t1获得了锁");
						Thread.sleep(5000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					} finally {
						lock.unlock();
					}               	
                }
                else {
                	System.out.println("t1未获得锁");
                }                
            }
        };
        t1.start();
        
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
        
        
        Thread t2 = new Thread() {
            public void run() {
            	if(lock.tryLock()) {
                	try {
                		System.out.println("t2获得了锁");
						Thread.sleep(2000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					} finally {
						lock.unlock();
					}               	
                }
                else {
                	System.out.println("t2未获得锁");
                }    
            }
        };
        t2.start();

        
        Thread t3 = new Thread() {
            public void run() {
            	if(lock.tryLock()) {
                	try {
                		System.out.println("t3获得了锁");
						Thread.sleep(3000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					} finally {
						lock.unlock();
					}               	
                }
                else {
                	System.out.println("t3未获得锁");
                }    
            }
        };
        t3.start();
    }

}

执行结果

t1获得了锁
t2未获得锁
t3未获得锁

可以看出,锁lock被线程t1占用,这时线程t2、t3尝试获取锁失败,并没有继续等待获取锁,而是继续执行。

tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。

package com.flychuer.demo3;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockTest1 {

	static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread t1 = new Thread() {
            public void run() {
                if(lock.tryLock()) {
                	try {
                		System.out.println("t1获得了锁");
						Thread.sleep(5000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					} finally {
						lock.unlock();
					}               	
                }
                else {
                	System.out.println("t1未获得锁");
                }                
            }
        };
        t1.start();
        
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
        
        
        Thread t2 = new Thread() {
            public void run() {
            	try {
					if(lock.tryLock(2000, TimeUnit.MILLISECONDS)) {
						try {
							System.out.println("t2获得了锁");
							Thread.sleep(2000);
						} catch (InterruptedException e) {
							e.printStackTrace();
						} finally {
							lock.unlock();
						}               	
					}
					else {
						System.out.println("t2未获得锁");
					}
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}    
            }
        };
        t2.start();

        
        Thread t3 = new Thread() {
            public void run() {
            	try {
					if(lock.tryLock(6000, TimeUnit.MILLISECONDS)) {
						try {
							System.out.println("t3获得了锁");
							Thread.sleep(3000);
						} catch (InterruptedException e) {
							e.printStackTrace();
						} finally {
							lock.unlock();
						}               	
					}
					else {
						System.out.println("t3未获得锁");
					}
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}    
            }
        };
        t3.start();
    }

}

运行结果

t1获得了锁
t2未获得锁
t3获得了锁

当t2试图获得锁时,t1还在占用锁,t2等待2s仍未获得锁,t2将不再等待锁,并继续执行后续代码。t3在等待的5s内,t1释放了锁,最终t3获得了锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值