2019独角兽企业重金招聘Python工程师标准>>>

乐观锁、悲观锁、公平锁、自旋锁、偏向锁、轻量级锁、重量级锁、锁膨胀...难理解?不存的!来,话不多说,带你飙车。

上一篇介绍了线程池的使用,在享受线程池带给我们的性能优势之外,似乎也带来了另一个问题:线程安全的问题。

那什么是线程的安全问题呢?

一、线程安全问题的产生

线程安全问题:指的是在多线程编程中,同时操作同一个可变的资源之后,造成的实际结果与预期结果不一致的问题。

比如:A和B同时向C转账10万元。如果转账操作不具有原子性,A在向C转账时,读取了C的余额为20万,然后加上转账的10万,计算出此时应该有30万,但还未来及将30万写回C的账户,此时B的转账请求过来了,B发现C的余额为20万,然后将其加10万并写回。然后A的转账操作继续——将30万写回C的余额。这种情况下C的最终余额为30万,而非预期的40万。

如果上面的内容您还没有理解,没关系,我们来看下面非安全线程的模拟代码:

public class ThreadSafeSample {public int number;public void add() {for (int i = 0; i < 100000; i++) {int former = number++;int latter = number;if (former != latter-1){System.out.printf("非相等 former=" +  former + " latter=" + latter);}}}public static void main(String[] args) throws InterruptedException {ThreadSafeSample threadSafeSample = new ThreadSafeSample();Thread threadA = new Thread(new Runnable() {@Overridepublic void run() {threadSafeSample.add();}});Thread threadB = new Thread(new Runnable() {@Overridepublic void run() {threadSafeSample.add();}});threadA.start();threadB.start();threadA.join();threadB.join();}
}

我电脑运行的结果: 非相等 => former=5555 latter=6061

可以看到,仅仅是两个线程的低度并发,就非常容易碰到 former 和 latter 不相等的情况。这是因为,在两次取值的过程中,其他线程可能已经修改了number.

二、线程安全的解决方案

线程安全的解决方案分为以下几个维度(参考《码出高效:Java开发手册》):

  • 数据单线程可见(单线程操作自己的数据是不存在线程安全问题的,ThreadLocal就是采用这种解决方案);
  • 数据只读;
  • 使用线程安全类(比如StringBuffer就是一个线程安全类,内部是使用synchronized实现的);
  • 同步与锁机制;

解决线程安全核心思想是:“要么只读,要么加锁”,解决线程安全的关键在于合理的使用Java提供的线程安全包java.util.concurrent简称JUC。

三、线程同步与锁

Java 5 以前,synchronized是仅有的同步手段,Java 5的时候增加了ReentrantLock(再入锁)它的语义和synchronized基本相同,比synchronized更加灵活,可以做到更多的细节控制,比如锁的公平性/非公平性指定。

3.1 synchronized

synchronized 是 Java 内置的同步机制,它提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其他试图获取的线程只能等待或者阻塞在那里。

3.1.1 synchronized 使用

synchronized 可以用来修饰方法和代码块。

3.1.1.1 修饰代码块

synchronized (this) {int former = number++;int latter = number;//...
}

3.1.1.2 修饰方法

public synchronized void add() {//...
}

3.1.2 synchronized 底层实现原理

synchronized 是由一对 monitorenter/monitorexit 指令实现的,Monitor 对象是同步的基本实现单元。在 Java 6 之前,Monitor的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作,性能也很低。但在Java 6的时候,JVM 对此进行了大刀阔斧地改进,提供了三种不同的 Monitor 实现,也就是常说的三种不同的锁:偏向锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。

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

偏向锁是为了解决在没有多线程的访问下,尽量减少锁带来的性能开销。

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

重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

3.1.2.2 锁膨胀(升级)原理

Java 6 之后优化了 synchronized 实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,减低了锁带来的性能消耗,也就是我们常说的锁膨胀或者叫锁升级,那么它是怎么实现锁升级的呢?

