Java锁的深度化--重入锁、读写锁、乐观锁、悲观锁

时间:2021-10-30 20:09:12

Java锁

  锁一般来说用作资源控制,限制资源访问,防止在并发环境下造成数据错误

  锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized(重量级) 和 ReentrantLock(轻量级)等等 ) 。这些已经写好提供的锁为我们开发提供了便利。

一、重入锁

  重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
  在JAVA环境下 ReentrantLock 和synchronized 都是 可重入锁。

  synchronized和ReentrantLock就是重入锁对应的实现

  synchronized重量级的锁

  ReentrantLock轻量级的锁   lock()代表加入锁    unlock()代表释放锁

1、不可重入锁

  说明当没有释放该锁时。其他线程获取该锁会进行等待

MyLock:

package com.zn.lockTest;

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();
}
}

MyLockTest:

package com.zn.lockTest;

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();
}
}

控制台效果:

  Java锁的深度化--重入锁、读写锁、乐观锁、悲观锁

2、synchronized可重入性

  如果当前A持有一把锁,在A业务内部调用B,那么B也同样拥有这把锁的使用权限

MyLock:

package com.zn.lockTest;

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();
}
}

MyLockTest:

package com.zn.lockTest;

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();
}
}

控制台效果:

  Java锁的深度化--重入锁、读写锁、乐观锁、悲观锁

3、ReentrantLock

  同样具有可重入性

package com.zn.lockTest;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; public class MyLockTest { //ReentrantLock
//创建锁对象
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();
}
}

控制台效果:

  Java锁的深度化--重入锁、读写锁、乐观锁、悲观锁

4、ReentrantLock底层

    /**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
//默认非公平锁
sync = new NonfairSync();
} /**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
//如果为true代表公平锁,否则为非公平锁
sync = fair ? new FairSync() : new NonfairSync();
}

MyReentrantLock:

package com.zn.lockTest;

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();
}
}
}
}

二、读写锁

  相比Java中的锁(Locks in Java)里Lock实现,读写锁更复杂一些。

  假设你的程序中涉及到对一些共享资源的读和写操作,且写操作没有读操作那么频繁。在没有写操作的时候,两个线程同时读一个资源没有任何问题,所以应该允许多个线程能在同时读取共享资源。但是如果有一个线程想去写这些共享资源,就不应该再有其它线程对该资源进行读或写(译者注:也就是说:读-读能共存,读-写不能共存,写-写不能共存)。这就需要一个读/写锁来解决这个问题。

  Java5在java.util.concurrent包中已经包含了读写锁。

package com.zn.lockTest;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock; 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();
} }
}

控制台效果:

  Java锁的深度化--重入锁、读写锁、乐观锁、悲观锁

三、乐观锁

  总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制或CAS操作实现。

  version方式:

    一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

  核心SQL语句:

    update table set x=x+1, version=version+1 where id=#{id} and version=#{version};

  CAS操作方式:

    即compare and swap 或者 compare and set,涉及到三个操作数(V、E、N),数据所在的内存值(V),预期值(E),新值(N)。当需要更新时,判断当前内存值与之前取到的值是否相等,若相等,则用新值更新,若失败则重试,一般情况下是一个自旋操作,即不断的重试。

四、悲观锁

  总是假设最坏的情况,每次取数据时都认为其他线程会修改,所以都会加锁(读锁、写锁、行锁等),当其他线程想要访问数据时,都需要阻塞挂起。可以依靠数据库实现,如行锁、读锁和写锁等,都是在操作之前加锁,在Java中,synchronized的思想也是悲观锁。