多线程编程学习四(Lock 的使用)

一、前言

    本文要介绍使用Java5中 Lock 对象,同样也能实现同步的效果,而且在使用上更加方便、灵活,主要包括 ReentrantLock 类的使用和ReentrantReadWriteLock 类的使用。

    lock 与 synchronized 关键字的区别?

    • synchronized 是java内置关键字;Lock是个java类。
    • synchronized 会自动释放锁;Lock需在finally中手工释放锁(unlock()方法释放锁)
    • synchronized 的锁可重入、不可中断等待、非公平;而Lock锁可重入、可中断等待(利用超时机制)、可公平。
    • synchronized 的 wait() 和 notify() 或 notifyAll() 方法可以实现一个隐含的条件。而 Lock锁可以同时绑定多个 Condition 对象。

    等待可中断是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行时间非常长的同步块很有帮助。

    在Java 5之前,当一个线程获取不到锁而被阻塞在 synchronized 之外时,对该线程进行中断操作,此时该线程的中断标志位会被修改,但线程依旧会阻塞在synchronized上,等待着获取锁;在Java 5中,AQS 同步器提供了 acquireInterruptibly(int arg) 方法,这个方法在等待获取同步状态时,如果当前线程被中断,会立刻返回,并抛出 InterruptedException。

二、使用ReentrantLock 类

1、在java多线程中,可以使用 synchronized 关键字来实现线程之间同步互斥,但在JDK1.5中新增加的 ReentrantLock(重入锁) 也能达到同样的效果,并且在扩展功能上也更加强大,比如具有嗅探锁定、多路分支通知等功能,而且在使用上也比 synchronized 更加的灵活。

2、调用lock.lock()代码的线程就持有了“对象监视器”,即lock 持有的是对象锁,依赖于该类的实例存在。

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

3、关键字synchronized 与wait() 和 notify()/notifyAll() 方法相结合可以实现等待/通知模式,类ReentrantLock 也可以实现同样的功能,但需要借助于Condition对象。

