java多线程基础之Lock类

本文深入探讨了Java并发编程中的核心概念,包括Lock类的高级功能、Condition类的灵活运用及读写锁机制,旨在帮助开发者掌握高效、精确的线程控制技巧。

前言

JVM提供了synchronized关键字来实现对变量的同步访问以及用wait和notify来实现线程间通信。在jdk1.5以后,JAVA提供了Lock类来实现和synchronized一样的功能,并且还提供了Condition来显示线程间通信。Lock类是Java类来提供的功能,丰富的api使得Lock类的同步功能比synchronized的同步更强大。

本文主要介绍以下内容:

  1. Lock类
  2. Lock类其他功能
  3. Condition类
  4. Condition类其他功能
  5. 读写锁

Lock类

Lock类实际上是一个接口,我们在实例化的时候实际上是实例化实现了该接口的类Lock lock = new ReentrantLock();。用synchronized的时候,synchronized可以修饰方法,或者对一段代码块进行同步处理。针对需要同步处理的代码设置对象监视器,比整个方法用synchronized修饰要好。Lock类的用法也是这样,通过Lock对象lock,用lock.lock来加锁,用lock.unlock来释放锁。在两者中间放置需要同步处理的代码。

public class MyConditionService {

    private Lock lock = new ReentrantLock();
    public void  testMethod() {
        lock.lock();
        for (int i = 0 ;i < 5;i++){
            System.out.println("ThreadName = " + Thread.currentThread().getName() + (" " + (i + 1)));
        }
        lock.unlock();
    }

    public static void main(String[] args) throws  Exception{
        MyConditionService service = new MyConditionService();
        new Thread(service::testMethod).start();
        new Thread(service::testMethod).start();
        new Thread(service::testMethod).start();
        new Thread(service::testMethod).start();
        new Thread(service::testMethod).start();

        Thread.sleep(1000 * 5);
    }
}

可以看到每个线程的打印操作都是同步进行的。通过下面的例子,可以看出Lock对象加锁的时候也是一个对象锁,持有对象监视器的线程才能执行同步代码,其他线程只能等待该线程释放对象监视器。

public class MyConditionService {

    private Lock lock = new ReentrantLock();

