什么是锁?
锁在JAVA中是一个非常重要的概念,尤其在当今互联网时代,高并发的场景下,更离不开锁.锁(lock)与互斥(mutex)是一种同步机制,用于在有许多执行线程的环境中强制对资源的访问限制.锁旨在强制实施互斥排,并发控制策略.
举一个生活中的例子:大家去超市的时候,随声携带了背包,需要将包存在柜子中.极端假设一下,如果柜子只有一个,同时来了三个人A,B,C都想存背包.这个场景就造成了多线程,多线程自然离不开锁.如下所示:
A,B,C都想往柜子里面放背包,可是柜子只有一个只能放一件东西,那要怎么办呢?这个时候就出现了锁的概念,三个人中谁抢到了锁,谁就可以使用这个柜子,其他的人只能等待.比如A抢到了锁,那么A使用柜子,B和C只能等待.
代码示例
将上面的例子放映到程序中,创建一个柜子的类:
public class Cabinet { //柜子中存放的数字 private int number; public int getNumber() { return number; } public void setNumber(int number) { this.number = number; }}
创建一个用户类:
public class User { //柜子 private Cabinet cabinet; //存储的数字 private int number; //构造方法 public User(Cabinet cabinet, int number) { this.cabinet = cabinet; this.number = number; } //使用柜子的方法 public void userCabinet(){ cabinet.setNumber(number); }}
在用户的构造方法中,需要传入两个参数,一个是要使用的柜子,另一个是要存储的数字.
创建测试类,模拟三个用户使用柜子:
public class Test { public static void main(String args[]){ Cabinet cabinet = new Cabinet(); for (int i = 0; i < 3; i++) { final int number = i; new Thread(() -> { User user = new User(cabinet,number); user.userCabinet(); System.out.println("我是用户: " + number + ",我存储的数字是: " + cabinet.getNumber()); },String.valueOf(i)).start(); } }}
我们来分析下这个main函数的过程:
创建柜子的实例,由于假设的场景中只有一个柜子,所以我们只创建了一个柜子的实例.
新建线程池,其中有三个线程,每个线程执行一个用户的操作.
新建用户实例,传入柜子的实例,然后传入用户要存储的数字,分别是1,2,3也分别对应着A,B,C
调用使用柜子的操作,向柜子中放入要存储的数字,然后取出数字并打印.
运行结果:
我是用户: 0,我存储的数字是: 1我是用户: 2,我存储的数字是: 1我是用户: 1,我存储的数字是: 1
从结果中我们看出,三个用户在柜子中的数字都变成了1
再次运行:
我是用户: 0,我存储的数字是: 2我是用户: 2,我存储的数字是: 2我是用户: 1,我存储的数字是: 2
这次变成了1,这是为什么呢?
问题就出在user.userCabinet();这个方法上,这是因为柜子这个实例没有加锁的原因,三个用户并行的执行,向柜子中存储他们的数字,虽然同时操作,但是在具体赋值的时候,也是有顺序的,因为变量number只占有一块内存,只存储一个值,存储最后的线程锁设置的值,至于哪个线程在最后则完全不确定.赋值语句执行完成后,进入到打印语句,此时取到的是最后设置的值,就想上面的打印结果一样.
如何解决这个问题呢?我们在赋值语句上加锁,这当多个线程同时赋值时,谁抢到了这把锁,谁才能赋值.这样保证同一时刻只能有一个线程进行赋值操作.
在程序中如何加锁呢?这就要使用JAVA中的一个关键字---synchronized.synchronized分为synchronized方法和synchronized同步代码块
synchronized方法,是把synchronized关键字写在方法上,表示这个方法是加了锁的,当多个线程同时调用这个方法时,只有获得锁的线程才可以执行.
public synchronized String getTicket(){ return "hello";}
我们可以看到getTicket()方法加了锁,当多个线程并发执行的时候,只有获得到锁的线程才可以执行
synchronized快的语法:
synchronized (对象锁){ ......}
我们将需要加锁的语句写在synchronized块内,在对象锁的位置需要填写加锁的对象,当多个线程并发执行的时候,只有获得这个对象的锁,才能执行后面的语句,其他的线程只能等待.
回到我们的示例当中,我们可以在设置number的方法上加锁,这样保证同时只有一个线程能调用这个方法:
public class Cabinet { //柜子中存放的数字 private int number; public int getNumber() { return number; } public synchronized void setNumber(int number) { this.number = number; }}
我们在set方法上加了synchronized关键字,这样在存储数字的时候,就不会并行的去执行了
我是用户: 0,我存储的数字是: 1我是用户: 1,我存储的数字是: 1我是用户: 2,我存储的数字是: 1
结果还是错误的
for (int i = 0; i < 3; i++) { final int number = i; new Thread(() -> { User user = new User(cabinet,number); user.userCabinet(); System.out.println("我是用户: " + number + ",我存储的数字是: " + cabinet.getNumber()); },String.valueOf(i)).start();}
我们可以看到在userCabinet和打印的方法是两个语句,并没有保持原子性,虽然在set上加了锁,但是在打印时又存在一个并发,打印语句是有锁的,但是不能确定哪个线程去执行.
每个线程都初始化了user对象,总共有三个user对象,而cabinet对象只有一个,所以对cabinet使用synchronized块:
synchronized (cabinet){ user.userCabinet(); System.out.println("我是用户: " + number + ",我存储的数字是: " + cabinet.getNumber());}
运行结果
我是用户: 0,我存储的数字是: 0我是用户: 1,我存储的数字是: 1我是用户: 2,我存储的数字是: 2
由于我们加上了synchronized块,保证了存储和取出的原子性.
总结
通过上面的示例,了解了在多线程情况下,造成的变量值前后不一致的问题,以及锁的作用.在使用了锁以后,可以避免这种混乱的现象.