悲观锁
认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
synchronized关键字和Lock的实现类都是悲观锁
适用场景:适合写操作多的场景,先加锁可以保证写操作时数据正确。
乐观锁
认为自己在使用数据时不会有别的线程修改数据或资源,所以不会添加锁。
在Java中是通过使用无锁编程来实现,只是在更新数据的时候去判断,之前有没有别的线程更新了这个数据。
如果这个数据没有被更新,当前线程将自己修改的数据成功写入。
如果这个数据已经被其他线程更新,则根据不同的实现方式执行不同的操作,比如放弃修改、重试抢锁等
判断规则
版本号机制Version
最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。
适用场景:适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升
synchronized
一个对象里如果有多个synchronized方法,某一时刻内,只要一个线程去调用其中的一个synchronized方法了,其它的线程都只能等待,换句话说,某一时刻内,只能有唯一的一个线程去访问这些synchronized方法。
锁的是当前对象this,被锁定后,其他的线程都不能进入到当前的对象的其他synchronized方法
类中的普通方法和同步锁方法无关
new 两个对象后,就不是同一把锁了
类中的静态同步方法
对于普通同步方法,锁的是当前实例对象,通常指this,具体的一个个new Class(); 所有的普通同步方法用的都是同一把锁:实例对象本身
对于静态同步方法,锁的是当前类的Class模板,Class模板只有一个,一个Class模板可以new出多个Class对象
对于同步方法块, 锁的是synchronized括号内的对象
当一个线程试图访问同步代码时它必须得到锁,正常退出或抛出异常时必须释放锁。
所有的同步方法用的都是同一把锁–实例对象本身,就是new出来的具体实例对象本身,本类this
也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁
所有的静态同步方法用的也是同一把锁–类对象本身,就是唯一模板Class
具体实例对象this和唯一模板Class,这两把锁是两个不同的对象,所以静态同步方法和普通同步方法之间是不会有竟态关系的
但是一旦一个静态同步方法获取锁后,其他的静态同步方法必须等待该方法释放锁后才能获取锁。
为什么任何一个对象都可以成为一个锁?
每个对象天生都带着一个对象监视器(objectMonitor),每一个被锁住的对象都会和Monitor关联起来
公平锁
指多个线程按照申请锁的顺序来获取锁,类似排队买票,先来的人先买后来的人在队尾排着,这是公平的
Lock lock = new RenntrantLock(true); //true表示公平锁,先来先得
非公平锁
值多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级反转或者饥饿的状态(某个线程一直得不到锁)
Lock lock = new RenntrantLock(false); //false表示非公平锁,后来的也可能先获得锁 Lock lock = new RenntrantLock(); // 默认非公平锁
为什么会有公平锁/非公平锁的升级?默认非公平锁?
恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU的时间片,尽量减少CPU的空闲状态时间。
使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求获取同步状态,然后释放同步状态,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销
如果为了更高的吞吐量,很显然非公平锁比较合适,因为节省很多线程切换时间,吞吐量自然就上去了。
否则就用公平锁,公平使用,雨露均沾
可重入锁
又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。
如果是1个有synchronized修饰的递归调用方法,程序第二次进入被自己阻塞了那岂不是无解了
所以Java中RenntrantLock和synchronized都是可重入锁,可重入锁的一个优点是可以定成都避免死锁。
隐式锁
//同步代码块 final Object object = new Object(); new Thread(() -> { synchronized (object){ System.out.println(Thread.currentThread().getName()+"\t ----外层调用"); synchronized (object){ System.out.println(Thread.currentThread().getName()+"\t ----中层调用"); synchronized (object){ System.out.println(Thread.currentThread().getName()+"\t ----内层调用"); } } } },"t1").start();
//同步方法 public class ReEntryLockDemo{ public synchronized void m1() { //指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。 System.out.println(Thread.currentThread().getName()+"\t ----come in"); m2(); System.out.println(Thread.currentThread().getName()+"\t ----end m1"); } public synchronized void m2() { System.out.println(Thread.currentThread().getName()+"\t ----come in"); m3(); } public synchronized void m3() { System.out.println(Thread.currentThread().getName()+"\t ----come in"); } public static void main(String[] args) { ReEntryLockDemo reEntryLockDemo = new ReEntryLockDemo(); new Thread(() -> { reEntryLockDemo.m1(); },"t1").start(); } }
指的是可重复可递归调用的锁,在外层使用锁之后,在内存仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。
简答的来说就是:在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的
显示锁
static Lock lock = new ReentrantLock(); public static void main(String[] args) { new Thread(() -> { lock.lock(); try { System.out.println(Thread.currentThread().getName()+"\t ----come in外层调用"); lock.lock(); try { System.out.println(Thread.currentThread().getName()+"\t ----come in内层调用"); }finally { lock.unlock(); } }finally { // 由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待。 //lock.unlock();// 正常情况,加锁几次就要解锁几次 } },"t1").start(); new Thread(() -> { lock.lock(); try { System.out.println(Thread.currentThread().getName()+"\t ----come in外层调用"); }finally { lock.unlock(); } },"t2").start(); }
死锁
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法继续推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。
public static void main(String[] args) { final Object objectA = new Object(); final Object objectB = new Object(); new Thread(() -> { synchronized (objectA){ System.out.println(Thread.currentThread().getName()+"\t 自己持有A锁,希望获得B锁"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (objectB){ System.out.println(Thread.currentThread().getName()+"\t 成功获得B锁"); } } },"A").start(); new Thread(() -> { synchronized (objectB){ System.out.println(Thread.currentThread().getName()+"\t 自己持有B锁,希望获得A锁"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (objectA){ System.out.println(Thread.currentThread().getName()+"\t 成功获得A锁"); } } },"B").start(); }
排查死锁
idea终端
jps -l jstack 进程变化
jdk安装目录bin/jconsole
根据包名选择进程,连接,选择线程,下边检测死锁
总结:
锁的到底是什么?
对象锁、类锁
公平锁和非公平锁
多个线程在抢锁竞争同一份资源的时候采用的策略是公平还是效率优先
可重入锁
同一个线程持有同一把锁,自己进入自己的内部方法以后可以自动获得锁,不会导致死锁
为什么任何一个对象都可以成为一个锁?
objectMonitor.hpp
指针指向monitor对象(也称为管程或监视器锁)的其实地址。每个对象都存在着一个monitor与之关联,当一个monitor被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是ObjectMonitor实现的