锁:
自旋锁: 为了判断某个条件是否成立,将代码写在While循环中。一直去判断这个条件。
为了不放弃CPU的执行事件,循环使用CAS技术对数据进行尝试操作。
悲观锁:假设会发生并发冲突,同步所有对数据的操作。
乐观锁:假设没有发生冲突,在修改数据时如果发现版本号不一样了,重新读数据然后尝试修改。
独享锁:将读数据和写数据分开锁。读是读的锁,写是写的锁。
独享锁(写):给资源加写独享锁。同一时间只有一个线程可以写,其他线程都可以读。(单写,只有一个线程可以获取到写锁)
独享锁(读):加上读锁后,只能读、不能写(多读,多个线程可以获取读锁)
可重入锁:只要拿到第一把钥匙,后面的们都可以打开
不可重入锁:每一个资源都需要获取新的钥匙
公平锁:获取锁的机制是公平的,先到的线程先获取锁。
非公平锁:后来的线程可能先获取到锁。
锁的概念和synchronized关键字:
基于对象监视器实现的、基本的线程通信机制。Java中的每一个对象都于一个监视器关联,线程可以锁定或者解锁监视器。
同步关键字不仅可以实现同步,根据JVM规范还可以保证可见性。(应为lock\unlock需要实现Happens-before原则)
锁的范围:类锁、对象锁、锁消除、锁粗化。(这些关键字都是在文档:Java SE 6 Performance White Pater 中找到的。)
对象锁:
public synchronized void test(){}
类锁:
public synchronized static void test(){} // 加了static关键字
锁粗化(运行时JIT编译优化):由于多段代码用到了同一个锁,编译时:将者写锁的粒度粗化,同步代码块放大,将这些代码段包含。
public void test(){
synchronized(this){A段}
synchronized(this){B段}
}
优化后:
public void test(){
synchronized(this){A段,B段}
}
锁消除:(运行是JIT优化):将锁取消。
例如:StringBuilder是线程安全的,每一个append方法中都有锁。JIT对含有append的热点代码去掉了锁。
(JIT觉得没有线程安全问题的时候才会优化,临界区内没有竞争条件。)
synchronized锁的实现原理:
JVM对锁的优化会经历:偏向锁、轻量级锁、重量级锁。
偏向锁:(优化后的锁机制,默认开启的)
1:锁对象中有个标志位,表示是否开启了偏向锁。还有个位置存放线程ID,默认值为0。
2:线程来了判断是否是对象是否是偏向锁,如果是,接着判断锁对象中的线程ID是否为0,如果为0:可用,然后将其改成自己的线程ID。
3:如果只有一个线程,其实就是无锁。应为锁ID的位置一直是一个线程ID。
其实就是判断锁对象中的线程ID是否是有线程ID,以此判断锁对象是否被使用。
如果多个线程争抢含有偏向锁的锁:
第一个线程修改锁对象的线程ID后
第二个线程来了以后判断对象锁的线程ID已经被写过了:出现了争抢锁的情况。
这个时候锁机制改成轻量级锁。使用CAS机制判断锁对象的锁标志位(自旋一定此处后进入阻塞,因为很消耗资源)。
轻量级锁:
1:判断存放对象的内存空间中的某一个位置的状态,这个位置标着了该对象是否加锁。我们简称为锁标志位
2:一个线程获取到该对象的锁时:CAS机制判断对象中的锁标志位,如果成功,修改对象锁标志位、线程栈中存储该对象的锁信息。
其实就是每个线程来了都要同步代码块中是否有别的线程正在使用。(判断锁对象的锁标志位)
重量级锁-(监视器锁)
CAS自旋n次后如果还没有获取到锁,锁会升级为重量级锁,进入阻塞。
monitor也叫做管程,一个对象对应一个monitor。存放了等待该线程的队列等。以此来实现对锁的等待、争抢、释放。
![]()
其他:
事务中调用费事的接口。事务开始时会和数据库建立连接,如果这个时候做一些费事的事情。会使数据库连接时间过长。
Lock接口的使用:
lock 获取锁,如果锁已经被别的线程占用,进入等待。线程在等待的过程中被中断了,报错。
lockInterruptibly 获取锁,如果已被人占用,进入等待。线程在等待的过程中被中断了,抛出异常、结束等待。
tryLock 尝试获取锁,立即返回获取到或者获取不到。
unlock 释放锁
ReentrantLock:可重入锁
独享锁;支持公平锁、非公平锁两种模式。
掉用了几此lock。就需要调用几次unlock。
class test0{
private final ReentrantLock lock = new ReentrantLock();
void m(){
lock.lock();
try{
x();
}finally {
lock.unlock();
}
}
void x(){
lock.lock();
// TODO 执行逻辑
lock.unlock();
}
}
ReadWriteLock:读写锁
维护了两个锁,读锁、写锁。
读锁可以被多个线程获取,写锁只能有一个线程获取。存在读锁将获取不到写锁。
HashTable就是使用了同步关键字,读和写都只有一个线程可以获取。如果是读多写少性能就比较差了,但是线程安全的。被concurrentHashMap取代。
class test1{
int i = 0;
private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public void main(String[] args) {
new Thread(() -> read()).start();
new Thread(() -> read()).start();
new Thread(() -> write()).start();
}
// 多个线程可以获取读锁
public void read(){
readWriteLock.readLock().lock();
// 读
readWriteLock.readLock().unlock();
}
// 只有一个线程可以获取写锁
public void write(){
readWriteLock.writeLock().lock();
// 写
readWriteLock.writeLock().unlock();
}
}
锁降级:
写锁降级为读锁。把持住当前的写锁的同时,获取读锁,然后释放写锁。(可以保证数据不会被多次修改,原因:有读锁的时候写锁不会被获取到)
场景:缓存中间件
读锁中先判断缓存,缓存中没有的化去DB中获取数据。
如果大量的线程去读锁,则会有会大量的线程查询DB。缓存雪崩。
解决:释放读锁,开启写锁。查数据库。
class test2{
// 创建一个map拥于缓存数据。
private Map<String , Object> map = new HashMap<>();
// 使用可重入的读写锁
private static ReadWriteLock rwl = new ReentrantReadWriteLock();
public Object get(String id){
Object value = null;
// 首先开启读锁,从缓存中去取
rwl.readLock().lock();
try{
if(map.get(id) == null){
// 必须释放读锁
rwl.readLock().unlock();
// 查询数据库,为了避免大量的线程去查询数据库。这里先使用写锁锁住(其他的读写线程都进不来了,赢得了读数据库的时间)。
rwl.writeLock().lock();
try{
// 会有多个读线程都被挡在写锁之外,为了保证只查询一次数据库。再次判断一次是否有别的线程已经查询过了。
if(map.get(id) == null){
// TODO 读数据库。然后将结果写入到Map中缓存。
}else {
value = map.get(id);
}
// 将写锁降级为读锁,这样就不会有别的线程能够修改这个值了。保证数据唯一性。(存在读锁的时候获取不到写锁)
rwl.readLock().lock();
}finally {
rwl.writeLock().unlock();
}
}else {
value = map.get(id);
}
}finally {
rwl.readLock().unlock();
}
return value;
}
}
Condition机制:
用于替代wait/notify
Object中的wait(),notify(),notifyAll(),配合synchronized使用。可以唤醒一个多所有线程。无法准确的唤醒具体的线程。
Condition需要配置Lock使用,提供多个等待集合、更加精准的控制唤醒(底层使用park/unpark机制实现)
![]()