【Java 多线程并发】 锁

Metadata

title: 【Java 多线程并发】 锁
date: 2023-07-03 15:36
tags:
  - 行动阶段/完成
  - 主题场景/程序
  - 笔记空间/KnowladgeSpace/ProgramSpace/BasicsSpace
  - 细化主题/Java/多线程并发
categories:
  - Java
keywords:
  - Java/多线程并发
description: 【Java 多线程并发】 锁

概述

相关结论

  • 对于普通同步方法,锁的是当前实例对象,通常指this,所有的同步方法用的都是同一把锁—>实例对象本身
  • 对于静态同步方法,锁的是当前类的Class对象
  • 对于同步方法块,锁的是synchronized括号内的对象

悲观锁

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

乐观锁

认为自己在使用数据的时候不会有别的线程修改数据或资源,不会添加锁,Java中使用无锁编程来实现,只是在更新的时候去判断,之前有没有别的线程更新了这个数据,如果这个数据没有被更新,当前线程将自己修改的数据成功写入,如果已经被其他线程更新,则根据不同的实现方式执行不同的操作

公平锁

多个线程按照申请锁的顺序来获取锁

非公平锁

多个线程获取锁的顺序并不是按照申请的顺序,有可能后申请的线程比先申请的线程优先获取锁
有可能造成优先级反转或者饥饿的状态

可重入锁(递归锁)

在同一线程在外层方法获取到锁的时侯,在进入该线程的内层方法会自动获取锁

写锁(独占锁)

该锁一次只能被一个线程所持有。

读锁(共享锁)

该锁可被多个线程所持有。

分段锁

内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock

自旋锁

指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁

可中断锁

阻塞等待中断

锁的相关结论

  • 对于普通同步方法,锁的是当前实例对象,通常指this,所有的同步方法用的都是同一把锁—>实例对象本身
  • 对于静态同步方法,锁的是当前类的Class对象
  • 对于同步方法块,锁的是synchronized括号内的对象

类锁和对象锁的区别

  • 对象锁是当前对象实例的锁,只有是同一个对象才会涉及是否做同步
  • 类锁故名思议就是这个class的锁,只要是同类,即使不同的对象也必须同步

当synchronized(Person.class)时会发现,即使是不同的两个person对象也会按照顺序来同步执行。它会保证只要是Person类的实例对象,那么在同一时间只有一个线程对其访问,其他线程只能等待该线程操作完将锁释放后才能进行操作。即相同类的不同的实例共用一个锁,当一个线程执行完后才能执行另一个线程。这里线程0是操作的p1对象,线程1操作的是p2对象,p1和p2都是Person类的两个不同对象,但是因为这里synchronized获取的是类锁,也就是说只要是这个Person类的对象,同一时间就只能有一个线程对其进行操作,所以虽然两个线程操作的是不同的Person对象,但是还是会对两个线程进行同步操作。

当synchronized(o1)的时候会发现只有同一个person对象才会同步,不同的person对象不会发生同步的行为。它只会保证同一个Person类的实例对象在同一时间只能有一个线程对其进行访问,但是如果是Person类的不同对象,是不会进行同步操作的。即不同对象对应不同的锁。这里线程0是操作的p1对象,线程1操作的是p2对象,p1和p2都是Person类的两个不同对象,synchronized获取的是对象锁,也就是说只有相同的Person类对象才会进行同步操作,不同的对象各自是独立的,不会进行同步操作,所以两个线程可以同时对各自的p1,p2对象进行操作,互不冲突。

乐观锁和悲观锁

悲观锁

认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改,synchronizedLock的实现类都是悲观锁,适合写操作多的场景,先加锁可以保证写操作时数据正确,显示的锁定之后再操作同步资源—–狼性锁

乐观锁

认为自己在使用数据的时候不会有别的线程修改数据或资源,不会添加锁,Java中使用无锁编程来实现,只是在更新的时候去判断,之前有没有别的线程更新了这个数据,如果这个数据没有被更新,当前线程将自己修改的数据成功写入,如果已经被其他线程更新,则根据不同的实现方式执行不同的操作,比如:放弃修改、重试抢锁等等。判断规则有:版本号机制Version,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。—–适合读操作多的场景,不加锁的特性能够使其读操作的性能大幅提升,乐观锁则直接去操作同步资源,是一种无锁算法,得之我幸不得我命—佛系锁

公平锁和非公平锁

公平锁

是指多个线程按照申请锁的顺序来获取锁,这里类似于排队买票,先来的人先买,后来的人再队尾排着,这是公平的—– Lock lock = new ReentrantLock(true)—表示公平锁,先来先得。

非公平锁

是指多个线程获取锁的顺序并不是按照申请的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级反转或者饥饿的状态(某个线程一直得不到锁)—- Lock lock = new ReentrantLock(false)—表示非公平锁,后来的也可能先获得锁,默认为非公平锁。

可重入锁(递归锁)

概念说明

是指在同一线程在外层方法获取到锁的时侯,在进入该线程的内层方法会自动获取锁(前提,锁对象的是同一个对象),不会因为之前已经获取过还没释放而阻塞———优点之一就是可一定程度避免死锁。

可重入锁种类

  • 隐式锁(即synchronized关键字使用的锁),默认是可重入锁
    • 在一个synchronized修饰的方法或者代码块的内部调用本类的其他synchronized修饰的方法或者代码块时,是永远可以得到锁。
  • 显式锁(即Lock)也有ReentrantLock这样的可重入锁

写锁(独占锁)/读锁(共享锁)

独享锁是指该锁一次只能被一个线程所持有。
共享锁是指该锁可被多个线程所持有。

上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。

  • 互斥锁在Java中的具体实现就是ReentrantLock
  • 读写锁在Java中的具体实现就是ReadWriteLock
  • Synchronized也是互斥锁

分段锁

分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。

我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。

当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在哪一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。

但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。

分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

自旋锁/可中断锁

自旋锁

在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁(即当循环的条件被其他线程改变时 才能进入临界区),这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

可中断锁

synchronized 是不可中断的,Lock 是可中断的,这里的可中断建立在阻塞等待中断,运行中是无法中断的。

无锁->偏向锁->轻量锁->重量锁

这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。锁是在资源中的,是要访问资源(如对象实例,Class类实例,属性变量,代码块等)的一部分,线程是要取得资源中的锁。

  • 偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
  • 轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
  • 重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

死锁及排查

概念

死锁是指两个或两个以上的线程在执行过程中,因抢夺资源而造成的一种互相等待的现象,若无外力干涉,则它们无法再继续推进下去。
产生原因:

  • 系统资源不足
  • 进程运行推进顺序不合适
  • 系统资源分配不当

如何排查死锁

  • 纯命令
    • jps -l
    • jstack 进程编号
  • 图形化
    • jconsole