一、乐观锁与悲观锁
1、悲观锁
1、认为在使用数据时一定有别的线程来修改数据
,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改(一上来就加锁)。
2、synchronized
关键字和Lock的实现类
都是悲观锁。
3、适合写操作多的场景,先加锁可以保证写操作时数据正确。
public synchronized void m1() {
}
ReentrantLock lock = new ReentrantLock();
public void m2() {
lock.lock();
try {
}finally {
lock.unlock();
}
}
2、乐观锁
1、认为在使用数据时不会有别的线程来修改数据
,所以不会加锁,只是在更新数据的时候去判断有没有别的线程修改过这个数据:
-
如果数据没有被其他线程修改,当前线程将自己的修改的数据成功写入。
-
如果数据已经被其他线程修改,则根据不同的实现方法实现不同的操作。
2、乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的
。还有一种是通过版本号机制,修改数据时判断拿到的版本号和数据库中的版本号是否一致,不一致则不操作;如果版本号一致,则修改数据,并将版本号加1
。
3、适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升
。
private AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.incrementAndGet();
二、公平锁和非公平锁
1、概述
1、公平锁:是指多个线程按照申请锁的顺序来获取锁,先来后到,先来先服务就是公平的,也就是队列。
2、非公平锁:是指在多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取到锁,在高并发的情况下,有可能造成优先级反转或者饥饿现象(也就是某个线程一直得不到锁即为饥饿)。对于synchronized而言,也是一种非公平锁
2、创建方式
1、并发包ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或者非公平锁,默认是非公平锁,因为非公平锁的优点在于吞吐量比公平锁大。
public class LockTest {
public static void main(String[] args) {
Lock lock = new ReentrantLock(true);
}
}
3、两者的区别
1、公平锁就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列中的第一个,就占用锁,否者就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。
2、非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。对于synchronized而言,也是一种非公平锁
4、默认非公平锁的解释
1、恢复挂起的线程到真正获取到锁是有时间差的,从CPU角度上看,这个时间差存在很明显的,所以非公平锁能更充分的利用CPU的时间片,尽量减少CPU空闲时间。
2、使用多线程很重要的考量点是线程切换的开销,当使用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销
。
3、使用说明:如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了;否则那就用公平锁,大家公平使用。
三、可重入锁递归锁ReentrantLock
1、概述
1、可重入锁(也叫递归锁)指的是同一线程
外层方法获得锁之后,内层递归方法仍然能获取该锁的代码,在同一线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。也就是说,线程可以进入任何一个它已经拥有的锁所同步的代码块(前提是锁对象必须是同一个对象)
。
2、Java中ReentrantLock、synchronized就是典型的可重入锁
。
3、作用:避免死锁
2、验证synchronized可重入
1、在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的
。
public class SyncLockTest {
public static void main(String[] args) {
SyncLockTest test = new SyncLockTest();
new Thread(() ->{
test.method1();
}, "t1").start();
new Thread(() ->{
test.method1();
}, "t2").start();
}
public synchronized void method1() {
System.out.println(Thread.currentThread().getName() + "调用method1方法");
method2();
}
public synchronized void method2() {
System.out.println(Thread.currentThread().getName() + "调用method2方法");
}
}
3、synchronized实现重入锁原理
1、每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针
。
2、当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。
3、在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁
。
4、当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为0代表锁已被释放。
4、验证ReentrantLock可重入
注意:加锁几次就要解锁几次,否则导致加锁解锁次数不一致,第二个线程无法获取到锁,导致一直等待的情况
。
public class ReenLockTest {
Lock lock = new ReentrantLock();
public static void main(String[] args) {
ReenLockTest test = new ReenLockTest();
new Thread(() ->{
test.method1();
}, "t1").start();
new Thread(() ->{
test.method1();
}, "t2").start();
}
public void method1() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "调用method1方法");
method2();
} finally {
lock.unlock();
}
}
public void method2() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "调用method2方法");
} finally {
lock.unlock();
}
}
}
5、死锁及排查
死锁说明及排查方法:
四、自旋锁SpinLock
1、概述
1、自旋锁(spinlock)是指尝试获取锁的时候线程不会立即阻塞,而是采用循环的方式去尝试获取锁
。
-
好处:减少线程上下文切换的消耗。循环比较获取直到成功为止,没有类似于wait的阻塞。
-
缺点:当不断自旋的线程越来越多的时候,会因为执行while循环不断的消耗CPU资源。
2、CAS底层使用的就是自旋
,自旋就是多次尝试,多次方法,不会阻塞的状态的就是自旋
2、实现自旋锁
public class SpinLockTest {
AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void lock() {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "进入lock");
while (!atomicReference.compareAndSet(null, thread)) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(thread.getName() + "正在自旋");
}
}
public void unLock() {
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread, null);
System.out.println(thread.getName() + "退出,设置为null");
}
public static void main(String[] args) {
SpinLockTest test = new SpinLockTest();
new Thread(() -> {
test.lock();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.unLock();
}, "线程A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
test.lock();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.unLock();
}, "线程B").start();
}
}
五、独占锁(写)/共享锁(读)/互斥锁
1、概述
1、独占锁:指该锁一次只能被一个线程所持有,对ReentrantLock和Synchronized而言都是独占锁。
2、共享锁:指该锁可以被多个线程所持有。
3、对于ReentrantReadWriteLock其读锁是共享锁,写锁是独占锁
。写入的时候只能一个线程写,读的时候可以多个线程同时读,但是不能同时存在读写线程。
2、为什么会有读锁和写锁
1、使用ReentrantLock创建锁的时候,是独占锁,也就是说一次只能一个线程访问,但是有一个读写分离场景,读的时候想同时进行,因此原来独占锁的并发性就没这么好了,因为读锁并不会造成数据不一致的问题,因此可以多个人共享读。
2、多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行,但是如果一个线程想去写共享资源,就不应该再有其它线程可以对该资源进行读或写。
-
读-读:能共存
-
读-写:不能共存
-
写-写:不能共存
3、读操作没有完成之前,写锁是无法获取的
。
3、读写锁问题分析
1、实现一个读写缓存的操作,假设开始没有加锁的时候,会出现什么情况
public class ReadWriteLockTest {
public static void main(String[] args) {
CacheMap cache = new CacheMap();
for (int i = 1; i <= 5; i++) {
final int threadName = i;
new Thread(() -> {
cache.put(threadName + "", UUID.randomUUID().toString().substring(0, 6));
}, "写线程 " + threadName).start();
}
try {
TimeUnit.MILLISECONDS.sleep(700);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 1; i <= 5; i++) {
final int threadName = i;
new Thread(() -> {
cache.get(threadName + "");
}, "读线程 " + threadName).start();
}
}
}
class CacheMap {
private volatile Map<String, Object> map = new HashMap<>();
public void put(String k, Object v) {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + " 正在写入 " + k);
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(k, v);
System.out.println(thread.getName() + " 写入成功 ");
}
public void get(String k) {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + " 正在读取 " + k);
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object v = map.get(k);
System.out.println(thread.getName() + " 读取成功 " + v);
}
}
运行结果:可以看到在写入的时候,写操作被其他线程打断了,就造成了还没有写入完成,其他线程又开始写,造成了数据不一致。
写线程 5 正在写入 5
写线程 1 正在写入 1
写线程 3 正在写入 3
写线程 4 正在写入 4
写线程 2 正在写入 2
读线程 1 正在读取 1
读线程 2 正在读取 2
读线程 3 正在读取 3
读线程 4 正在读取 4
读线程 5 正在读取 5
写线程 5 写入成功
写线程 1 写入成功
写线程 2 写入成功
写线程 4 写入成功
写线程 3 写入成功
读线程 4 读取成功 null
读线程 1 读取成功 80e053
读线程 3 读取成功 null
读线程 2 读取成功 null
读线程 5 读取成功 null
4、读写锁问题解决
1、上面的代码是没有加锁的,这样就会造成线程在进行写入操作的时候,被其它线程频繁打断,从而不具备原子性,这个时候,就需要用到读写锁来解决了。
public class ReadWriteLockTest {
public static void main(String[] args) {
CacheMap cache = new CacheMap();
for (int i = 1; i <= 5; i++) {
final int threadName = i;
new Thread(() -> {
cache.put(threadName + "", UUID.randomUUID().toString().substring(0, 6));
}, "写线程 " + threadName).start();
}
try {
TimeUnit.MILLISECONDS.sleep(900);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 1; i <= 5; i++) {
final int threadName = i;
new Thread(() -> {
cache.get(threadName + "");
}, "读线程 " + threadName).start();
}
}
}
class CacheMap {
private volatile Map<String, Object> map = new HashMap<>();
private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public void put(String k, Object v) {
readWriteLock.writeLock().lock();
try {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + " 正在写入 " + k);
TimeUnit.MILLISECONDS.sleep(300);
map.put(k, v);
System.out.println(thread.getName() + " 写入成功 ");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readWriteLock.writeLock().unlock();
}
}
public void get(String k) {
readWriteLock.readLock().lock();
try {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + " 正在读取 " + k);
TimeUnit.MILLISECONDS.sleep(300);
Object v = map.get(k);
System.out.println(thread.getName() + " 读取成功 " + v);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
}
}
}
运行结果:从运行结果可以看出,写入操作是一个一个线程进行执行的,并且中间不会被打断,而读操作的时候,是同时5个线程进入,然后并发读取。
写线程 3 正在写入 3
写线程 3 写入成功
写线程 5 正在写入 5
写线程 5 写入成功
写线程 1 正在写入 1
写线程 1 写入成功
写线程 2 正在写入 2
写线程 2 写入成功
写线程 4 正在写入 4
写线程 4 写入成功
读线程 3 正在读取 3
读线程 1 正在读取 1
读线程 2 正在读取 2
读线程 5 正在读取 5
读线程 4 正在读取 4
读线程 2 读取成功 389838
读线程 3 读取成功 94f449
读线程 1 读取成功 b48dec
读线程 5 读取成功 f822f0
读线程 4 读取成功 11c5a1
5、锁降级
1、ReentrantWriteReadLock有锁降级的特性,将写锁降级为读锁,锁的严苛程度变强叫做升级,反之叫做降级
。
2、锁降级:遵循先获取写锁,再获取读锁,再释放写锁的次序,写锁能够降级成为读锁
。
3、写锁降级:
-
如果同一个线程持有了写锁,在没有释放写锁的前提下,它还可以继续获得读锁(可重入特性
),这个就是写锁的降级,降级成为了读锁。
-
按照先获取写锁,再获取读锁,再释放写锁的次序。如果释放了写锁,那么就完全转为了读锁。
-
注意:读锁是无法升级到写锁的;读锁没有释放,写锁无法获得
public class LockDownGradeTest1 {
public static void main(String[] args) {
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
writeLock.lock();
System.out.println("写入...");
readLock.lock();
System.out.println("读取...");
writeLock.unlock();
readLock.unlock();
}
}
public class LockDownGradeTest1 {
public static void main(String[] args) {
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
readLock.lock();
System.out.println("读取...");
writeLock.lock();
System.out.println("写入...");
writeLock.unlock();
readLock.unlock();
}
}
6、总结
1、写锁和读锁是互斥的(是指线程间的互斥,但当前线程仍然可以获取写锁又获取读锁,但是获取到了读锁就不能继续获取写锁),这是因为读写锁要保持写操作的可见性
。如果允许读锁在被获取的情况下对写锁的获取,那么正在运行的其他读线程无法感知到当前写线程的操作。
2、如果有线程正在读,那么写线程需要等待读线程释放锁之后,才能获取写锁。
六、邮戳锁StampedLock
1、概述
1、它是Java8在java.util.concurrent.locks新增的一个API,也是对JDK1.5中的读写锁ReentrantReadWriteLock的优化。
2、StampedLock它是由锁饥饿问题
引出来的。
3、一个StampedLock状态是由版本和模式两个部分组成,锁获取方法返回一个数字作为票据stamp,它用相应的锁状态表示并控制访问,数字0表示没有写锁被授权访问。在读锁上分为悲观锁和乐观锁。
2、锁饥饿问题
1、ReentrantReadWriteLock实现了读写分离,但是如果读取执行情况很多,写入很少的情况下,使用ReentrantReadWriteLock可能会使写入线程遭遇饥饿(Starvation)问题,也就是写入线程迟迟无法竞争到锁定而一直处于等待状态
。
2、使用公平策略
可以一定程度缓解锁饥饿问题new ReentrantReadWriteLock(true)
,但是公平策略是以牺牲系统吞吐量为代价的。
3、ReentrantReadWriteLock的读锁被占用的时候,其他线程尝试获取写锁的时候会被阻塞。但是,StampedLock采取乐观获取锁后,其他线程尝试获取写锁时不会被阻塞,这其实是对读锁的优化,所以,在获取乐观读锁后,还需要对结果进行校验。
3、StampedLock的特点
1、所有获取锁的方法,都返回一个邮戳(Stamp),Stamp为0表示获取失败,其余都表示成功
;
2、所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致
;
3、StampedLock是不可重入的,如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁
。
4、StampedLock三种访问模式
1、读模式(Reading):功能和ReentrantReadWriteLock的读锁类似
2、写模式(Writing):功能和ReentrantReadWriteLock的写锁类似
3、乐观读模式(Optimistic reading):无锁机制,类似于数据库中的乐观锁,支持读写并发,很乐观认为读取时没人修改,假如被修改再实现升级为悲观读模式。
5、StampedLock的读写模式
读锁没有释放,写锁无法获得,程序阻塞
public class StampedLockTest {
static StampedLock stampedLock = new StampedLock();
static int number = 1;
public static void main(String[] args) throws InterruptedException {
StampedLockTest lockTest = new StampedLockTest();
new Thread(() -> {
lockTest.read();
}, "读线程").start();
Thread.sleep(1000);
new Thread(() -> {
lockTest.write();
}, "写线程").start();
}
public static void write() {
System.out.println(Thread.currentThread().getName() + " 准备写入...");
long stamp = stampedLock.writeLock();
try {
number = number + 9;
} finally {
stampedLock.unlockWrite(stamp);
}
System.out.println(Thread.currentThread().getName() + " 写入完成,值为:" + number);
}
public static void read() {
System.out.println(Thread.currentThread().getName() + " 正在读取中...");
long stamp = stampedLock.readLock();
try {
for (int i = 0; i < 4; i++) {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " 正在读取中...");
}
int result = number;
System.out.println(Thread.currentThread().getName() + " 读取的值为:" + result);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
stampedLock.unlockRead(stamp);
}
}
}
6、StampedLock的乐观读模式
无锁机制,类似于数据库中的乐观锁,支持读写并发,很乐观认为读取时没人修改,假如被修改再实现升级为悲观读模式
public class StampedLockTest1 {
static StampedLock stampedLock = new StampedLock();
static int number = 1;
public static void main(String[] args) throws InterruptedException {
StampedLockTest1 lockTest = new StampedLockTest1();
new Thread(() -> {
lockTest.tryOptimisticRead();
}, "读线程").start();
Thread.sleep(2000);
new Thread(() -> {
lockTest.write();
}, "写线程").start();
}
public static void write() {
System.out.println(Thread.currentThread().getName() + " 准备写入...");
long stamp = stampedLock.writeLock();
try {
number = number + 9;
} finally {
stampedLock.unlockWrite(stamp);
}
System.out.println(Thread.currentThread().getName() + " 写入完成,值为:" + number);
}
public static void tryOptimisticRead() {
long stamp = stampedLock.tryOptimisticRead();
int result = number;
System.out.println("读取之前锁标记是否被修改(true:无修改,false:被修改)" + stampedLock.validate(stamp));
for (int i = 0; i < 4; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 正在读取中... 锁标记:" + stampedLock.validate(stamp));
}
if (!stampedLock.validate(stamp)) {
System.out.println("有写锁介入...锁标记被修改...");
try {
stamp = stampedLock.readLock();
result = number;
System.out.println("从乐观读 升级为 悲观读");
System.out.println("悲观读后的值为:" + result);
} finally {
stampedLock.unlockRead(stamp);
}
}
System.out.println(Thread.currentThread().getName() + " 读取成功,值为:" + result);
}
}