闭关修炼(六)各种锁

难者不会,会者不难



java中的锁你听过哪些?

悲观锁,乐观锁,分段锁,重入锁,读写锁,CAS锁,排他锁(基本上是自带的功能),自旋锁,分布式锁(muji)

悲观锁

什么是悲观锁?

老生常谈的问题。

悲观锁每次在拿数据时,都会上锁,自带排他锁的功能。

举个具体的例子,有张Order表,里有id,orderId,state属性,另外张Money表中,里有id,orderId,money属性,现在有个业务执行三条sql语句:
select * from order where orderId=1 and state=0,如果select到内容继续往下执行:
update set state=1 where orderId=1,
update money set money=moeny+money where orderId=1

此时有两个jdbc连接,同时执行以上三条sql语句,会发生什么?产生重复读问题,都读到state=0,select出了数据,都继续往下执行了,导致money更新不对,那么要如何解决呢?

使用悲观锁,在sql中将第一句改为select * from order where orderId=1 and state=0 for update,在查的时候,十分悲观,整个事务只允许一个连接进行操作,拿不到锁的连接只能一直等着,等待拿到锁的连接提交完事务释放锁资源。

悲观锁的缺点

因为只能保证一个连接进行操作,效率十分低。项目中查询量比较大的情况下,不会使用悲观锁。

乐观锁

什么是乐观锁?

比较简单,表示比较乐观,它在别人在做修改的时候,不会上锁,不过加称为版本标识的判断(类似CAS无锁机制),主要特点是本质上没有锁,使用版本标识和影响行数(如version-版本号)进行控制,乐观锁有效的根本原因是sql执行的原子性。

回到具体例子,还是有两个连接执行那三句sql:
select * from order where orderId=1 and state=0,如果select到内容继续往下执行:
update set state=1 where orderId=1,
update money set money=moeny+money where orderId=1

使用乐观锁,Order表添加version字段,update的sql执行改为
update set state=1,set version=version+1 where orderId=1 and version=version

当一个连接执行select语句,获取version版本号,update语句中where附加version进行查找,修改完毕后将version+1,此时另外一个连接也select到了数据,不过version是旧的,再根据旧的version就使用不了update语句了,此时它的影响行数为0。这里的影响行数就是数据库返回给你的成功修改的行数值。

如果影响行数>0,执行第三句sql,第一个连接成功修改了第二句update语句,因此它的影响行数大于0,可以执行第三句sql。

悲观锁和乐观锁的区别?

使用场景的区别?

如果查询量小,可以使用悲观锁,在请求量大时,多个请求来时,悲观锁只能让一个请求执行。

使用乐观锁使用版本控制操作,要使用乐观锁的话需要自己在表中添加一个版本标识字段,常规下(绝大多数)使用乐观锁。

重入锁

什么现象非重入锁会产生而重入锁不会产生?

死锁现象

具体见之前的死锁例子,当时已经有用到重入的概念了。

重入锁有哪些?

在Java中,ReentrantLock和synchronized都是可重入锁。

什么是重入锁

重入锁,又称为递归锁,指的是同一线程的外层函数获得锁资源之后,内层的递归函数仍然有该锁使用权。

可重入锁-synchronized例子

get中调用set方法,函数进行嵌套,锁能够进行传递

import lombok.SneakyThrows;

class MyTThread implements Runnable {
    public void get() {
        System.out.println(Thread.currentThread().getId() + " get()");
        set();
    }

    @SneakyThrows
    public void set() {
        Thread.sleep(100);
        System.out.println(Thread.currentThread().getId() + " set()");
    }

    @Override
    public void run() {
        get();
    }
}

public class Test2 {
    public static void main(String[] args) {
        MyTThread myTThread = new MyTThread();
        new Thread(myTThread).start();
        new Thread(myTThread).start();
        new Thread(myTThread).start();
    }
}

这段代码执行结果是

11 get()
13 get()
12 get()
13 set()
11 set()
12 set()

我们希望get(),set()交替执行,我们在两个方法都加上synchronized关键字修饰即可

	public synchronized void get() {
        System.out.println(Thread.currentThread().getId() + " get()");
        set();
    }

    @SneakyThrows
    public synchronized void set() {
        Thread.sleep(100);
        System.out.println(Thread.currentThread().getId() + " set()");
    }

那么是为什么呢?

