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