一、悲观锁和乐观锁概念

悲观锁和乐观锁是一种广义的锁概念,Java中没有哪个Lock实现类就叫PessimisticLock或OptimisticLock,而是在数据并发情况下的两种不同处理策略。

针对同一个数据并发操作,悲观锁认为自己在使用数据时,一定有其它的线程操作数据,因此获取数据前先加锁确保数据使用过程中不会被其它线程修改;乐观锁则认为自己在使用数据的时候不会被其它线程修改。基于两者的不同点我们不难发现:

(1)悲观锁适用于写操作较多的场景。

(2)乐观锁适用于读操作较多的场景。

二、乐观锁的一种实现方案

乐观锁通常采用了无锁编程方法,基于CAS(Compare And Swap)算法实现,下面重点介绍一下该算法:

先看一个例子,假设有100个线程同时并发,且每个线程累加1000次,那么结果很容易算出时100,000,实现代码如下:

 1 public class Test {
2 private static int sum = 0;
3
4 public static void main(String[] args) throws InterruptedException {
5 final CountDownLatch latch = new CountDownLatch(100);
6 for (int i = 0; i < 100; i++) {
7 new Thread(() -> {
8 for (int j = 0; j < 1000; j++) {
9 sum += 1;
10 }
11 latch.countDown();
12 }).start();
13 }
14 latch.await();
15 System.out.println(String.format("Sum=%s", sum));
16 }
17 }

很显然,由于资源(sum变量)同步的问题,上述代码运行结果跟我们预期不一样,而且每次结果也不一样。

那么sum变量增加volatile修饰符呢?结果还是有问题,这是因此为sum +=1不是原子语句,很显然我们需要把sum+=1这个语句加锁,那么每次执行结果都一样且跟预期(100,000)相符。

定义一个可重入锁

1 private static Lock lock = new ReentrantLock();

资源加锁

1 lock.lock();
2 sum += 1;
3 lock.unlock();

ReentrantLock是基于悲观锁实现方案,每次加锁、释放锁都涉及到用户态和内核态切换(保存、恢复线程上下文以及线程调度等),因此性能损失较大。那么乐观锁又是如何实现的呢?实现方法如下:

 1 public class Test {
2 private static AtomicInteger sum = new AtomicInteger(0);
3
4 public static void main(String[] args) throws InterruptedException {
5 final CountDownLatch latch = new CountDownLatch(100);
6 for (int i = 0; i < 100; i++) {
7 new Thread(() -> {
8 for (int j = 0; j < 1000; j++) {
9 sum.addAndGet(1);
10 }
11 latch.countDown();
12 }).start();
13 }
14
15 latch.await();
16
17 System.out.println(String.format("Sum=%s", sum.get()));
18 }
19 }

上述这个例子会出现频繁写入,在实际工程中并不一定适合乐观锁,这里主要讲解一下乐观锁实现原理。

AtomicInteger是针对Integer类型的封装,除此之外还包括AtomicLong、AtomicReference等,下面重分析addAndGet这个方法。

addAndGet会调用unsafe.getAndAddInt,第一个参数是AtomaticInteger实例(sum对象);第三个参数是我们传入要累加的值;第二个参数valueOffset是AtomaticInteger中value属性(我们每次累加的结果就是保存在value中)的偏移地址,初始化代码如下:

getAndAddInt实现代码如下:

其中,var5 = this.getIntVolatile(var1, var2),var1是sum对象、var2是value的偏移量地址,getIntVolatile就是根据偏移量地址读取sum对象中存储的value值,即var5=value

compareAndSwapInt(var1, var2, var5, var5 + var4),var1是sum对象,var2是sum对象中value的偏移量地址,var5是之前读取的value值,var5+var4是本次操作期望写入的value新值。写入新值之前会判断最新的value值是否和之前获取的值(var5)相等,相等的话更新新值并返回true;否则直接返回false,不做任何操作。

当写入成功时就会跳出do-while循环,否则会一直重试,注意整个循环体是没有阻塞的,因此也避免了线程上下文切换。

compareAndSwapInt是Java的native方法,并不由Java语言实现,其底层依赖于CPU提供的指令集(比如x86的cmpxchg )保证其操作的原子性。

三、轻量级自旋锁

自旋锁是指当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程阻塞状态,自旋锁的好处是避免线程上下文切换,但是坏处也很明显,如果没有获取到锁时会不停的循环监测,这个循环监测过程就是自旋操作。

