|
1 | | -https://blog.csdn.net/javaloveiphone/article/details/59483188 |
| 1 | +一、锁 |
| 2 | +java提供的锁机制有两种,关键字和concurrent包下的lock锁 |
| 3 | +1.关键字volatile:synchronized是Java中的关键字,用于实现内置锁(Intrinsic Lock)机制。它可以用于方法或代码块上,用来标识需要同步访问的代码区域。当线程进入synchronized保护的代码区域时,会尝试获取对象的内置锁,如果锁没有被其他线程占用,则获取成功,执行代码区域,执行完毕后释放锁。如果锁已被其他线程占用,则线程进入阻塞状态,等待锁的释放。 |
| 4 | +synchronized关键字最主要有以下3种应用方式,下面分别介绍: |
| 5 | +- 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁 |
| 6 | +- 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁 |
| 7 | +- 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。 |
2 | 8 |
|
3 | | -线程是方法吗?是一个方法一个线程 |
| 9 | +synchronized底层语义原理: |
| 10 | +对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充 |
4 | 11 |
|
5 | | -百千万数据导出一个Excel,一分钟解决,你会怎么做 |
6 | 12 |
|
| 13 | +2.关键字volatile |
| 14 | + |
| 15 | +3.锁的分类 |
| 16 | +乐观锁和悲观锁 |
| 17 | +独占锁和共享锁 |
| 18 | +互斥锁和读写锁 |
| 19 | +公平锁和非公平锁 |
| 20 | +可重入锁 自旋锁 分段锁 |
| 21 | +- 锁升级 |
| 22 | +JDK1.6 为了提升性能减少获得锁和释放锁所带来的消耗,引入了4种锁的状态:无锁、偏向锁、轻量级锁和重量级锁,它会随着多线程的竞争情况逐渐升级,但不能降级。 |
| 23 | +- 无锁 |
| 24 | +无锁状态其实就是上面讲的乐观锁,这里不再赘述。 |
| 25 | +- 偏向锁 |
| 26 | +Java偏向锁(Biased Locking)是指它会偏向于第一个访问锁的线程,如果在运行过程中,只有一个线程访问加锁的资源,不存在多线程竞争的情况,那么线程是不需要重复获取锁的,这种情况下,就会给线程加一个偏向锁。 |
| 27 | +偏向锁的实现是通过控制对象Mark Word的标志位来实现的,如果当前是可偏向状态,需要进一步判断对象头存储的线程 ID 是否与当前线程 ID 一致,如果一致直接进入。 |
| 28 | +注:如果不加偏向锁,那么一个线程进入同步块时需要重复获取锁,有很大获取锁的开销;如果对象的锁状态是偏向锁,并且当前线程是持有偏向锁的线程,那么JVM会直接让线程进入同步块,无需额外的同步操作 |
| 29 | +- 轻量级锁 |
| 30 | +当线程竞争变得比较激烈时,偏向锁就会升级为轻量级锁,轻量级锁认为虽然竞争是存在的,但是理想情况下竞争的程度很低,通过自旋方式等待上一个线程释放锁。 |
| 31 | +注:如果在撤销偏向锁的过程中,JVM发现除了持有偏向锁的线程外,没有其他线程竞争该锁,那么JVM会将偏向锁状态转换为轻量级锁状态(偏向锁转轻量级锁) |
| 32 | +- 重量级锁 |
| 33 | +如果线程并发进一步加剧,线程的自旋超过了一定次数,或者一个线程持有锁,一个线程在自旋,又来了第三个线程访问时(反正就是竞争继续加大了),轻量级锁就会膨胀为重量级锁,重量级锁会使除了此时拥有锁的线程以外的线程都阻塞。 |
| 34 | +升级到重量级锁其实就是互斥锁了,一个线程拿到锁,其余线程都会处于阻塞等待状态。 |
| 35 | +注:如果在撤销偏向锁的过程中,JVM发现除了持有偏向锁的线程外,还有其他线程竞争该锁,那么JVM会将偏向锁状态转换为重量级锁状态。此时,JVM会先暂停持有偏向锁的线程,并尝试使用CAS操作将对象的Mark Word替换为指向重量级锁的数据结构。(偏向锁转重量级锁) |
| 36 | +在 Java 中,synchronized 关键字内部实现原理就是锁升级的过程:无锁 --> 偏向锁 --> 轻量级锁 --> 重量级锁。这一过程在后续讲解 synchronized 关键字的原理时会详细介绍 |
| 37 | +4.锁优化技术(锁粗化、锁消除) |
| 38 | + |
| 39 | +锁粗化就是将多个同步块的数量减少,并将单个同步块的作用范围扩大,本质上就是将多次上锁、解锁的请求合并为一次同步请求。 |
| 40 | +举个例子,一个循环体中有一个代码同步块,每次循环都会执行加锁解锁操作 |
| 41 | +``` |
| 42 | +private static final Object LOCK = new Object(); |
| 43 | +for (int i=0;i<100;i++) { |
| 44 | + synchronized (LOCK) { |
| 45 | + //do something |
| 46 | + } |
| 47 | + } |
| 48 | +``` |
| 49 | +``` |
| 50 | +private static final Object LOCK = new Object(); |
| 51 | +synchronized (LOCK) { |
| 52 | + for (int i=0;i<100;i++) { |
| 53 | + |
| 54 | + } |
| 55 | + } |
| 56 | +``` |
| 57 | +锁消除 |
| 58 | +锁消除是指虚拟机编译器在运行时检测到了共享数据没有竞争的锁,从而将这些锁进行消除 |
| 59 | +上面代码中有一个 test 方法,主要作用是将字符串 s1 和字符串 s2 串联起来。 |
| 60 | +test 方法中三个变量s1, s2, stringBuffer, 它们都是局部变量,局部变量是在栈上的,栈是线程私有的,所以就算有多个线程访问 test 方法也是线程安全的。 |
| 61 | +我们都知道 StringBuffer 是线程安全的类,append 方法是同步方法,但是 test 方法本来就是线程安全的,为了提升效率,虚拟机帮我们消除了这些同步锁,这个过程就被称为锁消除。 |
| 62 | +``` |
| 63 | +public String test(String s1,String s2) { |
| 64 | + StringBuffer stringBuilder = new StringBuffer(); |
| 65 | + stringBuilder.append(s1); |
| 66 | + stringBuilder.append(s2); |
| 67 | + return stringBuilder.toString(); |
| 68 | + } |
| 69 | +``` |
| 70 | + |
| 71 | +二、CAS |
| 72 | + |
| 73 | +CAS存在三大问题:ABA问题,循环时间长且开销大,以及只能保证一个共享变量的原子操作; |
| 74 | +ABA问题的场景如下: |
| 75 | + |
| 76 | +1.初始状态,共享变量的值为A。 |
| 77 | +2.线程1读取共享变量的值A。 |
| 78 | +3.在线程1读取值A的同时,线程2将共享变量的值从A修改为B,然后又修改回A。 |
| 79 | +4.线程1执行CAS操作,比较共享变量的值为A,发现与预期值相等,于是进行更新操作。 |
| 80 | +在这种情况下,CAS操作成功,因为线程1读取共享变量时,发现其值与预期值相等。然而,实际上共享变量的值已经发生了变化,虽然在值的变化过程中经历了A->B->A的变化,但是线程1并不知道这个过程,它仅仅是关注共享变量的当前值与预期值是否相等。 |
| 81 | + |
| 82 | +这就是ABA问题,CAS操作成功,但忽略了共享变量的值在过程中发生了其他变化。ABA问题可能导致一些潜在的风险和错误,特别是在并发数据结构和多线程算法中使用CAS时。 |
| 83 | + |
| 84 | +为了解决ABA问题,可以使用版本号或标记来跟踪共享变量的变化。每次共享变量发生变化时,版本号或标记也会相应地增加。这样,在CAS操作时,不仅要比较值是否相等,还要比较版本号或标记是否匹配。如果版本号或标记不匹配,说明共享变量在过程中发生了变化,CAS操作将失败。 |
| 85 | + |
| 86 | +**如果修改失败如何解决**: |
| 87 | +1.自旋重试:在获取失败后,可以使用自旋重试的方式,即反复尝试执行CAS操作,直到获取成功或达到一定的尝试次数或时间限制。自旋重试避免了线程的上下文切换,适用于短时间内锁竞争不激烈的情况。 |
| 88 | +2.适当延迟:为了减少不必要的CPU自旋消耗,在获取失败后可以适当地引入延迟。可以使用Thread.sleep()等方法在重试之前让线程进行短暂的休眠,以降低CPU占用。 |
| 89 | +3.加锁:如果CAS操作失败的次数较多或存在激烈的竞争情况,可以考虑使用传统的加锁机制,如使用synchronized关键字或ReentrantLock,来保证线程的互斥访问。加锁可以避免无限自旋的情况,但也会引入线程的上下文切换和额外的开销。 |
| 90 | +4.采用其他策略:根据具体的业务场景和需求,还可以采用其他策略来处理CAS获取失败的情况。例如,可以选择放弃当前操作、回退到其他实现方式,或者根据具体情况进行异常处理等。 |
| 91 | + |
| 92 | +二、为什么频繁创建线程会需要时间,具体那部分的时间 |
| 93 | + |
| 94 | +线程分为守护线程和非守护线程 |
| 95 | +当Java程序的所有非守护线程都执行完毕时,程序会正常退出,无论是否还存在守护线程。非守护线程是程序的主要执行线程,它们执行程序的核心逻辑。一旦所有非守护线程执行完毕,JVM会自动退出,无需显式地停止守护线程。 |
| 96 | + |
| 97 | +然而,如果还存在活跃的非守护线程,程序仍然可以继续执行。在这种情况下,守护线程的行为会有所影响。 |
| 98 | + |
| 99 | +守护线程是一类特殊的线程,它的生命周期与非守护线程不同。当程序中只剩下守护线程时,JVM会判断程序已经不再需要继续执行,因此会强制终止所有守护线程,然后退出程序。 |
7 | 100 |
|
8 | 101 |
|
9 | 102 | [面试官:Java线程与底层操作系统线程是一 一对应的吗? 中篇[一]_mob60475706bec5的技术博客_51CTO博客](https://blog.51cto.com/u_15127698/2842977) |
0 commit comments