Java并发编程深入学习——Lock锁

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java并发编程深入学习——Lock锁相关的知识,希望对你有一定的参考价值。

Lock锁介绍

??在Java 5.0之前,在协调对共享对象的访问时可以使用的机制只有synchronized和volatile。Java 5.0 增加了一种新的机制:ReentrantLock.它并不是一种替代内置加锁的方法,而是当内置加锁机制不适用时,作为一种可选择的高级功能。

Lock接口

Lock接口位于java.util.concurrent.locks包中,它定义了一组抽象的加锁操作。

public interface Lock {
    //获取锁
    void lock();
    // 如果当前线程未被中断,则获取锁
    void lockInterruptibly() throws InterruptedException;
    //仅在调用时锁为空闲状态才获取该锁
//如果锁可用,则获取锁,并立即返回值 true。如果锁不可用,则此方法将立即返回值 false
    boolean tryLock();

  //如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

   //释放锁
    void unlock();
    //返回绑定到此 Lock 实例的新 Condition 实例
    Condition newCondition();
}

??ReetrantLock 实现了Lock接口,并提供了与synchronized相同的互斥性和内存可见性,在获取ReentrantLock时,有着与进入同步代码块相同的内存语义,在释放ReentrantLock时,同样有着与退出同步代码块相同的内存语义。

lock锁与synchronized锁对比

为什么要创建一种与内置锁如此相似的新加锁机制?在大多数情况下,内置锁都能很好地工作,但在功能上存在一些局限性。

  1. 内置锁无法中断一个正在等待获取锁的线程,或者无法在请求获取一个锁时无限地等待下去。
  2. 内置锁必须在获取该锁的代码块中释放,这虽然简化了编码工作,并且与异常处理操作实现了很好的交互,但却无法实现非阻塞结构的加锁规则。

所以需要一种更加灵活的加锁机制,lock锁便应运而生。

Lock锁的标准使用形式如下:

Lock lock = new ReentrantLock();
      if (lock.tryLock()) {//尝试获取锁
          try {
              //更新对象状态
              //捕获异常,并在必要时恢复不变性条件
          } finally {
              lock.unlock();//注意要记得释放锁
          }
      } else {
          // 获取锁失败执行其他操作
      }

如果没有使用finally来释放Lock,那么程序出错时,将很难追踪到最初发生错误的位置,因为没有记录应该释放锁的位置和时间。

这一点也是ReetrantLock不能完全替代synchronized的原因,因为它更加危险,程序并没有自动清除锁的机制,使用起来需要格外小心。

锁的分类

1.可重入锁

??当某一个线程请求一个由其他线程持有的锁时,发去请求的线程就会阻塞。由于内置锁可重入特性的存在,如果某个线程视图获得一个已经由它自己持有的锁,那么这个请求却会成功。

??如果锁具备可重入性,则称作为可重入锁。.像synchronized和ReentrantLock都是可重入锁,重入性表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配

重入锁的实现机制如下:

??为每个锁关联一个获取计数值和一个所有者线程。当计数器为0时这个锁被认为没有被任何线程持有。当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取计数器置为1。如果同一个线程再次获取这个锁,计数器将递增,而当线程退出同步代码块时,计数器会相应地递减。当计数器为0时,这个锁将被释放。

  可举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。

  看下面这段代码就明白了:

class MyClass {
   public synchronized void method1() {
       method2();
   }

   public synchronized void method2() {

   }
}

  上述代码中的两个方法method1和method2都用synchronized修饰了,假如某一时刻,线程A执行到了method1,此时线程A获取了这个对象的锁,而由于method2也是synchronized方法,假如synchronized不具备可重入性,此时线程A需要重新申请锁。但是这就会造成一个问题,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁。

  而由于synchronized和Lock都具备可重入性,所以不会发生上述现象。

2.可中断锁

  可中断锁:顾名思义,就是可以相应中断的锁。

  在Java中,synchronized就不是可中断锁,而Lock是可中断锁。

  如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。

  下面的例子展示了中断锁的场景。

public class TestLockInterrupt {
    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        final TestLockInterrupt test = new TestLockInterrupt();


        Thread ta = new Thread() {
            @Override
            public void run() {
                System.out.println("A线程启动了!->准备打印....");

                try {
                    test.print("A", "aaaa");
                } catch (InterruptedException e) {
                    System.out.println("A线程收到中断异常");
                }
            }
        };
        Thread tb = new Thread() {
            @Override
            public void run() {
                System.out.println("B线程启动了!->准备打印...");
                try {
                    test.print("B", "bbbb");
                } catch (InterruptedException e) {
                    System.out.println("B线程收到中断异常");
                }
            }
        };

