多线程第四讲

各种各样的锁

1、锁的分类

根据分类标准我们把锁分为以下 7 大类别,分别是:

偏向锁/轻量级锁/重量级锁;

第一种分类是偏向锁/轻量级锁/重量级锁,这三种锁特指 synchronized 锁的状态,通过在对象头中的 mark word 来表明锁的状态。

偏向锁

一个对象被初始化后,还没有任何线程来获取它的锁时,那么它就是可偏向的,当有第一个线程来访问它并尝试获取锁的时候,它就将这个线程记录下来,以后如果尝试获取锁的线程正是偏向锁的拥有者,就可以直接获得锁,开销很小,性能最好。

轻量级锁

JVM 开发者发现在很多情况下,synchronized 中的代码是被多个线程交替执行的,而不是同时执行的,也就是说并不存在实际的竞争,或者是只有短时间的锁竞争,用 CAS 就可以解决,这种情况下,用完全互斥的重量级锁是没必要的。轻量级锁是指当锁原来是偏向锁的时候,被另一个线程访问,说明存在竞争,那么偏向锁就会升级为轻量级锁,线程会通过自旋的形式尝试获取锁,而不会陷入阻塞。

重量级锁

重量级锁是互斥锁,它是利用操作系统的同步机制实现的,所以开销相对比较大。当多个线程直接有实际竞争,且锁竞争时间长的时候,轻量级锁不能满足需求,锁就会膨胀为重量级锁。重量级锁会让其他申请却拿不到锁的线程进入阻塞状态。           

你可以发现锁升级的路径:无锁→偏向锁→轻量级锁→重量级锁。

综上所述,偏向锁性能最好,可以避免执行 CAS 操作。而轻量级锁利用自旋和 CAS 避免了重量级锁带来的线程阻塞和唤醒,性能中等。重量级锁则会把获取不到锁的线程阻塞,性能最差。

可重入锁/非可重入锁;

可重入锁指的是线程当前已经持有这把锁了,能在不释放这把锁的情况下,再次获取这把锁。同理,不可重入锁指的是虽然线程当前持有了这把锁,但是如果想再次获取这把锁,也必须要先释放锁后才能再次尝试获取。对于可重入锁而言,最典型的就是 ReentrantLock 了,正如它的名字一样,reentrant 的意思就是可重入,它也是 Lock 接口最主要的一个实现类。

共享锁/独占锁;

共享锁指的是我们同一把锁可以被多个线程同时获得,而独占锁指的就是,这把锁只能同时被一个线程获得。我们的读写锁,就最好地诠释了共享锁和独占锁的理念。读写锁中的读锁,是共享锁,而写锁是独占锁。读锁可以被同时读,可以同时被多个线程持有,而写锁最多只能同时被一个线程持有。

公平锁/非公平锁;

公平锁的公平的含义在于如果线程现在拿不到这把锁,那么线程就都会进入等待,开始排队,在等待队列里等待时间长的线程会优先拿到这把锁,有先来先得的意思。而非公平锁会在一定情况下,忽略掉已经在排队的线程,发生插队现象。

悲观锁/乐观锁;

悲观锁的概念是在获取资源之前,必须先拿到锁,以便达到“独占”的状态,当前线程在操作资源的时候,其他线程由于不能拿到锁,所以其他线程不能来影响我,典型的案例,synchronized关键字和Lock接口。而乐观锁恰恰相反,它并不要求在获取资源前拿到锁,也不会锁住资源;相反,乐观锁利用 CAS 理念,在不独占资源的情况下,完成了对资源的修改,典型案例为原子类(Atomic包下)。

数据库数据库中同时拥有悲观锁和乐观锁两种

例如 select  for  update就是典型的悲观锁再提交之前不允许第三方修改数据,但是这会带来一定的性能消耗,在高并发的情况下不可取。

