内容主要参考自《实战Java高并发程序设计》
Reentrantlock基本使用
Reentrantlock意为重入锁,可以算是对synchronized关键字的扩展,所以其作用也是用来保证线程间的同步,一个简单的示例如下:
public class ReentrantlockDemo implements Runnable {
private static ReentrantLock reentrantLock = new ReentrantLock(); //创建ReentrantLock对象
static int a = 0;
public ReentrantlockDemo() {
}
@Override
public void run() {
reentrantLock.lock(); //加锁
try {
//每个线程对变量a进行100000次累计额
for (int i = 0; i < 100000; i++) {
a++;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
reentrantLock.unlock(); //释放锁
}
}
public static void main(String[] args) throws InterruptedException {
ReentrantlockDemo r = new ReentrantlockDemo();
//创建两个线程,并打印输出a的结果
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("a = " + a);
}
}
在run方法中,首先在要同步的位置调用lock()方法上锁,然后结尾调用unlock()方法释放锁,最后的效果等同于使用synchronized关键字。
为了做对比,将线程中同步的部分改为synchronized关键字的形式
public class ReentrantlockDemo implements Runnable {
private static ReentrantLock reentrantLock = new ReentrantLock(); //创建ReentrantLock对象
static int a = 0;
public ReentrantlockDemo() {
}
@Override
public void run() {
synchronized (this){
for (int i = 0; i < 100000; i++) {
a++;
}
}
}
public static void main(String[] args) throws InterruptedException {
ReentrantlockDemo r = new ReentrantlockDemo();
//创建两个线程,并打印输出a的结果
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("a = " + a);
}
}
从两者的对比中可以看到,synchronized是通过关键字的形式来使用的,而Reentrantlock是通过对象的方式使用,加锁解锁都需要写在代码中,所以Reentrantlock会更灵活,使用也会复杂一些。
Reentrantlock注意事项
第一点是,一个加锁操作必须对应一个释放锁的操作,因为Reentrantlock是可重入锁,所以一个线程在加锁后可以再次持有同一把锁,这样就相当于加了两次锁,所以也要显示的释放两次锁
@Override
public void run() {
reentrantLock.lock();
reentrantLock.lock();
try {
a ++;
}catch (Exception e){
e.printStackTrace();
}
finally {
//释放两次锁
reentrantLock.unlock();
reentrantLock.unlock();
}
}
第二点是,释放锁的操作最好放到finally代码块中,因为Reentrantlock的加锁和释放锁操作都是要在代码中体现,这样就保证即使程序出现了异常,也能正确的释放锁。
Reentrantlock一些高级使用
设置锁等待时间
当一个线程迟迟得不到一把锁时,继续等待可能也是浪费时间。Reentrantlock可以给线程设置一个等待锁的时间,这样当超时还未获取锁时,线程就会放弃等待。
这里主要调用的是tryLock()方法
tryLock()的返回值为boolean,为true是线程获取到了锁,为false是指超过了等待时间线程还未获取锁。
tryLock()接收两个参数,第一个参数是等待时间,第二个参数是时间单位。无参的方法是将等待时间设置为0,也就是在线程获取不到锁时会立即返回false。
简单的示例如下
@Override
public void run() {
try {
//线程尝试获取锁
if (!reentrantLock.tryLock(1l, TimeUnit.SECONDS)) {
System.out.println("线程" + Thread.currentThread().getName() + "未获取锁");
return;
}
//每个线程对变量a进行100000次累计额
for (int i = 0; i < 100000; i++) {
a++;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放前先判断线程是否有锁
if (reentrantLock.isHeldByCurrentThread()) {
reentrantLock.unlock(); //释放锁
}
}
}
公平锁
公平锁是指,锁会根据线程的申请先后顺序分配给线程,比如线程A先申请了锁,之后线程B也申请了锁,在公平锁下,锁会先被A持有,而在非公平锁下,A和B不一定谁会先持有锁。
要使用公平锁,只需要在Reentrantlock的构造方法中传递一个true参数即可
公平锁会维护一个线程申请的队列,所以会产生额外的开销,因此默认情况下锁都是非公平的,在没有特殊需求的情况下,使用默认的非公平锁即可。
Conditinon的使用
Condition对象是配合Reentrantlock使用的,其作用几乎等同于synchronized中常用的wait()方法和notify()、notifyAll()方法。
Condition中几个常用的方法
- await():作用同wait()方法,让线程释放当前锁并进入等待状态,await()参数可以传递两个参数,分别是等待的时间和时间单位,无参表示无限等待。
- signal():作用同notify()方法,随机唤醒一个处于等待状态的线程。
- signalAll():作用同notifyAll()方法,唤醒所有处于等待状态的线程。
一个简单的示例如下:
public class ReentrantlockDemo implements Runnable {
private static ReentrantLock reentrantLock = new ReentrantLock(true); //创建ReentrantLock对象
private static Condition condition = reentrantLock.newCondition(); //通过Reentrantlock获取Condition对象
int flag;
public ReentrantlockDemo(int flag) {
this.flag = flag;
}
@Override
public void run() {
System.out.println("线程" + Thread.currentThread().getName() + "进入");
reentrantLock.lock();
try {
System.out.println("线程" + Thread.currentThread().getName() + "获取锁");
//flag为1表示当前线程等待,为2表示唤醒等待的线程
if(flag == 1){
System.out.println("线程" + Thread.currentThread().getName() + "等待");
condition.await();
System.out.println("线程" + Thread.currentThread().getName() + "被唤醒");
}
else{
System.out.println("线程" + Thread.currentThread().getName() + "唤醒等待线程");
condition.signal();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
System.out.println("线程" + Thread.currentThread().getName() + "结束");
}
}
public static void main(String[] args) throws InterruptedException {
//这里虽然创建了两个Runnable实例,但锁是被static修饰的,所以两个实例使用的同一把锁
ReentrantlockDemo r1 = new ReentrantlockDemo(1);
ReentrantlockDemo r2 = new ReentrantlockDemo(2);
Thread t1 = new Thread(r1,"thread1");
Thread t2 = new Thread(r2,"thread2");
t1.start();
Thread.sleep(1000);
t2.start();
}
}
输出结果
通过synchronized方式实现该程序
public class ReentrantlockDemo implements Runnable {
private static ReentrantLock reentrantLock = new ReentrantLock(true); //创建ReentrantLock对象
private static Condition condition = reentrantLock.newCondition(); //通过Reentrantlock获取Condition对象
private static Object lock = new Object();
int flag;
public ReentrantlockDemo(int flag) {
this.flag = flag;
}
@Override
public void run() {
System.out.println("线程" + Thread.currentThread().getName() + "进入");
synchronized (lock){
try {
System.out.println("线程" + Thread.currentThread().getName() + "获取锁");
//flag为1表示当前线程等待,为2表示唤醒等待的线程
if(flag == 1){
System.out.println("线程" + Thread.currentThread().getName() + "等待");
lock.wait();
System.out.println("线程" + Thread.currentThread().getName() + "被唤醒");
}
else{
System.out.println("线程" + Thread.currentThread().getName() + "唤醒等待线程");
lock.notify();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("线程" + Thread.currentThread().getName() + "结束");
}
}
}
public static void main(String[] args) throws InterruptedException {
//这里虽然创建了两个Runnable实例,但锁是被static修饰的,所以两个实例使用的同一把锁
ReentrantlockDemo r1 = new ReentrantlockDemo(1);
ReentrantlockDemo r2 = new ReentrantlockDemo(2);
Thread t1 = new Thread(r1,"thread1");
Thread t2 = new Thread(r2,"thread2");
t1.start();
Thread.sleep(1000);
t2.start();
}
}
可以看到,通过Conditio实现等待和唤醒与通过Object实现方式没有太大区别,基本上是通用的逻辑。
这里要注意的是,在调用Condition的方法时,需要保证当前线程已经拿到对应的锁,否则会抛出异常