Java深入学习(5):锁
可重入锁:
简单来说,支持重复加锁,有可重用性
特征:锁可以传递,方法递归传递
目的:避免了死锁现象
代码:
public class Test implements Runnable { @Override public void run() { method1(); } public synchronized void method1() { System.out.println("method1"); method2(); } public synchronized void method2() { System.out.println("method2"); } public static void main(String[] args) { new Thread(new Test()).start(); } }
打印:
method1
method2
分析:如果锁不能重用,那么这里将会出现死锁问题
使用ReentrantLock锁:
public class TestLock implements Runnable { //重入锁 private Lock reentrantLock = new ReentrantLock(); @Override public void run() { method1(); } public void method1() { try { reentrantLock.lock(); System.out.println("method1"); method2(); } catch (Exception e) { e.printStackTrace(); } finally { reentrantLock.unlock(); } } public void method2() { try { reentrantLock.lock(); System.out.println("method2"); } catch (Exception e) { e.printStackTrace(); } finally { reentrantLock.unlock(); } } public static void main(String[] args) { new Thread(new TestLock()).start(); } }
读写锁:
高并发的时候,写操作的同时应当不允许读操作
(多线程中:读-读共存;读-写、写-写都不可以共存)
代码:制造读写操作的线程安全问题
public class TestWriteLock { Map<String, String> cache = new HashMap<>(); //写入元素 public void put(String key, String value) { try { System.out.println("开始写入key : " + key + " value : " + value); Thread.sleep(50); cache.put(key, value); System.out.println("完成写入key : " + key + " value : " + value); } catch (Exception e) { e.printStackTrace(); } } //读取元素 public String get(String key) { System.out.println("开始读取key : " + key); String value = cache.get(key); System.out.println("读取成功key : " + key + " value : " + value); return value; } public static void main(String[] args) { TestWriteLock test = new TestWriteLock(); Thread readThread = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10; i++) { test.put("i", i + ""); } } }); Thread writeThread = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10; i++) { test.get("i"); } } }); readThread.start(); writeThread.start(); } }
观察打印:发现不合理
开始写入key : i value : 0 开始读取key : i 读取成功key : i value : null .................................
分析:在没有写入完成的时候,就开始了读取,得到的结果为空
解决:
1.使用synchronized,虽然可以解决,但是效率低下,写操作同时不能读,产生阻塞
2.使用读写锁
public class TestWriteLock { Map<String, String> cache = new HashMap<>(); ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); ReentrantReadWriteLock.ReadLock readLock = lock.readLock(); //写入元素 public void put(String key, String value) { try { writeLock.lock(); System.out.println("开始写入key : " + key + " value : " + value); Thread.sleep(50); cache.put(key, value); System.out.println("完成写入key : " + key + " value : " + value); } catch (Exception e) { e.printStackTrace(); } finally { writeLock.unlock(); } } //读取元素 public String get(String key) { String value = ""; try { readLock.lock(); System.out.println("开始读取key : " + key); value = cache.get(key); System.out.println("读取成功key : " + key + " value : " + value); } catch (Exception e) { e.printStackTrace(); } finally { readLock.unlock(); } return value; } public static void main(String[] args) { TestWriteLock test = new TestWriteLock(); Thread readThread = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10; i++) { test.put("i", i + ""); } } }); Thread writeThread = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10; i++) { test.get("i"); } } }); readThread.start(); writeThread.start(); } }
观察打印:完美解决
乐观锁:
简单来讲,乐观锁就是没有锁,无阻塞无等待
一条SQL语句做示范:
UPDATE TABLE SET X=X+1,VERSION=VERSION+1 WHERE ID=#{id} AND VERSION=#{version}
在高并发地情况下,假设初始version是1,请求1到来,根据id和version能查到,所以允许更新
请求2同时做操作,但是根据id和version已经查不到了(被请求1修改了),所以不允许更新
悲观锁:
简单来讲,重量级锁, 会阻塞,会进行等待
可以理解为上锁之后只允许一个线程来操作,也就是Java中的synchronized
原子类:
一段模拟线程安全问题的代码:
public class ThreadTest implements Runnable { private static int count = 1; @Override public void run() { while (true) { Integer count = getCount(); if (count >= 100) { break; } System.out.println(count); } } public synchronized Integer getCount() { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } return count++; } public static void main(String[] args) { ThreadTest t = new ThreadTest(); new Thread(t).start(); new Thread(t).start(); } }
观察打印后发现果然出现了线程安全问题
一种修改方式:效率较低
public synchronized Integer getCount() {
使用原子类:乐观锁,底层没有加锁,使用CAS无锁技术
public class ThreadTest implements Runnable { // 线程安全 private AtomicInteger atomicInteger = new AtomicInteger(); @Override public void run() { while (true) { Integer count = getCount(); if (count >= 100) { break; } System.out.println(count); } } public Integer getCount() { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } return atomicInteger.incrementAndGet(); } public static void main(String[] args) { ThreadTest t = new ThreadTest(); new Thread(t).start(); new Thread(t).start(); } }
CAS无锁技术(Compare And Swap):
翻译过来为:比较再交换
本地内存中存放共享内存的副本
比如主内存中有i=0,复制到两个线程的本地内存中
两个线程执行了i++,本地内存都变成i=1,然后刷新入主内存
CAS算法:
它包含三个参数CAS(V,E,N):
V表示要更新的变量(主内存)
E表示预期值(本地内存)
N表示新值(新值)
仅当V值等于E值时(主内存=本地内存),才会将V的值设为N
如果V值和E值不同(主内存!=本地内存),则说明已经有其他线程做了更新,则当前线程什么都不做
最后,CAS返回当前V的真实值。
观察原子类的源码:
/** * Atomically increments by one the current value. * * @return the updated value */ public final int incrementAndGet() { for (;;) { //获取当前值 int current = get(); //设置期望值 int next = current + 1; //调用Native方法compareAndSet,执行CAS操作 if (compareAndSet(current, next)) //成功后才会返回期望值,否则无线循环 return next; } }
CAS无锁机制的缺点:
1.死循环
2.ABA问题:如果变量V初次读取的时候是A,并且在准备赋值的时候检查到它仍然是A,那能说明它的值没有被其他线程修改过了吗
(如果在这段期间曾经被改成B,然后又改回A,那CAS操作就会误认为它从来没有被修改过。针对这种情况,java并发包中提供了一个带有标记的原子引用类AtomicStampedReference,它可以通过控制变量值的版本来保证CAS的正确性。)
JVM数据同步:采用分布式锁
自旋锁和互斥锁的区别:
悲观和乐观锁的区别,自旋锁是死循环不会阻塞,互斥锁是同一时间只有一个线程访问数据