Object类中的 wait() 方法相当于 Condition 类中的 await() 方法
Object类中的 wait(long timeout) 方法相当于Condition类中的 await(long time,TimeUnit unit) 方法
Object类中的 notify() 方法相当于 Condition 类中的 signal() 方法
Object类中的 notifyAll() 方法相当于 Condition 类中的 signalAll() 方法

 Myservice {
    private Condition condition=lock.newCondition();

    //等待
     waitMethod(){
        try {
            lock.lock();
            System.out.println("A");
            condition.await();调用的Condition的await等待方法也需要在同步方法中,否则会报错
            System.out.println("B");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    唤醒
     signal(){
         {
            lock.lock();
            System.out.println("现在开始唤醒...");
            condition.signal();
        } {
            lock.unlock();
        }
    }
}
View Code

4、使用多个 Condition 对象 实现线程之间的选择性通知。

通过定义多个Condition实现选择性通知,可以唤醒指定种类的线程,这是
    控制部分线程行为的方便形式
    private Condition conditionA=lock.newCondition();
    private Condition conditionB= awaitA(){
         {
            lock.lock();
            System.out.println("awaitA begin");
            conditionA.await();
            System.out.println("awaitA end" {
            lock.unlock();
        }
    }

     awaitB(){
         {
            lock.lock();
            System.out.println("awaitB begin");
            conditionB.await();
            System.out.println("awaitB end" signalA(){
         {
            lock.lock();
            System.out.println("现在开始唤醒awaitA");
            conditionA.signalAll();
        } signalB(){
         {
            lock.lock();
            System.out.println("现在开始唤醒awaitB");
            conditionB.signalAll();
        } {
            lock.unlock();
        }
    }
}
View Code

 Run
{
    static void main(String[] args) throws InterruptedException
    {
        MyService myService= MyService();
        Thread threadA= Thread(){
            @Override
             run()
            {
                super.run();
                myService.awaitA();
            }
        };

        Thread threadB=.run();
                myService.awaitB();
            }
        };

        threadA.start();
        threadB.start();
        Thread.sleep(1000);
        myService.signalA();
        Thread.sleep(1000);
        myService.signalB();

    }
}
View Code

5、公平锁和非公平锁

公平锁:表示线程获得锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO先进先出顺序。
非公平锁:一种获得锁的抢占机制,是随机获取锁的,和公平锁不一样的就是先来的不一定先得到锁,这种方式可能造成某些线程一直拿不到锁,结果也就是不公平的了。

 Service {
    private Lock lock;

    public Service(boolean isFair)
    {
        通过这种方式创建公平锁(true)和非公平锁(false)
        lock= ReentrantLock(isFair);
    }

     methodA(){
         {
            lock.lock();
            System.out.println(Thread.currentThread().getName()+"正在运行");
        } {
            lock.unlock();
        }
    }
}
View Code

 Run {
     main(String[] args)
    {
        final Service service=new Service(true);
        Runnable runnable= Runnable() {
            @Override
             run()
            {
             service.methodA();
            }
        };

        Thread[] threads=new Thread[10];
        for (int i=0;i<10;i++){
            threads[i]= Thread(runnable);
            threads[i].setName("线程"+(i+1));
            threads[i].start();
        }
    }
}
View Code

    公平锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换。非公平性锁虽然可能造成线程“饥饿”,但极少的线程切换,保证了其更大的吞吐量。

 6、ReentrantLock 常用方法介绍

(1) int getHoldCount() 查询当前线程保持此锁定的个数,也就是线程中调用lock方法的次数。

(2) int getQueueLength() 返回正等待此锁定的线程估计数,比如有5个线程,1个线程正占用了这个Lock锁在执行,则调用此方法返回的就是4。该值仅是估计的数字,因为在此方法遍历内部数据结构的同时,线程的数目可能动态地变化。此方法用于监视系统状态,不用于同步控制。

(3) int getWaitQueueLength(Condition condition) 返回等待与此锁定相关的给定条件Condition的线程估计数,比如有五个线程,每个线程都执行了同一个condition对象的await()方法,则调用此方法返回的值就是5。

private ReentrantLock lock= {
            lock.lock();
            System.out.println("A getHoldCount 调用lock的次数=>"+lock.getHoldCount());
            Thread.sleep(2000);
            System.out.println("A getQueueLength 正在等待的线程数=>"+lock.getQueueLength());
            condition.await();
        }  (InterruptedException e) {
            e.printStackTrace();
        } 测试getWaitQueueLength方法
    public Integer methodC(){
         {
            lock.lock();
            return lock.getWaitQueueLength(condition);
        } {
            lock.unlock();
        }

    }
}
View Code

 Run{

     InterruptedException {
        Service service= Service();
        Runnable runnable= Runnable()
        {
            @Override
             run()
            {
               service.methodA();
            }
        };

        Thread[] threads=new Thread[5 Thread(runnable);
            threads[i].start();
        }

        Thread.sleep(1000);
        System.out.println("执行了同一个Condition对象的的await()的线程有:"+service.methodC());
    }
}
View Code

(4) boolean hasQueuedThread(Thread thread) 查询指定的线程是否正在等待获取此锁定。

(5) boolean hasQueuedThreads() 查询是否有线程正在等待获取此锁定。

(6) boolean hasWaiters(Condition condition) 查询是否有线程正在等待与此锁定有关的condition条件

(7) boolean isFair() 判断是不是公平锁。

(8) boolean isHeldByCurrentThread() 查询当前线程是否保持此锁定。

(9) boolean isLocked() 查询此锁定是否由任意线程保持。

(10) void lockInterruptibly() 如果当前线程未被中断,则获取锁定,如果已经被中断,则出现异常。和 lock() 方法的区别在于该方法会响应中断,即在锁的获取中可以中断当前线程。

(11) boolean tryLock() 仅在调用时锁定未被另一个线程锁定的情况下,才获得此锁定。

(12) boolean tryLock(long timeout,TimeUnit unit) 如果锁定在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁定。

 Service
{
    测试lockInterruptibly
     {
            lock.lockInterruptibly();
            System.out.println("methodA=》"+Thread.currentThread().getName());
        }  {
            if (lock.isHeldByCurrentThread()){如果当前线程依旧保持对此锁的锁定,则释放
                lock.unlock();
            }
        }
    }
    测试tryLock
     methodB(){
          if (lock.tryLock()){
              System.out.println(Thread.currentThread().getName()+"获得锁");
          }else{
              System.out.println(Thread.currentThread().getName()+"未获得锁");
          }
    }
}
View Code

 InterruptedException
    {
        Service service= run()
            {
                service.methodA();
                service.methodB();
            }
        };

        Thread threadA= Thread(runnable);
        threadA.setName("A");
        threadA.start();

        Thread.sleep(1000);
        Thread threadB= Thread(runnable);
        threadB.setName("B");
        threadB.start();
        threadB.interrupt();
    }
}
View Code

(13) lock.awaitUninterruptibly():这个线程将不会被中断,一直睡眠直到其他线程调用signal()或signalAll()方法。

(14) lock.awaitUntil(Date date):这个线程将会一直睡眠直到:

    • 它被中断
    • 其他线程在这个condition上调用singal()或signalAll()方法
    • 指定的日期已经到了

    在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移出队列(或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用 tryRelease(int arg) 方法释放同步状态,然后唤醒头节点的后继节点。

三、使用ReentrantReadWriteLock 类 

      类 RenntrantLock 具有完全互斥排他的效果,即同一时间只有一个线程在执行 RenntrantLock.lock() 方法后面的任务。这样做虽然保证了实例变量的线程安全性,但效率却是非常低下的,因为即使有时候锁内没有写入内容,而也要等锁释放后,才能进行读取。所以JDK提供了一种读写锁 ReentrantReadWriteLock 类,使用它可以加快运行效率。             
      ReentrantReadWriteLock 有两个锁,都支持可重入,一个是读操作相关的锁,也称为共享锁;另一个是写操作相关的锁,也叫排他锁。也就是多个读锁之间不互斥、读锁与写锁互斥、写锁与写锁互斥。在没有线程 Thread 进行写入操作时,进行读取操作的多个Thread 都可以获取读锁。而进行写入操作的 Thread 只有在获取写锁后才能进行写入操作。即多个 Thread 可以同时进行读取操作,但是同一个时刻只允许一个 Thread 进行写入操作。
      读写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状态,“按位切割使用”将这个变量切分成了两个部分,高16位表示读,低16位表示写。
 
读读不互斥:

 Read
{
    private ReentrantReadWriteLock lock= ReentrantReadWriteLock();

     read(){
         {
            lock.readLock().lock();
            System.out.println(Thread.currentThread().getName()+"正在读"+System.currentTimeMillis());
            Thread.sleep(100 {
            lock.readLock().unlock();
        }
    }
}
View Code

 main(String[] args)
    {
        Read read= Read();
        Runnable runnable= run()
            {
                read.read();
            }
        };
        Thread[] threads= Thread(runnable);
            threads[i].start();
            通过结果可以看到所有线程几乎同时进入lock()方法
            后面的代码,读读不互斥,可以提高程序运行效率,允许
            多个线程同时执行lock()方法后面的代码
        }
    }
}
View Code
写写互斥:

 Write
{
     write(){
         {
            lock.writeLock().lock();
            System.out.println(Thread.currentThread().getName()+"正在写"+System.currentTimeMillis());
            Thread.sleep(2000 {
            lock.writeLock().unlock();
        }
    }
}
View Code

 main(String[] args)
    {
        Write write= Write();
        Runnable runnable= run()
            {
                write.write();
            }
        };
        Thread[] threads=通过结果可以看到所有线程每隔两秒运行一次,写写互斥,线程之间是同步运行的
        }
    }
}
View Code

