引言

上一篇文章中我们说过,volatile通过lock指令保证了可见性、有序性以及“部分”原子性。但在大部分并发问题中,都需要保证操作的原子性,volatile并不具有该功能,这时就需要通过其他手段来达到线程安全的目的,在Java编程中,我们可以通过锁、synchronized关键字,以及CAS操作来达到线程安全的目的。

synchronized

在Java的并发编程中,保证线程同步最为程序员所熟悉的就是synchronized关键字,synchronized关键字最为方便的地方是他不需要显示的管理锁的释放,极大减少了编程出错的概率。

在Java1.5及以前的版本中,synchronized并不是同步最好的选择,由于并发时频繁的阻塞和唤醒线程,会浪费许多资源在线程状态的切换上,导致了synchronized的并发效率在某些情况下不如ReentrantLock。在Java1.6的版本中,对synchronized进行了许多优化,极大的提高了synchronized的性能。只要synchronized能满足使用环境,建议使用synchronized而不使用ReentrantLock。

synchronized的三种使用方式

  1. 修饰实例方法,为当前实例加锁,进入同步方法前要获得当前实例的锁。
  2. 修饰静态方法,为当前类对象加锁,进入同步方法前要获得当前类对象的锁。
  3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁。

这三种使用方式大家应该都很熟悉,有一个要注意的地方是对静态方法的修饰可以和实例方法的修饰同时使用,不会阻塞,因为一个是修饰的Class类,一个是修饰的实例对象。下面的例子可以说明这一点:

public class SynchronizedTest {public static synchronized void StaticSyncTest() {for (int i = 0; i < 3; i++) {System.out.println("StaticSyncTest");try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}public synchronized void NonStaticSyncTest() {for (int i = 0; i < 3; i++) {System.out.println("NonStaticSyncTest");try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}
}public static void main(String[] args) throws InterruptedException {SynchronizedTest synchronizedTest = new SynchronizedTest();new Thread(new Runnable() {@Overridepublic void run() {SynchronizedTest.StaticSyncTest();}}).start();new Thread(new Runnable() {@Overridepublic void run() {synchronizedTest.NonStaticSyncTest();}}).start();
}//StaticSyncTest
//NonStaticSyncTest
//StaticSyncTest
//NonStaticSyncTest
//StaticSyncTest
//NonStaticSyncTest

代码中我们开启了两个线程分别锁定静态方法和实例方法,从打印的输出结果中我们可以看到,这两个线程锁定的是不同对象,可以并发执行。

synchronized的底层原理

我们看一段synchronized关键字经过编译后的字节码:

if (null == instance) {   synchronized (DoubleCheck.class) {if (null == instance) {   instance = new DoubleCheck();   }}
}



可以看到synchronized关键字在同步代码块前后加入了monitorenter和monitorexit这两个指令。monitorenter指令会获取锁对象,如果获取到了锁对象,就将锁计数器加1,未获取到则会阻塞当前线程。monitorexit指令会释放锁对象,同时将锁计数器减1。

JDK1.6对synchronized的优化

JDK1.6对对synchronized的优化主要体现在引入了“偏向锁”和“轻量级锁”的概念,同时synchronized的锁只可升级,不可降级:

这里我不打算详细讲解每种锁的实现,想了解的可以参照《深入理解Java虚拟机》,只简单说下自己的理解。

偏向锁的思想是指如果一个线程获得了锁,那么就从无锁模式进入偏向模式,这一步是通过CAS操作来做的,进入偏向模式的线程每一次访问这个锁的同步代码块时都不需要再进行同步操作,除非有其他线程访问这个锁。

偏向锁提高的是那些带同步但无竞争的代码的性能,也就是说如果你的同步代码块很长时间都是同一个线程访问,偏向锁就会提高效率,因为他减少了重复获取锁和释放锁产生的性能消耗。如果你的同步代码块会频繁的在多个线程之间访问,可以使用参数-XX:-UseBiasedLocking来禁止偏向锁产生,避免在多个锁状态之间切换。

偏向锁优化了只有一个线程进入同步代码块的情况,当多个线程访问锁时偏向锁就升级为了轻量级锁。

轻量级锁的思想是当多个线程进入同步代码块后,多个线程未发生竞争时一直保持轻量级锁,通过CAS来获取锁。如果发生竞争,首先会采用CAS自旋操作来获取锁,自旋在极短时间内发生,有固定的自旋次数,一旦自旋获取失败,则升级为重量级锁。

轻量级锁优化了多个线程进入同步代码块的情况,多个线程未发生竞争时,可以通过CAS获取锁,减少锁状态切换。当多个线程发生竞争时,不是直接阻塞线程,而是通过CAS自旋来尝试获取锁,减少了阻塞线程的概率,这样就提高了synchronized锁的性能。

synchronized的等待唤醒机制

synchronized的等待唤醒是通过notify/notifyAll和wait三个方法来实现的,这三个方法的执行都必须在同步代码块或同步方法中进行,否则将会报错。

wait方法的作用是使当前执行代码的线程进行等待,notify/notifyAll相同,都是通知等待的代码继续执行,notify只通知任一个正在等待的线程,notifyAll通知所有正在等待的线程。wait方法跟sleep不一样,他会释放当前同步代码块的锁,notify在通知任一等待的线程时不会释放锁,只有在当前同步代码块执行完成之后才会释放锁。下面的代码可以说明这一点:

public static void main(String[] args) throws InterruptedException {waitThread();notifyThread();
}private static Object lockObject = new Object();private static void waitThread() {Thread watiThread = new Thread(new Runnable() {@Overridepublic void run() {synchronized (lockObject) {System.out.println(Thread.currentThread().getName() + "wait-before");try {TimeUnit.SECONDS.sleep(2);lockObject.wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "after-wait");}}},"waitthread");watiThread.start();
}private static void notifyThread() {Thread watiThread = new Thread(new Runnable() {@Overridepublic void run() {synchronized (lockObject) {System.out.println(Thread.currentThread().getName() + "notify-before");lockObject.notify();try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();} System.out.println(Thread.currentThread().getName() + "after-notify");}}},"notifythread");watiThread.start();
}//waitthreadwait-before
//notifythreadnotify-before
//notifythreadafter-notify
//waitthreadafter-wait

代码中notify线程通知之后wait线程并没有马上启动,还需要notity线程执行完同步代码块释放锁之后wait线程才开始执行。

CAS

在synchronized的优化过程中我们看到大量使用了CAS操作,CAS全称Compare And Set(或Compare And Swap),CAS包含三个操作数:内存位置(V)、原值(A)、新值(B)。简单来说CAS操作就是一个虚拟机实现的原子操作,这个原子操作的功能就是将旧值(A)替换为新值(B),如果旧值(A)未被改变,则替换成功,如果旧值(A)已经被改变则替换失败。

可以通过AtomicInteger类的自增代码来说明这个问题,当不使用同步时下面这段代码很多时候不能得到预期值10000,因为noncasi[0]++不是原子操作。

private static void IntegerTest() throws InterruptedException {final Integer[] noncasi = new Integer[]{ 0 };for (int i = 0; i < 10; i++) {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {for (int j = 0; j < 1000; j++) {noncasi[0]++;}}});thread.start();}while (Thread.activeCount() > 2) {Thread.sleep(10);}System.out.println(noncasi[0]);
}//7889