    public void methodA() {
        try{
            lock.lock();
            System.out.println("methodA begin ThreadName=" + Thread.currentThread().getName() +
                    " time=" + System.currentTimeMillis());
            Thread.sleep(1000 * 5);
        } catch(Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public static void main(String[] args) throws  Exception{
        MyConditionService service = new MyConditionService();
        new Thread(service::methodA,"A1").start();
        new Thread(service::methodA, "A2").start();
        new Thread(service::methodA, "A3").start();
        new Thread(service::methodA, "A4").start();
        new Thread(service::methodA, "A5").start();

        Thread.sleep(1000 * 5);
    }
}

可以看出Lock类加锁是对象锁。针对同一个lock对象执行的lock.lock是获得对象监视器的线程才能执行同步代码 其他线程都要等待。在这个例子中,加锁,和释放锁都是在try-finally。这样的好处是在任何异常发生的情况下,都能保障锁的释放。

Lock类其他的功能

如果Lock类只有lock和unlock方法也太简单了,Lock类提供了丰富的加锁的方法和对加锁的情况判断。主要有

  • 实现锁的公平
  • 获取当前线程调用lock的次数,也就是获取当前线程锁定的个数
  • 获取等待锁的线程数
  • 查询指定的线程是否等待获取此锁定
  • 查询是否有线程等待获取此锁定
  • 查询当前线程是否持有锁定
  • 判断一个锁是不是被线程持有
  • 加锁时如果中断则不加锁,进入异常处理
  • 尝试加锁,如果该锁未被其他线程持有的情况下成功

A.实现公平锁

在实例化锁对象的时候,构造方法有2个,一个是无参构造方法,一个是传入一个boolean变量的构造方法。当传入值为true的时候,该锁为公平锁。默认不传参数是非公平锁。

公平锁:按照线程加锁的顺序来获取锁
非公平锁:随机竞争来得到锁
此外,JAVA还提供isFair()来判断一个锁是不是公平锁。

B.获取当前线程锁定的个数

Java提供了getHoldCount()方法来获取当前线程的锁定个数。所谓锁定个数就是当前线程调用lock方法的次数。一般一个方法只会调用一个lock方法,但是有可能在同步代码中还有调用了别的方法,那个方法内部有同步代码。这样,getHoldCount()返回值就是大于1。

C.下面的方法用来判断等待锁的情况

Java提供了getQueueLength()方法来得到等待锁释放的线程的个数。

Java提供了hasQueuedThread(Thread thread)查询该Thread是否等待该lock对象的释放。

同样,Java提供了一个简单判断是否有线程在等待锁释放即hasQueuedThreads()

D.下面的方法用来判断持有锁的情况

Java不仅提供了判断是否有线程在等待锁释放的方法,还提供了是否当前线程持有锁即isHeldByCurrentThread(),判断当前线程是否有此锁定。

同样,Java提供了简单判断一个锁是不是被一个线程持有,即isLocked()

E.下面的方法用来实现多种方式加锁

加锁时如果中断则不加锁,进入异常处理

Lock类提供了多种选择的加锁方法,lockInterruptibly()也可以实现加锁,但是当线程被中断的时候,就会加锁失败,进行异常处理阶段。一般这种情况出现在该线程已经被打上interrupted的标记了。

尝试加锁,如果该锁未被其他线程持有的情况下成功

Java提供了tryLock()方法来进行尝试加锁,只有该锁未被其他线程持有的基础上,才会成功加锁。

上面介绍了Lock类来实现代码的同步处理,下面介绍Condition类来实现wait/notify机制。

 

Condition类

Condition是Java提供了来实现等待/通知的类,Condition类还提供比wait/notify更丰富的功能,Condition对象是由lock对象所创建的。但是同一个锁可以创建多个Condition的对象,即创建多个对象监视器。这样的好处就是可以指定唤醒线程。notify唤醒的线程是随机唤醒一个。

下面,看一个例子,显示简单的等待/通知

public class ConditionWaitNotifyService {
    private Lock lock = new ReentrantLock();
    public Condition condition = lock.newCondition();

    public void await() {
        try{
            lock.lock();
            System.out.println("await的时间为 " + System.currentTimeMillis());
            condition.await();
            System.out.println("await结束的时间" + System.currentTimeMillis());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void signal() {

        try {
            lock.lock();
            System.out.println("sign的时间为 " + System.currentTimeMillis());
            condition.signal();
        } catch(Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    
    public static void main(String[] args) throws  Exception {
        ConditionWaitNotifyService service = new ConditionWaitNotifyService();
        new Thread(service::await, "t1").start();
        Thread.sleep(1000 * 3);
        new Thread(service::signal,"t2").start();
        Thread.sleep(1000);
    }
}

运行结果:

await的时间为 1570678996065
sign的时间为 1570678999066
await结束的时间1570678999066

condition对象通过lock.newCondition()来创建,用condition.await()来实现让线程等待,是线程进入阻塞。用condition.signal()来实现唤醒线程。唤醒的线程是用同一个conditon对象调用await()方法而进入阻塞。并且和wait/notify一样,await()和signal()也是在同步代码区内执行。
此外看出await结束的语句是在获取通知之后才执行,确实实现了wait/notify的功能。

对于等待/通知机制,简化而言,就是等待一个条件,当条件不满足时,就进入等待,等条件满足时,就通知等待的线程开始执行。为了实现这种功能,需要进行wait的代码部分与需要进行通知的代码部分必须放在同一个对象监视器里面。执行才能实现多个阻塞的线程同步执行代码,等待与通知的线程也是同步进行。对于wait/notify而言,对象监视器与等待条件结合在一起 即synchronized(对象)利用该对象去调用wait以及notify。但是对于Condition类,是对象监视器与条件分开,Lock类来实现对象监视器,condition对象来负责条件,去调用await以及signal。

Condition类的其他功能

和wait类提供了一个最长等待时间,awaitUntil(Date deadline)在到达指定时间之后,线程会自动唤醒。但是无论是await或者awaitUntil,当线程中断时,进行阻塞的线程会产生中断异常。Java提供了一个awaitUninterruptibly的方法,即使线程中断时,进行阻塞的线程也不会产生中断异常。

读写锁

Lock类除了提供了ReentrantLock的锁以外,还提供了ReentrantReadWriteLock的锁。读写锁分成两个锁,一个锁是读锁,一个锁是写锁。读锁与读锁之间是共享的,读锁与写锁之间是互斥的,写锁与写锁之间也是互斥的。

读读共享的例子:

public class ReadReadService {
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public void read() {
        try {
            try {
                lock.readLock().lock();
                System.out.println("获得读锁" + Thread.currentThread().getName() +
                        " " + System.currentTimeMillis());
                Thread.sleep(1000 * 10);
            } finally {
                lock.readLock().unlock();
            }
        } catch (InterruptedException e){
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        ReadReadService service = new ReadReadService();
        new Thread(service::read, "A").start();
        new Thread(service::read, "B").start();
    }
}

运行结果:两个线程几乎同时执行了同步代码

获得读锁A 1570686923325
获得读锁B 1570686923343

下面是写写互斥的例子:

public class WriteWriteService {

    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public void write() {
        try {
            try {
                lock.writeLock().lock();
                System.out.println("获得写锁" + Thread.currentThread().getName() +
                        " " + System.currentTimeMillis());
                Thread.sleep(1000 * 10);
            } finally {
                lock.writeLock().unlock();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws Exception{
        WriteWriteService writeWriteService = new WriteWriteService();
        new Thread(writeWriteService::write,"W1").start();
        new Thread(writeWriteService::write,"W2").start();
        Thread.sleep(1000 * 30);
    }
}

运行结果:

获得写锁W1 1570687401651
获得写锁W2 1570687411652

 

读写互斥例子省略~

总结

本文介绍了新的同步代码的方式Lock类以及新的等待/通知机制的实现Condition类。本文只是很简单的介绍了他们的概念和使用的方式。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值