前言
任何一个新引入的知识都是为了解决以往系统中出现的问题,否则新引入的将变得毫无价值
如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁。
但当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象,通过Lock就可以实现。
Lock接口, 提供了与synchronized一样的锁功能。虽然它失去了像synchronize关键字隐式加锁解锁的便捷性,但是却拥有了锁获取和释放的可操作性,可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性
Lock必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象
一个线程获取多少次锁,就必须释放多少次锁。这对于内置锁也是适用的,每一次进入和离开synchronized方法(代码块),就是一次完整的锁获取和释放。
锁的分类
- 悲观锁:
悲观锁,每次去请求数据的时候,都认为数据会被抢占更新(悲观的想法);所以每次操作数据时都要先加上锁,其他线程修改数据时就要等待获取锁。适用于写多读少的场景,synchronized就是一种悲观锁
乐观锁:
在请求数据时,觉得无人抢占修改。等真正更新数据时,才判断此期间别人有没有修改过(预先读出一个版本号或者更新时间戳,更新时判断是否变化,没变则期间无人修改);和悲观锁不同的是,期间数据允许其他线程修改
自旋锁:
一句话,魔力转转圈。当尝试给资源加锁却被其他线程先锁定时,不是阻塞等待而是循环再次加锁
在锁常被短暂持有的场景下,线程阻塞挂起导致CPU上下文频繁切换,这可用自旋锁解决;但自旋期间它占用CPU空转,因此不适用长时间持有锁的场景
lock
Lock
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition(); }
|
一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生():
锁【lock.lock】必须紧跟try代码块,且unlock要放到finally第一行。
ReentrantLock
可重入锁, 支持重入性,表示能够对共享资源重复加锁,即当前线程获取该锁再次获取不会被阻塞。ReentrantLock实现了Lock接口的,并且ReentrantLock提供了更多的方法
JVM允许同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁,就叫做可重入锁
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
| public class ReentrantLockTest1 { private ArrayList<Integer> arrayList = new ArrayList<Integer>(); private Lock lock = new ReentrantLock();
public static void main(String[] args) { final LocksTest test = new LocksTest();
new Thread(() -> test.insert(Thread.currentThread())).start();
new Thread(() -> test.insert(Thread.currentThread())).start(); }
public void insert(Thread thread) { lock.lock(); try { System.out.println(thread.getName() + "得到了锁"); for (int i = 0; i < 5; i++) { arrayList.add(i); } } catch (Exception e) { e.printStackTrace(); } finally { System.out.println(thread.getName() + "释放了锁"); lock.unlock(); } }
}
|
一般情况下通过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
| public class ReentrantLockTest1 { 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) { e.printStackTrace(); }finally { System.out.println(thread.getName()+"释放了锁"); lock.unlock(); } } else { System.out.println(thread.getName()+"获取锁失败"); } } }
|
由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException
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 51 52 53 54 55 56
| public class InterruptTest {
private Lock lock = new ReentrantLock(); public static void main(String[] args) { InterruptTest test = new InterruptTest(); MyThread thread1 = new MyThread(test); MyThread thread2 = new MyThread(test); thread1.start(); thread2.start();
try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } thread2.interrupt(); }
public void insert(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; } } } finally { System.out.println(Thread.currentThread().getName()+"执行finally"); lock.unlock(); System.out.println(thread.getName()+"释放了锁"); } }
}
class MyThread extends Thread { private InterruptTest test; public MyThread(InterruptTest test) { this.test = test; } @Override public void run() {
try { test.insert(Thread.currentThread()); } catch (InterruptedException e) { System.out.println(Thread.currentThread().getName()+"被中断"); } } }
|
ReadWriteLock
ReadWriteLock也是一个接口,只有两个方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public interface ReadWriteLock {
Lock readLock();
Lock writeLock(); }
|
一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作
使用ReadWriteLock时,适用条件是同一个数据,有大量线程读取,但仅有少数线程修改, 适合读多写少的场景
ReentrantReadWriteLock
ReentrantReadWriteLock实现了ReadWriteLock接口,并添加了可重入的特性
如果在系统中,读操作次数远远大于写操作,则读写锁就可以发挥最大的功效,提升系统的性能
Lock和synchronized的选择
总结来说,Lock和synchronized有以下几点不同:
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
5)Lock可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择
Condition
它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。使用Condition可以实现等待/唤醒,并且能够唤醒制定线程
Condition可以替代wait和notify;Condition对象必须从Lock对象获取
LockSupport
LockSupport是一个工具类,可以让线程在任意位置阻塞,也可以在任意位置唤醒,它的内部其实两类主要的方法:park(停车阻塞线程)和unpark(启动唤醒线程):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| // 暂停当前线程 public static void park(Object blocker);
// 暂停当前线程,不过有超时时间的限制 public static void parkNanos(Object blocker, long nanos); public static void parkNanos(long nanos);
// 暂停当前线程,直到某个时间 public static void parkUntil(Object blocker, long deadline); public static void parkUntil(long deadline);
// 无期限暂停当前线程 public static void park();
// 恢复当前线程 public static void unpark(Thread thread);
//blocker的作用是在dump线程的时候看到阻塞对象的信息 public static Object getBlocker(Thread t);
//java14新增了设置blocker的方法 public static void setCurrentBlocker(Object blocker);
|
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
| public class LockSupportTest { public static Object u = new Object(); static ChangeObjectThread t1 = new ChangeObjectThread("t1"); static ChangeObjectThread t2 = new ChangeObjectThread("t2");
public static class ChangeObjectThread extends Thread { public ChangeObjectThread(String name) { super(name); } @Override public void run() { synchronized (u) { System.out.println(Thread.currentThread() +"in " + getName()); LockSupport.park(); if (Thread.currentThread().isInterrupted()) { System.out.println(Thread.currentThread() +"被中断了"); } System.out.println(Thread.currentThread() + "继续执行"); } } }
public static void main(String[] args) throws InterruptedException { t1.start(); Thread.sleep(1000L); t2.start(); Thread.sleep(3000L); t1.interrupt(); LockSupport.unpark(t2); t1.join(); t2.join(); } }
|
LockSuport主要是针对Thread进进行阻塞处理,可以指定阻塞队列的目标对象,每次可以指定具体的线程唤醒。Object.wait()是以对象为纬度,阻塞当前的线程和唤醒单个(随机)或者所有线程
park和unpark可以实现类似wait和notify的功能,但是并不和wait和notify交叉,也就是说unpark不会对wait起作用,notify也不会对park起作用
StampedLock
之前的锁或多或少都存在一些缺点,比如synchronized不可中断等,ReentrantLock 未能读写分离实现,虽然ReentrantReadWriteLock能够读写分离了,但是对于其写锁想要获取的话,就必须没有任何其他读写锁存在才可以,这实现了悲观读取。而且如果读操作很多,写很少的情况下,线程有可能遭遇饥饿问题。
饥饿问题:ReentrantReadWriteLock实现了读写分离,想要获取读锁就必须确保当前没有其他任何读写锁了,但是一旦读操作比较多的时候,想要获取写锁就变得比较困难了,因为当前有可能会一直存在读锁。而无法获得写锁
所以java8引入了新的锁StampedLock,这个类没有直接实现Lock或者ReadWriteLock方法,源码中是把他当作一个单独的类来实现的。相比于普通的ReentranReadWriteLock主要多了一种乐观读的功能。当然,一个StampedLock可以通过asReadLock,asWriteLock,asReadWriteLock方法来得到全部功能的子集
AbstractQwnableSynchronizer
抽象拥有同步器,简称AOS
AbstractQueuedSynchronizer
提供了一个基于FIFO队列,可以用于构建锁或者其他相关同步装置的基础框架, 简称AQS
AbstractQueuedLongSynchronizer
扩展自AbstractQueuedSynchronizer
—todo 未完待续—