从本篇开始,咱们来好好梳理一下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个用户应用柜子的场景。
/** * @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函数,看看失去的打印后果是什么?
我是用户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中都有哪些对于锁的解决方案,以及我的项目中所用到的实战。