今天来聊聊 Java 中的锁:
举个例子:
两个线程,依次 start 后,执行同一个对象的同一段方法,会出现什么样的后果?
我们写一段代码看看:
public class Demo {
public static void main(String[] args) {
UserInfo userInfo = new UserInfo(10, "小明");
Thread thread = new Thread() {
@Override
public void run() {
userInfo.printUserName(currentThread().getName());
}
};
Thread a = new Thread(thread, "A");
Thread b = new Thread(thread, "B");
a.start();
b.start();
}
}
class UserInfo {
private Integer userId;
private String userName;
public UserInfo(Integer userId, String userName) {
this.userId = userId;
this.userName = userName;
}
public Integer getUserId() {
return userId;
}
public String getUserName() {
return userName;
}
@Override
public String toString() {
return "UserInfo{" +
"userId=" + userId +
", userName='" + userName + '\'' +
'}';
}
public void printUserName(String name){
if (name != null && "A".equals(name)){
try {
userId = 111111;
System.out.println("My name is A");
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
userId = 222222;
System.out.println("My name is B");
}
System.out.println(name + ":" + getUserId());
}
}
运行上面的代码,控制台的会打印出如下内容:
结果中,A线程的 userId 被设置成了 222222 这个值,但是我们在代码中,当 name 为 "A" 值时,我们设置的 userId 是为 111111 这个值的。
为什么会出现这种情况呢? 我们看代码,代码中,printUserName(String name) 方法中,当 name 为 “A”时,A线程修改完 userId 的值后,会调用 sleep() 方法,当我们运行 main 函数时,A线程修改完 userId 的值后,进入睡眠,这时,B线程进来修改 userId 的值,接着同时打印 name 和 userId 的值,A线程苏醒,打印 name 和 userId 的值;因为,A,B两个线程公用的是同一个 UserInfo 对象,在A线程睡眠期间,B线程修改 userId的值,实际上是在A线程修改以后的基础上进行修改的,所以,A线程苏醒后,同时打印 name 和 userId 时,其中 userId 的值其实是最后一次被修改的值(最后一次被修改是B线程做的修改)。
像这种情况:A线程读取到了B线程中修改的值,我们一般管他叫做 脏读。
要解决这种情况,需要给方法加一个 java 中的 synchronized关键字,就可以了。
下图为增加关键字后,运行结果:
synchronized 关键字的作用呢,就是被其修饰的方法,或者代码段,一次只能有一个线程执行,只有当一个线程执行完被 synchronized 修饰的方法或者代码段时,才允许另一个线程进入。
synchronized 的机制就是,多个线程在准备进入被其修饰的代码段或者方法,会进行去竞争获取锁;一个对象只有一把锁,争抢到锁的线程进入被其修饰的代码段或者方法中运行,其他线程等待;当争抢到锁的线程运行完被修饰的代码块或者方法时,会释放掉锁,这时,等待的线程再次竞争锁。
synchronized 的注意点:
1.当其修饰静态方法时,其加锁对象为 当前类的 class 对象。
2.当其修饰普通成员方法时,加锁对象为 当前类的实例。
3.当其修饰代码块时,加锁对象为 参数对象实例,如下:
synchronized (name){
userId = 111111;
}
其加锁对象为 name 实例。
另外,再补充一点,关于 原子性与可见性的问题,synchronized 修饰的方法可代码块,是可以保证方法或者代码块执行的原子性,同时又可以保证执行过程中,方法或者代码块以外声明的变量被修改后的可见性。
保证原子性:一次只能有一个线程进入被其修改的方法或者代码块,在这期间,其他线程无法进入该方法或代码块执行,执行线程不受其他线程的干扰,成功则成功,失败则抛出异常,退出同步方法或代码块,并释放锁。
保证可见性:synchronized 修饰的方法或者代码块保证可见性,是通过保证原子性从而实现可见性的。
为什么我这么说呢,因为,synchronized 保证原子性,确保了一次只有一个线程去修改成员变量,也就是说同一时刻,没有其他的线程在执行该段代码,不会对数据造成影响,持有锁的当前线程运行完后,退出同步代码块或方法,释放锁;等待获取锁的任意一个线程得到锁,进入同步代码块进行数据操作,这是,它再去获取数据时,已经是上一个线程修改完后的值,故此,synchronized保证了可见性。
能保证原子性,则必然能保证可见性。
在 Java 中,volatile 关键字修饰的变量,也可以保证可见性,但是,请永远牢记:volatile 能保证可见性,不能保证原子性。
先看下图:
再配合一段代码:
public class Demo {
public static void main(String[] args) {
ThreadTest threadTest = new ThreadTest();
threadTest.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
threadTest.setaBoolean(false);
}
}
class ThreadTest extends Thread {
private boolean aBoolean = true;
public void setaBoolean(boolean aBoolean) {
this.aBoolean = aBoolean;
}
@Override
public void run() {
System.out.println("进入 Run 方法");
while (aBoolean){
}
System.out.println("退出 Run 方法");
}
}
运行上面的代码,永远不会打印 "退出 Run 方法";
因为,main()函数中创建 ThreadTest 实例,其实例是存储在上图的 主内存 位置,当在 main()函数中调用 start() 方法,线程运行 run() 函数时,会将 主内存里的 ThreadTest 实例中所有 run()函数中用到的变量和对象加载到工作内存中,然后进行数据的操作,当修改某一变量时,先修改工作内存中的值,然后在从工作内存中写入到主内存,完成修改,请注意,这一修改为原子性操作。
而上面代码中,run()函数只有将数据写入工作内存的动作,main()函数的赋值操作,只是改变了主内存中的值,并未修改线程工作内存中的值,故此,run()函数将会陷入死循环。
接下来,我们回到 volatile 关键字上来,是通过使线程在运行 run() 函数时,不将数据加载到工作内存,当 run() 函数中需要使用时,直接去主内存中读取,修改时,直接写入主内存;以此来保证可见性的。
好了,今天就先唠这些,关于Java中的锁,这个只是个开头,后续可能还会再写几篇,Emmm 作为懒癌晚期病人,但愿我能早日研究明白