Java 并发--- Lock

从Java 5以后,在java.util.concurrent.locks包下提供了另一种方式来实现同步访问,那就是Lock。java

synchronized的缺陷

若是一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其余线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种状况:ide

  1. 获取锁的线程执行完了该代码块,而后线程释放对锁的占有;
  2. 线程执行发生异常,此时JVM会让线程自动释放锁。

那么若是这个获取锁的线程因为要等待IO或者其余缘由(好比调用sleep方法)被阻塞了,可是又没有释放锁,其余线程便只能等待。this

所以就须要有一种机制能够不让等待的线程一直无期限地等待下去。线程

再举个例子:当有多个线程读写文件时,读操做和写操做会发生冲突现象,写操做和写操做会发生冲突现象,可是读操做和读操做不会发生冲突现象。code

可是采用synchronized关键字来实现同步的话,就会致使一个问题:若是多个线程都只是进行读操做,因此当一个线程在进行读操做时,其余线程只能等待没法进行读操做。排序

所以就须要一种机制来使得多个线程都只是进行读操做时,线程之间不会发生冲突,经过Lock就能够办到。接口

另外,经过Lock能够知道线程有没有成功获取到锁。这个是synchronized没法办到的。进程

总结一下,也就是说Lock提供了比synchronized更多的功能。可是要注意如下几点:资源

  1. Lock不是Java语言内置的,synchronized是Java语言的关键字,所以是内置特性。Lock是一个类,经过这个类能够实现同步访问;
  2. Lock和synchronized有一点很是大的不一样,采用synchronized不须要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完以后,系统会自动让线程释放对锁的占用;而Lock则必需要用户去手动释放锁,若是没有主动释放锁,就有可能致使出现死锁现象。

java.util.concurrent.locks包下经常使用的类

Lock

Lock是一个接口:get

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用来获取锁的。unLock()方法是用来释放锁的。
***
lock方法用来获取锁,若是锁已被其余线程获取,则进行等待。

使用Lock必须在try{}catch{}块中进行,而且将释放锁的操做放在finally块中进行,以保证锁必定被被释放,防止死锁的发生。一般使用Lock来进行同步的话,是如下面这种形式去使用的:

Lock lock = ...;
lock.lock();
try{
    //处理任务
}catch(Exception ex){
     
}finally{
    lock.unlock();   //释放锁
}

tryLock()方法是有返回值的,它表示用来尝试获取锁,若是获取成功,则返回true,若是获取失败(即锁已被其余线程获取),则返回false,也就说这个方法不管如何都会当即返回。在拿不到锁时不会一直在那等待。

tryLock(long time, TimeUnit unit)方法和tryLock()方法是相似的,只不过区别在于这个方法在拿不到锁时会等待必定的时间,在时间期限以内若是还拿不到锁,就返回false。若是一开始拿到锁或者在等待期间内拿到了锁,则返回true。

通常状况下经过tryLock来获取锁时是这样使用的:

Lock lock = ...;
if(lock.tryLock()) {
     try{
         //处理任务
     }catch(Exception ex){
         
     }finally{
         lock.unlock();   //释放锁
     } 
}else {
    //若是不能获取锁,则直接作其余事情
}

lockInterruptibly()方法比较特殊,当经过这个方法去获取锁时,若是线程正在等待获取锁,则这个线程可以响应中断,即中断线程的等待状态。也就是说,当两个线程同时经过lock.lockInterruptibly()想获取某个锁时,倘若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法可以中断线程B的等待过程。

因为lockInterruptibly()的声明中抛出了异常,因此lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException。

所以lockInterruptibly()通常的使用形式以下:

public void method() throws InterruptedException {
    lock.lockInterruptibly();
    try {  
     //.....
    }
    finally {
        lock.unlock();
    }  
}

注意,当一个线程获取了锁以后,是不会被interrupt()方法中断的。由于自己在前面的文章中讲过单独调用interrupt()方法不能中断正在运行过程当中的线程,只能中断阻塞过程当中的线程。