锁膨胀(升级)原理: 在锁对象的对象头里面有一个ThreadId字段,在第一次访问的时候ThreadId为空,JVM让其持有偏向锁,并将ThreadId设置为其线程id,再次进入的时候会先判断ThreadId是否尤其线程id一致,如果一致则可以直接使用,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,不会堵塞,执行一定次数之后就会升级为重量级锁,进入堵塞,整个过程就是锁膨胀(升级)的过程。

3.1.2.3 自旋锁

自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

3.1.2.4 乐观锁/悲观锁

悲观锁和乐观锁并不是某个具体的“锁”而是一种是并发编程的基本概念。

悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。

乐观锁则与 Java 并发包中的 AtomicFieldUpdater 类似,也是利用 CAS 机制,并不会对数据加锁,而是通过对比数据的时间戳或者版本号,来实现乐观锁需要的版本判断。

3.1.2.5 公平锁/非公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁。

非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。

如果使用 synchronized 使用的是非公平锁,是不可设置的,这也是主流操作系统线程调度的选择。通用场景中,公平性未必有想象中的那么重要,Java 默认的调度策略很少会导致 “饥饿”发生。非公平锁的吞吐量大于公平锁。

非公平锁吞吐量大于公平锁的原因:

比如A占用锁的时候,B请求获取锁,发现被A占用之后,堵塞等待被唤醒,这个时候C同时来获取A占用的锁,如果是公平锁C后来者发现不可用之后一定排在B之后等待被唤醒,而非公平锁则可以让C先用,在B被唤醒之前C已经使用完成,从而节省了C等待和唤醒之间的性能消耗,这就是非公平锁比公平锁吞吐量大的原因。

3.2 ReentrantLock

ReentrantLock只能修饰代码块,使用ReentrantLock必须手动unlock释放锁,不然锁永远会被占用。

3.2.1 ReentrantLock 使用

ReentrantLock reentrantLock = new ReentrantLock(true); // 设置为true为公平锁,默认是非公平锁
reentrantLock.lock();
try {}finally {reentrantLock.unlock();
}

3.2.2 ReentrantLock 优势

  • 具备尝试非阻塞地获取锁的特性:当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁;

  • 能被中断地获取锁的特性:与synchronized不同,获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放;

  • 超时获取锁的特性:在指定的时间范围内获取锁;如果截止时间到了仍然无法获取锁则返回。

3.2.3 ReentrantLock 注意事项

  • 在finally中释放锁,目的是保证在获取锁之后,最终能够被释放;
  • 不要将获取锁的过程写在try块内,因为如果在获取锁时发生了异常,异常抛出的同时,也会导致锁无故被释放;
  • ReentrantLock提供了一个newCondition的方法,以便用户在同一锁的情况下可以根据不同的情况执行等待或唤醒的动作;

3.3 synchronized和ReentrantLock区别

从性能角度,synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大。但是在 Java 6 中对其进行了非常多的改进,在高竞争情况下,ReentrantLock 仍然有一定优势。在大多数情况下,无需太纠结于性能,还是考虑代码书写结构的便利性、可维护性等。

主要区别如下:

  1. ReentrantLock使用起来比较灵活,但是必须有释放锁的配合动作;
  2. ReentrantLock必须手动获取与释放锁,而synchronized不需要手动释放和开启锁;
  3. ReentrantLock只适用于代码块锁,而synchronized可用于修饰方法、代码块等;

参考资料

《码出高效:Java开发手册》

Java核心技术36讲:http://t.cn/EwUJvWA

Java中的锁分类:https://www.cnblogs.com/qifengshi/p/6831055.html

课程推荐:

转载于:https://my.oschina.net/u/3471412/blog/2906818