另外,写读、读写都是互斥的,就不举例了。总之,只要出现"写"操作,就是互斥的!原因在于:读写锁要确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。

      锁降级:锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。简单来说就是同一个线程中,写锁处理数据的同时,对读锁获取(防止其他写线程争抢到锁,从而改变数据)

 LockDowngrade {

    private ReentrantReadWriteLock lock =  ReentrantReadWriteLock();
    private ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
    private ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();

    private volatile Boolean update = false;

     processData() {
        readLock.lock();
        if (!update) {
             必须先释放读锁
            readLock.unlock();
             锁降级从写锁获取到开始
            writeLock.lock();
             {
                update) {
                     准备数据的流程(略)
                    update = ;
                }
                readLock.lock();
            }  {
                writeLock.unlock();
            }
            锁降级完成,写锁降级为读锁
        }
         使用数据的流程(略)
        }  {
            readLock.unlock();
        }
    }
}

四、Lock 锁的实现原理

    一些同步组件(ReentrantLock、ReentrantReadWriteLock 和 CountDownLatch 等)的实现基本都是通过聚合了一个同步器(AbstractQueuedSynchronizer)的子类来完成线程访问控制的。可以这样理解二者之间的关系:锁或其它同步组件是面向使用者的,它定义了使用者与锁交互的接口,隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域。

    队列同步器 AbstractQueuedSynchronizer(基于模板方式模式),是用来构建锁或者其他同步组件的基础框架,它使用了一个 int成员变量(state)表示同步状态,通过内置的 FIFO 队列来完成资源获取线程的排队工作。

    同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的 3 个方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))来进行操作,因为它们能够保证状态的改变是安全的。子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口。它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态。

    独占锁的获取机制:

    在 Object 的监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包中的 Lock(更确切地说是同步器)拥有一个同步队列和多个等待队列。

    当调用,ConditionObject.await() 时,相当于同步队列的首节点(获取了锁的节点)移动到Condition的等待队列(看起来是移动,实际上是构建了一个新的 Node)中,然后释放同步状态,唤醒同步队列中的后继节点,然后当前线程会进入等待状态 — LockSupport.park()。如果等待期间被中断,会抛出 InterruptedException。

    当调用,ConditionObject.signal() 时,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移到同步队列中(获取锁的过程),并使用 LockSupport.unpark 唤醒节点中的线程。

    当调用,ConditionObject.signalAll() 时,相当于对等待队列中的每个节点均执行一次 signal()方法,效果就是将等待队列中所有节点全部移动到同步队列中,并唤醒每个节点的线程。

相关文章

摘要: 原创出处 https://www.bysocket.com 「公众号:泥瓦匠...
摘要: 原创出处 https://www.bysocket.com 「公众号:泥瓦匠...
今天犯了个错:“接口变动,伤筋动骨,除非你确定只有你一个...
Writer :BYSocket(泥沙砖瓦浆木匠)微 博:BYSocket豆 瓣:...
本文目录 线程与多线程 线程的运行与创建 线程的状态 1 线程...