        ta.start();
        tb.start();
        try {

            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //判定Lock是否还被某个线程持有
        if (((ReentrantLock) lock).isLocked()) {
            System.out.println("等了两秒还没有获得锁,直接中断!");
            tb.interrupt();
        }

    }


    public void print(String tName, String content) throws InterruptedException {
        lock.lockInterruptibly();
        try {
            //lock.lock();
            System.out.println("线程" + tName + "获取锁并打印内容" + content);
            //模拟耗时操作,使某个线程能够在较长时间独占锁
            Thread.currentThread().sleep(5000);
            //int i = 1;
           // while (i < 1000000000) {
            //    i++;
           // }
        } finally {
            lock.unlock();
            System.out.println("线程" + tName + "释放了锁");
        }
    }


}

运行结果

A线程启动了!->准备打印....
线程A获取锁并打印内容aaaa
B线程启动了!->准备打印...
等了两秒还没有获得锁,直接中断!
B线程收到中断异常
线程A释放了锁

3.公平锁

  公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该锁,这种就是公平锁。

  非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。

  在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。

  而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。在ReentrantLock中定义了2个静态内部类,一个是NotFairSync,一个是FairSync,分别用来实现非公平锁和公平锁。

这两个类的定义如下:

 /**
     * Sync object for non-fair locks
     */
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

    /**
     * Sync object for fair locks
     */
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }

        /**
         * Fair version of tryAcquire.  Don‘t grant access unless
         * recursive call or no waiters or is first.
         */
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }


  我们可以在创建ReentrantLock对象时,通过以下方式来设置锁的公平性:

ReentrantLock lock = new ReentrantLock(true);

  如果参数为true表示为公平锁,为fasle为非公平锁。默认情况下,如果使用无参构造器,则是非公平锁。
  

其他常用方法

另外在ReentrantLock类中定义了很多方法,比如:

  isFair()        //判断锁是否是公平锁
  isLocked()    //判断锁是否被任何线程获取了
  isHeldByCurrentThread()   //判断锁是否被当前线程获取了
  hasQueuedThreads()   //判断是否有线程在等待该锁

在ReentrantReadWriteLock中也有类似的方法,同样也可以设置为公平锁和非公平锁。不过要记住,ReentrantReadWriteLock并未实现Lock接口,它实现的是ReadWriteLock接口。

4.读写锁

  读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。

  正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。

  ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。这个接口定义如下:
  

  public interface ReadWriteLock {
       //获取读锁
        Lock readLock();
    //获取写锁
       Lock writeLock();
    }

下面的例子展示了读写锁的一些基本用法和特性。

public class TestMain {

    public static void main(String[] args) {
        ReadWriteLock lock = new ReentrantReadWriteLock();
        final Lock readLock = lock.readLock();
        final Lock writeLock = lock.writeLock();
        final Resource resource = new Resource();
        final Random random = new Random();
        for (int i = 0; i < 20; ++i)

        {//写线程
            new Thread() {
                public void run() {
                    writeLock.lock();
                    try {
                        resource.setValue(resource.getValue() + 1);
                        System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date()) + " - " + Thread.currentThread() + "获取了写锁,修正数据为:" + resource.getValue());
                        Thread.sleep(random.nextInt(1000));//随机休眠
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        writeLock.unlock();
                    }
                }

            }.start();
        }
        for (int i = 0; i < 20; ++i)

        {//读线程
            new Thread() {
                public void run() {
                    readLock.lock();
                    try {
                        System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date()) + " - " + Thread.currentThread() + "获取了读锁,读取的数据为:" + resource.getValue());
                        Thread.sleep(random.nextInt(1000));//随机休眠
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        readLock.unlock();
                    }
                }
            }.start();
        }

    }


}
//资源类定义
class Resource {
    private int value;

    public void setValue(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }
}

运行结果