本节还是基于CAS操作实现一个简单的自旋锁,代码如下:

 1 public class SimpleSpinLock {
2
3 private AtomicReference<Thread> atomicReference = new AtomicReference<>();
4
5 public void lock() {
6 Thread currentThread = Thread.currentThread();
7 //没有获取到锁时候,处于自旋过程而不是阻塞状态。
8 while (!atomicReference.compareAndSet(null, currentThread)) {
9 }
10 System.out.println(String.format("Lock success. atomic=%s", atomicReference.get().getName()));
11 }
12
13 public void unLock() {
14 Thread currentThread = Thread.currentThread();
15 if (atomicReference.compareAndSet(currentThread, null)) {
16 System.out.println(String.format("Unlock success. atomic=%s", currentThread.getName()));
17 } else {
18 System.out.println(String.format("Unlock failure. atomic=%s", currentThread.getName()));
19 }
20 }
21 }
22
23 public class Test {
24 private static int sum = 0;
25 private static SimpleSpinLock lock = new SimpleSpinLock();
26
27 public static void main(String[] args) throws InterruptedException {
28 final CountDownLatch latch = new CountDownLatch(100);
29 for (int i = 0; i < 100; i++) {
30 Thread thread = new Thread(() -> {
31 for (int j = 0; j < 1000; j++) {
32 lock.lock();
33 sum++;
34 lock.unLock();
35 }
36 latch.countDown();
37 });
38 thread.setName(String.format("CountThread-%s", i));
39 thread.start();
40 }
41
42 latch.await();
43
44 System.out.println(String.format("Sum=%d", sum));
45 }
46 }

上述SimpleSpinLock是一个最简的实现方案,假如某个线程一直申请不到锁,那么就会一直处于空转自旋状态,这个使用我们通常会设置一个自旋次数,超过这个次数(比如10次)时膨胀成重量级的互斥锁,减少CPU空转消耗。

那么本节的最后一个问题,在实际工程使用中如何定义自旋次数?

JDK1.6引入了自适应自旋锁,所谓自适应自旋锁,就意味着自旋的次数不再是固定的,具体规则如下:

自旋次数通常由前一次在同一个锁上的自旋时间及锁的拥有者的状态决定。比如线程T1自旋10次成功,那么等到下一个线程T2自旋时,也会默认认为T2自旋10次。

如果T2自旋了5次就成功了,那么此时这个自旋次数就会缩减到5次。

四、偏向锁

偏向锁是JDK 1.6提出的一种锁优化方式。其核心思想是如果资源没有竞争,就取消之前已经取得锁得线程同步操作。具体实现方案如下:

  • 某一线程第一次获取锁时便进入偏向模式,当该线程再次请求这个锁时,无需再进行相关得同步操作(不需要CAS计算)。
  • 如果在此期间有其它线程进行了锁请求,则锁退出偏向模式。
  • 当锁处于偏向模式时,虚拟机中的Mark Word会记录获得锁得线程ID。

最后我们看一下Mark Word在哪里:

五、再谈synchronized

看完偏向锁实现方案,你是否和我一样有这样的疑问?没有资源竞争情况偏向锁才有用,一旦有有竞争偏向锁就失效了,那么在没有资源竞争的情况下,我为什么要加锁呢?好吧,本节的最后我将回答这个问题。

synchronized在JDK 1.5的早期版本中使用重量级锁(通过Monitor关联到操作系统的互斥锁),效率很低,因此JDK 1.6做了大幅度优化,整个资源同步过程支持锁升级(无锁、偏向锁、轻量级锁、重量级锁),且升级后不能降级。这一升级过程都伴随着Mark Word存储内容的改变,Mark Word会根据对象的不同状态存放不同的数据,数据格式如下:

好吧,到这里我们来回答一下开头的那个疑惑,早期的Java版本提供了Vector、HashTable、StringBuffer 等这些线程安全的集合,其内部实现依赖于synchronized实现重量级锁,因此效率低下,但是开发人员使用这些集合时大部分都是在单线程环境下,并不会出现资源竞争的场景,因此在后续优化synchornized时,顺便增加了这个偏向锁在保证可能出现并发的情况下提高的Vector、HashTable执行效率。然而今天我们在写Java代码时,任何一本编码规范都有要求我们优先考虑ArrayList、HashMap、StringBuilder这些非线程安全的集合,那么我们还需要偏向锁吗?O(∩_∩)O

