很重要,对于理解 锁的是哪个对象很重要以及volatile的可见性并不能代表数据的一致性以及数据的原子性,因此volatile并非是数据安全的。
模拟银行账户读写,数据是否一致
public class Account_01 {
private String name;
private int balance;
void set(String name, int balance) {
// 此处睡两秒是为了保证 main 主线程先执行一次getBalance方法
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.name = name;
this.balance = balance;
}
int getBalance(String name) {
return balance;
}
public static void main(String[] args) throws InterruptedException {
Account_01 a = new Account_01();
new Thread(() -> a.set("zhangsan", 2), "t1").start();
System.out.println("thread t1 exe set before balance value : " + a.getBalance("zhangsan"));
// 此处睡3 秒是为了保证线程 t1 set 方法 执行完毕
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread t1 exe set after balance value: " + a.getBalance("zhangsan"));
}
}
输出结果:
thread t1 exe set before balance value : 0
thread t1 exe set after balance value: 2
出现了脏读现象,现在为了保持 写入的数据和读到的数据保持一致,代码做以下更改。
加synchronized
public class Account_01 {
private String name;
private int balance;
synchronized void set(String name, int balance) {
// 此处睡两秒是为了保证 main 主线程先执行一次getBalance方法
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.name = name;
this.balance = balance;
}
synchronized int getBalance(String name) {
return balance;
}
public static void main(String[] args) throws InterruptedException {
Account_01 a = new Account_01();
new Thread(() -> a.set("zhangsan", 2), "t1").start();
System.out.println("thread t1 exe set before balance value : " + a.getBalance("zhangsan"));
// 此处睡3 秒是为了保证线程 t1 set 方法 执行完毕
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread t1 exe set after balance value: " + a.getBalance("zhangsan"));
}
}
输出结果:
thread t1 exe set before balance value : 2
thread t1 exe set after balance value: 2
读和写结果是一致的,此处说明了 synchronized 锁定的是当前对象,当 我们去调用get 方法时提示有锁,需要等待set方法执行完毕之后才可以执行 get方法。 重点,方法上的锁是对象的。
为啥不用volatile 呢, 试试看下输出结果。
public class Account_01 {
private String name;
private volatile int balance;
synchronized void set(String name, int balance) {
// 此处睡两秒是为了保证 main 主线程先执行一次getBalance方法
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.name = name;
this.balance = balance;
}
int getBalance(String name) {
return balance;
}
public static void main(String[] args) throws InterruptedException {
Account_01 a = new Account_01();
new Thread(() -> a.set("zhangsan", 2), "t1").start();
System.out.println("thread t1 exe set before balance value : " + a.getBalance("zhangsan"));
// 此处睡3 秒是为了保证线程 t1 set 方法 执行完毕
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread t1 exe set after balance value: " + a.getBalance("zhangsan"));
}
}
输出结果:
thread t1 exe set before balance value : 0
thread t1 exe set after balance value: 2
输出结果 是读和写不一致,再一次说明了 volatile 只能保持数据的可见性,并不能说明数据的一致性,synchronized 才可以保证数据的原子性和一致性。
用了 synchronized 之后还需要用volatile 吗? 不需要。****synchronized 已经保证了多线程之间数据的一致性了。
模拟多线程写,用map存
public class Account_01 {
private String name;
private volatile int balance;
private Map<String, Integer> map = new HashMap<>();
synchronized void set(String name, int balance) {
balance = (Objects.isNull(map.get(name)) ? 0 : map.get(name)) + balance;
map.put(name, balance);
}
synchronized int getBalance(String name) {
return (Objects.isNull(map.get(name)) ? 0 : map.get(name));
}
public static void main(String[] args) throws InterruptedException {
Account_01 a = new Account_01();
CountDownLatch start = new CountDownLatch(1);
CountDownLatch end = new CountDownLatch(20);
for (int i = 0; i < 20; i++) {
new Thread(() -> {
try {
start.await();
a.set("zhangsan", 2);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
end.countDown();
}
}, "t1").start();
}
start.countDown();
end.await();
System.out.println("输出结果: " + a.getBalance("zhangsan"));
}
}
输出结果:
输出结果: 40 // get 方法加锁了,等到 set 完成释放锁之后才拿到锁执行get 方法获取到值
是否为同一把锁
@SuppressWarnings("all")
public class Account_01 {
private String name;
private volatile int balance;
private ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
void set(String name, int balance) {
synchronized (this) {
balance = (Objects.isNull(map.get(name)) ? 0 : map.get(name)) + balance;
}
map.put(name, balance);
}
synchronized int getBalance(String name) {
return (Objects.isNull(map.get(name)) ? 0 : map.get(name));
}
public static void main(String[] args) throws InterruptedException {
for (int n = 0; n < 500000; n++) {
Account_01 a = new Account_01();
CountDownLatch start = new CountDownLatch(1);
CountDownLatch end = new CountDownLatch(20);
for (int i = 0; i < 20; i++) {
new Thread(() -> {
try {
start.await();
a.set("zhangsan", 2);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
end.countDown();
}
}, "t1").start();
}
start.countDown();
end.await();
if (a.getBalance("zhangsan") != 40) {
System.out.println("Incorrect result output: " + a.getBalance("zhangsan"));
}
System.out.println("Correct result output: " + a.getBalance("zhangsan"));
}
}
}
输出结果:
Correct result output: 40
Correct result output: 40
Incorrect result output: 38
Correct result output: 38
Correct result output: 40
Correct result output: 40
Incorrect result output: 38
Correct result output: 38
Correct result output: 40
Correct result output: 40
Correct result output: 40
Correct result output: 40
Correct result output: 40
Correct result output: 40
Correct result output: 40
Correct result output: 40‘
很显然输出结果里面有读写不一致的情况。这个代码里面我们用到了ConcurrentHashMap 为啥还读写不一致呢,个人认为 读和写并不是同一把锁导致的。所以要体现出读写一致,必定需要使用同一把锁