【分布式锁的演化】什么是锁?

从本篇开始,咱们来好好梳理一下Java开发中的锁,经过一些具体简单的例子来描述清楚从Java单体锁到分布式锁的演化流程。本篇咱们先来看看什么是锁,如下老猫会经过一些平常生活中的例子也说清楚锁的概念。java

描述

锁在Java中是一个很是重要的概念,在当今的互联网时代,尤为在各类高并发的状况下,咱们更加离不开锁。那么到底什么是锁呢?在计算机中,锁(lock)或者互斥(mutex)是一种同步机制,用于在有许多执行线程的环境中强制对资源的访问限制。锁能够强制实施排他互斥、并发控制策略。举一个生活中的例子,你们都去超市买东西,若是咱们带了包的话,要放到储物柜。咱们再把这个例子极端一下,假如柜子只有一个,那么此时同时来了三我的A、B、C都要往这个柜子里放东西。那么这个场景就是一个多线程,多线程天然也就离不开锁。简单示意图以下多线程

存储柜子模型

A、B、C都要往柜子里面放东西,但是柜子只能存放一个东西,那么怎么处理?这个时候咱们就引出了锁的概念,三我的中谁先抢到了柜子的锁,谁就可使用这个柜子,其余的人只能等待。好比C抢到了锁,C就可使用这个柜子,A和B只能等待,等到C使用完毕以后,释放了锁,AB再进行抢锁,谁先抢到了,谁就有使用柜子的权利。并发

抽象成代码

咱们其实能够将以上场景抽象程相关的代码模型,咱们来看一下如下代码的例子。分布式

/**
 * @author kdaddy@163.com
 * @date 2020/11/2 23:13
 */
public class Cabinet {
    //表示柜子中存放的数字
    private int storeNumber;

    public int getStoreNumber() {
        return storeNumber;
    }
    public void setStoreNumber(int storeNumber) {
        this.storeNumber = storeNumber;
    }
}

柜子中存储的是数字。函数

而后咱们把3个用户抽象成一个类,以下代码高并发

/**
 * @author kdaddy@163.com
 * @date 2020/11/7 22:03
 */
public class User {
    // 柜子
    private Cabinet cabinet;
    // 存储的数字
    private int storeNumber;

    public User(Cabinet cabinet, int storeNumber) {
        this.cabinet = cabinet;
        this.storeNumber = storeNumber;
    }
    // 表示使用柜子
    public void useCabinet(){
        cabinet.setStoreNumber(storeNumber);
    }
}

在用户的构造方法中,须要传入两个参数,一个是要使用的柜子,另外一个是要存储的数字。以上咱们把柜子和用户都已经抽象完毕,接下来咱们再来写一个启动类,模拟一下3个用户使用柜子的场景。this

/**
 * @author kdaddy@163.com
 * @date 2020/11/7 22:05
 */
public class Starter {
    public static void main(String[] args) {
        final Cabinet cabinet = new Cabinet();
        ExecutorService es = Executors.newFixedThreadPool(3);

        for(int i= 1; i < 4; i++){
            final int storeNumber = i;
            es.execute(()->{
                User user = new User(cabinet,storeNumber);
                user.useCabinet();
                System.out.println("我是用户"+storeNumber+",我存储的数字是:"+cabinet.getStoreNumber());
            });
        }
        es.shutdown();
    }
}

咱们仔细的看一下这个main函数的过程线程

  • 首先建立一个柜子的实例,因为场景中只有一个柜子,因此咱们只建立了一个柜子实例。
  • 而后咱们新建了一个线程池,线程池中一共有三个线程,每一个线程执行一个用户的操做。
  • 再来看看每一个线程具体的执行过程,新建用户实例,传入的是用户使用的柜子,咱们这里只有一个柜子,因此传入这个柜子的实例,而后传入这个用户所须要存储的数字,分别是1,2,3,也分别对应了用户1,2,3。
  • 再调用使用柜子的操做,也就是想柜子中放入要存储的数字,而后马上从柜子中取出数字,并打印出来。

咱们运行一下main函数,看看获得的打印结果是什么?code

我是用户1,我存储的数字是:3
我是用户3,我存储的数字是:3
我是用户2,我存储的数字是:2

从结果中,咱们能够看出三个用户在存储数字的时候两个都是3,一个是2。这是为何呢?咱们期待的应该是每一个人都能获取不一样的数字才对。其实问题就是出在"user.useCabinet();"这个方法上,这是由于柜子这个实例没有加锁的缘由,三个用户并行执行,向柜子中存储他们的数字,虽然3个用户并行同时操做,可是在具体赋值的时候,也是有顺序的,由于变量storeNumber只有一块内存,storeNumber只存储一个值,存储最后的线程所设置的值。至于哪一个线程排在最后,则彻底不肯定,赋值语句执行完成以后,进入打印语句,打印语句取storeNumber的值并打印,这时storeNumber存储的是最后一个线程锁所设置的值,3个线程取到的值有两个是相同的,就像上面打印的结果同样。对象

