1、 Lock和synchronized 的对比:
Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现,synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定,但是使用Lock则不行,lock是通过代码实现的,要保证锁定一定会被释放,就必须将 unLock()放到finally{} 中;
synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
Lock可以让等待锁的线程响应中断,线程可以中断去干别的事务,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
Lock可以提高多个线程进行读操作的效率。
举个例子:
当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。
如果采用synchronized关键字来实现同步的话,就会导致一个问题:
如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。
因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。
另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。
2、锁的相关概念:
2.1、乐观锁 VS 悲观锁:
乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不同角度。在Java和数据库中都有此概念对应的实际应用。
先说概念。对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。
乐观锁则认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。
乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。
悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
2.2、公平锁 VS 非公平锁:
公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。
非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。
2.3、可重入锁 VS 非可重入锁:
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。下面用示例代码来进行分析:
在上面的代码中,类中的两个方法都是被内置锁synchronized修饰的,doSomething()方法中调用doOthers()方法。因为内置锁是可重入的,所以同一个线程在调用doOthers()时可以直接获得当前对象的锁,进入doOthers()进行操作。
如果是一个不可重入锁,那么当前线程在调用doOthers()之前需要将执行doSomething()时获取当前对象的锁释放掉,实际上该对象锁已被当前线程所持有,且无法释放。所以此时会出现死锁。
3、java.util.concurrent.locks包下常用的类(J.U.C):
3.1、Lock
首先要说明的就是Lock,通过查看Lock的源码可知,Lock是一个接口:
public interface Lock { void lock(); void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long time, TimeUnit unit) throws InterruptedException; void unlock(); Condition newCondition(); }
下面来逐个讲述Lock接口中每个方法的使用,lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用来获取锁的。unLock()方法是用来释放锁的。newCondition() 方法返回新建的Condition, 简单来说通过Condition 接口中的await(),signal(),signalAll()等方法可以实现线程间的等待通知机制.
(和使用synchronized结合Object上的wait和notify方法实现的效果一样,但是Condition 拥有更高级的用法),具体的下面介绍.
在Lock中声明了四个方法来获取锁,那么这四个方法有何区别呢?
lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。
tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
lockInterruptibly()方法表示当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。举例子: 当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
注意:当一个线程获取了锁之后,是不会被interrupt()方法中断的。因为本身在前面的文章中讲过单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。
由于在前面讲到如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。通常使用Lock来进行同步的话,是以下面这种形式去使用的:
Lock lock = ...; lock.lock(); try{ //处理任务 }catch(Exception ex){ }finally{ lock.unlock(); //释放锁 }
3.2、ReentrantLock:
ReentrantLock,意思是“可重入锁",意味着ReentrantLock是一个可重入锁,ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock既可以实现公平锁也可以实现非公平锁.
Lock lock=new ReentrantLock()//默认是非公平锁 Lock lock=new ReentrantLock(true);//公平锁 Lock lock=new ReentrantLock(false);//非公平锁
常用方法:
getHoldCount() 查询当前线程保持此锁的次数,也就是执行此线程执行lock方法的次数
getQueueLength()返回正等待获取此锁的线程估计数,比如启动10个线程,1个线程获得锁,此时返回的是9
getWaitQueueLength(Condition condition)返回等待与此锁相关的给定条件的线程估计数。比如10个线程,用同一个condition对象,并且此时这10个线程都执行了condition对象的await方法,那么此时执行此方法返回10
hasWaiters(Condition condition)查询是否有线程等待与此锁有关的给定条件(condition),对于指定contidion对象,有多少线程执行了condition.await方法
hasQueuedThread(Thread thread)查询给定线程是否等待获取此锁
hasQueuedThreads()是否有线程等待此锁
isFair()该锁是否公平锁
isHeldByCurrentThread() 当前线程是否保持锁锁定,线程的执行lock方法的前后分别是false和true
isLock()此锁是否有任意线程占用
lockInterruptibly()当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。
tryLock()尝试获得锁,能获得锁就返回true,不能就立即返回false.
tryLock(long timeout TimeUnit unit)增加时间限制,如果超过该时间段还没获得锁,返回false
* lock和lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程,前者不会抛出异常,而后者会抛出异常.
newCondition()方法: 返回一个新的Condition对象.
3.2.1 、Condition
Object类的wait和notify/notify与synchronized关键字配合完成线程间的等待/通知机制,而Condition与Lock配合使用也是可以完成等待/通知机制的,前者是java底层级别的,后者是语言级别的,具有更高的可控制性和扩展性。但是这两者在使用方式以及功能特性上还是有区别的。Object的监视器方法与Condition接口对比如下:
Condition提供的方法:
public interface Condition { void await() throws InterruptedException; void awaitUninterruptibly(); long awaitNanos(long nanosTimeout) throws InterruptedException; boolean await(long time, TimeUnit unit) throws InterruptedException; boolean awaitUntil(Date deadline) throws InterruptedException; void signal(); void signalAll(); }
解释:
await() 当前线程进入等待状态直到被通知(signal)或中断。
awaitUninterruptibly() 当前线程进入等待状态直到被通知,该方法不响应中断。
awaitNanos(long nanosTimeout) 当前线程进入等待状态直到被通知、中断或者超时,返回值表示剩余超时时间。
await(long time, TimeUnit unit) 当前线程进入等待状态直到被通知、中断或者到某个时间。如果没有到指定时间就被通知,方法返回true,否则,表示到了指定时间,返回false。
awaitUntil(Date deadline) 当前线程进入等待状态直到被通知、中断或者到某个时间。如果没有到指定时间就被通知,方法返回true,否则,表示到了指定时间,返回false。
signal() 唤醒一个等待在Condition上的线程,该线程从等待方法返回前必须获得与Condition相关联的锁。
signalAll() 唤醒所有等待在Condition上的线程,能够从等待方法返回的线程必须获得与Condition相关联的锁。
Condition、ReentrantLock 使用例子:
public class LockExample { public static void main(String[] args) throws Exception{ ReentrantLock reentrantLock = new ReentrantLock(); Condition condition1 = reentrantLock.newCondition(); Condition condition2 = reentrantLock.newCondition(); new Thread(() -> { try { reentrantLock.lock(); log.info("Thread0 get lock"); log.info("Thread0 wait signal"); long time = condition1.awaitNanos(4000000000L); log.info("Thread0 continue"); System.out.println("Thread 0等待剩余时间: "+ time); Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } finally { log.info("Thread0 release lock"); reentrantLock.unlock(); } }).start(); Thread.sleep(500); new Thread(() -> { try { reentrantLock.lock(); log.info("Thread1 get lock"); log.info("Thread1 wait signal"); log.info("Thread1 continue"); boolean await = condition2.await(2, TimeUnit.SECONDS); System.out.println("Thread1 是否超过了预计等待时间" + await); log.info("send signal to thread0"); condition1.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { log.info("Thread1 release lock"); reentrantLock.unlock(); } }).start(); Thread.sleep(500); new Thread(() -> { try { reentrantLock.lock(); log.info("Thread2 get lock"); log.info("Thread2 wait signal"); log.info("Thread2 continue"); Thread.sleep(4000); log.info("send signal to thread1"); condition2.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { log.info("Thread2 release lock"); reentrantLock.unlock(); } }).start(); } }
结果:
00:51:04.554 [Thread-0] INFO com.mmall.concurrency.example.lock.LockExample6 - Thread0 get lock 00:51:04.557 [Thread-0] INFO com.mmall.concurrency.example.lock.LockExample6 - Thread0 wait signal 00:51:05.054 [Thread-1] INFO com.mmall.concurrency.example.lock.LockExample6 - Thread1 get lock 00:51:05.054 [Thread-1] INFO com.mmall.concurrency.example.lock.LockExample6 - Thread1 wait signal 00:51:05.054 [Thread-1] INFO com.mmall.concurrency.example.lock.LockExample6 - Thread1 continue 00:51:05.555 [Thread-2] INFO com.mmall.concurrency.example.lock.LockExample6 - Thread2 get lock 00:51:05.555 [Thread-2] INFO com.mmall.concurrency.example.lock.LockExample6 - Thread2 wait signal 00:51:05.555 [Thread-2] INFO com.mmall.concurrency.example.lock.LockExample6 - Thread2 continue 00:51:09.556 [Thread-2] INFO com.mmall.concurrency.example.lock.LockExample6 - send signal to thread1 00:51:09.556 [Thread-2] INFO com.mmall.concurrency.example.lock.LockExample6 - Thread2 release lock Thread1 是否超过了预计等待时间false 00:51:09.556 [Thread-1] INFO com.mmall.concurrency.example.lock.LockExample6 - send signal to thread0 00:51:09.556 [Thread-1] INFO com.mmall.concurrency.example.lock.LockExample6 - Thread1 release lock 00:51:09.556 [Thread-0] INFO com.mmall.concurrency.example.lock.LockExample6 - Thread0 continue Thread 0等待剩余时间: -999019400 00:51:11.557 [Thread-0] INFO com.mmall.concurrency.example.lock.LockExample6 - Thread0 release lock
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.setName("Thread1"); thread2.setName("Thread2"); thread1.start(); thread2.start(); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } thread2.interrupt(); } public void insert(Thread thread) throws InterruptedException { //注意,如果需要正确中断等待锁的线程,必须将获取锁放在外面,然后将InterruptedException抛出 lock.lockInterruptibly(); //lock.lock(); try { System.out.println(thread.getName() + "得到了锁"); long startTime = System.currentTimeMillis(); for (; ; ) { if (System.currentTimeMillis() - startTime >= 5000L) 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() + "被中断"); } } }
执行结果:
Thread1得到了锁 Thread2被中断 Thread1执行finally Thread1释放了锁
Thread2根本没有执行,等待的时候就终端等待了,并且抛出了InterruptedException 异常.
3.3、ReadWriteLock:
ReadWriteLock也是一个接口,在它里面只定义了两个方法:
public interface ReadWriteLock { Lock readLock(); Lock writeLock(); }
一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。
读写锁的阻塞情况如下图:
由于读写锁本身的实现就远比独占锁复杂,因此,读写锁比较适用于以下情形:
高频次的读操作,相对较低频次的写操作;
读操作所用时间不会太短。(否则读写锁本身的复杂实现所带来的开销会成为主要消耗成本)
下面的ReentrantReadWriteLock实现了ReadWriteLock接口。
3.4、ReentrantReadWriteLock:
ReentrantReadWriteLock的意思是"可重入的读写锁", 那么ReentrantReadWriteLock是 1. 一个可重入锁, 2. 既可以实现公平锁也可以实现非公平锁, 3. 支持使用Condition, 4. 支持锁降级
注解:
所谓锁降级,就是:先获取写锁,然后获取读锁,最后释放写锁,这样写锁就降级成了读锁。但是,读锁不能升级到写锁。简言之,就是:写锁可以降级成读锁,读锁不能升级成写锁。但是写锁并不会直接降成读锁, 不会随着读锁的释放而释放, 因此需要显式地释放写锁.
ReentrantReadWriteLock类最主要的有两个方法:readLock()和writeLock()用来获取读锁和写锁。
举例:
public class ReentrantReadWriteLockDemo { private final Map<String, Data> map = new TreeMap<>(); private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); private final Lock readLock = lock.readLock(); private final Lock writeLock = lock.writeLock(); public Data get(String key) { readLock.lock(); try { return map.get(key); } finally { readLock.unlock(); } } public Set<String> getAllKeys() { readLock.lock(); try { return map.keySet(); } finally { readLock.unlock(); } } public Data put(String key, Data value) { writeLock.lock(); try { return map.put(key, value); } finally { readLock.unlock(); } } class Data { } }
其他的一些方法:
参考文档:
https://blog.csdn.net/qq_38293564/article/details/80554516
https://www.cnblogs.com/wuhan729/p/8601108.html
https://segmentfault.com/a/1190000015562389
还没有评论,来说两句吧...