是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
自旋锁有两种表现形式:
1、AtomicIneger 中的使用场景,自旋是为了修改一个变量,直接用CAS去进行操作
2、如下形式,通过自旋实现了一把锁
public class Demo2_SpinLock {
private AtomicReference owner = new AtomicReference();
public void lock() {
Thread current = Thread.currentThread();
// 利用CAS
while (!owner.compareAndSet(null, current)) {
// DO nothing
}
}
public void unlock() {
Thread current = Thread.currentThread();
owner.compareAndSet(current, null);
}
public static void main(String args[]){
Demo2_SpinLock lock = new Demo2_SpinLock();
lock.lock();
//do something
lock.unlock();
}
}
假定没有冲突,在修改数据时如果发现数据和之前获取的不一致,则读最新数据,修改后重试修改。
假定会发生并发冲突,同步所有对数据的相关操作,从读数据就开始上锁。synchronized就是典型的悲观锁。
给资源加上写锁,线程可以修改资源,其他线程不能再加锁; (只能单个线程写)
给资源加上读锁后只能读不能改,其他线程也只能加读锁,不能加写锁; (可多个线程读)
应用场景:给数据库连接加说,限制数据库连接数量,同一时间只能有指定数量的连接,这个锁就是共享锁。
概念: ReadWriteLock维护了一对关联锁,一个只用于读操作,一个只用于写操作;其中读锁可以由多个线程同时持有,而写锁是拍他的。另外同一时间,两把锁不能被不同线程持有。
使用场景: 适合读取操作多于写入操作的场景,改进互斥锁的性能。比如:集合的并发线程安全性改造、缓存组件。
锁降级: 指的是写锁降级成为读锁。持有写锁的同时,再次获取读锁,随后释放写锁的过程。写锁是线程独占,读锁是共享,所以写->读是降级。(读->写,是不能实现的)
线程拿到一把锁之后,该线程可以多次自由进入同一把锁所同步的其他代码就称可重入锁;获取锁之后,即使是该线程也不能进入同一把锁的其他代码就称为不可重入锁。
可重入锁和补可重入锁都是针对同一线程获取到锁的情况下讨论的,即使是可重入锁也不能让多个线程同时获取到锁,如果多个线程可获取到那么就是共享锁了。
使用可重入锁时,锁了多少次锁,就要释放多少次才能把锁真正的释放掉。
package com.dongnao.concurrent.period5;
import com.dongnao.concurrent.period4.KodyLock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo1_ReentrantTest {
private static int i = 0;
//可重入锁
private final static ReentrantLock lc = new ReentrantLock();
// private final static KodyLock lc = new KodyLock();
public static void add() throws InterruptedException {
lc.lock();
i++;
System.out.println("here i am...");
Thread.sleep(1000L);
add();
lc.unlock();
}
public static void main(String args[]) throws InterruptedException {
add();
}
}
上面代码中ReentrantLock就是一个可重入锁,程序会一直输出here i am...
,如果ReentrantLock是不可重入锁,那么只会输出一次here i am...
,第二次调用add方法的时候就会阻塞住。1
输出结果:
here i am...
here i am...
here i am...
here i am...
here i am...
here i am...
here i am...
。。。
补充:synchronized是可重入锁。
可重入锁特性的实现需要解决以下两个问题。
1)线程再次获取锁。锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。
2)锁的最终释放。
nonfairTryAcquire方法增加了再次获取同步状态的处理逻辑:通过判断当前线程是否为获取锁的线程来决定获取操作是否成功,如果是获取锁的线程再次请求,则将同步状态值进行增加并返回true,表示获取同步状态成功。同步状态表示锁被一个线程重复获取的次数。
如果该锁被获取了n次,那么前(n-1)次tryRelease(int releases)方法必须返回false,而只有同
步状态完全释放了,才能返回true。可以看到,该方法将同步状态是否为0作为最终释放的条件,当同步状态为0时,将占有线程设置为null,并返回true,表示释放成功。
争抢锁的顺序,如果是按先来后到,则为公平,否则就是非公平锁。
每个对象在内存中存储的除了基本的字段属性值(如果字段是复杂对象的话,那么存储的就是对象引用),还有一个对象头。
对象头里面的Mark Word记录这对象锁的状态,有如下四种状态:
在未锁定的状态下,可以通过CAS来抢锁,抢到的是轻量级锁
轻量级锁中的自旋有一定的次数限制,超过了次数限制,轻量级锁升级为重量级锁。
在JDK6 以后,默认已经开启了偏向锁这个优化,通过JVM 参数 -XX:-UseBiasedLocking 来禁用偏向锁,若偏向锁开启,只有一个线程抢锁,可获取到偏向锁。
1、处于Runnable状态而还没运行的线程1,会去抢owner,抢到之后开启偏向锁,线程运行。
2、此时如果有线程2来抢锁,那么锁会升级成轻量级锁。
3、如果线程2抢不到锁(线程1还没有释放锁),那么线程2会自旋继续抢锁,自旋有一定的限制,自旋超过一定的次数,锁再次升级为重量级锁,抢不到锁的线程会进入entyList集合中等待执行(在重量级配图中所示)。
4、如果线程2,抢到了锁(在线程2开始之前,线程1已经释放了锁),那么锁还是偏向锁的状态。
5、如果运行中的线程调用了wait等方法,那么线程就会进入WaitSet,处于Waiting状态。
6、如果有线程调用了notify方法,那么WaitSet(WaitSet中有线程的情况下)中的一个线程就会进入到EntryList中,参与或等待抢锁。如果调用的是notifyAll,那么WaitSet(WaitSet中有线程的情况下)中的所有线程都会进入到EntryList中,参与或等待抢锁。
注意:
1、wait、notify、notifyAll只能在synchronized关键字中使用,且调用wait、notify、notifyAll的对象与锁对象相同,否则会抛出IllegalMonitorStateException异常。
2、wait() 方法调用后,会破坏原子性。
补充:
1、偏向标记第一次有用,出现过争用后就没用了。 -XX:-UseBiasedLocking 禁用使用偏置锁定;
2、偏向锁,本质就是无锁,如果没有发生过任何多线程争抢锁的情况,JVM认为就是单线程,无需做同步(jvm为了少干活:同步在JVM底层是有很多操作来实现的,如果是没有争用,就不需要去做同步操作)
修改mark word如果失败,会自旋CAS一定次数,该次数可以通过参数配置:超过次数,仍未抢到锁,则锁升级为重量级锁,进入阻塞。
monitor也叫做管程,计算机操作系统原理中有提及类似概念。一个对象会有一个对应的monitor。