当使用AtomicInteger的getAndIncrement方法来实现自增之后相当于将casi.getAndIncrement()操作变成了原子操作:

private static void AtomicIntegerTest() throws InterruptedException {AtomicInteger casi = new AtomicInteger();casi.set(0);for (int i = 0; i < 10; i++) {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {for (int j = 0; j < 1000; j++) {casi.getAndIncrement();}}});thread.start();}while (Thread.activeCount() > 2) {Thread.sleep(10);}System.out.println(casi.get());
}//10000

当然也可以通过synchronized关键字来达到目的,但CAS操作不需要加锁解锁以及切换线程状态,效率更高。

再来看看casi.getAndIncrement()具体做了什么,在JDK1.8之前getAndIncrement是这样实现的(类似incrementAndGet):

private volatile int value;public final int incrementAndGet() {for (;;) {int current = get();int next = current + 1;if (compareAndSet(current, next))return next;}
}

通过compareAndSet将变量自增,如果自增成功则完成操作,如果自增不成功,则自旋进行下一次自增,由于value变量是volatile修饰的,通过volatile的可见性,每次get()都能获取到最新值,这样就保证了自增操作每次自旋一定次数之后一定会成功。

JDK1.8中则直接将getAndAddInt方法直接封装成了原子性的操作,更加方便使用。

public final int getAndIncrement() {return unsafe.getAndAddInt(this, valueOffset, 1);
}

CAS操作是实现Java并发包的基石,他理解起来比较简单但同时也非常重要。Java并发包就是在CAS操作和volatile基础上建立的,下图中列举了J.U.C包中的部分类支撑图:

