目录
1. Lock锁
1.1 Lock锁介绍
1.2 Lock锁的其他加锁方式
1.3 Lock和synchronized对比
2.监视器锁
1. Lock锁
1.1 Lock锁介绍
我们知道使用同步方法或同步代码块会自动加锁和解锁,那有没有办法可以自己控制加锁和解锁的时机呢?
java在JDK1.5之后提供了一个新的锁对象Lock,他有两个常用的方法lock和unlock,一个是手动加锁,另一个是手动解锁。但是Lock本身是一个接口不能实例化对象,通常我们在使用时需要用到它的一个实现类ReentrantLock来实例化对象。也就是:
Lock l=new ReentrantLock();
并且还能同时设置是否为公平锁。 所谓公平锁和非公平锁,就是看加锁的过程是否公平,公平就是先来先加锁,非公平就是允许后来的线程插队,先获得锁。一般使用非公平锁,比如一个线程先来但是要运行1小时,另一个线程后来只需要运行1秒,如果使用公平锁那么第二个线程需要等1小时才能加锁,而使用非公平锁则允许第二个线程先获得锁,且第一个线程只用等1秒也能获取到锁,总体等待时间由1小时缩短为1秒。可以用以下代码来设置:
//true表示设置为公平锁,false表示设置为非公平锁
Lock l=new ReentrantLock(true);
默认的锁是非公平锁,可以查看源码:
使用案例:先用同步代码块来实现卖票功能:
@Override
public void run() {
//模拟卖票过程,总共100张票,售完为止
while (true){
//对卖票过程进行加锁
synchronized (object){
if(ticketnum<100) {
ticketnum++;
System.out.println(Thread.currentThread().getName() + "正在售卖第" + ticketnum + "张票");
}else{
break;
}
}
//通常卖票是有时间间隔的,用sleep来模拟
//由于父类Thread的run方法没有抛异常,所以子类的run方法也不能抛异常,只能使用try-catch
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
然后使用Lock锁实现,是不是仅需要在同步代码块中要执行的代码的前后分别加上lock和unlock就可以了呢?我们可以试一试:这是在前后加上lock和unclock的方法:
注意使用继承Thread类的方式实现多线程所创建的lock对象必须是静态的,这个lock就是锁对象,必须唯一,当然如果使用Runnable接口实现的那就可以是普通变量。
public class MyThread extends Thread{
//ticketnum表示当前正在售卖第几张票
//使用static将其变为共享的
public static int ticketnum=0;
//锁对象,用于加锁操作
public static Lock lock=new ReentrantLock();;
@Override
public void run() {
//模拟卖票过程,总共100张票,售完为止
while (true){
//对卖票过程进行加锁
lock.lock();
if(ticketnum<100) {
ticketnum++;
System.out.println(Thread.currentThread().getName() + "正在售卖第" + ticketnum + "张票");
}else{
break;
}
lock.unlock();
//通常卖票是有时间间隔的,用sleep来模拟
//由于父类Thread的run方法没有抛异常,所以子类的run方法也不能抛异常,只能使用try-catch
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
运行结果:
乍一看没有什么问题,但是仔细看就会发现程序并没有停止运行,这是哪里出现了问题呢?
在上面的代码中,如果判断ticketnum>=100就会break退出循环,但在判断之前执行了加锁操作,break之后循环跳出,unlock解锁操作自然就没有执行,所以此时还处在上锁状态,需要将锁释放程序才能停止运行。
所以不只是在要执行的代码前后加上lock和unlock那么简单。在上面的例子中我们可以在break前加上一个unlock操作就能解决问题,但这样做unlock就会出现两次,如果有多个break那就需要重复加上多个unlock。那怎样只需要写一个unlock就能保证加的锁最后都能被释放呢?
保证加的锁最后都能被释放换句话讲就是无论中间执行什么代码,最后解锁的操作都会执行,这和try-catch-finally中的finally部分很契合,finally部分的代码无论任何情况最后都会被执行,所以我们可以使用finally来解决这个问题:
public class MyThread extends Thread{
//ticketnum表示当前正在售卖第几张票
//使用static将其变为共享的
public static int ticketnum=0;
//锁对象,用于加锁操作
public static Lock lock=new ReentrantLock();;
@Override
public void run() {
//模拟卖票过程,总共100张票,售完为止
while (true){
//对卖票过程进行加锁
lock.lock();
//使用finally实现
try {
if(ticketnum<100) {
ticketnum++;
System.out.println(Thread.currentThread().getName() + "正在售卖第" + ticketnum + "张票");
}else{
break;
}
}finally {
lock.unlock();
}
//通常卖票是有时间间隔的,用sleep来模拟
//由于父类Thread的run方法没有抛异常,所以子类的run方法也不能抛异常,只能使用try-catch
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
运行结果为:
为了不用考虑使用lock手动加锁后是否最后都能解锁的问题,通常使用如下方式:
lock.lock();
try{
需要锁住的代码
}finally{
lock.unlock();
}
catch块根据实际情况可以加上,用于捕捉异常并处理。
1.2 Lock锁的其他加锁方式
Lock除了普通的lock方法获取锁外,还有其他的方式获取锁:
- trylock()
这种方式只是尝试获取锁,如果获取不到则不会等待,会立即返回获取锁的结果,成功获取返回true,获取失败则返回false;lock方法则是会先尝试获取锁,如果获取不到则陷入等待,直到成功获取锁。
使用格式为:
if(lock.trylock()){
try{
//要锁住的代码
}catch(异常){
//异常处理代码
}finally{
lock.unlock();
}
}
- trylock(long time,Time.Unit unit)
这种方式也是尝试获取锁,但可以设置等待时间,要传入时间的值和单位;所谓等待时间,就是如果当前获取锁失败还可以等待多长时间,如果在这段时间内成功获取到了锁则返回true,反之返回false。
使用格式为:
if(lock.trylock(时间的值,时间的单位)){
try{
//要锁住的代码
}catch(异常){
//异常处理代码
}finally{
lock.unlock();
}
}
- lockInterruptibly()
这个方法可以在获取锁时响应中断,使用时需要使用try-catch包围或者抛出异常。当线程获取不到锁时会进入等待状态,此时若通过线程实例调用interrupt方法可以中断等待过程,而lockInterruptibly()方法允许在获取不到锁时响应interrupt方法的执行,进而中断等待过程。interrupt方法只会中断阻塞过程中的线程,并不会中断正常运行的线程。
使用格式为:
//或者直接抛出异常
try {
l.lockInterruptibly();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try{
//要锁住的代码
}catch(异常){
//异常处理代码
}finally{
lock.unlock();
}
1.3 Lock和synchronized对比
使用Lock加锁和使用synchronized加锁有什么不同呢?
- synchronized是java的一个内置关键字,而Lock则是一个接口。
- synchronized是自动加锁,如果线程1已经获取到了锁,线程2再要获取这个锁时会自动陷入等待,并且整个过程不可见,无法判断当前是否成功获得了锁;而Lock可以通过trylock()方法尝试获取锁,该方法不会陷入等待并且会立即返回结果,可以通过返回值判断当前是否获取到了锁。
- synchronized会自动解锁,而Lock必须要手动释放锁,如果不释放则线程就不会停止运行。
- synchronized的锁是可重入、非公平的锁,并且获取不到锁后陷入的等待过程不可中断(因为加锁过程是自动的,我们无法控制);而Lock的锁是可重入的锁,但可以选择是否是非公平的锁,也可以使用lockInterruptibly方法响应中断。
- synchronized一般用于对少量代码加锁,Lock一般用于对大量代码加锁。少量代码一般所需的运行时间短,对应的加锁时间就短,当有其他线程也获取这个锁时等待时间就短,就没必要考虑等待过程不可中断的问题,反正等不了多长时间锁就释放了;而大量代码通常执行的时间较长,加锁时间也就越长,这意味着其他线程需要等待很长时间,此时如果想要中断等待过程只能用Lock。
- synchronized有内置监视器锁,而Lock需要手动添加监视器锁,可以选择Condition监视器锁。
2.监视器锁
上面说到在使用Lock手动加锁和解锁时要使用try-catch-finally包围,保证加上的锁最后都能被释放,但这样做是否也能保证代码正常运行呢?
如果不使用等待唤醒机制,程序能够正常运行,但如果使用了wait和notify,则程序会报错。就像下面的例子,实现A、B、C、D四个线程依次执行两次:
//资源类
public class Example1 {
//标志位,初始值设为1,让A先执行
private int flag=1;
//创建锁对象
Lock lock=new ReentrantLock();
public void A(){
for(int i=0;i<2;i++) {
//加锁
lock.lock();
try {
while (flag != 1) {
lock.wait();
}
System.out.println("线程A正在运行中...");
flag = 2;
lock.notifyAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public void B(){
for(int i=0;i<2;i++) {
//加锁
lock.lock();
try {
while (flag != 2)
lock.wait();
System.out.println("线程B正在运行中...");
flag = 3;
lock.notifyAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public synchronized void C(){
for(int i=0;i<2;i++) {
//加锁
lock.lock();
try {
while (flag != 3)
lock.wait();
System.out.println("线程C正在运行中...");
flag = 4;
lock.notifyAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public synchronized void D(){
for(int i=0;i<2;i++) {
//加锁
lock.lock();
try {
while (flag != 4)
lock.wait();
System.out.println("线程D正在运行中...");
flag = 1;
lock.notifyAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
//实现
public class Main {
public static void main(String[] args) {
//创建资源类对象
Example1 example1=new Example1();
//创建A、B、C、D四个线程
Thread ta=new Thread(()->{
example1.A();
});
Thread tb=new Thread(()->{
example1.B();
});
Thread tc=new Thread(()->{
example1.C();
});
Thread td=new Thread(()->{
example1.D();
});
//启动四个线程
ta.start();
tb.start();
tc.start();
td.start();
}
}
运行结果:
可以看到虽然运行了四次,但异常抛出了八次,并且都是IllegalMonitorStateException异常,说明每次线程在运行时都出现了同样的异常。
在Java中wait、notify 或notifyAll 方法必须在同步块或同步方法中调用,否则将抛出IllegalMonitorStateException 异常,这是因为这些方法是与对象的内部锁(也称为监视器锁)关联的。当一个线程调用一个对象的wait 方法时,它必须已经通过synchronized关键字获得了该对象的监视器锁。如果没有获得锁,也就是当前线程并不是锁对象的监视器锁的持有者,就会抛出 IllegalMonitorStateException 异常。同样,调用notify或notifyAll也需要线程已经拥有该对象的监视器锁。
那什么是监视器锁呢,监视器锁和锁对象又有什么关系呢?
内置监视器锁也称为JVM锁或者同步锁,是Java语言提供的一种基本锁机制,内置监视器锁通常通过synchronized关键字实现。每个Java对象都有一个与之相关联的监视器锁(也称为内置锁,由JVM生成并管理),当线程要访问被锁定的代码块时,它必须先获得与该对象相关联的监视器锁。当代码块使用synchronized关键字加锁时会自动获取锁对象的内置监视器锁,执行完该代码块后会自动释放锁。如果另一个线程已经持有了这个锁,则当前线程就会阻塞等待,直到该锁被释放。由于锁对象是唯一的,所以获取的监视器锁也是同一个锁,只有持有这个锁的线程才能够访问被锁住的资源。
回到上面的例子,只加了Lock锁并不会获取锁对象的监视器锁,这就导致当前线程不是锁对象的监视器锁的持有者,所以运行时抛出了异常,同时这也意味着wait、notify以及notifyAll只能用于同步代码块或者同步方法中,只有synchronized才能获取到监视器锁。所以代码可以改为:
//资源类
public class Example1 {
//标志位,初始值设为1,让A先执行
private int flag=1;
public synchronized void A() throws IOException {
for(int i=0;i<2;i++) {
try {
while (flag != 1) {
//由于加锁的对象flag是非静态的,所以同步方法的锁对象使用的是this
this.wait();
}
System.out.println("线程A正在运行中...");
flag = 2;
this.notifyAll();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public synchronized void B(){
for(int i=0;i<2;i++) {
try {
while (flag != 2)
this.wait();
System.out.println("线程B正在运行中...");
flag = 3;
this.notifyAll();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public synchronized void C(){
for(int i=0;i<2;i++) {
try {
while (flag != 3)
this.wait();
System.out.println("线程C正在运行中...");
flag = 4;
this.notifyAll();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public synchronized void D(){
for(int i=0;i<2;i++) {
try {
while (flag != 4)
this.wait();
System.out.println("线程D正在运行中...");
flag = 1;
this.notifyAll();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
//实现
public class Main {
public static void main(String[] args) throws IOException {
//创建资源类对象
Example1 example1=new Example1();
//创建A、B、C、D四个线程
Thread ta=new Thread(()->{
try {
example1.A();
} catch (IOException e) {
throw new RuntimeException(e);
}
});
Thread tb=new Thread(()->{
example1.B();
});
Thread tc=new Thread(()->{
example1.C();
});
Thread td=new Thread(()->{
example1.D();
});
//启动四个线程
ta.start();
tb.start();
tc.start();
td.start();
}
}
运行结果:
其实还有一个问题没有回答,那就是使用Lock锁后,为什么不使用等待唤醒机制时,如果添加了try-catch-finally之后就能正常运行呢(前提是业务代码没问题)?
这是因为虽然手动添加了Lock锁不会获取到监视器锁,但是只有在调用wait、notify以及notifyAll时才需要监视器锁,当使用sleep、join等方法时不会用到监视器锁,自然也就不会抛出异常了。