另一种是利用版本号version字段实现的乐观锁,在获取及修改数据时都不需要加锁,但是我们在获取完数据并计算完毕,准备更新数据时,会检查版本号和获取数据时的版本号是否一致,如果一致就直接更新,如果不一致,说明计算期间已经有其他线程修改过这个数据了,那我就可以选择重新获取数据,重新计算,然后再次尝试更新数据。

举例代码如下

UPDATE student
    SET 
        name = ‘小田’,
        version= 2
    WHERE   id= 100
        AND version= 1

两种锁各自的使用场景
悲观锁适合用于并发写入多、临界区代码复杂、竞争激烈等场景,这种场景下悲观锁可以避免大量的无用的反复尝试等消耗。乐观锁适用于大部分是读取,少部分是修改的场景,也适合虽然读写都很多,但是并发并不激烈的场景。在这些场景下,乐观锁不加锁的特点能让性能大幅提高。

自旋锁/非自旋锁;

自旋锁的理念是如果线程现在拿不到锁,并不直接陷入阻塞或者释放 CPU 资源,而是开始利用循环,不停地尝试获取锁,这个循环过程被形象地比喻为“自旋”,就像是线程在“自我旋转”。相反,非自旋锁的理念就是没有自旋的过程,如果拿不到锁就直接放弃,或者进行其他的处理逻辑,例如去排队、陷入阻塞等。

自旋锁的好处,那就是自旋锁用循环去不停地尝试获取锁,让线程始终处于 Runnable 状态,节省了线程状态切换带来的开销。

自旋锁的坏处,它需要不停得去尝试获取锁。如果这把锁一直不能被释放,那么这种尝试只是无用的尝试,会白白浪费处理器资源。也就是说,虽然一开始自旋锁的开销低于线程切换,但是随着时间的增加,后期甚至会超过线程切换的开销。

适用场景
所以我们就要看一下自旋锁的适用场景。首先,自旋锁适用于并发度不是特别高的场景,以及临界区比较短小的情况,这样我们可以利用避免线程切换来提高效率。可是如果临界区很大,线程一旦拿到锁,很久才会释放的话,那就不合适用自旋锁,因为自旋会一直占用 CPU 却无法拿到锁,白白消耗资源。

可中断锁/不可中断锁。

在 Java 中,synchronized 关键字修饰的锁代表的是不可中断锁,一旦线程申请了锁,就没有回头路了,只能等到拿到锁以后才能进行其他的逻辑处理。而我们的 ReentrantLock 是一种典型的可中断锁,例如使用 lockInterruptibly 方法在获取锁的过程中,突然不想获取了,那么也可以在中断之后去做其他的事情,不需要一直傻等到获取到锁才离开。

2、synchronized背后的“monitor”

synchronized 修饰的代码块

public class SynTest {
    public void synBlock() {
        synchronized (this) {
            System.out.println("hello");
        }
    }
}

首先用 cd 命令切换到 SynTest.java 类所在的路径,然后执行 javac SynTest.java,于是就会产生一个名为 SynTest.class 的字节码文件,然后我们执行 javap -verbose SynTest.class,就可以看到对应的反汇编内容。

3: monitorenter
13: monitorexit
19: monitorexit

会有如上图得一些汇编指令,

JVM 要保证每个 monitorenter 必须有与之对应的 monitorexit,monitorenter 指令被插入到同步代码块的开始位置,而 monitorexit 需要插入到方法正常结束处和异常处两个地方,这样就可以保证抛异常的情况下也能释放锁

可以把执行 monitorenter 理解为加锁,执行 monitorexit 理解为释放锁,每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为 0,我们来具体看一下 monitorenter 和 monitorexit 的含义:

monitorenter
执行 monitorenter 的线程尝试获得 monitor 的所有权,会发生以下这三种情况之一:

a. 如果该 monitor 的计数为 0,则线程获得该 monitor 并将其计数设置为 1。然后,该线程就是这个 monitor 的所有者。

b. 如果线程已经拥有了这个 monitor ,则它将重新进入,并且累加计数。

