锁: 自旋锁: 为了判断某个条件是否成立,将代码写在While循环中。一直去判断这个条件。 为了不放弃CPU的执行事件,循环使用CAS技术对数据进行尝试操作。 悲观锁:假设会发生并发冲突,同步所有对数据的操作。 乐观锁:假设没有发生冲突,在修改数据时如果发现版本号不一样了,重新读数据然后尝试修改。 独享锁:将读数据和写数据分开锁。读是读的锁,写是写的锁。 独享锁(写):给资源加写独享锁。同一时间只有一个线程可以写,其他线程都可以读。(单写,只有一个线程可以获取到写锁) 独享锁(读):加上读锁后,只能读、不能写(多读,多个线程可以获取读锁) 可重入锁:只要拿到第一把钥匙,后面的们都可以打开 不可重入锁:每一个资源都需要获取新的钥匙 公平锁:获取锁的机制是公平的,先到的线程先获取锁。 非公平锁:后来的线程可能先获取到锁。 锁的概念和synchronized关键字: 基于对象监视器实现的、基本的线程通信机制。Java中的每一个对象都于一个监视器关联,线程可以锁定或者解锁监视器。 同步关键字不仅可以实现同步,根据JVM规范还可以保证可见性。(应为lockunlock需要实现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机制实现)