重入锁
概念
- 重入锁,也叫做递归锁(避免在传递中产生死锁现象),指的是同一线程外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响
- 重入锁有多种实现(如 synchronized(重量级) 和 ReentrantLock(轻量级)等等 ) ,这些已经写好提供的锁为我们开发提供了便利
样例
- synchronized
public class SynchronizedDemo implements Runnable { @Override public void run() { set(); } public synchronized void get() { System.out.println("get方法"); } public synchronized void set() { System.out.println("set方法"); get(); } public static void main(String[] args) { new Thread(new SynchronizedDemo()).start(); } }
- ReentrantLock
由上面的synchronized和ReentrantLock 锁的demo中,get和set方法都加锁了,执行set方法的时候,当执行到get方法时get方法也加了锁,get与set都属用的同一把锁,执行get方法时,set方法并没有释放锁,但是get方法并没有去重新获取锁资源,由于可以看出set方法将锁资源传递给了get方法,才可以正常执行。如果synchronized和ReentrantLock锁不具备重入性的话,则执行get方法时会重新获取锁资源,但是set方法并没有释放锁资源,则get与set方法会产生死锁情况,代码不能够正常执行。public class ReentrantLockDemo implements Runnable { private ReentrantLock lock = new ReentrantLock(); @Override public void run() { set(); } public void get() { try { lock.lock(); System.out.println("get方法"); } catch (Exception ex) { } finally { lock.unlock(); } } public void set() { try { lock.lock(); System.out.println("set方法"); } catch (Exception ex) { } finally { lock.unlock(); } get(); } public static void main(String[] args) { new Thread(new ReentrantLockDemo()).start(); } }
读写锁
概念
ReadWriteLock同Lock一样也是一个接口,提供了readLock和writeLock两种锁的操作机制,一个是只读的锁,一个是写锁
- 两个线程同时读一个资源没有任何问题,所以应该允许多个线程能在同时读取共享资源。但是如果有一个线程想去写这些共享资源,就不应该再有其它线程对该资源进行读或写
互斥原则
- 读-读能共存
- 读-写不能共存
- 写-写不能共存
样例
模拟jvm内置缓存(无锁)
- 代码样例
public class ReadWriteLockDemo { private volatile Map<String, String> cache = new HashMap<>(); public void put(String key, String value) { System.out.println("put开始,key=" + key); try { Thread.sleep(1000); } catch (InterruptedException e) { } cache.put(key, value); System.out.println("put结束,key=" + key); } public String get(String key) { System.out.println("get开始,key=" + key); try { Thread.sleep(1000); } catch (InterruptedException e) { } System.out.println("get结束,key=" + key); return cache.get(key); } public static void main(String[] args) { ReadWriteLockDemo demo = new ReadWriteLockDemo(); //写线程 new Thread(() -> { for (int i = 0; i < 5; i++) { demo.put(i + "", i + ""); } }).start(); //读线程 new Thread(() -> { for (int i = 0; i < 5; i++) { demo.get(i+""); } }).start(); } }
- 执行结果
执行结果可以看出,当正在在写入数据的时候,同时也发生了读操作,这样读操作的时候很容易读取到脏数据put开始,key=0 get开始,key=0 get结束,key=0 put结束,key=0 get开始,key=1 put开始,key=1 get结束,key=1 put结束,key=1
模拟jvm内置缓存(读写锁)
- 代码样例
public class ReadWriteLockDemo { private volatile Map<String, String> cache = new HashMap<>(); //读写锁 private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); private ReentrantReadWriteLock.ReadLock readLock = rwl.readLock(); private ReentrantReadWriteLock.WriteLock writeLock = rwl.writeLock(); public void put(String key, String value) { try { writeLock.lock(); System.out.println("put开始,key=" + key); Thread.sleep(1000); cache.put(key, value); System.out.println("put结束,key=" + key); } catch (Exception e) { } finally { writeLock.unlock(); } } public String get(String key) { try { readLock.lock(); System.out.println("get开始,key=" + key); Thread.sleep(1000); System.out.println("get结束,key=" + key); return cache.get(key); } catch (Exception e) { return null; } finally { readLock.unlock(); } } public static void main(String[] args) { ReadWriteLockDemo demo = new ReadWriteLockDemo(); //写线程 new Thread(() -> { for (int i = 0; i < 5; i++) { demo.put(i + "", i + ""); } }).start(); //读线程 new Thread(() -> { for (int i = 0; i < 5; i++) { demo.get(i + ""); } }).start(); } }
- 执行结果
执行结果看出,读写操作的开始和结束都是成对存在,由此可见:当写的时候,读操作并没有发生,读的时候,写操作也并没有发生,保证了读写操作时,线程安全问题。put开始,key=0 put结束,key=0 get开始,key=0 get结束,key=0 put开始,key=1 put结束,key=1 put开始,key=2 put结束,key=2
乐观锁
概念
- 总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制或CAS操作实现
- 乐观锁本质无锁,效率比较高,无阻塞,无等待,但是需要重试机制
实现方式
- version方式
一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功
核心SQL语句:update table set x=x+1, version=version+1 where id=#{id} and version=#{version};
- CAS操作方式
即compare and swap 或者 compare and set,涉及到三个操作数,数据所在的内存值,预期值,新值。当需要更新时,判断当前内存值与之前取到的值是否相等,若相等,则用新值更新,若失败则重试,一般情况下是一个自旋操作,即不断的重试
悲观锁
概念
- 总是假设最坏的情况,每次取数据时都认为其他线程会修改,所以都会加锁(读锁、写锁、行锁等),当其他线程想要访问数据时,都需要阻塞挂起
- 悲观锁属于重量级锁,会阻塞,会等待其他线程执行完成才执行
- 可以依靠数据库实现,如行锁、读锁和写锁等,都是在操作之前加锁
- 在Java中,synchronized的思想也是悲观锁
CAS无锁模式
什么是CAS
- CAS:Compare and Swap,即比较再交换
- jdk5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁
- JDK 5之前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁
CAS算法理解
(1)与锁相比,使用比较交换(下文简称CAS)会使程序看起来更加复杂一些。但由于其非阻塞性,它对死锁问题天生免疫,并且,线程间的相互影响也远远比基于锁的方式要小。更为重要的是,使用无锁的方式完全没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,因此,它要比基于锁的方式拥有更优越的性能。
(2)无锁的好处:
- 在高并发的情况下,它比有锁的程序拥有更好的性能
- 天生就是死锁免疫的
(3)CAS算法的过程是这样:它包含三个参数CAS(V,E,N): V表示要更新的变量(主内存),E表示预期值(本地内存),N表示新值。仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值
- 如果V=E(主内存值与本地内存值一致),说明:没有被修改过,将V的值设置为新值N
- 如果V!=E(主内存值与本地内存值不一致),说明:已经被修改过,重新刷新主内存值,然后循环进行比较
(4)CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理
(5)简单地说,CAS需要你额外给出一个期望值,也就是你认为这个变量现在应该是什么样子的。如果变量不是你想象的那样,那说明它已经被别人修改过了。你就重新读取,再次尝试修改就好了
(6)在硬件层面,大部分的现代处理器都已经支持原子化的CAS指令。在JDK 5.0以后,虚拟机便可以使用这个指令来实现并发操作和并发数据结构,并且,这种操作在虚拟机中可以说是无处不在
CAS缺点
CAS存在一个很明显的问题,即ABA问题。
问题:如果变量V初次读取的时候是A,并且在准备赋值的时候检查到它仍然是A,那能说明它的值没有被其他线程修改过了吗?
如果在这段期间曾经被改成B,然后又改回A,那CAS操作就会误认为它从来没有被修改过。针对这种情况,java并发包中提供了一个带有标记的原子引用类AtomicStampedReference,它可以通过控制变量值的版本来保证CAS的正确性
CAS典型用法-Atomic原子类
- jdk 1.5实现(AtomicInteger.incrementAndGet())
public final int getAndAddInt(Object o, long offset, int delta) { int v; do { v = getIntVolatile(o, offset); } while (!compareAndSwapInt(o, offset, v, v + delta)); return 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; } }
- jdk 1.8实现(AtomicInteger .incrementAndGet())
/** * Atomically increments by one the current value. * * @return the updated value */ public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; } public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; } public native int getIntVolatile(Object var1, long var2); public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
自旋锁
概念
- 是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名
- CAS模式采用自旋锁
互斥锁(悲观锁)与自旋锁区别
- 互斥锁需要等待与阻塞,属于悲观锁,一次只能一个线程拥有互斥锁,其他线程只有等待
- 自旋锁属于乐观锁,是一种特殊的互斥锁,当资源被枷锁后,其他线程想要再次加锁,此时该线程不会被阻塞睡眠而是陷入循环等待状态(CPU不能做其它事情),循环检查资源持有者是否已经释放了资源,这样做的好处是减少了线程从睡眠到唤醒的资源消耗,但会一直占用CPU的资源。适用于资源的锁被持有的时间短,而又不希望在线程的唤醒上花费太多资源的情况
- 互斥锁: 线程会从sleep(加锁)—》running(解锁),过程中有上下文的切换,cpu的抢占,信号的发送等开销
- 自旋锁: 线程一直都是running(加锁—》解锁),死循环检测锁的标志位,机制不复杂
公平锁与非公平锁
实现原理
在公平的锁中,如果有另一个线程持有锁或者有其他线程在等待队列中等待这个所,那么新发出的请求的线程将被放入到队列中。而非公平锁上,只有当锁被某个线程持有时,新发出请求的线程才会被放入队列中(此时和公平锁是一样的)。所以,它们的差别在于非公平锁会有更多的机会去抢占锁
公平锁
- 公平锁在于每次都是从等待执行的链表以此从队首取值
final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } #hasQueuedPredecessors的实现 public final boolean hasQueuedPredecessors() { Node t = tail; // Read fields in reverse initialization order Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); }
- 公平锁样例
public class FairLock implements Runnable { /** * true 表示 ReentrantLock 的公平锁 */ private ReentrantLock lock = new ReentrantLock(true); @Override public void run() { try { lock.lock(); Thread.sleep(50); System.out.println(Thread.currentThread().getName() + "获得锁"); } catch (Exception ex) { } finally { lock.unlock(); } } public static void main(String[] args) { FairLock fairLock = new FairLock(); for (int i = 0; i < 5; i++) { new Thread(fairLock).start(); } } }
- 执行结果
可以看到,获取锁的线程顺序正是线程启动的顺序Thread-0获得锁 Thread-1获得锁 Thread-2获得锁 Thread-3获得锁 Thread-4获得锁
非公平锁
- 在等待锁的过程中,如果有任意新的线程妄图获取锁,都是有很大几率直接获取到锁
final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } }
- 非公平锁样例
public class NotFairLock implements Runnable { /** * false 表示 ReentrantLock 的非公平锁 */ private ReentrantLock lock = new ReentrantLock(false); @Override public void run() { try { lock.lock(); Thread.sleep(50); System.out.println(Thread.currentThread().getName() + "获得锁"); } catch (Exception ex) { } finally { lock.unlock(); } } public static void main(String[] args) { NotFairLock fairLock = new NotFairLock(); for (int i = 0; i < 5; i++) { new Thread(fairLock).start(); } } }
- 执行结果
可以看出非公平锁对锁的获取是乱序的,即有一个抢占锁的过程Thread-0获得锁 Thread-2获得锁 Thread-1获得锁 Thread-3获得锁 Thread-4获得锁
分布式锁
如果在不同的jvm中保证数据同步,需要使用分布式锁技术
实现方式:有数据库实现、缓存实现、Zookeeper分布式锁