c. 如果其他线程已经拥有了这个 monitor,那个这个线程就会被阻塞,直到这个 monitor 的计数变成为 0,代表这个 monitor 已经被释放了,于是当前这个线程就会再次尝试获取这个 monitor。

monitorexit
monitorexit 的作用是将 monitor 的计数器减 1,直到减为 0 为止。代表这个 monitor 已经被释放了,已经没有任何线程拥有它了,也就代表着解锁,所以,其他正在等待这个 monitor 的线程,此时便可以再次尝试获取这个 monitor 的所有权。

 synchronized 修饰的方法

会有一个 ACC_SYNCHRONIZED 标志。当某个线程要访问某个方法的时候,会首先检查方法是否有 ACC_SYNCHRONIZED 标志,如果有则需要先获得 monitor 锁,然后才能开始执行方法,方法执行之后再释放 monitor 锁。其他方面, synchronized 方法和刚才的 synchronized 代码块是很类似的,例如这时如果其他线程来请求执行方法,也会因为无法获得 monitor 锁而被阻塞

3、Lock

Lock 和 synchronized 是两种最常见的锁,锁是一种工具,用于控制对共享资源的访问,而 Lock 和 synchronized 都可以达到线程安全的目的,但是在使用上和功能上又有较大的不同。所以 Lock 并不是用来代替 synchronized 的,而是当使用 synchronized 不合适或不足以满足要求的时候,Lock 可以用来提供更高级功能的。下面来看看Lock接口的几种方法

public interface Lock {
    void lock(); //加锁
    void lockInterruptibly() throws InterruptedException; //可以响应中断的获取锁
    boolean tryLock();//尝试获取锁 
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;//带超时时间的尝试
    void unlock();//解锁 通常放在finally块中
    Condition newCondition();
}

Lock 和 synchronized 的相同点

(1).保证资源线程安全

(2).保证可见性

(3).都可重入

不同点:

(1).用法不同一个是关键字修饰方法或者代码块 一个是java接口 使用Lock锁对象调用lock()和unlock()方法等

(2).多把锁而言加解锁顺序不同 synchronized加解锁顺序相反 Lock可以不按反顺序解锁

(3).synchronized不够灵活 使用局限 Lock灵活方法多

(4).synchronized 锁只能同时被一个线程拥有,但是 Lock 锁没有这个限制
例如在读写锁中的读锁,是可以同时被多个线程持有的,可是 synchronized 做不到。

(5).原理不同 一个内置锁由 JVM 实现获取锁和释放锁的原理,还分为偏向锁、轻量级锁、重量级锁。
Lock 根据实现不同,有不同的原理,例如 ReentrantLock 内部是通过 AQS 来获取和释放锁的。

(6)公平性 
ReentrantLock 等 Lock 实现类可以根据自己的需要来设置公平或非公平,synchronized 则不能设置

(7).性能

java6之前 synchronized 性能较差  6之后对锁进行了升级优化性能查不到

4、读写锁ReadWriteLock

读写锁的获取规则
我们在使用读写锁时遵守下面的获取规则:

如果有一个线程已经占用了读锁,则此时其他线程如果要申请读锁,可以申请成功。

如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁,因为读写不能同时操作。

如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,都必须等待之前的线程释放写锁,同样也因为读写不能同时,并且两个线程不应该同时写。

所以我们用一句话总结:要么是一个或多个线程同时有读锁,要么是一个线程有写锁,但是两者不会同时出现。也可以总结为:读读共享、其他都互斥(写写互斥、读写互斥、写读互斥)。

 

5、jvm锁的优化

(1).自适应的自旋锁