这就是因为synchronized是重入锁,线程①进行get()方法后占用this锁,调用set()时候,set方法仍然有this锁的使用权,而其他的线程②和③因为获取不到this锁被阻塞,只能等待线程①的set()方法执行完毕释放锁。

执行结果如下:

11 get()
11 set()
13 get()
13 set()
12 get()
12 set()

举一反三,使用同步代码块的时候,只要外层和内层函数使用的是同一个锁对象,那么就是可重入的;如果外层和内层函数使用的是不是同一个锁对象,那么就是非可重入的。

可重入锁-ReentrantLock例子

这个例子和上一个例子是一样的,不作过多解释

import lombok.SneakyThrows;
import java.util.concurrent.locks.ReentrantLock;

class MyTThread implements Runnable {
    private ReentrantLock lock = new ReentrantLock();
    public synchronized void get() {
        lock.lock();
        System.out.println(Thread.currentThread().getId() + " get()");
        set();
        lock.unlock();
    }

    @SneakyThrows
    public synchronized void set() {
        lock.lock();
        Thread.sleep(100);
        System.out.println(Thread.currentThread().getId() + " set()");
        lock.unlock();
    }

    @Override
    public void run() {
        get();
    }
}

public class Test2 {
    public static void main(String[] args) {
        MyTThread myTThread = new MyTThread();
        new Thread(myTThread).start();
        new Thread(myTThread).start();
        new Thread(myTThread).start();
    }
}

重入锁的好处?

外层函数能将锁资源传递给内层函数,内层函数无需重新获取锁资源,效率提高。

注意的小点

内层函数释放锁资源不影响外层函数的锁资源

读写锁

考虑这样一个场景,两个线程对一个共享文件进行读写操作,同时读是没有问题的,但是如果有一个线程想去写这个共享文件,那么就不应该有其他的线程对该资源进行读或写。

读写锁的机制?

无论多少个线程如果都在读,其他的线程可以读或写,只要一个线程正在写,其他线程不可以读或写。

例子

首先是不加锁的情况

import lombok.Data;
import lombok.SneakyThrows;

import java.util.HashMap;
import java.util.Map;

@Data
class Cache {
    static private volatile Map<String, Object> map = new HashMap<>();

    @SneakyThrows
    public static Object write(String key, Object value){
        System.out.println("正在开始写..." + ", key: " + key + ",value: " + value);
        Thread.sleep(100);
        Object o =  map.put(key, value);
        System.out.println("结束写..." + ", key: " + key + ",value: " + value);
        return o;
    }

    @SneakyThrows
    public static Object read(String key){
        System.out.println("正在开始读..." + ", key: " + key);
        Thread.sleep(100);
        Object o =  map.get(key);
        System.out.println("结束读..." + ", key: " + key + ",value: " + o);
        return o;
    }
}

public class Test3 {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    Cache.write(i + "", i + "");
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    Cache.read(i + "");
                }
            }
        }).start();
    }
}

运行结果可以看到,9正在写,还没有结束写,9就开始读了,读的结果是null,数据发生了异常
在这里插入图片描述
如何解决呢?使用ReentrantReadwriteLock-读写锁,使用起来还是十分容易的。

import lombok.Data;
import lombok.SneakyThrows;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

@Data
class Cache {
    static private volatile Map<String, Object> map = new HashMap<>();
    static private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    // 读锁
    static Lock r = readWriteLock.readLock();
    // 写锁
    static Lock w = readWriteLock.writeLock();

    @SneakyThrows
    public static Object write(String key, Object value) {
        w.lock();
        System.out.println("正在开始写..." + ", key: " + key + ",value: " + value);
        Thread.sleep(100);
        Object o = map.put(key, value);
        System.out.println("结束写..." + ", key: " + key + ",value: " + value);
        w.unlock();
        return o;

    }

    @SneakyThrows
    public static Object read(String key) {
        r.lock();
        System.out.println("正在开始读..." + ", key: " + key);
        Thread.sleep(100);
        Object o = map.get(key);
        System.out.println("结束读..." + ", key: " + key + ",value: " + o);
        r.unlock();
        return o;
    }
}

public class Test3 {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    Cache.write(i + "", i + "");
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    Cache.read(i + "");
                }
            }
        }).start();
    }
}

结果就不会出现写一半的时候出现读了
在这里插入图片描述

CAS无锁机制

CAS是什么意思?

Compare And Swap

什么是CAS无锁机制?