Java核心(三)并发中的线程同步与锁相关推荐

  1. Java 多线程和并发编程:(二)线程同步 Lock 锁

    线程同步 Lock 锁 1.Lock 锁 2.步骤 3.Lock 与 synchronized 的区别 1.Lock 锁 Lock 锁:对需要上锁的地方上锁 JDK1.5 后新增的功能 与 Synch ...

  2. Linux中的线程同步机制-futex

    Linux中的线程同步机制(一) -- Futex 引子 在编译2.6内核的时候,你会在编译选项中看到[*] Enable futex support这一项,上网查,有的资料会告诉你"不选这 ...

  3. java线程同步的实现_Java并发编程(三) - 实战:线程同步的实现

    synchronized关键字 首先,来看一个多线程竞争临界资源导致的同步不安全问题. package com.example.weishj.mytester.concurrency.sync; /* ...

  4. 对Java多线程编程的初步了解,实现多线程的三种方式以及多线程并发安全的线程同步机制

    什么叫进程?什么叫线程? 进程相当于一个应用程序,线程就是进程中的一个应用场景或者说是一个执行单元,一个进程可以启动多个线程,每个线程执行不同的任务,一个线程不能单独存在,他必须是进程的一部分,当进程 ...

  5. Java Learning:并发中的同步锁(synchronized)

    引言 最近一段时间,实验室已经倾巢出动找实习了,博主也凑合了一把,结果有悲有喜,BAT理所应当的跪了,也收到了其他的offer,总的感受是有必要夯实基础啊. 言归正传,最近在看到java多线程的时候, ...

  6. JAVA并发编程3_线程同步之synchronized关键字

    在上一篇博客里讲解了JAVA的线程的内存模型,见:JAVA并发编程2_线程安全&内存模型,接着上一篇提到的问题解决多线程共享资源的情况下的线程安全问题. 不安全线程分析 public clas ...

  7. Java并发编程之线程同步

    线程安全就是防止某个对象或者值在多个线程中被修改而导致的数据不一致问题,因此我们就需要通过同步机制保证在同一时刻只有一个线程能够访问到该对象或数据,修改数据完毕之后,再将最新数据同步到主存中,使得其他 ...

  8. java 终止方法_Java中终止线程的三种方法

    Java中终止线程的三种方法 Thread.stop, Thread.suspend, Thread.resume 和Runtime.runFinalizersOnExit 这些终止线程运行的方法已经 ...

  9. java中的线程同步机制讲解

    文章目录 锁 锁的概述 锁的作用 保证原子性 保证可见性 保证有序性 锁的分类 内部锁 synchronized 概念 原理 使用 可重入性 显示锁 使用 内存屏障 概念 分类 按可见性划分 按有序性 ...

最新文章

  1. QIIME 2用户文档. 20命令行界面q2cli(2019.7)
  2. OSChina 周六乱弹 —— 买楼出一块钱,你们出么?
  3. 1.1 对象的概念及面向对象的三个基本特征
  4. boost::graph模块实现读写graphml的测试程序
  5. IAR astyle代码美化
  6. 10分钟解决Redis安装和Springboot整合
  7. Redis学习总结(3)——Redis整合Spring结合使用缓存实例
  8. ffmpeg下载rtmp flv
  9. 音视频开发-websocket教程
  10. Java多线程核心技术
  11. 二叉树的三种非递归遍历
  12. FL计算机软件,FL Studio水果编曲软件
  13. 大学计算机知识竞赛幽默主持词,知识竞赛幽默主持词.docx
  14. java读取文件的万能解决方案
  15. 打开计算机显示远程控制,win7系统远程协助怎么打开?开启远程协助功能教程...
  16. 小程序上传图片/上传视频
  17. JQ实现一个放大镜效果
  18. Pytorch.Dataloader 详细深度解读和微修改源代码心得
  19. JFFS2文件系统挂载过程(5)
  20. Linux协议栈--NAPI机制

热门文章

  1. linux ftp做yum源,在RedHat5下架设yum源服务器(FTP)
  2. python读取 application_python PyQt5.QtWidgets.QApplication类(sys.argv)(app应用对象类)...
  3. CMU算法新教材的获取方法
  4. softsign与tanh的比较
  5. 福师《计算机应用基础》期末考试,21年2月福师《计算机应用基础》期末考试A卷答案...
  6. linux加密格式化吗,linux环境下给文件加密/解密的方法
  7. sql能查到数据 dataset对象里面没有值_新零售数据分析报告
  8. python批量处理csv_Python批量处理csv并保存过程代码解析
  9. 科学计算机乱码,软件界面乱码可以这么“破”
  10. 去掉“3_人民日报语料”中每行前边的数字编号,改成“1, 2,......”