悲观锁和乐观锁
我觉得悲观锁和乐观锁更多的是指一种思想,实现锁的不同方式。数据库、git都有不同的对应悲观锁和乐观锁思想的应用.
-
悲观锁一般是互斥锁,
但是1.阻塞和唤醒会带来性能损耗,要去切换用户态切换状态,查看要唤醒的线程等等;2.而且可能导致永久阻塞,比如发生了无限循环,死锁,那么阻塞等待获取锁的线程就可能永久等待了。3.还可能导致线程优先级混乱。适合并发多竞争很激烈的情况,代码复杂 或者循环量大java中最常见的悲观锁就是synchronized和lock类,但是注意,synchronized引入了偏向锁、轻量级锁等优化措施,readWriteLock在读的时候是共享锁,写的时候是独占锁。总体上来说还是得先拿到锁,才能执行
-
乐观锁是非互斥锁,
认为自己在操作的时候不会有其他线程干扰,所以不会锁住操作对象。更新的时候去再去比较对象数据是否被修改过,如果没修改过最好,如果发生了修改,就要选择放弃,抛弃,重试等策略。适合并发写入少,读取多的情况,提高读取的性能乐观锁基本都是基于CAS算法实现的,注意ABA问题,可以加版本号解决。有原子类,并发容器
共享锁和独占锁
独占锁 ,又称排它锁、独享锁
共享锁,又称读锁,获取到锁后可以查看但不能修改删除。为什么要这样设计呢 ?是因为很多线程读并不会造成线程安全问题,所以如果允许多个线程来读,就可以提高性能。
ReentrantReadWriteLock读写锁,其中读锁是共享锁,写锁时独占锁。
要么是多读,要么是一写。更具体点说就是,多个线程读没问题;以如果已经有线程在读,那么其他线程申请写锁则必须要等待释放读锁;如果一个线程已经在写,那么其他线程申请读或写都必须要等待释放写锁。
ReentrantReadWriteLock公平锁:不允许任何插队,不管写锁还是读锁,只要队列里已经有线程了就应该阻塞等待
ReentrantReadWriteLock非公平:
- 写锁可以随时插队,即不需要阻塞
- 读锁仅在等待队列中头结点不是写线程时可以插队:
举个例子:假设线程2和线程4正在同时读,线程3想要写入,所以进入了等待队列且在头结点。然后线程5过来想要读,那么此时允不允许线程5去同时读呢?
如果允许线程5插队读,则可能不停的有线程来插队读,写的线程就可能饥饿,所以ReentrantReadWriteLock不允许插队读,如果有写线程在排队则必须进入队列排队
ReentrantReadWriteLock支持写锁降级为读锁,但不支持读锁升级为写锁(避免死锁)。
为什么需要锁降级?比如一个任务刚开始需要写锁拿到某个日志文件,但是后续都只需要读就行,显然如果还是一直持有写锁性能就会差很多,如果可以降级为读锁,就能允许其他线程一起读,性能就会好很多。
为什么不支持锁升级为写锁呢?可能造成死锁,比如两个线程同时准备升级为写锁
公平锁和非公平锁
公平锁指的是完全按照线程请求的顺序来分配锁;非公平是不完全按照线程请求顺序,注意不是完全随机的,在一定情况下可以插队。
为什么要设计非公平锁呢?
非公平可以避免去唤醒线程时的空档期,提高使用性能,提高吞吐量。但有可能造成线程饥饿,即某些线程一直都拿不到锁
synchronize是非公平锁,ReentrantLock默认是非公平锁,但也可以构造公平锁。其实对应源码的实现很简单,就是在获取锁时是否放入队列
//非公平锁 的尝试获取final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}//公平锁的 尝试获取protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {//唯一的区别就在于这里,如果锁没被任何一个线程拿到,不像上面直接去cas争抢,//而是会hasQueuedPredecessors去判断是否有其他线程等待的时间更长if (!hasQueuedPredecessors() &&compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0)throw new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}
注意tryLock是个特例sync.nonfairTryAcquire(1);
,即使设置的公平锁,也会非公平的去争抢锁。
可重入锁和不可重入锁
什么是可重入锁呢?简单来说,就是一个线程可以多次拿到一个锁,Reentrant和synchronize都是可重入锁
有什么好处?
避免死锁,如果不是可重入锁,你拿到锁了,然后你想进入锁的另一个方法,你拿不到了!可能造成死锁
避免了重复的加锁和解锁
自旋锁和阻塞锁
也就是准备获取锁的线程无法获取到锁时,就先自旋,不用阻塞,避免了线程切换带来的开销。但是可能带来CPU的浪费,因此有自适应自旋锁。
synchronize引入的轻量级锁就是自旋锁的最好应用。适合少量线程竞争,且每个线程持有锁的时间不长的情况
可中断锁和不可中断锁
如果某个线程获取到了锁正在执行,线程B正在等待获取锁,可是由于等待时间过长,我们可以中断线程B,这就是可中断锁。
synchronize就是不可中断锁,lock是可中断锁,try( time)和lockInterruptibly都能响应中断。
锁优化
- JVM提供了锁粗化和锁消除来优化锁
- 我们在并发编程时要注意,尽量缩小同步代码块的范围,尽量不要锁住方法,减少加锁的次数。锁中不要再包含锁,容易造成死锁。选择合适的锁和工具类