Java锁
锁一般来说用作资源控制,限制资源访问,防止在并发环境下造成数据错误
锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized(重量级) 和 ReentrantLock(轻量级)等等 ) 。这些已经写好提供的锁为我们开发提供了便利。
一、重入锁
重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
在JAVA环境下 ReentrantLock 和synchronized 都是 可重入锁。
synchronized和ReentrantLock就是重入锁对应的实现
synchronized重量级的锁
ReentrantLock轻量级的锁 lock()代表加入锁 unlock()代表释放锁
1、不可重入锁
说明当没有释放该锁时。其他线程获取该锁会进行等待
MyLock:
packagecom.zn.lockTest;public classMyLock {//标识锁是否可用 如果值为true代表当前有线程正在使用该锁,如果为false代表没有人用锁
private boolean isLocked=false;//获取锁:加锁
public synchronized void lock() throwsInterruptedException {//判断当前该锁是否正在使用
while(isLocked){
wait();
}//当前没有人使用情况下就占用该锁
isLocked=true;
}//释放锁
public synchronized voidunLock(){//将当前锁资源释放
isLocked=false;//唤起正在等待使用锁的线程
notify();
}
}
MyLockTest:
packagecom.zn.lockTest;public classMyLockTest {
MyLock myLock=newMyLock();//A业务方法
public void print() throwsInterruptedException {//获取一把锁
myLock.lock();
System.out.println("print业务方法");
doAdd();//释放锁
myLock.unLock();
}//B业务方法
public void doAdd() throwsInterruptedException {//获取一把锁
myLock.lock();
System.out.println("doAdd业务方法");//释放锁
myLock.unLock();
}public static void main(String[] args) throwsInterruptedException {
MyLockTest test=newMyLockTest();
test.print();
}
}
控制台效果:
2、synchronized可重入性
如果当前A持有一把锁,在A业务内部调用B,那么B也同样拥有这把锁的使用权限
MyLock:
packagecom.zn.lockTest;public classMyLock {//标识锁是否可用 如果值为true代表当前有线程正在使用该锁,如果为false代表没有人用锁
private boolean isLocked=false;//获取锁:加锁
public synchronized void lock() throwsInterruptedException {//判断当前该锁是否正在使用
while(isLocked){
wait();
}//当前没有人使用情况下就占用该锁
isLocked=true;
}//释放锁
public synchronized voidunLock(){//将当前锁资源释放
isLocked=false;//唤起正在等待使用锁的线程
notify();
}
}
MyLockTest:
packagecom.zn.lockTest;public classMyLockTest {
//A业务方法
public synchronized void print() throwsInterruptedException {//获取了一把锁
System.out.println("print业务方法");
doAdd();
}//B业务方法
public synchronized void doAdd() throwsInterruptedException {
System.out.println("doAdd业务方法");//释放锁
}public static void main(String[] args) throwsInterruptedException {
MyLockTest test=newMyLockTest();
test.print();
}
}
控制台效果:
3、ReentrantLock
同样具有可重入性
packagecom.zn.lockTest;importjava.util.concurrent.locks.Lock;importjava.util.concurrent.locks.ReentrantLock;public classMyLockTest {//ReentrantLock//创建锁对象
Lock lock=newReentrantLock();//A业务方法
public void print() throwsInterruptedException {//获取了一把锁
lock.lock();
System.out.println("print业务方法");
doAdd();//释放锁
lock.unlock();
}//B业务方法
public void doAdd() throwsInterruptedException {//获取了一把锁
lock.lock();
System.out.println("doAdd业务方法");//释放锁
lock.unlock();
}public static void main(String[] args) throwsInterruptedException {
MyLockTest test=newMyLockTest();
test.print();
}
}
控制台效果:
4、ReentrantLock底层
/*** Creates an instance of {@codeReentrantLock}.
* This is equivalent to using {@codeReentrantLock(false)}.*/
publicReentrantLock() {//默认非公平锁
sync = newNonfairSync();
}/*** Creates an instance of {@codeReentrantLock} with the
* given fairness policy.
*
*@paramfair {@codetrue} if this lock should use a fair ordering policy*/
public ReentrantLock(booleanfair) {//如果为true代表公平锁,否则为非公平锁
sync = fair ? new FairSync() : newNonfairSync();
}
MyReentrantLock:
packagecom.zn.lockTest;public classMyReentrantLock {//标识锁是否可用 如果值为true代表当前有线程正在使用该锁,如果为false代表没有人用锁
private boolean isLocked=false;//当前线程
Thread lockedBy=null;//加锁数量计数
Integer lockedCount=0;//加锁
public synchronized void lock() throwsInterruptedException {//获取当前线程
Thread thread=Thread.currentThread();//判断当前是否正在使用锁,如果正在使用则对比当前使用要使用锁的线程和之前使用锁的线程是否一致//如果一致代表可以重入,继续使用锁,不会发生阻塞//如果不一致代表当前不是一个线程,则等待
while (isLocked && thread!=lockedBy){
wait();
}//占用锁
isLocked=true;//计数+1
lockedCount++;//赋值线程
lockedBy=thread;
}//释放锁
public synchronized voidunlock(){//判断当前是否是用一个线程
if(Thread.currentThread()==this.lockedBy){//锁使用计数器-1
lockedCount--;//判断计数器是否为0,如果为0则释放锁,然后唤醒正在等待的线程
if(lockedCount==0){
isLocked=false;
notify();
}
}
}
}
二、读写锁
假设你的程序中涉及到对一些共享资源的读和写操作,且写操作没有读操作那么频繁。在没有写操作的时候,两个线程同时读一个资源没有任何问题,所以应该允许多个线程能在同时读取共享资源。但是如果有一个线程想去写这些共享资源,就不应该再有其它线程对该资源进行读或写(译者注:也就是说:读-读能共存,读-写不能共存,写-写不能共存)。这就需要一个读/写锁来解决这个问题。
Java5在java.util.concurrent包中已经包含了读写锁。
packagecom.zn.lockTest;importjava.util.HashMap;importjava.util.Map;importjava.util.concurrent.locks.Lock;importjava.util.concurrent.locks.ReentrantReadWriteLock;public classReadWriteLock {//创建一个集合
static Map map=new HashMap();//创建一个读写锁
static ReentrantReadWriteLock lock=newReentrantReadWriteLock();//获取读锁
static Lock readLock=lock.readLock();//获取写锁
static Lock writeLock=lock.writeLock();//写操作
publicObject 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写操作执行完毕~");returnput;
}catch(InterruptedException e) {
e.printStackTrace();
}finally{
writeLock.unlock();
}return null;
}//写操作
publicObject get(String key){
readLock.lock();try{
System.out.println("Read正在执行读操作~");
Thread.sleep(100);
String value=map.get(key);
System.out.println("Read读操作执行完毕~");returnvalue;
}catch(InterruptedException e) {
e.printStackTrace();
}finally{
readLock.unlock();
}return null;
}public static voidmain(String[] args) {
ReadWriteLock lock=newReadWriteLock();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();
}
}
}
控制台效果:
三、乐观锁
总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制或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的思想也是悲观锁。