Java中的锁

简介: Java中的锁

悲观锁


认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。


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都是可重入锁,可重入锁的一个优点是可以定成都避免死锁

1673407777387.jpg


隐式锁


//同步代码块
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();
    }


死锁


死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法继续推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。

1673407837280.jpg

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


根据包名选择进程,连接,选择线程,下边检测死锁

1673407876873.jpg


总结:


锁的到底是什么?


对象锁、类锁

公平锁和非公平锁


多个线程在抢锁竞争同一份资源的时候采用的策略是公平还是效率优先

可重入锁


同一个线程持有同一把锁,自己进入自己的内部方法以后可以自动获得锁,不会导致死锁

为什么任何一个对象都可以成为一个锁?


objectMonitor.hpp

1673407891469.jpg

1673407900887.jpg

指针指向monitor对象(也称为管程或监视器锁)的其实地址。每个对象都存在着一个monitor与之关联,当一个monitor被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是ObjectMonitor实现的

相关文章
|
3月前
|
安全 Java 调度
Java编程时多线程操作单核服务器可以不加锁吗?
Java编程时多线程操作单核服务器可以不加锁吗?
49 2
|
1月前
|
缓存 Java
java中的公平锁、非公平锁、可重入锁、递归锁、自旋锁、独占锁和共享锁
本文介绍了几种常见的锁机制,包括公平锁与非公平锁、可重入锁与不可重入锁、自旋锁以及读写锁和互斥锁。公平锁按申请顺序分配锁,而非公平锁允许插队。可重入锁允许线程多次获取同一锁,避免死锁。自旋锁通过循环尝试获取锁,减少上下文切换开销。读写锁区分读锁和写锁,提高并发性能。文章还提供了相关代码示例,帮助理解这些锁的实现和使用场景。
java中的公平锁、非公平锁、可重入锁、递归锁、自旋锁、独占锁和共享锁
|
1月前
|
Java 开发者
Java 中的锁是什么意思,有哪些分类?
在Java多线程编程中,锁用于控制多个线程对共享资源的访问,确保数据一致性和正确性。本文探讨锁的概念、作用及分类,包括乐观锁与悲观锁、自旋锁与适应性自旋锁、公平锁与非公平锁、可重入锁和读写锁,同时提供使用锁时的注意事项,帮助开发者提高程序性能和稳定性。
52 3
|
2月前
|
Java
Java 中锁的主要类型
【10月更文挑战第10天】
|
3月前
|
存储 缓存 安全
【Java面试题汇总】多线程、JUC、锁篇(2023版)
线程和进程的区别、CAS的ABA问题、AQS、哪些地方使用了CAS、怎么保证线程安全、线程同步方式、synchronized的用法及原理、Lock、volatile、线程的六个状态、ThreadLocal、线程通信方式、创建方式、两种创建线程池的方法、线程池设置合适的线程数、线程安全的集合?ConcurrentHashMap、JUC
【Java面试题汇总】多线程、JUC、锁篇(2023版)
|
3月前
|
算法 Java 关系型数据库
Java中到底有哪些锁
【9月更文挑战第24天】在Java中,锁主要分为乐观锁与悲观锁、自旋锁与自适应自旋锁、公平锁与非公平锁、可重入锁以及独享锁与共享锁。乐观锁适用于读多写少场景,通过版本号或CAS算法实现;悲观锁适用于写多读少场景,通过加锁保证数据一致性。自旋锁与自适应自旋锁通过循环等待减少线程挂起和恢复的开销,适用于锁持有时间短的场景。公平锁按请求顺序获取锁,适合等待敏感场景;非公平锁性能更高,适合频繁加解锁场景。可重入锁支持同一线程多次获取,避免死锁;独享锁与共享锁分别用于独占和并发读场景。
|
2月前
|
安全 Java 开发者
java的synchronized有几种加锁方式
Java的 `synchronized`通过上述三种加锁方式,为开发者提供了从粗粒度到细粒度的并发控制能力,满足了不同场景下的线程安全需求。合理选择加锁方式对于提升程序的并发性能和正确性至关重要,开发者应根据实际应用场景的特性和性能要求来决定使用哪种加锁策略。
33 0
|
2月前
|
Java 应用服务中间件 测试技术
Java21虚拟线程:我的锁去哪儿了?
【10月更文挑战第8天】
46 0
|
3月前
|
Java 数据库
JAVA并发编程-一文看懂全部锁机制
曾几何时,面试官问:java都有哪些锁?小白,一脸无辜:用过的有synchronized,其他不清楚。面试官:回去等通知! 今天我们庖丁解牛说说,各种锁有什么区别、什么场景可以用,通俗直白的分析,让小白再也不怕面试官八股文拷打。
|
4月前
|
小程序 Java 开发工具
【Java】@Transactional事务套着ReentrantLock锁,锁竟然失效超卖了
本文通过一个生动的例子,探讨了Java中加锁仍可能出现超卖问题的原因及解决方案。作者“JavaDog程序狗”通过模拟空调租赁场景,详细解析了超卖现象及其背后的多线程并发问题。文章介绍了四种解决超卖的方法:乐观锁、悲观锁、分布式锁以及代码级锁,并重点讨论了ReentrantLock的使用。此外,还分析了事务套锁失效的原因及解决办法,强调了事务边界的重要性。
124 2
【Java】@Transactional事务套着ReentrantLock锁,锁竟然失效超卖了