在 JDK 1.6 中引入了自适应的自旋锁来解决长时间自旋的问题。自适应意味着自旋的时间不再固定,而是会根据最近自旋尝试的成功率、失败率,以及当前锁的拥有者的状态等多种因素来共同决定。自旋的持续时间是变化的,自旋锁变“聪明”了。比如,如果最近尝试自旋获取某一把锁成功了,那么下一次可能还会继续使用自旋,并且允许自旋更长的时间;但是如果最近自旋获取某一把锁失败了,那么可能会省略掉自旋的过程,以便减少无用的自旋,提高效率。

(2).锁消除

根据这样的思想,接下来我们就来举一个锁消除的例子,经过逃逸分析之后,如果发现某些对象不可能被其他线程访问到,那么就可以把它们当成栈上数据,栈上数据由于只有本线程可以访问,自然是线程安全的,也就无需加锁,所以会把这样的锁给自动去除掉。

public synchronized StringBuffer append(Object obj) {
    toStringCache = null;
    super.append(String.valueOf(obj));
    return this;
}

从代码中可以看出,这个方法是被 synchronized 修饰的同步方法,因为它可能会被多个线程同时使用。

但是在大多数情况下,它只会在一个线程内被使用,如果编译器能确定这个 StringBuffer 对象只会在一个线程内被使用,就代表肯定是线程安全的,那么我们的编译器便会做出优化,把对应的 synchronized 给消除,省去加锁和解锁的操作,以便增加整体的效率。

(3).锁粗化

相当于是把几个 synchronized 块合并为一个较大的同步块。这样做的好处在于在线程执行这些代码时,就无须频繁申请与释放锁了,这样就减少了性能开销。不适用于循环场景

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 《多线程编程实战第二版》是一本经典的多线程编程书籍,全书共分为14章,内容详实,完整涵盖了多线程编程的各个方面。 本书第一章介绍了多线程编程的背景和概念,并讨论了多线程编程的优势和缺点。第二章介绍了多线程编程中最重要的概念——线程。通过对线程的创建、启动、挂起和停止等操作的详细介绍,为后续开发做好了铺垫。 第三章和第四章分别介绍了线程的同步和互斥技术。这两章内容非常重要,因为多线程编程中,线程之间的合作和竞争是非常常见的情况。掌握了同步和互斥技术,才能编写出高质量的多线程程序。 第五章至第七章介绍了线程池的原理和实现。线程池是一种重要的线程管理技术,可以提高多线程程序的可维护性和可靠性。 第八章至第十二章介绍了多线程编程中的高级技术,例如线程调度、异常处理、锁协议和信号量等技术。这些技术对于编写高质量的多线程程序非常有帮助。 第十三章介绍了多线程编程中的常见问题和错误,并提供了解决方法。这些问题和错误非常实用,可以帮助程序员避免一些常见的多线程编程错误。 最后,第十四章提供了编写高质量多线程程序的一些最佳实践和指导。这些实践和指导非常有价值,可以提高程序员的编程水平。 总而言之,《多线程编程实战第二版》是一本非常优秀的多线程编程书籍,适合所有对多线程编程感兴趣的程序员学习。 ### 回答2: 《多线程编程实战第二版》是一本关于Java多线程编程的经典书籍,该书全面深入地介绍了Java多线程编程的各个方面。读者可以通过阅读本书学习到多线程编程的基本概念、多线程的并发性和同步性、线程池、线程协作等内容。 本书的重点是实战。作者通过丰富的代码示例和实际案例来展示Java多线程编程在实际项目中的应用。例如,作者为读者介绍了如何使用线程池来提高程序的性能和效率,如何通过线程协作来避免线程之间的竞争和冲突。 此外,该书还深入介绍了Java 5引入的Lock和Condition机制,以及Java 8中引入的CompletableFuture和Stream API。这些新特性为多线程编程带来了新的思路和新的解决方案。读者可以通过本书的学习深入理解并掌握这些新特性的使用方式。 总之,《多线程编程实战第二版》是一本Java多线程编程的必备参考书籍。它不仅介绍了多线程编程的基础知识和经典案例,还提供了实战经验和高级技巧。无论你是初学者还是有经验的开发者,都可以从中受益。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值