锁机制

参考链接

常见的锁机制

Java中常用的锁机制

浅谈Java锁机制

java锁机制

不可不说的Java“锁”事

锁机制:一种保护机制,在多线程的情况下,保证操作数据的正确性 / 一致性

Synchronized 锁【S】

  • synchronized 机制是 给共享资源上锁只有拿到锁的线程才可以访问共享资源,这样就可以强制使得对共享资源的访问都是顺序的
  • Synchronized 是Java 关键字,属于Java 的内置特性
  • 基于 JVM 来保证数据同步
  • 无需手动释放锁,会一直等待

Lock锁【L】

  • 硬件层面 ,依赖特殊的CPU指令实现数据同步的
  • ReentrantLock:默认非公平但可实现公平的
  • 需要手动释放锁,可中断线程

线程是否要锁住同步资源

在这里插入图片描述

悲观锁【S、RL】

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

悲观锁在Java中的使用,就是利用 各种锁

  • 悲观锁 适合写操作多 的场景,先加锁可以保证写操作时数据正确
  • 重量级锁是悲观锁的一种
  • 悲观锁基本都是在显式的锁定之后再操作同步资源

	// synchronized
	public synchronized void testMethod() {
		// 操作同步资源
	}
	
	// ReentrantLock
	private ReentrantLock lock = new ReentrantLock(); // 需要保证多个线程使用的是同一个锁
	public void modifyPublicResources() {
		lock.lock();
		// 操作同步资源
		lock.unlock();
	}

乐观锁

乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)

乐观锁在Java中的使用,是 无锁编程,常常采用的是 CAS算法

【这使得乐观锁能够做到不锁定同步资源也可以正确的实现线程同步】

中转站:CAS 无锁机制

  • 典型的例子就是Java原子类中的递增操作就通过CAS自旋实现的
  • 乐观锁 适合读操作多 的场景,不加锁的特点能够使其读操作的性能大幅提升
  • 自旋锁、轻量级锁与偏向锁属于乐观锁
  • 乐观锁则直接去操作同步资源
	// 需要保证多个线程使用的是同一个AtomicInteger
	private AtomicInteger atomicInteger = new AtomicInteger();  
	
	//执行自增1
	atomicInteger.incrementAndGet(); 

多个线程竞争锁流程(锁的状态)【S】

前提知识:

Java对象头

synchronized是悲观锁,在操作同步资源之前需要给同步资源先加锁,这把锁就是存在Java对象头里的

Monitor

  • 可以理解为一个同步工具或一种同步机制,通常被描述为一个对象,每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁
  • Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表
  • 每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用

无锁状态

  • 无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功
  • CAS原理及应用即是无锁的实现

以下三种锁是指锁的 状态,并且是 针对 Synchronized,通过对象监视器在对象头中的字段来表明的

synchronized通过Monitor来实现线程同步

Monitor是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的线程同步

偏向锁 通过对比Mark Word解决加锁问题,避免执行CAS操作

轻量级锁 通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能

重量级锁 将除了拥有锁的线程以外的线程都阻塞

偏向锁

一段同步代码一直被一个线程所访问,那么该线程会 自动获取锁,降低获取锁的代价

轻量级锁

当锁是 偏向锁 的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能

重量级锁

  • 当锁为 轻量级锁 的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁,重量级锁会让其他申请的线程进入 阻塞,性能降低
  • 依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”

获取锁失败,线程是否堵塞

前提知识:

  • 阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间
  • 如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长
  • 在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失
  • 如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁

在这里插入图片描述

自旋锁

中转站:

自旋锁

尝试获取锁的线程不会立即阻塞(尽可能的减少线程的阻塞),而是采用循环的方式去 尝试获取锁

实现原理

  • CAS

好处

  • 避免切换线程的开销

缺点

  • 循环会 消耗CPU,占用了处理器的时间
  • 自旋锁在获取锁前一直都是占用cpu做无用功,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要cpu的线程又不能获取到cpu,造成cpu的浪费

适用

  • 锁的 竞争不激烈,且 占用锁时间非常短 的代码块来说性能能大幅度的提升
  • 因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗

非自旋

多线程竞争锁时是否需要排队

公平锁

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

优点

  • 等待锁的线程不会饿死

缺点

  • 整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞
  • CPU唤醒阻塞线程的开销比非公平锁大

非公平锁【S、RL】

多个线程获取锁的顺序 并不是按照申请锁的顺序,多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待

优点

  • 可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程

缺点

  • 处于等待队列中的线程可能会饿死,或者等很久才会获得锁

一个线程多个流程能否获得相同锁

可重入锁【S 、RL】

究竟什么是可重入锁?

”递归锁“:广义上的可重入锁指 可重复可递归调用 的锁,在外层使用锁之后,在内层仍然可以使用(前提得是同一个对象或者class)


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

在上面的代码中,类中的两个方法都是被内置锁synchronized修饰的,doSomething()方法中调用doOthers()方法。因为内置锁是可重入的,所以同一个线程在调用doOthers()时可以直接获得当前对象的锁,进入doOthers()进行操作。

如果是一个不可重入锁,那么当前线程在调用doOthers()之前需要将执行doSomething()时获取当前对象的锁释放掉,实际上该对象锁已被当前线程所持有,且无法释放。所以此时会出现死锁

优点

  • 一定程度避免死锁

实现原理

通过重入锁ReentrantLock以及非可重入锁NonReentrantLock的源码来对比分析死锁产生原因

  • 首先ReentrantLock和NonReentrantLock都继承父类AQS,其父类AQS中维护了一个同步状态status来计数重入次数,status初始值为0

  • 当线程尝试获取锁时

    • 可重入锁先尝试获取并更新status值,如果status == 0表示没有其他线程在执行同步代码,则把status置为1,当前线程开始执行。如果status != 0,则判断当前线程是否是获取到这个锁的线程,如果是的话执行status+1,且当前线程可以再次获取锁
    • 非可重入锁是直接去获取并尝试更新当前status的值,如果status != 0的话会导致其获取锁失败,当前线程阻塞
  • 释放锁时

    • 可重入锁同样先获取当前status的值,在当前线程是持有锁的线程的前提下。如果status-1 == 0,则表示当前线程所有重复获取锁的操作都已经执行完毕,然后该线程才会真正释放锁
    • 非可重入锁则是在确定当前线程是持有锁的线程之后,直接将status置为0,将锁释放

多个线程能否共享相同锁

独享锁 / 共享锁这是广义上的说法,互斥锁 / 读写锁就分别对应具体的实现

独享锁与共享锁是 通过AQS 来实现的

锁升级:读锁到写锁 (不支持)

锁降级:写锁到读锁 (支持)

【通过ReentrantLock(互斥锁)和ReentrantReadWriteLock(读写锁)介绍独享锁和共享锁】

独享锁(互斥锁)【S、RL】

一次只能被 一个线程 所持有

共享锁(读写锁)

可被 多个线程 所持有

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值