锁机制
1.锁是干什么用的
锁一般来说用作资源控制,限制资源访问,防止在并发环境下造成数据错误
2.重入锁
重入锁也叫作递归锁,指的是同一个线程外层函数获取到一把锁后,内层函数同样具有这把锁的控制权限
synchronized和ReentrantLock就是重入锁对应的实现
synchronized重量级的锁
ReentrantLock轻量级的锁 lock()代表加入锁 unlock()代表释放锁
不可重入锁:说明当没有释放该锁时。其他线程获取该锁会进行等待
public class MyLock { //标识锁是否可用 如果值为true代表当前有线程正在使用该锁,如果为false代表没有人用锁 private boolean isLocked=false; //获取锁:加锁 public synchronized void lock() throws InterruptedException { //判断当前该锁是否正在使用 while (isLocked){ wait(); } //当前没有人使用情况下就占用该锁 isLocked=true; } //释放锁 public synchronized void unLock(){ //将当前锁资源释放 isLocked=false; //唤起正在等待使用锁的线程 notify(); } } public class MyLockTest { MyLock myLock=new MyLock(); //A业务方法 public void print() throws InterruptedException { //获取一把锁 myLock.lock(); System.out.println("print业务方法"); doAdd(); //释放锁 myLock.unLock(); } //B业务方法 public void doAdd() throws InterruptedException { //获取一把锁 myLock.lock(); System.out.println("doAdd业务方法"); //释放锁 myLock.unLock(); } public static void main(String[] args) throws InterruptedException { MyLockTest test=new MyLockTest(); test.print(); } }
synchronized可重入性:如果当前A持有一把锁,在A业务内部调用B,那么B也同样拥有这把锁的使用权限
public class MyLockTest { //A业务方法 public synchronized void print() throws InterruptedException { //获取了一把锁 System.out.println("print业务方法"); doAdd(); } //B业务方法 public synchronized void doAdd() throws InterruptedException { System.out.println("doAdd业务方法"); //释放锁 } public static void main(String[] args) throws InterruptedException { MyLockTest test=new MyLockTest(); test.print(); } }
ReentrantLock同样具有可重入性
public class MyLockTest { //创建锁对象 Lock lock=new ReentrantLock(); //A业务方法 public void print() throws InterruptedException { //获取了一把锁 lock.lock(); System.out.println("print业务方法"); doAdd(); //释放锁 lock.unlock(); } //B业务方法 public void doAdd() throws InterruptedException { //获取了一把锁 lock.lock(); System.out.println("doAdd业务方法"); //释放锁 lock.unlock(); } public static void main(String[] args) throws InterruptedException { MyLockTest test=new MyLockTest(); test.print(); } } ReentrantLock底层: public ReentrantLock() { //默认非公平锁 sync = new NonfairSync(); } public ReentrantLock(boolean fair) { //如果为true代表公平锁,否则为非公平锁 sync = fair ? new FairSync() : new NonfairSync(); } public class MyReentrantLock { //标识锁是否可用 如果值为true代表当前有线程正在使用该锁,如果为false代表没有人用锁 private boolean isLocked=false; //当前线程 Thread lockedBy=null; //加锁数量计数 Integer lockedCount=0; //加锁 public synchronized void lock() throws InterruptedException { //获取当前线程 Thread thread=Thread.currentThread(); //判断当前是否正在使用锁,如果正在使用则对比当前使用要使用锁的线程和之前使用锁的线程是否一致 //如果一致代表可以重入,继续使用锁,不会发生阻塞 //如果不一致代表当前不是一个线程,则等待 while (isLocked && thread!=lockedBy){ wait(); } //占用锁 isLocked=true; //计数+1 lockedCount++; //赋值线程 lockedBy=thread; } //释放锁 public synchronized void unlock(){ //判断当前是否是用一个线程 if(Thread.currentThread()==this.lockedBy){ //锁使用计数器-1 lockedCount--; //判断计数器是否为0,如果为0则释放锁,然后唤醒正在等待的线程 if(lockedCount==0){ isLocked=false; notify(); } } } }
3.读写锁
并发线程下,所有线程都执行读的操作,会不会有问题
并发线程下,部分读部分写会不会有问题 会发生写冲突
并发线程下,所有线程都执行写会不会有问题 会发生写冲突
public class ReadWriteLock { //创建一个集合 static Map<String,String> map=new HashMap<String,String>(); //创建一个读写锁 static ReentrantReadWriteLock lock=new ReentrantReadWriteLock(); //获取读锁 static Lock readLock=lock.readLock(); //获取写锁 static Lock writeLock=lock.writeLock(); //写操作 public Object put(String key,String value){ writeLock.lock(); try { System.out.println("Write正在执行写操作~"); Thread.sleep(100); String put = map.put(key, value); System.out.println("Write写操作执行完毕~"); return put; } catch (InterruptedException e) { e.printStackTrace(); }finally { writeLock.unlock(); } return null; } //写操作 public Object get(String key){ readLock.lock(); try { System.out.println("Read正在执行读操作~"); Thread.sleep(100); String value = map.get(key); System.out.println("Read读操作执行完毕~"); return value; } catch (InterruptedException e) { e.printStackTrace(); }finally { readLock.unlock(); } return null; } public static void main(String[] args) { ReadWriteLock lock=new ReadWriteLock(); for (int i = 0; i < 10; i++) { int finalI = i; new Thread(()->{ try { //写操作 lock.put(finalI +"","value"+finalI); //读操作 System.out.println(lock.get(finalI+"")); } catch (Exception e) { e.printStackTrace(); } }).start(); } } }
4.乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
乐观锁( Optimistic Locking ) 其实就是一种思想。相对悲观锁而言,乐观锁假设认为数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。
上面提到的乐观锁的概念中其实已经阐述了它的具体实现细节:主要就是两个步骤:冲突检测和数据更新。其实现方式有一种比较典型的就是 Compare and Swap ( CAS )。
CAS:
CAS是乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
CAS 操作中包含三个操作数 —— 需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B。否则处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了“ 我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。 ”这其实和乐观锁的冲突检查+数据更新的原理是一样的。
这里再强调一下,乐观锁是一种思想。CAS是这种思想的一种实现方式。
5.悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如Java里面的同步原语synchronized关键字的实现也是悲观锁。
悲观锁机制存在以下问题:
1. 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
2. 一个线程持有锁会导致其它所有需要此锁的线程挂起。
3. 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。
对比于悲观锁的这些问题,另一个更加有效的锁就是乐观锁。其实乐观锁就是:每次不加锁而是假设没有并发冲突而去完成某项操作,如果因为并发冲突失败就重试,直到成功为止。