Java
开发当中实现代码的同步,多线程操作共享资源时都是离不开锁的。接下来我们来了解一下Java
中所提供的synchronized
这个关键字和Lock
这个接口。
synchronized
与Lock
的区别首先我们看看两者的区别有哪些
类别
synchronized
Lock
存在层次
Java
的关键字,在JVM
层面上
是一个类
锁的释放
1. 以获取锁的线程执行完同步代码,释放锁; 2. 线程执行发生异常,JVM
会让线程释放锁
在finally
代码块中必须释放锁, 不然容易造成死锁。
锁的获取
如果A线程获取锁,B线程等待。 如果A线程阻塞,B线程会一直等待
分情况而定,Lock
有多个锁获取方式, 线程可以不用一直等待。
锁状态
无法判断
无法判断
锁类型
可重入 不可中断 非公平
可重入 可判断 可公平
性能
少量同步
大量同步
synchronized
的不足synchronized
是java
中的一个关键字,是java
语言内置的特性。在开发中我们都知道如果一个代码块被synchronized
修饰了,当一个线程获取了对应的锁,并执行该代码块时,其它线程只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:
获取锁的线程执行完了该代码块,然后线程释放对锁的占有。
线程执行发生异常,此时JVM
会让线程自动释放锁。
如果这个获取锁的线程由于要等待IO
或者其他原因被阻塞了,但是又没有释放锁,那么其他的线程只能一直等待,这样就对执行效率有很大的影响。
因此就需要有一种机制可以不让等待的线程一直无期限的等待下去(比如等待一定时间中断响应),通过Lock
就可以办到。通过Lock
线程之间不会发生冲突,另外通过Lock
可以知道线程有没有成功获取到锁,这个是synchronized
无法办到的。
也就是说Lock
提供了比synchronized
更多的功能,但是要注意以下两点:
Lock
不是Java
语言内置的,synchronized
是Java
语言的关键字,因此是内置特性。Lock
是一个类,通过这个类可以实现同步访问。
Lock
和synchronized
有一个非常大的不同,采用synchronized
不需要开发者去手动释放锁,当synchronized
方法或者synchronized
代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock
则必须要开发者去手动释放锁,如果没有主动释放锁,就有可能出现死锁现象。
Lock
介绍通过查看Lock
源码可以得知Lock
是一个接口
1 2 3 4 5 6 7 8 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()
方法是平常使用的最多的一个方法,就是用来获取锁,如果锁已被其他线程获取,则进入等待。
由于使用Lock
,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock
必须在try{}catch{}
块中进行,并且将释放锁的操作放在finally
块中进行,以保证锁一定被释放,防止死锁的发生。如:
1 2 3 4 5 6 7 8 9 Lock lock = ...; lock.lock(); try { }catch (Exception ex){ }finally { lock.unlock(); }
tryLock()
tryLock()
方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true
,如果获取失败返回false
,也就是说这个方法无论如何都会立即返回。在获取不到锁时不会一直在等待。
tryLock(long time,TimeUnit unit)
方法和tryLock()
方法类似,只不过区别在于这个方法在获取不到锁时会等待一定的时间,在时间期限之内如果还是没有获取到锁,就返回false
。如果一开或者在等待期间获取到锁了,则返回true
。
所以,一般情况下通过tryLock()
来获取锁是是这样使用的:
1 2 3 4 5 6 7 8 9 10 11 12 Lock lock = ...; if (lock.tryLock()) { try { }catch (Exception ex){ }finally { lock.unlock(); } }else { }
lockInterruptibly()
lockInterruptibly()
方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就是说,当两个线程同时通过lock.lockInterruptibly()
想获取某个锁时,假如这时A
线程获取到了锁,而线程B
只能在等待,那么对线程B调用threadB.interrupt()
方法能够中断线程B
的等待过程。
由于lockInterruptibly()
的声明中抛出了异常,所以lock.lockInterruptibly()
必须放在try
块中或者调用lockInterruptibly()的方法外声明抛出InterruptedException
lockInterruptibly()一般的使用形式如下:
1 2 3 4 5 6 7 8 9 public void method () throws InterruptedException { lock.lockInterruptibly(); try { } finally { lock.unlock(); } }
这里需要注意的就是,当一个线程获取了锁之后,是不会被interrupt()
访求中断的,单独调用interrupt()
方法是不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。
因此当通过lockInterruptibly()
方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以 响应中断的。而用synchronized
修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。
Lock
使用ReentrantLock
意思是可重入锁,ReentrantLock
是唯一实现了Lock接口的类,并且ReentrantLock
提供了更多的方法。下面通过一些实例看一下如何使用ReentrantLock
。
lock()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import java.util.ArrayList;import java.util.List;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class Test { private List<Integer> list = new ArrayList<Integer>(); public static void main (String[] args) { final Test test = new Test(); new Thread(){ public void run () { test.add(Thread.currentThread()); } }.start(); new Thread() { public void run () { test.add(Thread.currentThread()); } }.start(); } public void add (Thread thread) { Lock lock = new ReentrantLock(); lock.lock(); try { System.out.println(thread.getName() + " 获取到了锁" ); for (int i = 0 ; i < 5 ; i++) { list.add(i); } } catch (Exception e) { }finally { System.out.println(thread.getName() + " 释放了锁" ); lock.unlock(); } } }
1 2 3 4 Thread-1 获取到了锁 Thread-0 获取到了锁 Thread-1 释放了锁 Thread-0 释放了锁
怎么会出现这种结果呢?第二人线程怎么会在第一个线程释放锁之前获取到了锁,原因在于,在add()方法中的lock是一个局部变量,每个线程执行该方法都会保存一个副本,那么当每个线程执行到lock.lock()处获取的是不同的锁,所以就会发生上面这种情况。
那么修改代码,只要将lock声明为类属性就可以,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import java.util.ArrayList;import java.util.List;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class Test { private List<Integer> list = 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.add(Thread.currentThread()); } }.start(); new Thread() { public void run () { test.add(Thread.currentThread()); } }.start(); } public void add (Thread thread) { lock.lock(); try { System.out.println(thread.getName() + " 获取到了锁" ); for (int i = 0 ; i < 5 ; i++) { list.add(i); } } catch (Exception e) { }finally { System.out.println(thread.getName() + " 释放了锁" ); lock.unlock(); } } }
1 2 3 4 Thread-1 获取到了锁 Thread-1 释放了锁 Thread-0 获取到了锁 Thread-0 释放了锁
这样就正确了。
tryLock()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import java.util.ArrayList;import java.util.List;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class Test { private List<Integer> list = 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.add(Thread.currentThread()); } }.start(); new Thread() { public void run () { test.add(Thread.currentThread()); } }.start(); } public void add (Thread thread) { if (lock.tryLock()){ try { System.out.println(thread.getName() + " 获取到了锁" ); for (int i = 0 ; i < 5 ; i++) { list.add(i); } } catch (Exception e) { }finally { System.out.println(thread.getName() + " 释放了锁" ); lock.unlock(); } }else { System.out.println(thread.getName() + " 获取锁失败" ); } } }
1 2 3 Thread-0 获取到了锁 Thread-1 获取锁失败 Thread-0 释放了锁
lockInterruptibly()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class Test { private Lock lock = new ReentrantLock(); public static void main (String[] args) { Test test = new Test(); MyThread thd1 = new MyThread(test); MyThread thd2 = new MyThread(test); thd1.start(); thd2.start(); try { Thread.sleep(2000 ); } catch (Exception e) { e.printStackTrace(); } thd2.interrupt(); } public void add (Thread thread) throws InterruptedException { lock.lockInterruptibly(); try { System.out.println(thread.getName() + " 获取到了锁" ); long startTime = System.currentTimeMillis(); for (; ;) { if (System.currentTimeMillis() - startTime > Integer.MAX_VALUE) { break ; } } } catch (Exception e) { }finally { System.out.println(Thread.currentThread().getName() + " 执行finally" ); lock.unlock(); System.out.println(thread.getName() + " 释放了锁" ); } } } class MyThread extends Thread { private Test test; public MyThread (Test test) { this .test = test; } @Override public void run () { try { test.add(Thread.currentThread()); } catch (Exception e) { System.out.println(Thread.currentThread().getName() + " 被中断" ); } } }
1 2 Thread-0 获取到了锁 Thread-1 被中断
运行后,发现thd2能够补正确中断。
读写锁ReadWriteLock
ReadWriteLock
也是一个接口,在它里面只定义了两个方法:
1 2 3 4 5 6 7 8 9 10 11 12 public interface ReadWriteLock { Lock readLock () ; Lock writeLock () ; }
一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成两个锁分配给线程,从而使得多个线程可以同时进行读操作。下面的ReentrantReadWriteLock
实现了ReadWriteLock
接口。
ReentrantReadWriteLock
ReentrantReadWriteLock
里面提供了很多丰富的方法,不过最主要的两个方法是:readLock()
和writeLock()
用来获取锁和写锁。
下面通过几个例子来看一下ReentrantReadWriteLock
具体的用法
假如有多个线程要同时进行读操作,先看一下synchronized
达到的效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class Test { 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 synchronized void get (Thread thread) { long startTime = System.currentTimeMillis(); while (System.currentTimeMillis() - startTime <= 1 ) { System.out.println(thread.getName() + " 正在进行读操作" ); } System.out.println(thread.getName() + " 读操作完毕" ); } }
1 2 3 4 5 6 7 8 9 Thread-1 正在进行读操作 Thread-1 正在进行读操作 Thread-1 正在进行读操作 Thread-1 读操作完毕 Thread-0 正在进行读操作 Thread-0 正在进行读操作 Thread-0 正在进行读操作 Thread-0 正在进行读操作 Thread-0 读操作完毕
这段程序输出结果是直到Thread-1
执行完读操作之后,才会打印Thread-0
执行读操作的信息。
修改成读写锁,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import java.util.concurrent.locks.ReadWriteLock;import java.util.concurrent.locks.ReentrantReadWriteLock;public class Test { private ReadWriteLock 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 startTime = System.currentTimeMillis(); while (System.currentTimeMillis() - startTime <= 1 ) { System.out.println(thread.getName() + " 正在进行读操作" ); } System.out.println(thread.getName() + " 读操作完毕" ); } catch (Exception e) { e.printStackTrace(); }finally { rwl.readLock().unlock(); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Thread-0 正在进行读操作 Thread-0 正在进行读操作 Thread-0 正在进行读操作 Thread-1 正在进行读操作 Thread-1 正在进行读操作 Thread-1 正在进行读操作 Thread-1 正在进行读操作 Thread-1 正在进行读操作 Thread-1 正在进行读操作 Thread-1 正在进行读操作 Thread-1 正在进行读操作 Thread-0 正在进行读操作 Thread-1 正在进行读操作 Thread-0 正在进行读操作 Thread-1 正在进行读操作 Thread-0 读操作完毕 Thread-1 读操作完毕
这样就说明了线程-0和线程-1同时在进行读操作,大大的提升了读操作的效率。
不过要注意的是,
如果有一个线程已经占用了读锁,此时其它的线程要申请写锁,则申请写锁的线程一直等待释放读锁。
如果有一个线程已经占用了写锁,此时其它线程如果申请写锁或者读锁,申请的线程一直等待释放写锁。
Lock
和synchronized
的选择总的来说,Lock
和synchronized
有以下几点不同:
Lock
是一个接口,而synchronized
是Java
中的一个关键字,synchronized
是内置的语言实现;
synchronized
在发生异常时,JVM
会自动释放线程占用的锁,因此不会导致死锁发生;而Lock
在发生异常时,如果没有主动通过unLock()
方法释放锁,则很可能造成死锁,因此使用Lock
时需要在finally块中释放锁;
Lock
可以让等待锁的线程响应中断,而synchronized
则不行,使用synchronized
时,等待的线程一直会等待下去,不能够响应中断;
通过Lock
可以知道有没有成功获取锁,而synchronized
却无法办到;
Lock
可以提高多个线程进行读操作的效率。
从性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时,此时Lock
的性能要远远优于synchronized
,所以说,在具体使用时要根据适当情况选择。