所以当经过lockInterruptibly()方法获取某个锁时,若是不能获取到,只有进行等待的状况下,是能够响应中断的。

而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是没法被中断的,只有一直等待下去。

ReentrantLock

ReentrantLock,意思是“可重入锁”,ReentrantLock是惟一实现了Lock接口的类,而且ReentrantLock提供了更多的方法。

示例1 lock()的正确使用方法

public class Test {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    public static void main(String[] args)  {
        final Test test = new Test();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
    }  
     
    public void insert(Thread thread) {
        Lock lock = new ReentrantLock();    //注意这个地方
        lock.lock();
        try {
            System.out.println(thread.getName()+"获得了锁");
            for(int i=0;i<5;i++) {
                arrayList.add(i);
            }
        } catch (Exception e) {
            // TODO: handle exception
        }finally {
            System.out.println(thread.getName()+"释放了锁");
            lock.unlock();
        }
    }
}

输出结果为:

Thread-0获得了锁
Thread-1获得了锁
Thread-0释放了锁
Thread-1释放了锁

第二个线程怎么会在第一个线程释放锁以前获得了锁?缘由在于,在insert方法中的lock变量是局部变量,每一个线程执行该方法时都会保存一个副本,那么理所固然每一个线程执行到lock.lock()处获取的是不一样的锁,因此就不会发生冲突。

因此,只须要将lock声明为类的属性便可。

示例2 tryLock()的使用方法

public class Test {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private Lock lock = new ReentrantLock();    //注意这个地方
    public static void main(String[] args)  {
        final Test test = new Test();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
    }  
     
    public void insert(Thread thread) {
        if(lock.tryLock()) {
            try {
                System.out.println(thread.getName()+"获得了锁");
                for(int i=0;i<5;i++) {
                    arrayList.add(i);
                }
            } catch (Exception e) {
                // TODO: handle exception
            }finally {
                System.out.println(thread.getName()+"释放了锁");
                lock.unlock();
            }
        } else {
            System.out.println(thread.getName()+"获取锁失败");
        }
    }
}

输出结果为:

Thread-0获得了锁
Thread-1获取锁失败
Thread-0释放了锁

示例3 lockInterruptibly()响应中断的使用方法

public class Test {
    private Lock lock = new ReentrantLock();   
    public static void main(String[] args)  {
        Test test = new Test();
        MyThread thread1 = new MyThread(test);
        MyThread thread2 = new MyThread(test);
        thread1.start();
        thread2.start();
         
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread2.interrupt();
    }  
     
    public void insert(Thread thread) throws InterruptedException{
        lock.lockInterruptibly();   //注意,若是须要正确中断等待锁的线程,必须将获取锁放在外面,而后将InterruptedException抛出
        try {  
            System.out.println(thread.getName()+"获得了锁");
            long startTime = System.currentTimeMillis();
            for(    ;     ;) {
                if(System.currentTimeMillis() - startTime >= Integer.MAX_VALUE)
                    break;
                //插入数据
            }
        }
        finally {
            System.out.println(Thread.currentThread().getName()+"执行finally");
            lock.unlock();
            System.out.println(thread.getName()+"释放了锁");
        }  
    }
}
 
class MyThread extends Thread {
    private Test test = null;
    public MyThread(Test test) {
        this.test = test;
    }
    @Override
    public void run() {
         
        try {
            test.insert(Thread.currentThread());
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName()+"被中断");
        }
    }
}

运行以后,发现thread2可以被正确中断。

ReadWriteLock

ReadWriteLock是一个接口,在它里面只定义了两个方法:

public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}

一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操做分开,分红2个锁来分配给线程,从而使得多个线程能够同时进行读操做。

ReentrantReadWriteLock

ReentrantReadWriteLock实现了ReadWriteLock接口,经过例子来看ReentrantReadWriteLock具体用法。

public class Test {
    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
     
    public static void main(String[] args)  {
        final Test test = new Test();
         
        new Thread(){
            public void run() {
                test.get(Thread.currentThread());
            };
        }.start();
         
        new Thread(){
            public void run() {
                test.get(Thread.currentThread());
            };
        }.start();
         
    }  
     