Java并发(4)- synchronized与CAS相关推荐

  1. Java并发编程-无锁CAS与Unsafe类及其并发包Atomic

    [版权申明]未经博主同意,谢绝转载!(请尊重原创,博主保留追究权) http://blog.csdn.net/javazejian/article/details/72772470 出自[zejian ...

  2. Java 并发实践 — ConcurrentHashMap 与 CAS

    转载自 Java 并发实践 - ConcurrentHashMap 与 CAS 最近在做接口限流时涉及到了一个有意思问题,牵扯出了关于concurrentHashMap的一些用法,以及CAS的一些概念 ...

  3. Java并发编程 synchronized保证线程安全的原理

    文章转载致博客 blog.csdn.net/javazejian/- 自己稍加完善. 线程安全是并发编程中的重要关注点,应该注意到的是,造成线程安全问题的主要诱因有两点,一是存在共享数据(也称临界资源 ...

  4. Java并发编程—Synchronized底层优化(偏向锁、轻量级锁)

    原文作者:Matrix海 子 原文地址:Java并发编程:Synchronized底层优化(偏向锁.轻量级锁) 目录 一.重量级锁 二.轻量级锁 三.偏向锁 四.其他优化 五.总结 一.重量级锁 上篇 ...

  5. Java 并发编程—Synchronized关键字

    原文作者:liuxiaopeng 原文地址:Java并发编程:Synchronized及其实现原理 目录 一.Synchronized的基本使用 二.Synchronized 原理 三.运行结果解释 ...

  6. Java并发编程 Synchronized及其实现原理

    Synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法.Synchronized的作用主要有三个:(1)确保线程互斥的访问同步代码(2)保证共享变量的修改能够及时可见 ...

  7. Java并发编程synchronized详解

    一.关于临界区.临界资源.竞态条件和解决方法 首先看如下代码,thread1对变量i++做500次运算,thread2对i--做500次运算,但是最终的结果却可能为是正数,负数,0不一样的结果. pa ...

  8. Java并发编程-synchronized锁优化

    目录 1.小故事 2.轻量级锁 3.锁膨胀 4.自旋优化 5.偏向锁 5.1.概述 5.2.偏向锁状态 5.3.偏向锁撤销 5.3.1.调用对象hashCode 5.3.2.其它线程使用对象 5.3. ...

  9. Java并发编程-synchronized底层原理

    synchronized底层原理与Monitor密切相关 1.Java对象头 以 32 位虚拟机为例 普通对象 对象的类型,如Student类型,Teacher类型等是由KlassWord来表示的,它 ...

  10. Java并发(六)——CAS、AQS、Lock、通信工具类

    文章目录 CAS.AQS.Lock.通信工具类 1 CAS 1.1 Unsafe类 1.2 Atomic包 2 AQS 3 Condition 4 ReentrantLock 4.1 公平锁部分源码 ...

最新文章

  1. php curl异步跳转,php curl批处理--可控并发异步
  2. Acwing第 4 场周赛【未完结】
  3. MySQL数据步骤管控_数据管理的发展过程分为哪三个阶段
  4. 计算机考试的基础知识高考,计算机考试基础知识试题..doc
  5. C# 取电信公网IP并发送邮件
  6. AcWing 1922. 懒惰的牛(前缀和 or 双指针)
  7. div并排排列的两种方法浅谈。
  8. 用简道云做一个可以账号密码登陆和找回密码并查询修改已填信息的公开表单
  9. Windows 10 修改域用户密码
  10. 计算导论与c语言基础pdf下载,Cousera 计算导论与C语言基础 学习笔记
  11. 【服务器数据恢复】异常断电导致ESXI系统无法连接存储的数据恢复
  12. 股票集合竞价什么意思?集合竞价时间及集合竞价技巧?
  13. 解决安卓apk在其他手机无法安装问题
  14. 【书单】程序设计好书推荐
  15. java minma_Java Core.minMaxLoc方法代码示例
  16. #今日论文推荐# 陈天奇、王威廉等人推荐:ACL最佳论文奖得主给新入行研究者的一点建议
  17. 插值算法之:拉格朗日插值
  18. 基于 Bitwarden 搭建密码管理器(群晖 Docker)
  19. Java Spire.Presentation 之PPT文本图片内容提取
  20. iPhone、iPad所有字体UIFont

热门文章

  1. 搭建自己的在线作图工具
  2. 计算机专业建设广东的措施,计算机应用技术专业建设自评报告.doc
  3. android sim卡插拔广播,Android监听SIM卡插拔的方式
  4. 202203电子学会青少年软件编程python三级真题
  5. 又一匹创新黑马:Gwallet全球路演正式开启
  6. 学校计算机老师关心学生的作文600字,【热门】学校学生作文600字三篇
  7. 寺田仓库荣获第27届万宝龙国际艺术赞助大奖
  8. [Microsoft][ODBC SQL Server Driver ]超时已过期的解决过程和方法
  9. Vue学习(四)—— vue中的ajax
  10. lotus中的CGI域