Java知识总结(七)

本文详细介绍了Java中的各种锁机制,包括乐观锁、悲观锁、自旋锁、公平锁、非公平锁、可重入锁、非可重入锁、独享锁、共享锁、分段锁、锁粗化和锁销除。通过对比各种锁的特点和应用场景,帮助读者理解如何在并发环境中选择合适的锁策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Java锁

不可不说的Java“锁”事 - 美团技术团队 (meituan.com)

img

乐观锁

对同一数据进行并发操作,乐观锁在进行操作数据时总是乐观认为不会有别的线程修改数据,因此它不会对资源加锁。当自己在进行数据修改之前它会先进行判断有没有其他线程修改了数据,如果数据没有修改,就写入数据。如果数据被修改了,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。

java中乐观锁基本上通过**CAS(Compare And Swap(比较与交换),是一种无锁算法)**操作实现的,,CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。

应用场景:适合读操作多的场景,不加锁的特性让读操作的性能得到很大的提升

悲观锁

对同一数据进行并发操作,悲观锁总认为别的线程会改写数据,因此在获取资源前它会对资源加锁,确保数据不会被更改。

Java中,synchronized关键字和Lock的实现类都是悲观锁

应用场景:适合写操作多的场景,加锁的特性确保数据的准确性

自旋锁

多个线程并发操作同一个资源,发现该资源已经被占用,一般的情况是需要等待该资源的锁被释放之后,其他线程去争夺锁,获得锁的线程执行,其他线程等待(上下文的切换),从运行态转变为阻塞态这个状态转换的过程需要耗费时间如果说当前占用资源的线程运行的时长比其他线程状态转换时长还要短,那么可以考虑不需要进入阻塞状态,而是让线程等待一会(自旋一会),如果自旋之后资源已经被释放,那么就不必阻塞直接获取同步资源即可

img

注意:线程自旋的过程也是要消耗cpu的,因此需要设定一个自旋的时间(默认是10次,可以使用-XX:PreBlockSpin来更改),来确保资源能否在规定时间内获取到,如果获取不到,那么线程就从自旋转为阻塞。

优缺点:

  • 自旋锁适合在对于资源竞争不激烈的情况下,尽量减少线程阻塞再到唤醒的过程,线程自旋的时间小于线程状态切换的时间,能够大幅度提升性能。
  • 在资源竞争激烈或者持有同步锁的线程执行耗时很长,这个时候自旋锁就不适用。因为让线程一直占着cpu,但是线程又没有做事,纯属浪费资源和时间,并且此时其他需要cpu的线程无法得到,导致性能大大下降。

自旋锁开启

JDK1.4.2中引入,使用-XX:+UseSpinning来开启,-XX:PreBlockSpin 为自旋次数。

JDK1. 6中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。

适应性自旋锁

1.6之前的自旋锁的次数一定程度是写死的,之后的自适应锁次数不在固定,是由上一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定的

如果对于同一个锁对象,自旋等待并且成功在自选周期内获得了锁资源,线程成功执行,那么JVM认为下一次这个自旋的过程很可能成功,因此会把自旋的周期持续相对更长的时间。反之,自旋过程很少获取锁资源,JVM认为这个自旋执行效率可能不高,可能考虑取消自旋进入阻塞状态,避免资源的浪费。

公平锁

多个线程按照顺序获取锁,需要获取锁的线程进入队列中等待,队列中最先发起请求锁的线程,也就是队列中的第一个线程能够最先获得锁,这就是公平锁。

好处:队列中等待锁资源的线程都能够获得锁,不会出现有些线程一直得不到锁的情况

缺点:整体吞吐量降低,因为每个没有获得锁的线程都要去队列中排队等待,cpu每次都要进行线程状态的转换,增加了开销

排队按顺序打水

image-20210725104704513

非公平锁

如果当前资源有其他线程正在时候,这个时候又来其他线程也需要这个资源,它并不会马上进入队列中等待,其他线程会尝试去等待锁资源的释放,如果获得了锁直接使用,不会去队列中等待(插队),如果没有获取到锁,它才会去队列中等待。

好处:减少了线程阻塞到唤醒的过程中的开销,整体吞吐效率提升

缺点:位于等待队列中的线程可能要等待很长时间才能获取到锁,甚至可能永远都获取不到

插队打水

img

非公平锁实际执行的效率要远远超出公平锁,除非程序有特殊需要,否则最常用非公平锁的分配机制。

可重入锁(递归锁)

当一个线程调用同步方法时获取对象锁,执行方法内部的代码,方法内部中又有同步方法,他会自动获取锁(前提是锁的对象一定是同一个对象或者类),该线程不会因为已经得到的锁没有释放而阻塞。Sychronized和ReentrantLock都是可重入锁。

例如:

public class Widget {
    public synchronized void doSomething() {
        System.out.println("方法1执行...");
        doOthers();
    }

    public synchronized void doOthers() {
        System.out.println("方法2执行...");
    }
}
/*
线程获取Widget的对象锁调用doSomething方法,调用doOthers方法,synchronized是可重入锁,线程继续获取锁调用doOthers方法
*/