Java基础之(一)——从synchronized优化看Java锁概念的更多相关文章

  1. synchronized优化手段:锁膨胀、锁消除、锁粗化和自适应自旋锁...

    synchronized 在 JDK 1.5 时性能是比较低的,然而在后续的版本中经过各种优化迭代,它的性能也得到了前所未有的提升,上一篇中我们谈到了锁膨胀对 synchronized 性能的提升,然 ...

  2. java基础---->多线程之synchronized(六)

    这里学习一下java多线程中的关于synchronized的用法.我来不及认真地年轻,待明白过来时,只能选择认真地老去. synchronized的简单实例 一. synchronized在方法上的使 ...

  3. Java基础学习总结(68)——有关Java线程方面的面试题

    不管你是新程序员还是老手,你一定在面试中遇到过有关线程的问题.Java 语言一个重要的特点就是内置了对并发的支持,让 Java 大受企业和程序员的欢迎.大多数待遇丰厚的 Java 开发职位都要求开发者 ...

  4. synchronized 优化手段之锁膨胀机制!

    synchronized 在 JDK 1.5 之前性能是比较低的,在那时我们通常会选择使用 Lock 来替代 synchronized.然而这个情况在 JDK 1.6 时就发生了改变,JDK 1.6 ...

  5. Java基础14:离开IDE,使用java和javac构建项目

    更多内容请关注微信公众号[Java技术江湖] 这是一位阿里 Java 工程师的技术小站,作者黄小斜,专注 Java 相关技术:SSM.SpringBoot.MySQL.分布式.中间件.集群.Linux ...

  6. Java基础学习总结(70)——开发Java项目常用的工具汇总

    要想全面了解java开发工具,我们首先需要先了解一下java程序的开发过程,通过这个过程我们能够了解到java开发都需要用到那些工具. 首先我们先了解完整项目开发过程,如图所示: 从上图中我们能看到一 ...

  7. java基础 (记事本编写hello world,path,classpath,java的注释符)

    一:java的基本信息 jre 是指java运行环境,jdk 是指 java 开发工具集(并且里面是自带有jre运行环境的) jvm是指java的虚拟机 java的源代码的后缀名是 .java (例如 ...

  8. java基础(一):我对java的三个环境变量的简单理解和配置

    首先说说java的三个环境变量:java_home,classpath,path java_home:jdk的安装路径[你一层一层点开安装路径,直到当前目录有一个bin目录,然后在地址栏里面右键单击复 ...

  9. java基础-构建命令行运行的java程序简要注意

    今天编写了一个运行在服务端的java工具类,才发现自己以前很少关注运营方面的内容,导致在服务端部署一个java的工具变得异常困难,其实这也是自己对java的了解不够造成的. 首先,当代码编写完成之后, ...

随机推荐

  1. [刷题] 70 Climbing Stairs

    要求 楼梯共有n个台阶,每次上一个台阶或两个台阶,一共有多少种上楼梯的方法? 示例 输入:n=3 [1,1,1],[1,2,],[2,1] 输出:n=3 实现 自顶向下(递归) 递归 1 class ...

  2. CentOS 7磁盘寻找不到,卡在sulogin,造成的开机失败问题--Error getting authority...

    今天早上使用内网gitlab仓库的时候,发现页面无法打开,ssh也无法连接. 到机房接上显示器,发现如下错误: Error getting authority: Error initializing ...

  3. mysql默认值

    1.创建表时添加默认值 语法: <字段名><类型><默认值> 实例: MySQL [wordpress]> create table ly_content(  ...

  4. liveCD版: CD光盘映像,和liveDVD一样,唯一的区别就是该版本中包含的软件包会少一点,安装系统时使用 U 盘或者CD光盘进行安装。

    https://man.linuxde.net/download/CentOS/ CentOS,英文全称"Community Enterprise Operating System" ...

  5. 云计算OpenStack---云计算、大数据、人工智能(14)

    一.互联网行业及云计算 在互联网时代,技术是推动社会发展的驱动,云计算则是一个包罗万象的技术栈集合,通过网络提供IAAS.PAAS.SAAS等资源,涵盖从数据中心底层的硬件设置到最上层客户的应用.给我 ...

  6. shell基础之后台运行脚本

    使shell脚本后台执行,基本的方法有两种,第一种为在脚本后面追加&符号,第二种为在脚本前面使用nohup命令,结尾再追加&符号 一.后台运行脚本1 1.执行脚本test.sh:./t ...

  7. git OpenSSL SSL_connect问题

    遇到这个问题,查找别人也遇到,省时间不写了直接复制 在使用Git来克隆仓库报了错误,如下: fatal: unable to access 'https://github.com/xingbuxing ...

  8. MyBatis 高级查询之一对一查询(九)

    高级查询之一对一查询 查询条件:根据游戏角色ID,查询账号信息 我们在之前创建的映射器接口 GameMapper.java 中添加接口方法,如下: /** * 根据角色ID查询账号信息 * @para ...

  9. python3 贪吃蛇小游戏

    贪吃蛇 #!/usr/bin/env python import pygame,sys,time,random from pygame.locals import * # 定义颜色变量 redColo ...

  10. Java反射机制详情(2)

    | |目录 运行环境 Java语言的反射机制 Class中的常用方法(获得类的构造方法) Class中的常用方法(获得类的属性) Class中的常用方法(获得类的方法) 反射动态调用类的成员 1.运行 ...