从本篇开始,咱们来好好梳理一下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函数的过程线程
咱们运行一下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中都有哪些关于锁的解决方案,以及项目中所用到的实战。