2016-09-13 10:16:59.947 - Thread[Thread-0,5,main]获取了写锁,修正数据为:1
2016-09-13 10:17:00.829 - Thread[Thread-1,5,main]获取了写锁,修正数据为:2
2016-09-13 10:17:01.502 - Thread[Thread-2,5,main]获取了写锁,修正数据为:3
2016-09-13 10:17:01.952 - Thread[Thread-3,5,main]获取了写锁,修正数据为:4
2016-09-13 10:17:02.641 - Thread[Thread-4,5,main]获取了写锁,修正数据为:5
2016-09-13 10:17:03.389 - Thread[Thread-5,5,main]获取了写锁,修正数据为:6
2016-09-13 10:17:04.380 - Thread[Thread-6,5,main]获取了写锁,修正数据为:7
2016-09-13 10:17:05.377 - Thread[Thread-7,5,main]获取了写锁,修正数据为:8
2016-09-13 10:17:06.306 - Thread[Thread-8,5,main]获取了写锁,修正数据为:9
2016-09-13 10:17:06.470 - Thread[Thread-9,5,main]获取了写锁,修正数据为:10
2016-09-13 10:17:06.696 - Thread[Thread-10,5,main]获取了写锁,修正数据为:11
2016-09-13 10:17:06.911 - Thread[Thread-11,5,main]获取了写锁,修正数据为:12
2016-09-13 10:17:07.141 - Thread[Thread-12,5,main]获取了写锁,修正数据为:13
2016-09-13 10:17:07.170 - Thread[Thread-13,5,main]获取了写锁,修正数据为:14
2016-09-13 10:17:07.449 - Thread[Thread-14,5,main]获取了写锁,修正数据为:15
2016-09-13 10:17:07.939 - Thread[Thread-15,5,main]获取了写锁,修正数据为:16
2016-09-13 10:17:08.252 - Thread[Thread-16,5,main]获取了写锁,修正数据为:17
2016-09-13 10:17:08.798 - Thread[Thread-17,5,main]获取了写锁,修正数据为:18
2016-09-13 10:17:09.119 - Thread[Thread-18,5,main]获取了写锁,修正数据为:19
2016-09-13 10:17:09.353 - Thread[Thread-19,5,main]获取了写锁,修正数据为:20
2016-09-13 10:17:10.336 - Thread[Thread-20,5,main]获取了读锁,读取的数据为:20
2016-09-13 10:17:10.336 - Thread[Thread-21,5,main]获取了读锁,读取的数据为:20
2016-09-13 10:17:10.336 - Thread[Thread-22,5,main]获取了读锁,读取的数据为:20
2016-09-13 10:17:10.336 - Thread[Thread-23,5,main]获取了读锁,读取的数据为:20
2016-09-13 10:17:10.336 - Thread[Thread-24,5,main]获取了读锁,读取的数据为:20
2016-09-13 10:17:10.336 - Thread[Thread-25,5,main]获取了读锁,读取的数据为:20
2016-09-13 10:17:10.336 - Thread[Thread-26,5,main]获取了读锁,读取的数据为:20
2016-09-13 10:17:10.336 - Thread[Thread-27,5,main]获取了读锁,读取的数据为:20
2016-09-13 10:17:10.337 - Thread[Thread-28,5,main]获取了读锁,读取的数据为:20
2016-09-13 10:17:10.337 - Thread[Thread-29,5,main]获取了读锁,读取的数据为:20
2016-09-13 10:17:10.337 - Thread[Thread-30,5,main]获取了读锁,读取的数据为:20
2016-09-13 10:17:10.337 - Thread[Thread-31,5,main]获取了读锁,读取的数据为:20
2016-09-13 10:17:10.337 - Thread[Thread-32,5,main]获取了读锁,读取的数据为:20
2016-09-13 10:17:10.337 - Thread[Thread-33,5,main]获取了读锁,读取的数据为:20
2016-09-13 10:17:10.338 - Thread[Thread-34,5,main]获取了读锁,读取的数据为:20
2016-09-13 10:17:10.338 - Thread[Thread-35,5,main]获取了读锁,读取的数据为:20
2016-09-13 10:17:10.338 - Thread[Thread-36,5,main]获取了读锁,读取的数据为:20
2016-09-13 10:17:10.338 - Thread[Thread-37,5,main]获取了读锁,读取的数据为:20
2016-09-13 10:17:10.338 - Thread[Thread-38,5,main]获取了读锁,读取的数据为:20
2016-09-13 10:17:10.338 - Thread[Thread-39,5,main]获取了读锁,读取的数据为:20

从运行结果可以看到,写操作被互斥访问执行,而读操作同一时刻并发执行,加快了程序运行效率,这也完全符合我们的认识,读操作并不涉及数据变更,不存在同步的问题,而synchronized的机制尚不能做到这点。


参考资料
1. 《Java并发编程实践》
2. 《Java编程思想》
3. Java并发编程:Lock
4. Java多线程系列–“JUC锁”01之 框架







以上是关于Java并发编程深入学习——Lock锁的主要内容,如果未能解决你的问题,请参考以下文章

并发编程7:深入理解Java虚拟机-锁优化

Java并发编程:Lock

转: Java并发编程之二十:并发新特性—Lock锁和条件变量(含代码)

Java并发编程--Lock

Java并发编程系列之十六:Lock锁

Java并发编程:死锁(含代码)