十一、java多线程基础之锁的深度化

一、重入锁

1.锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized(重量级) 和 ReentrantLock(轻量级)等等 ) 。这些已经写好提供的锁为我们开发提供了便利。重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
2.在JAVA环境下 ReentrantLock 和synchronized 都是 可重入锁。

3.代码

//重入锁  轻量级(Lock)与重量级锁(synchronized)---可重入性(递归锁)
public class Test001 implements Runnable {
    @Override
    public void run() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        set();
    }
    //synchronized代码块执行完毕的时候释放锁
    private synchronized void set() {
        System.out.println("set方法");
        get();
    }
    private synchronized void get() {
        System.out.println("synchronized  可具备可重入性-get方法");
    }
    public static void main(String[] args) {
        Test001 test001 = new Test001();
        Thread thread = new Thread(test001);
        thread.start();
        System.out.println(Thread.currentThread().getName()+"主线程结束");
    }
}

4.结果

main主线程结束
set方法
synchronized  可具备可重入性-get方法

5.代码

//演示lock锁是否具备  可重入性(特征:锁可以传递(方法递归传递)),下面的方法为啥会调两次,因为最后一次调用他已经知道第一次已经上锁了(不会在重新获取锁)
public class Test002 implements Runnable {
    Lock lock = new ReentrantLock();

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

    private void set() {
        try {
            //上锁
            lock.lock();
            System.out.println("set方法");
            get();
        } catch (Exception e) {
            //重入锁的目的就是避免死锁
        } finally {
            lock.unlock();//释放锁
        }
    }

    private void get() {
        try {
            lock.lock();
            System.out.println("lock  可具备可重入性-get方法");
        } catch (Exception e) {

        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        Test002 test002 = new Test002();
        Thread thread = new Thread(test002);
        thread.start();
    }
}

6.结果

set方法
lock  可具备可重入性-get方法

二、读写锁

1.相比Java中的锁(Locks in Java)里Lock实现,读写锁更复杂一些。假设你的程序中涉及到对一些共享资源的读和写操作,且写操作没有读操作那么频繁。在没有写操作的时候,两个线程同时读一个资源没有任何问题,所以应该允许多个线程能在同时读取共享资源。但是如果有一个线程想去写这些共享资源,就不应该再有其它线程对该资源进行读或写(译者注:也就是说:读-读能共存,读-写不能共存,写-写不能共存)。这就需要一个读/写锁来解决这个问题。Java5在java.util.concurrent包中已经包含了读写锁。尽管如此,我们还是应该了解其实现背后的原理。

2.代码

//读写锁  jvm内置缓存
public class Test003 {
    private volatile Map<String,String> caChe = new HashMap<>();
    //读写锁
    private ReentrantReadWriteLock  rw1 = new ReentrantReadWriteLock();
    //写入锁
    private WriteLock writeLock = rw1.writeLock();
    //读出锁
    private ReadLock readLock = rw1.readLock();
    //写入元素
    public void put(String key,String value){
        try {
            writeLock.lock();
            System.out.println("正在做写的操作,key:" + key + ",value:" + value + "开始.");
            Thread.sleep(100);
            caChe.put(key,value);
            System.out.println("正在做写的操作,key:" + key + ",value:" + value + "结束.");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            writeLock.unlock();
        }
    }
    //读取元素
    public String get(String key){
        try {
            readLock.lock();
            System.out.println("正在做读的操作,key:" + key + ",开始.");
            Thread.sleep(100);
            String value = caChe.get(key);
            System.out.println("正在做读的操作,key:" + key + ",结束.");
            return value;
        } catch (InterruptedException e) {
            e.printStackTrace();
            return null;
        }finally {
            readLock.unlock();
        }
    }

    public static void main(String[] args) {
        Test003 test003 = new Test003();
        //写线程
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0;i<10;i++){
                    test003.put("i",i+"");
                }
            }
        });
        //读线程
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0;i<10;i++){
                    test003.get(i+"");
                }
            }
        });
        t1.start();
        t2.start();
    }
}

3.结果

正在做写的操作,key:i,value:0开始.
正在做写的操作,key:i,value:0结束.
正在做写的操作,key:i,value:1开始.
正在做写的操作,key:i,value:1结束.
正在做读的操作,key:0,开始.
正在做读的操作,key:0,结束.
正在做写的操作,key:i,value:2开始.
正在做写的操作,key:i,value:2结束.
正在做写的操作,key:i,value:3开始.
正在做写的操作,key:i,value:3结束.
正在做写的操作,key:i,value:4开始.
正在做写的操作,key:i,value:4结束.
正在做读的操作,key:1,开始.
正在做读的操作,key:1,结束.
正在做写的操作,key:i,value:5开始.
正在做写的操作,key:i,value:5结束.
正在做写的操作,key:i,value:6开始.
正在做写的操作,key:i,value:6结束.
正在做写的操作,key:i,value:7开始.
正在做写的操作,key:i,value:7结束.
正在做读的操作,key:2,开始.
正在做读的操作,key:2,结束.
正在做写的操作,key:i,value:8开始.
正在做写的操作,key:i,value:8结束.
正在做写的操作,key:i,value:9开始.
正在做写的操作,key:i,value:9结束.
正在做读的操作,key:3,开始.
正在做读的操作,key:3,结束.
正在做读的操作,key:4,开始.
正在做读的操作,key:4,结束.
正在做读的操作,key:5,开始.
正在做读的操作,key:5,结束.
正在做读的操作,key:6,开始.
正在做读的操作,key:6,结束.
正在做读的操作,key:7,开始.
正在做读的操作,key:7,结束.
正在做读的操作,key:8,开始.
正在做读的操作,key:8,结束.
正在做读的操作,key:9,开始.
正在做读的操作,key:9,结束.

三、悲观锁与乐观锁

1.悲观锁

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

2.乐观锁

2.1.总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制或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,涉及到三个操作数,数据所在的内存值,预期值,新值。当需要更新时,判断当前内存值与之前取到的值是否相等,若相等,则用新值更新,若失败则重试,一般情况下是一个自旋操作,即不断的重试。

2.2.举例

举一个简单的例子: 假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。

  1. 操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 $50( $100-$50 )。
  2. 在操作员 A 操作的过程中,操作员B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 $20 ( $100-$20 )。
  3. 操作员 A 完成了修改工作,将数据版本号加一( version=2 ),连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。
  4. 操作员 B 完成了操作,也将版本号加一( version=2 )试图向数据库提交数据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。

这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员A 的操作结果的可能。

四、结束

Always keep the faith!!!

发布了122 篇原创文章 · 获赞 64 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/chenmingxu438521/article/details/103872814
今日推荐