CAS无锁机制和自旋锁是配套使用的,因为自旋锁的底层用的就是CAS无锁机制,CAS无锁机制效率非常高,CAS无锁机制其实和乐观锁是类似的概念,本身没有锁,而是用一个标识。

CAS体系中有三个参数,分别是V,E,N,V表示要更新的值,E表示期望值,N表示新值,线程执行先判断要更新的值V与期望值E,如果它们相同,说明没有任何线程更改,线程继续操作,将新值N覆盖V;如果V和E不同,说明其他线程更改过,当前线程不做任何操作,只把N覆盖V。

其实预期值E就是之前缓存的值,更新值V如果和预期值E不同的话,说明V被其他线程修改了,再进行操作共享数据将可能会发生冲突,所以不操作共享数据,只把新值N赋给V。

例子

看AtomicInteger的源码,

public class Test1 {
    public static void main(String[] args) {
        new AtomicInteger().incrementAndGet();
    }
}

ctrl+左键点进去,我发现我这里的源码和之前不一样了,应该被重构过,改到更底层去实现了,但是再怎么底层原理应该是不变的。

public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }

ctrl+左键进入unsafe.getAndAddInt,值得注意的是Unsafe中的方法是原子操作。

public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

首先是var5 = this.getIntVolatile(var1, var2);表示通过对象和偏移量获取变量的值,Unsafe类可以让java操作内存地址,就是比如这里通过对象和偏移量直接从内存中获取对应变量的值,取得的值赋给var5,这里的var5其实是要更新的值V,由于用了Volatile修饰,因此V是线程之间可见的,就是说不同的线程看到的V都是相同的一个值。

compareAndSwapInt方法是Java的native方法,并不由Java语言实现。
public final native boolean compareAndSwapInt(Object o, long offset,int expected, int x);

方法的作用是,读取传入对象o在内存中偏移量为offset位置的值与期望值expected(上次缓存的V值)作比较。相等就把x(这里的x就是var5+var4,等同于V+1)值赋值给offset位置的值。方法返回true。不相等,就取消赋值,方法返回false,继续执行getIntVolatile刷新V值,继续和上次缓存了V值的E比较,直到在其它线程执行CAS操作之前,抢先退出循环操作,执行+1操作。
unsafe.getAndAddInt(this, valueOffset, 1) + 1;

自旋锁

什么是自旋锁

自旋锁是采用让当前线程不停的在循环体内执行实现的,当循环的条件被其他线程改变时才能进入临界区。是不可重入的锁

例子

下面例子展示自旋锁现象,效果是线程卡死。

当第一个线程调用这个不可重入的自旋锁去加锁(调用lock函数)是没有问题的,但当再次调用lock的时候,因为自旋锁已经持有引用已经不为空了,该线程对象会误认为是别人的线程持有自旋锁,释放不了锁资源,程序直接卡死。

自旋锁使用了CAS原子操作(compareAndSet方法),lock函数将所有者owner设置为当前线程,并且预测原来的值为空;unlock函数将所有者owner设置为空,并预测值为当前对象。

当有第二个线程调用lock方法时,由于owner值不为空(设置为了其他或者当前线程对象),导致循环一致被执行,直至第一个线程调用了unlock函数将owner设为空,第二个线程才能进入临界区。

由于自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都要执行,占用CPU时间

package ch6;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.sql.Statement;
import java.util.concurrent.atomic.AtomicReference;


class MySpinLock {
    // 原子类 作用是对对象的引用,它可以保证你在修改对象引用时的线程安全性
    private AtomicReference<Thread> sign = new AtomicReference<>();

    public void lock(){
        Thread current = Thread.currentThread();
        while (!sign.compareAndSet(null, current)){

        }
    }

    public void unlock(){
        Thread current = Thread.currentThread();
        sign.compareAndSet(current, null);
    }
}
@Data
@AllArgsConstructor
public class Test2 implements Runnable{
    static int sum;
    private MySpinLock lock;

    public static void main(String[] args) {
        MySpinLock lock = new MySpinLock();
        for (int i = 0; i < 10; i++) {
            Test2 test2 = new Test2(lock);
            Thread thread =new Thread(test2);
            thread.start();
        }
    }

    @Override
    public void run() {
        this.lock.lock();
        this.lock.lock();
        sum++;
        this.lock.unlock();
        this.lock.unlock();
    }
}

分布式锁

//todo 先挖坑,挖坑填不填就不知道了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值