一.ReentrantLock特点:
1.1 是独占锁并且是可重入的:
独占互斥的,需要手动释放锁;
可重入的,但是要释放相同次数的锁;
1.2 默认是非公平锁,也可以实现公平锁:
非公平锁:当前线程直接尝试获取锁,不管自己是不是身处队尾;
公平锁:按照队列顺序来,前面还有就等待;
创建的时候,加上参数true是公平锁,公平锁能够避免线程饥饿
1.3 可以获取锁时限等待:(用来辅助解决死锁):
在规定时间内获取不到锁,tryLock(),返回true false;
1.4 可以中断线程lockInterruptibly():
1.5 condition:
condition 的使用代替了 wait 和 notify, 并且是在lock()方法调用之后使用,cndition signal()唤醒其他线程;
实现队列 :
private Lock lock = new ReentrantLock();
private Condition incon = lock.newCondition(); //进队列
private Condition outcon = lock.newCondition(); //出队列
private LinkedList<Integer> list = new LinkedList<Integer>();
private int size=100;
public void inLock(int i){
try {
lock.lock();
while (list.size() == size)
incon.await(); //等待
list.add(i);
System.out.println("入队 "+i);
outcon.signal(); //唤醒
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void outLock(){
try {
lock.lock();
while (list.size() == 0)
outcon.await(); //等待
int e= list.removeFirst();
System.out.println("出队列 "+e);
outcon.signal(); //唤醒
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
1.6 公平锁 非公平锁的介绍:
流程 是尝试获取锁,获取失败则封装成节点放入对象中;
而获取锁的具体过程是
公平锁;
先看下state =0,如果=,则说明是当前锁没被持有,并且判断是否有其他线程比当前线程在同步队列中等待的时间更长,后通过CAS操作获取同步状态,就获取锁;
否则检测下当前线程是否是等于锁线程,因为是可重入锁,所以会state++;等释放的时候在减去,一直减到0;
否则会添加到队列中;(在添加到双向队列中)
非公平锁:
先看下state =0,如果=,当前线程会直接先尝试修改state的状态,如果修改成功直接获得锁,如果修改不成功就还是按照公平锁来;
compareAndSetState 方法抢占式加锁,加锁成功则将自己设为持锁线程,并返回;
若加锁失败,则调用 acquire 方法,将线程置于同步队列尾部进行等待;
线程在同步队列中成功获取锁,则将自己设为持锁线程后返回;
若同步状态不为0,且当前线程为持锁线程,则执行重入逻辑;
1.7 AQS:reentrantLock基于aQs原理
AQS是基于链表的双向队列,头结点是当前获取锁的线程,对于所有没有获取锁的都排在队列中标,当头结点运行完成后,会从队列中删除;
2.Synchronize
synchronize 底层 任何一个对象都有一个monitor相关联,当montior被线程持有之后,对象就处于锁定状态,(存储在java对象头里的是);
原理: jvm 通过进入和退出对象监视器实现(montior)来实现方法 、同步块的同步。
具体是在调用同步块之前插入一个montiorenter指令,退出的时候插入一个montiorexit指令。保证同一时刻只能有一个线程获取,其他线程被阻塞等待。
同步代码块:
当线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁;monitorenter指令插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置。
同步方法:
当执行方法时,会检测Acc_SYNCHORNIZE 是否被设置过,如果设置了,就先进行获取montior,执行完之后释放montior,在执行期间其他线程不能够在获取。
对象在内存中的布局分为三块区域:对象头、对象实际数据和填充数据;
对象头:
对象头里面放的是: 类型指针(用来确定实例是属于哪个类) 和 标记字段;
标记字段放的是该实例运行时的一些信息,如哈希码,gc年龄代, monitor引用指针等;
monitor:是线程私有的数据结构,线程有一个monitor record表,每一个被锁住的对象都与一个montior相关联,
同时,montior中有一个ower字段,存放的是与持有锁的线程的唯一标志。
锁优化:
偏向锁:
在大多数的情况,往往是不存在竞争的,并且很可能是同一线程连续多次获取这个锁,为了降低获得锁的代价,出现了偏向锁。
当该锁第一次被线程访问的时候, 线程访问该锁并获取锁,在对象头和栈帧中的锁记录里存储该线程的id。当该现场再次访问的时候,只需要简单的测试西夏,看对象头markword中存在的是否是该线程id,如果是则说明该线程已经获取到了该锁,不必在去通过cas比较来获得锁。 如果测试失败,则先去对象头中查看是否存在偏向锁的标志符=1,如果存在说明是偏向锁,则进行cas竞争尝试让markword中指向当前线程的id,如果不是则进行cas竞争获取锁。偏向锁转向轻量级锁。
核心原理: 如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。
适用于: 没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了。
轻量级锁:
使用的是: 对绝大部分的锁,在整个同步周期内都不存在竞争”有访问锁,适用于不存在竞争的情况
当关闭偏向锁 或者 当多个竞争存在,导致偏向锁升级为轻量级锁。
具体
1.获取锁 。
第一步:判断是否处于无锁状态。如果是,则在当前线程的栈帧中创建一个lock record锁记录,将对象头中的markword存储到线程栈帧的锁记录中。 如果不是转向第三步。
第二步: 通过cas尝试将对象头中的markword指向线程栈帧中的锁记录表,如果成功表示当前线程获取锁,将锁标志位变成00。如果失败,尝试自旋锁获取,失败,转向第三步。
第三步:判断当前rmarkword指向的是否是当前线程的锁记录表,如果是,则表示当前线程已经获取锁。如果不是,存在竞争则转向重量级锁。
2. 释放锁
取出在lock record中的保存的markword数据,尝试cas替换当前对象的markword,成功表示释放锁了。失败,则表示存在锁竞争,膨胀成重量级锁。
重量级锁:
理念: 对绝大部分的锁,在整个同步周期内都不存在竞争”
适用场景: 线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。
锁粗化:
将多次连续的加锁/解锁操作,换成一个更大范围的锁操作。
锁消除:
对于些不必要的锁操作,撤销,节省无意义的锁操作消耗时间。
自旋锁:
当获取锁失败的时候,不会立刻挂起,而是持续一段时间进行尝试获取。看当前锁是不是可以在短时间内就释放了。
自适应自旋锁:
是为了解决自旋锁中自旋的次数问题,让jvm更加的智能。自旋的次数不固定,线程自旋成功了,下次这个锁自旋的次数增多;自旋失败了,成功的次数很少,那么这个锁对应的下次自旋的次数减少。
乐观锁悲观锁:
乐观锁: 乐观的认为在拿数据的时候,当前的数据没有其他线程进行修改,不加锁,当前线程在更新数据的时候,需要判断下当前数据是否被修改。
方式:
通过版本控制号 或者cas;
版本控制号:给数据设置个version版本控制号, 初始变量=0,线程在准备修改数据的时候,同时也会读取这个数字, 提交更新时, 会对比下现在数据的version和开始时读取的version, 二者一致才会进行提交version+1,否则重试,知道更新成功。
CAS:
在准备修改数据的时候,会通过cas进行比较,cas有三个值,一个是预期的现存进行比较的值A,一个是将要写入的值B,一个是需要读写的内存上的值V,如果内存地址上的值与预期的值相等,则修改值。
使用场景 :读操作比较多,而很少发生写操作竞争的时候。
缺点: cas 的ABA问题,如果其他线程占用时间过长,当前新线程自旋一直等待,占用cpu,循环时间长开销大。只能保证一个变量的原子性操作;a=a+b;无法保证
优点:使用与冲突比较少的情况,多读操作,这样的话可以减少锁的开销,提高吞吐量。
悲观锁: 悲观的认为每次取数据的时候,会有其他线程修改数据,所以先加上锁,保证自己在修改读取数据的时候,其他线程无法进行操作。同一时刻只能有一个线程进行操作。
向retrantLock synchroncized 都是悲观锁的实现。