    public void get(Thread thread) {
        rwl.readLock().lock();
        try {
            long start = System.currentTimeMillis();
             
            while(System.currentTimeMillis() - start <= 1) {
                System.out.println(thread.getName()+"正在进行读操做");
            }
            System.out.println(thread.getName()+"读操做完毕");
        } finally {
            rwl.readLock().unlock();
        }
    }
}

输出结果显示thread1和thread2在同时进行读操做。而若是get方法改为synchronized,输出结果为:thread1先执行完读操做,而后thread2再执行读操做。

public synchronized void get(Thread thread) {
        long start = System.currentTimeMillis();
        while(System.currentTimeMillis() - start <= 1) {
            System.out.println(thread.getName()+"正在进行读操做");
        }
        System.out.println(thread.getName()+"读操做完毕");
    }

不过要注意的是,若是有一个线程已经占用了读锁,此时其余线程若是要申请写锁,则申请写锁的线程会一直等待释放读锁。若是有一个线程已经占用了写锁,则此时其余线程若是申请写锁或者读锁,则申请的线程会一直等待释放写锁。

Lock和synchronized的选择

Lock和synchronized有如下几点不一样:

  1. Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
  2. synchronized在发生异常时,会自动释放线程占有的锁,所以不会致使死锁现象发生;而Lock在发生异常时,若是没有主动经过unLock()去释放锁,则极可能形成死锁现象,所以使用Lock时须要在finally块中释放锁;
  3. Lock可让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不可以响应中断;
  4. 经过Lock能够知道有没有成功获取锁,而synchronized却没法办到。
  5. Lock能够提升多个线程进行读操做的效率。

锁的相关概念

可重入锁

synchronized和ReentrantLock都是可重入锁。当一个线程执行到某个synchronized方法时,好比说method1,而在method1中会调用另一个synchronized方法method2,此时线程没必要从新去申请锁,而是能够直接执行方法method2。可重入锁最大的做用是避免死锁。

可中断锁

就是能够相应中断的锁。在Java中,synchronized就不是可中断锁,而Lock是可中断锁。

若是某一线程A正在执行锁中的代码,另外一线程B正在等待获取该锁,可能因为等待时间过长,线程B不想等待了,想先处理其余事情,咱们可让它中断本身或者在别的线程中中断它,这种就是可中断锁。

lockInterruptibly()的用法体现了Lock的可中断性。

公平锁

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

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

公平锁的好处是等待锁的线程不会饿死,可是总体效率相对低一些;非公平锁的好处是总体效率相对高一些,可是有些线程可能会饿死或者说很早就在等待锁,但要等好久才会得到锁。

在Java中,synchronized就是非公平锁,它没法保证等待的线程获取锁的顺序。而对于ReentrantLock和ReentrantReadWriteLock,它默认状况下是非公平锁,可是能够设置为公平锁。

公平锁可使用new ReentrantLock(true)实现。

读写锁

读写锁将对一个资源(好比文件)的访问分红了2个锁,一个读锁和一个写锁。正由于有了读写锁,才使得多个线程之间的读操做不会发生冲突。

ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。能够经过readLock()获取读锁,经过writeLock()获取写锁。

死锁

死锁是指两个或两个以上的进程在执行过程当中,因争夺资源而形成的一种互相等待的现象,若无外力做用,他们都将没法推动下去。这是一个严重的问题,由于死锁会让你的程序挂起没法完成任务,死锁的发生必须知足如下4个条件:

  1. 互斥条件:一个资源每次只能被一个进程使用。
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已得到的资源保持不放。
  3. 不剥夺条件:进程已得到的资源,在未使用完以前,不能强行剥夺。
  4. 循环等待条件:若干进程之间造成一种头尾相接的循环等待资源关系。

避免死锁最简单的方法就是阻止循环等待条件,将系统中全部的资源设置标志位、排序,规定全部的进程申请资源必须以必定的顺序作操做来避免死锁。

相关文章
相关标签/搜索