n还是打水问题

img

村民(线程)需要获取锁才能打水,现在管理员规定,一个锁可以和同一个村民多个水桶绑定,因此第一个水桶和锁绑定后打水,接着将锁和第二个水桶绑定打水,所有的水桶中都装满了水,将锁还给管理员,村民能够完成整个打水的流程。

非可重入锁

非重入锁修饰的方法,在线程获取对象锁调用doSomething方法需要线程释放当前的锁,重新获取doOthers方法的锁才能执行,然而这两个方法的锁是同一个,锁已经被当前线程所持有,无法释放也无法获取,会造成死锁。

打水问题

img

村民(线程)需要获得锁才能打水,管理规定一个锁只能锁定村民的一个水桶,村民将水桶a和锁锁定打水,这个锁无法释放,村民的水桶b没有锁无法打水,导致线程死锁。

独享锁

独享锁又叫排它锁,该锁只能被一个线程锁持有,若数据A被某个线程加了独享锁,该数据A不能再被加任何锁。获得独享锁的线程既能够读数据也能够修改数据,其他的线程只能等待。Synchronized和ReentrantLock以独占方式实现的互斥锁。

共享锁

允许多个线程同时获取锁,并发访问,共享资源。若数据B被线程加了共享锁,其他线程只能对B加共享锁,不能加排它锁。获得共享锁的线程只能够读数据,不能修改数据。

分段锁

ConcurrentHashMap在java7中就采用分段锁的思想,给每一段分别加上对应的一把锁,保证线程安全。

减少锁持有的时间

只用在有线程安全要求的程序上加锁

减少颗粒度

将大对象(会被很多线程访问),拆分成小对象,大大增加并行度,降低锁的竞争,锁的竞争度降低了,偏向锁、轻量级锁成功率才能得到提升。最经典的减小颗粒度案例:ConcurrentHashMap

锁粗化

为了保证并发的有效性,会要求每个线程持有锁的时间尽可能的短,即在使用完资源后,立刻释放锁资源。

凡是有度,若有一系列操作是针对同一个对象(操作的是同一把锁),每次操作都要加锁和解锁,就会觉得繁琐并且耗费了额外的资源,因此就对锁进行了粗化,在第一次操作时加锁,中间其他一系列的操作不再进行解锁、加锁,而是在最后一次操作时解锁,保证了效率的同时也减少了资源的消耗。

例如:StringBuffer中append方法。

package com.paddx.test.string;

public class StringBufferTest {
    StringBuffer stringBuffer = new StringBuffer();

    public void append(){
        stringBuffer.append("a");
        stringBuffer.append("b");
        stringBuffer.append("c");
        //一系列的操作进行锁的粗化,整个过程只进行一次加锁和解锁
    }
}
/* 
单单调用一次要进行加锁和解锁stringBuffer.append("a");
*/

锁销除

如果发现不可能被共享的对象,则可以消除这些对象的锁操作,删除不必要的加锁操作,发生在编译器级别

java的编译体系公有两次编译阶段,第一次将java源代码通过编译器编译为.class字节码文件。第二次通过JVM解释器转换成二进制机器码。
image-20210903144507676

第二阶段JVM通过解释器将字节码文件解释成对应操作系统的二进制机器码,传统的解释器功能是逐行读入,逐行解释,这样执行的效率不高,为了解决这个问题,引入了JIT(即时编译技术)

引入即使编译技术,通过解释器解释,当JVM发现某个方法或代码块运行特别频繁时,就将其认定为热点代码,JIT会把部分“热点代码”翻译成本地机器相关的机器码,并进行优化,然后再把翻译后的机器码缓存起来,以备下次使用。

JIT优化中最重要的一个就是逃逸分析

逃逸分析:当一个对象被定义在方法中时,该对象是一个局部变量,若它作为参数传递到了其他的方法中时(被外部方法所引用),就称为方法逃逸

append方法被synchronized锁修饰

 @Override
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }
public static StringBuffer craeteStringBuffer(String s1, String s2) {
    StringBuffer sh = new StringBuffer();
    sh.append(s1);
    sh.append(s2);
    return sh;
}//sh作为局部参数被外界引用了,因此sh是方法逃逸,可能造成线程不安全。
public class SynchronizedTest02 {

    public static void main(String[] args) {
        SynchronizedTest02 test02 = new SynchronizedTest02();
        //启动预热
        for (int i = 0; i < 10000; i++) {
            i++;
        }
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100000000; i++) {
            test02.append("abc", "def");
        }
        System.out.println("Time=" + (System.currentTimeMillis() - start));
    }

    public void append(String str1, String str2) {
        StringBuffer sb = new StringBuffer();
        sb.append(str1).append(str2);
    }
}
/*
 StringBuffer sb = new StringBuffer();作为append方法中的局部参数,它并没有被其他方法所以引用,因此无法逃逸,所以这个过程是线程安全的,没有必要进行加锁操作,就可以将锁消除
*/
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值