很重要,对于理解 锁的是哪个对象很重要以及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 为啥还读写不一致呢,个人认为 读和写并不是同一把锁导致的。所以要体现出读写一致,必定需要使用同一把锁