Java高并发之synchronized关键字

1、synchronized 简介

synchronized关键字是解决多线程之间访问资源的同步性。synchronized关键字可以保证被其修饰的方法或者代码块在同一时刻只能被一个线程访问。
JDK1.6之前,synchronized是重量级锁,开销大,因为监视器锁是依赖于底层的操作系统来实现的,Java的线程是映射到操作系统的原生线程之上的,就是是说要挂起或者唤醒一个线程,需要操作系统的帮忙,而操作系统实现线程之间的切换需要从用户空间切换到内核空间,这个切换耗时长,所有时间成本高,导致效率低下。
JDK1.6之后,Java官方在JVM层面上对synchronized有较大优化,提高了synchronized性能,引入了如自旋锁、适应性自旋锁、锁消除、偏向锁等技术。

2、怎么使用synchronized

在这里插入图片描述
synchronized关键字不仅可以用在方法上也可以用在代码块上,当用在实例方法时,锁住的是实例对象,用在静态方法时,锁住的是这个类对象。在代码块上也是如此,但是需要注意的是,当锁住的是类对象时,尽管new多个实例对象,但是它们都属于同一个类,依然会被锁住,来保持线程之间的同步关系。

3、对象锁(monitor)机制

在这里插入图片描述
通过一段代码来看

public class SynchronizedDemo {
    public static void main(String[] args) {
        synchronized (SynchronizedDemo.class) {
        }
        method();
    }
    private static void method() {
    }
}

synchronized修饰的同步代码块,锁住的是这个类对象,同时也锁住了下面的这个静态方法。要执行同步代码块中的逻辑,线程要先执行monitorenter命令,退出时执行monitorexit命令。
使用synchronized来同步,关键在于获取该对象的监视器monitor,获取到后才能往下执行,获取不到则进入同步队列等待,这个获取的过程是互斥的,即同一时刻只有一个线程能获取到monitor。上面的代码中,执行完同步代码块后,会接着去执行同步静态方法,这个时候线程是不需要再次获取monitor的,这就是锁的重入性即在同一个锁程中,线程不用再次获取同一把锁。
每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一。
synchronized修饰的方法并没有 monitorenter 指令和 monitorexit指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。

不过两者的本质都是对对象监视器 monitor 的获取。

4、锁获取和锁释放对内存的操作

在这里插入图片描述
原本内存中的是a = 0,当线程A获取到锁时,会先强制把主内存中的数据读取到本地内存中,然后在对数据进行操作,a+1,这时A线程的本地内存中a的值为1,当A释放锁时,也会强制将本地内存中的数据再写入到主内存中,这时主内存中a的值更新为1,同时A释放后,B线程马上获取到锁,B线程从主内存中读取数据,得到a的值为1。从整体上看,也就是A线程的操作结果对B线程是可见的。

5、synchronized优化

通过上面的介绍可以看出,synchronized最大的特征就是同一时刻只能有一个线程获得对象的监视器monitor,也就是互斥性,但是这种效率低下,因为每次只能通过一个线程。在JDK1.6之后对其做了相关优化,提高了效率。
先理解两个概念:

(1)CAS操作
a.什么是CAS

线程获取锁的策略是悲观锁策略,即假设每一次执行同步代码都会碰到冲突,所有当前线程获取到锁时就是阻塞其他线程获取到该对象的锁。而CAS则是乐观锁策略,就是假设所有线程访问共享资源时都不会发生冲突,就不会阻塞其他线程。如果出现了冲突,无锁操作就是使用CAS来鉴别线程是否出现冲突,出现冲突则重试当前操作直到没有冲突为止。

b.CAS怎么操作

CAS比较交换的过程通俗理解为CAS(v,o,n)v内存地址中存放的实际值o预期的值(旧值)n更新的新值。当vo相同时,则说明没有线程修改过共享资源的值,也就是当前共享资源没有被占用,则直接把n值赋予给v;当vo不同时,说明值被其他线程修改过,则返回v值。所有当有多个线程同时使用CAS访问一个变量时,只会有一个线程会成功,其他的会失败,失败的线程可以选择重试,或者挂起。

(2)Java对象头

在执行同步代码的时候线程会获取对象的monitor,即获取对象的锁,这里的锁其实也就是对象的标识信息。这个标识信息就是存放在Java对象的对象头中的Mark Word里。
Java对象头包含两部分:Mark Word和类型指针。
在这里插入图片描述
Mark Word存放了锁状态,hashCode,分代年龄,是否偏向锁和锁标志位信息
类型指针指向对象的类元数据,虚拟机通过这个指针确定该对象是哪个类的实例。
Java1.6后,锁一共有四种状态,级别从低到高依次为,无锁、偏向锁、轻量级锁和重量级锁。这几种状态会随着线程的竞争而升级,锁可以升级但是不能降级。这种策略是为了提升获取锁和释放锁的效率。对象的Mark Word变化如下图所示:
在这里插入图片描述

(3)偏向锁

当一个线程访问同步代码并获取锁时,会在对象头和栈帧的锁记录中记录当前线程的id,当下次访问时,就是判断对象头中记录的线程id是否指向当前线程,如果是的话,则不用CAS操作来获取锁和释放锁。表示线程已经获取了锁,如果不匹配的话,则进一步检查是否偏向锁存的值,是的话,则尝试用CAS将对象头的偏向锁指向当前线程,不是的话,则通过CAS来竞争锁。
偏向锁的释放使用了一种只有等到竞争出现才释放的机制,当有其他线程尝试竞争锁时,持有锁的线程才会释放锁。
偏向锁在Java6、7中默认是开启的,可通过-XX:-UseBiasedLocking=false来关闭,则程序会默认进入轻量级锁状态。
同时偏向锁开启在程序启动后是有几秒的延迟的,可通过-XX:BiasedLockingStartupDelay=0来关闭延迟。

(4)轻量级锁

线程在执行同步代码之前,JVM会在当前线程的栈帧中创建存储锁记录的空间,并将对象头的Mark Word复制到锁记录中,然后线程尝试用CAS将对象头的Mark Word替换为指向锁记录的指针,如果成功,则获取了锁,如果失败,则表示其他线程竞争锁,当前线程通过自旋尝试获取锁。
轻量级锁解锁时,会尝试将栈帧锁记录中的Mark Word替换回到对象头中,如果成功,则说明当前锁没有其他线程竞争,如果失败,则有其他线程竞争,锁就会升级为重量级锁。

(5)各种锁的比较

在这里插入图片描述

相关应用

使用synchronized实现线程安全的单例模式Volatile和Synchronized关键字实现单例模式(线程安全)
volatile相关详解Java高并发之volatile 关键字

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值