目录
一:synchronized
1.1:Synchronized是一个重量级锁
1.2:Synchronized底层实现原理
1.3:Synchronized锁存储位置
1.4:Synchronized锁的升级过程
二:Lock
2.1Lock
2.2ReentrantLock
2.2.1公平锁/非公平锁
2.2.2超时机制
2.2.3可重入锁
2.3读写锁ReentrantReadWriteLock
三:额外补充:
3.1lock深度解析:读写锁原理
前言:
多个线程共享同一个全局变量或静态变量,多个线程同时读数据不会发生数据安全性问题,但是有一个线程对数据进行写的时候,再有其他的线程来读写共享数据就有可能发生线程安全性问题。
例如:有有一个手工封装某一个模块的主键id接口中,多线程情况下,没有对全局变量加锁,可能会造成会有主键相同的情况。
常规操作方式也就是对公共代码、或者全局变量复制等公共资源进行加锁,例如:
1、synchronized锁(偏向锁,轻量级锁,重量级锁)
2、volatile锁,只能保证线程之间的可见性,但不能保证数据的原子性
3、jdk1.5并发包中提供的Atomic原子类
4、Lock锁
例如:某一个代码块进行加锁处理:
Object o=new Object();
synchronized(o.getClass()){
System.out.println(name+"开始执行");
try {
Thread.sleep(2000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(name+"执行完毕");
}
例如:整理api进行加锁
public synchronized String getValue(String name){
System.out.println(name+"开始执行");
try {
Thread.sleep(2000);
}catch (Exception e){
}
System.out.println(name+"执行完毕");
retuen null;
}
注:要注意的是,锁同一个对象的时候会造成访问资源冲突,不是同一个对象的时候,不会这样,例如:
SynchronizedTest t=new SynchronizedTest();
new Thread(new Runnable() {
@Override
public void run() {
t.getValue("线程1");
}
}).start();
SynchronizedTest t1=new SynchronizedTest();
new Thread(new Runnable() {
@Override
public void run() {
t1.getValue("线程2");
}
}).start();
SynchronizedTest t=new SynchronizedTest();
new Thread(new Runnable() {
@Override
public void run() {
t.getValue("线程1");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
t.getValue("线程2");
}
}).start();
这个是锁的api整体代码,测试情况是这的,如果锁关键字放在了static方法或者上面第一个锁示例,只锁部分代码时,这时候不管是不是一个对象了,都会进行锁资源访问;
Synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的 Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么 Synchronized 效率低的原因。因此,这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为 “重量级锁”。
同步方法通过ACC_SYNCHRONIZED 关键字隐式的对方法进行加锁。当线程要执行的方法被标注上ACC_SYNCHRONIZED时,需要先获得锁才能执行该方法。
同步代码块通过monitorenter和monitorexit执行来进行加锁。当线程执行到monitorenter的时候要先获得锁,才能执行后面的方法。当线程执行到monitorexit的时候则要释放锁。每个对象自身维护着一个被加锁次数的计数器,当计数器不为0时,只有获得锁的线程才能再次获得锁
Synchronized用的锁是存在java的对象头里面的。一个对象在new出来之后再内存中主要分为4个部分:
Mark Word:存储了对象的hashCode、GC信息、锁信息三部分。这部分占8字节。
Class Pointer:存储了指向类对象信息的指针。在64位JVM上有一个压缩指针选项-ClassPointer指针:-XX:+UseCompressedClassPointers 为4字节 不开启为8字节。默认是开启的。
实例数据(instance data):记录了对象里面的变量数据。引用类型:-XX:+UseCompressedOops 为4字节 不开启为8字节 Oops Ordinary Object Pointers
Padding:作为对齐使用,对象在64位服务版本中,规定对象内存必须要能被8字节整除,如果不能整除,那么久靠对齐来不。举个例子:new出了一个对象,内存只占用18字节,但是规定要能被8整除,所以padding=6
Mark Word存储结构如下:
Java SE 1.6 为了减少获得锁和释放锁带来的性能消耗,引入了 “偏向锁” 和 “轻量级锁”:锁一共有 4 种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。锁可以升级但不能降级。
1.偏向锁:大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中记录存储锁偏向的线程ID,以后该线程在进入同步块时先判断对象头的Mark Word里是否存储着指向当前线程的偏向锁,如果存在就直接获取锁。
2.轻量级锁:当其他线程尝试竞争偏向锁时,锁升级为轻量级锁。线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的MarkWord替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,标识其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
3.重量级锁:锁在原地循环等待的时候,是会消耗CPU资源的。所以自旋必须要有一定的条件控制,否则如果一个线程执行同步代码块的时间很长,那么等待锁的线程会不断的循环反而会消耗CPU资源。默认情况下锁自旋的次数是10 次,可以使用-XX:PreBlockSpin参数来设置自旋锁等待的次数。10次后如果还没获取锁,则升级为重量级锁
前言:
lock和synchronized相比,更能灵活的试算代码片段加锁
例如:手把手的进行锁获取和释放,先获得锁A,然后再获取锁B,当获取锁B后释放锁A同时获取锁C,等等代码逻辑枷锁
Lock 是一个接口类,里面实现类有两个,具体实现可区分
ReentrantLock、ReentrantReadWriteLock
demo示例如下:以防出现异常无法进行锁释放问题,加入tra捕获异常,finally里面进行释放锁
Lock lock = new ReentrantLock();
public void run(){
//获取锁
lock.lock();
try {
//公关资源处理,
}catch (Exception e){
}finally {
//释放锁
lock.unlock();
}
}
ReentrantLock
主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似。
CAS主要是进行比较进行替换,AQS主要是进行队列锁。,有时间补充这里;TODO待完善
默认创建时非功能锁,要想创建公平锁,创建对象时加入true
非公平锁:如果同时还有另一个线程进来尝试获取,那么有可能会让这个线程抢先获取;
公平锁:如果同时还有另一个线程进来尝试获取,当它发现自己不是在队首的话,就会排到队尾,由队首的线程获取到锁。
构造支持传入 fair
来指定是否是公平锁
tryLock(long timeout, TimeUnit unit)
提供了超时获取锁的功能。它的语义是在指定的时间内如果获取到锁就返回true
,获取不到则返回false
。
1.可重入锁。可重入锁是指同一个线程可以多次获取同一把锁。ReentrantLock和synchronized都是可重入锁。
2.可中断锁。可中断锁是指线程尝试获取锁的过程中,是否可以响应中断。synchronized是不可中断锁,而ReentrantLock则提供了中断功能。
3.公平锁与非公平锁。公平锁是指多个线程同时尝试获取同一把锁时,获取锁的顺序按照线程达到的顺序,而非公平锁则允许线程“插队”。synchronized是非公平锁,而ReentrantLock的默认实现是非公平锁,但是也可以设置为公平锁
demoe示例:
百度copy一个demo代码,理解一下使用即可
//声明读写锁
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//获取读锁
private Lock readLock = readWriteLock.readLock();
//获取写锁
private Lock writeLock = readWriteLock.writeLock();
private boolean isUpdate = true;
public String read(String key) {
readLock.lock();
System.out.println(Thread.currentThread().getName() + " 读操作正在执行。。。");
try {
Thread.sleep(2000);
return map.get(key);
} catch (Exception e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
return null;
}
/**
* 写操作
* @param key
* @param value
*/
public void write(String key, String value) {
writeLock.lock();
System.out.println(Thread.currentThread().getName() + " 写操作正在执行。。。");
try {
Thread.sleep(2000);
map.put(key, value);
} catch (Exception e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
}
/**
* 读写操作 锁的降级
*/
public void readWrite(){
readLock.lock(); //保证isUpdate是最新的数据
if (isUpdate) {
readLock.unlock();
writeLock.lock();
map.put("name", "admin");
readLock.lock(); //锁的降级,如果此处不加读锁,直接执行下一步释放写锁,多线程情况下有可能又被写锁抢占资源,通过此方法实现将写锁降级为读锁
writeLock.unlock();
}
String value = map.get("name");
System.out.println(value);
readLock.unlock();
}
public static void main(String[] args) throws Exception{
ReadWriteLockTest readWriteLockTest = new ReadWriteLockTest();
//写操作测试 可以知道写操作是互斥的
Runnable runnable1 = () -> {
for (int i = 0; i < 5; i++) {
readWriteLockTest.write("key" + i, "value"+i);
}
};
new Thread(runnable1).start();
new Thread(runnable1).start();
new Thread(runnable1).start();
//读操作测试 可以知道读操作是可以并发执行的
Runnable runnable2 = () -> {
for (int i = 0; i < 5; i++) {
System.out.println(readWriteLockTest.read("key" + i));
}
};
new Thread(runnable2).start();
new Thread(runnable2).start();
new Thread(runnable2).start();
//读写操作测试 锁的降级
Runnable runnable3 = () -> {
for (int i = 0; i < 5; i++) {
readWriteLockTest.readWrite();
}
};
new Thread(runnable3).start();
new Thread(runnable3).start();
new Thread(runnable3).start();
}
}
在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了。
针对这种场景,JAVA的并发包提供了读写锁 ReentrantReadWriteLock,它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁,描述如下
线程进入读锁的前提条件:
线程进入写锁的前提条件:
也就是说:
在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)。
在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)。
而读写锁有以下三个重要的特性:
(1)公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
(2)重进入:读锁和写锁都支持线程重进入。
(3)锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。
引用一篇博文,链接如下:
读写锁的读锁原理:如果有线程想申请读锁的话,首先会判断写锁是否被持有,如果写锁被持有且当前线程并不是持有写锁的线程,那么就会返回-1,获取锁失败,进入到等待队列等待。如果写锁未被线程所持有或者当前线程和持有写锁的线程是同一线程的话就会开始获取读锁。线程首先会判断读锁的数量是否超过65535个,如果没超过就CAS修改state变量的高16位的值,也就是将state的值+1,如果这个步骤失败的话它会循环这个操作,直到成功为止。CAS修改成功之后,代表读锁获取成功,会判断一下当前线程是否是第一次读线程,如果是,就设置第一次多线程和第一次读计数器(为了性能和可重入)。如果不是第一次获取读锁,判断一下是否是与第一次读线程相同,如果与第一次读线程是同一线程就将第一次读计数器+1。如果也不是第一次读线程,判断一下是否是最后一次读线程,如果是就将最后一次读计数器+1。如果都不是,就新建一个计数器,设置最后一次读线程为自己本身线程,然后刷新它的读计数器。
protected final int11 tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
// exclusiveCount(c) != 0 ---》 用 state & 65535 得到低 16 位的值。如果不是0,说明写锁别持有了。
// getExclusiveOwnerThread() != current----> 不是当前线程
// 如果写锁被霸占了,且持有线程不是当前线程,返回 false,加入队列。获取写锁失败。
// 反之,如果持有写锁的是当前线程,就可以继续获取读锁了。
if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
// 获取锁失败
return -1;
// 如果写锁没有被霸占,则将高16位移到低16位。
int r = sharedCount(c);// c >>> 16
// !readerShouldBlock() 和写锁的逻辑一样(根据公平与否策略和队列是否含有等待节点)
// 不能大于 65535,且 CAS 修改成功
if (!readerShouldBlock() && r < 65535 && compareAndSetState(c, c + 65536)) {
// 如果读锁是空闲的, 获取锁成功。
if (r == 0) {
// 将当前线程设置为第一个读锁线程
firstReader = current;
// 计数器为1
firstReaderHoldCount = 1;
}// 如果读锁不是空闲的,且第一个读线程是当前线程。获取锁成功。
else if (firstReader == current) {//
// 将计数器加一
firstReaderHoldCount++;
} else {// 如果不是第一个线程,获取锁成功。
// cachedHoldCounter 代表的是最后一个获取读锁的线程的计数器。
HoldCounter rh = cachedHoldCounter;
// 如果最后一个线程计数器是 null 或者不是当前线程,那么就新建一个 HoldCounter 对象
if (rh == null || rh.tid != getThreadId(current))
// 给当前线程新建一个 HoldCounter ------>详见下图get方法
cachedHoldCounter = rh = readHolds.get();
// 如果不是 null,且 count 是 0,就将上个线程的 HoldCounter 覆盖本地的。
else if (rh.count == 0)
readHolds.set(rh);
// 对 count 加一
rh.count++;
}
return 1;
}
// 死循环获取读锁。包含锁降级策略。
return fullTryAcquireShared(current);
}
引用两篇文章:深度解析:AQS原理
深度解析:读写锁原理