原创

Java并发编程(五)-Lock

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/lyhkmm/article/details/83754604

Lock

        Lock是java 1.5中引入的线程同步工具,它主要用于多线程下共享资源的控制。本质上Lock仅仅是一个接口(位于源码包中的java\util\concurrent\locks中),它包含以下方法

//尝试获取锁,获取成功则返回,否则阻塞当前线程
void lock(); 
//尝试获取锁,线程在成功获取锁之前被中断,则放弃获取锁,抛出异常 
void lockInterruptibly() throws InterruptedException; 
//尝试获取锁,获取锁成功则返回true,否则返回false 
boolean tryLock(); 
//尝试获取锁,若在规定时间内获取到锁,则返回true,否则返回false,未获取锁之前被中断,则抛出异常 
boolean tryLock(long time, TimeUnit unit) 
                                   throws InterruptedException; 
//释放锁
void unlock(); 
//返回当前锁的条件变量,通过条件变量可以实现类似notify和wait的功能,一个锁可以有多个条件变量
Condition newCondition();

        Lock有三个实现类,一个是ReentrantLock,另两个是ReentrantReadWriteLock类中的两个静态内部类ReadLock和WriteLock。

synchronized的缺陷

       synchronized是java中的一个关键字,也就是说是Java语言内置的特性。那么为什么会出现Lock呢?
  如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:
  1、获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
  2、线程执行发生异常,此时JVM会让线程自动释放锁。
  那么如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,严重影响程序执行效率。
  因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。
  再举个例子:当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。但是采用synchronized关键字来实现同步的话,就会导致一个问题:如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。
  因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。
  Lock提供了比synchronized更多的功能。但是要注意以下几点:
  1、Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;
  2、Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

ReentrantLock

       ReentrantLock,意思是“可重(chong)入锁”,是唯一实现了Lock接口的类。
       所谓可重入,意味着线程可以进入它已经拥有的锁的同步代码块儿。可以理解为一个锁可以被持有这个锁的人多次加锁,当然加多少次锁就需要解多少次。直到所有锁都解完其他人才能加锁。
       所谓不可重入锁,即若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞。可以理解为一个锁只能被持有这个锁的人加锁一次,必须释放这个锁后才能继续加锁。

使用方法

       ReentrantLock()默认构造方法是非公平锁,公平锁可以使用ReentrantLock(true)。多线程下访问(互斥)共享资源时, 访问前加锁,访问结束以后解锁,解锁的操作推荐放入finally块中。

//根据不同的实现Lock接口类的构造函数得到一个锁对象
Lock l = new ReentrantLock(); 
l.lock(); //获取锁位于try块的外面 
try { 
      // access the resource protected by this lock 
} finally { 
     l.unlock(); 
}

       注意,加锁位于对资源访问的try块的外部,特别是使用lockInterruptibly方法加锁时就必须要这样做,这为了防止线程在获取锁时被中断,这时就不必(也不能)释放锁。

Lock l = new ReentrantLock(); 
try {
     l.lockInterruptibly();//获取锁失败时不会执行finally块中的unlock语句
      try{
          //处理任务
     }finally{
          l.unlock();
     }
} catch (InterruptedException e) {
     e.printStackTrace();
}

       tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
  tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
  所以,一般情况下通过tryLock来获取锁时是这样使用的:

Lock lock = new ReentrantLock();
if(lock.tryLock()) {
     try{
         //处理任务
     }catch(Exception ex){
         e.printStackTrace();
     }finally{
         lock.unlock();   //释放锁
     } 
}else {
    //如果不能获取锁,则直接做其他事情
}

       具体使用方法

public class TestLock {
    //这里的锁要设置为全员变量,切勿设置为局部变量
    static Lock l = new ReentrantLock();
    static int threadNum=3;
    public static void main(String str[]){
        for(int i=1;i<=threadNum;i++){
            new Thread(new LockJob()).start();
        }
    }
    static class LockJob implements Runnable{
        @Override
        public void run() {
            l.lock(); //获取锁位于try块的外面
            try {
                System.out.println(Thread.currentThread().getName()+"得到了锁");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println(Thread.currentThread().getName()+"释放了锁");
                l.unlock();
            }
        }
    }
}

       运行结果

Thread-0得到了锁
Thread-0释放了锁
Thread-2得到了锁
Thread-2释放了锁
Thread-1得到了锁
Disconnected from the target VM, address: '127.0.0.1:56245', transport: 'socket'
Thread-1释放了锁

Condition

       lockInterruptibly(),tryLock和上面使用类似,单独说一下Condition条件锁。Condition有5个方法,通过方法名很容易明白其意思。