那么如何才能解决这个问题?这就须要咱们用到锁。咱们再赋值语句上加锁,这样当多个线程(此处表示用户)同时赋值的时候,谁能优先抢到这把锁,谁才可以赋值,这样保证同一个时刻只能有一个线程进行赋值操做,避免了以前的混乱的状况。

那么在程序中,咱们如何加锁呢?

下面咱们介绍一下Java中的一个关键字synchronized。关于这个关键字,其实有两种用法。

  • synchronized方法,顾名思义就是把synchronize的关键字写在方法上,它表示这个方法是加了锁的,当多个线程同时调用这个方法的时候,只有得到锁的线程才可以执行,具体以下:

    public synchronized String getTicket(){
            return "xxx";
        }

    以上咱们能够看到getTicket()方法加了锁,当多个线程并发执行的时候,只有得到锁的线程才能够执行,其余的线程只可以等待。

  • synchronized代码块。以下:

    synchronized (对象锁){
        ……
    }

    咱们将须要加锁的语句都写在代码块中,而在对象锁的位置,须要填写加锁的对象,它的含义是,当多个线程并发执行的时候,只有得到你写的这个对象的锁,才可以执行后面的语句,其余的线程只能等待。synchronized块一般的写法是synchronized(this),这个this是当前类的实例,也就是说得到当前这个类的对象的锁,才可以执行这个方法,此写法等同于synchronized方法。

回到刚才的例子中,咱们又是如何解决storeNumber混乱的问题呢?我们试着在方法上加上锁,这样保证同时只有一个线程能调用这个方法,具体以下。

/**
 * @author kdaddy@163.com
 * @date 2020/12/2 23:13
 */
public class Cabinet {
    //表示柜子中存放的数字
    private int storeNumber;

    public int getStoreNumber() {
        return storeNumber;
    }

    public synchronized void setStoreNumber(int storeNumber) {
        this.storeNumber = storeNumber;
    }
}

咱们运行一下代码,结果以下

我是用户2,我存储的数字是:2
我是用户3,我存储的数字是:2
我是用户1,我存储的数字是:1

咱们发现结果仍是混乱的,并无解决问题。咱们检查一下代码

es.execute(()->{
                User user = new User(cabinet,storeNumber);
                user.useCabinet();
                System.out.println("我是用户"+storeNumber+",我存储的数是:"+cabinet.getStoreNumber());
            });

咱们能够看到在useCabinet和打印的方法是两个语句,并无保持原子性,虽然在set方法上加了锁,可是在打印的时候又存在了并发,打印语句是有锁的,可是不能肯定哪一个线程去执行。因此这里,咱们要保证useCabinet和打印的方法的原子性,咱们使用synchronized块,可是synchronized块里的对象咱们使用谁的?这又是一个问题,user仍是cabinet?回答固然是cabinet,由于每一个线程都初始化了user,总共有3个User对象,而cabinet对象只有一个,因此synchronized要用cabine对象,具体代码以下

/**
 * @author kdaddy@163.com
 * @date 2020/12/7 22:05
 */
public class Starter {
    public static void main(String[] args) {
        final Cabinet cabinet = new Cabinet();
        ExecutorService es = Executors.newFixedThreadPool(3);

        for(int i= 1; i < 4; i++){
            final int storeNumber = i;
            es.execute(()->{
                User user = new User(cabinet,storeNumber);
                synchronized (cabinet){
                    user.useCabinet();
                    System.out.println("我是用户"+storeNumber+",我存储的数字是:"+cabinet.getStoreNumber());
                }
            });
        }
        es.shutdown();
    }
}

此时咱们再去运行一下:

我是用户3,我存储的数字是:3
我是用户2,我存储的数字是:2
我是用户1,我存储的数字是:1

因为咱们加了synchronized块,保证了存储和取出的原子性,这样用户存储的数字和取出的数字就对应上了,不会形成混乱,最后咱们用图来表示一下上面例子的总体状况。
最终模型

如上图所示,线程A,线程B,线程C同时调用Cabinet类的setStoreNumber方法,线程B得到了锁,因此线程B能够执行setStore的方法,线程A和线程C只能等待。

总结

经过上面的场景以及例子,咱们能够了解多线程状况下,形成的变量值先后不一致的问题,以及锁的做用,在使用了锁之后,能够避免这种混乱的现象,后续,老猫会和你们介绍一个Java中都有哪些关于锁的解决方案,以及项目中所用到的实战。

相关文章
相关标签/搜索