void await() throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
void signal();
void signalAll();

       其中await()方法相当于Object的wait()方法,Condition中的signal()方法相当于Object的notify()方法,Condition中的signalAll()相当于Object的notifyAll()方法。不同的是,Object中的这些方法是和同步锁捆绑使用的;而Condition是需要与互斥锁/共享锁捆绑使用的。
       Condition它更强大的地方在于:能够更加精细的控制多线程的休眠与唤醒。对于同一个锁,我们可以创建多个Condition,在不同的情况下使用不同的Condition。
       例如,假如多线程读/写同一个缓冲区:当向缓冲区中写入数据之后,唤醒”读线程”;当从缓冲区读出数据之后,唤醒”写线程”;并且当缓冲区满的时候,”写线程”需要等待;当缓冲区为空时,”读线程”需要等待。
       如果采用Object类中的wait(), notify(), notifyAll()实现该缓冲区,当向缓冲区写入数据之后需要唤醒”读线程”时,不可能通过notify()或notifyAll()明确的指定唤醒”读线程”,而只能通过notifyAll唤醒所有线程(但是notifyAll无法区分唤醒的线程是读线程,还是写线程)。 但是,通过Condition,就能明确的指定唤醒读线程。
       下面模拟往容量为5的队列进行50次增删操作,队列的容量始终保持在0-5之间。

public class TestCondition {
    //这里的锁要设置为全员变量,切勿设置为局部变量
    static Lock lock = new ReentrantLock();

    final static Condition insertCondition=lock.newCondition();

    final static Condition deleteCondition=lock.newCondition();
    
    static int num=50;

    static AtomicInteger AtomicInteger=new AtomicInteger(0);

    static ConcurrentLinkedQueue<String> concurrentLinkedQueue=new ConcurrentLinkedQueue();

    static CountDownLatch countDownLatch=new CountDownLatch(10);

    public static void main(String str[]){
        for(int i=1;i<=num;i++){
            //50次增删操作
            new Thread(new insertJob()).start();
            new Thread(new deleteJob()).start();
        }
    }
    static class insertJob implements Runnable{
        @Override
        public void run() {
            lock.lock();
            try{
                while (concurrentLinkedQueue.size()==5){
                    System.out.println(Thread.currentThread().getName()+":当前容器已满,等待有元素删除时继续执行!");
                    //当队列的元素等于5时,容器已经达到最大值无法继续进行插入操作
                    insertCondition.await();
                }
                AtomicInteger.getAndIncrement();
                concurrentLinkedQueue.add("我是元素");
                Thread.sleep(100);
                countDownLatch.countDown();
                //容器新增元素后,唤醒删除锁,表示可以进行删除操作;
                System.out.println(Thread.currentThread().getName()+"元素插入成功,当前队列大小:"+concurrentLinkedQueue.size());
                deleteCondition.signal();
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }
    }
    static class deleteJob implements Runnable{
        @Override
        public void run() {
            lock.lock();
            try{
                while (concurrentLinkedQueue.size()==0){
                    //当队列的元素等于0时,容器为空无法继续进行删除操作
                    System.out.println(Thread.currentThread().getName()+":当前容器已空,等待有元素插入时继续执行!");
                    deleteCondition.await();
                }
                concurrentLinkedQueue.poll();
                Thread.sleep(100);
                System.out.println(Thread.currentThread().getName()+"元素移除成功,当前队列大小:"+concurrentLinkedQueue.size());
                //容器删除元素后,唤醒增加锁,表示可以进行插入操作;
                insertCondition.signal();
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }
    }
}

小结

       Lock是AQS(AbstractQueuedSynchronizer队列同步器)自己维护当前等待资源的队列,AQS会在资源被释放后,依次唤醒队列中从前到后的所有节点,使他们对应的线程恢复执行,直到队列为空。
       Condition自己也维护了一个队列,该队列的作用是维护一个等待signal信号的队列,两个队列的作用是不同,事实上,每个线程也仅仅会同时存在以上两个队列中的一个,流程是这样的:

  1. 线程1调用reentrantLock.lock时,线程被加入到AQS的等待队列中。
  2. 线程1Condition.await()方法被调用时,该线程从AQS中移除,对应操作是锁的释放。接着马上被加入到Condition的等待队列中,以为着该线程需要signal信号。
  3. 线程2因为线程1释放锁的关系被唤醒,并判断可以获取锁,于是线程2获取锁,并被加入到AQS的等待队列中。
  4. 线程2调用signal方法,这个时候Condition的等待队列中只有线程1一个节点,于是它被取出来,并被加入到AQS的等待队列中。注意,这个时候线程1 并没有被唤醒。
  5. signal方法执行完毕,线程2调用reentrantLock.unLock()方法,释放锁。这个时候因为AQS中只有线程1,于是,AQS释放锁后按从头到尾的顺序唤醒线程时,线程1被唤醒,于是线程1回复执行。
  6. 一直到释放所整个过程执行完毕。

       可以看到,整个协作过程是靠结点在AQS的等待队列和Condition的等待队列中来回移动实现的,Condition作为一个条件类,很好的自己维护了一个等待信号的队列,并在适时的时候将结点加入到AQS的等待队列中来实现的唤醒操作。
       signal方法只是将Condition的Node节点修改了状态,并没有唤醒线程。要将修改状态后的Node唤醒,一种是再次调用await(),一种是调用unlock()。这两个方法内部都会执行release方法对队列里的Node解除阻塞。

正文到此结束
Loading...