引言

上一篇文章中我们说过,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 block
e.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 block
e.printStackTrace();
}
}
}
} public static void main(String[] args) throws InterruptedException { SynchronizedTest synchronizedTest = new SynchronizedTest();
new Thread(new Runnable() {
@Override
public void run() {
SynchronizedTest.StaticSyncTest();
}
}).start();
new Thread(new Runnable() {
@Override
public 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() { @Override
public 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() { @Override
public 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() { @Override
public 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() { @Override
public 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并发编程-synchronized指南

    在多线程程序中,同步修饰符用来控制对临界区代码的访问.其中一种方式是用synchronized关键字来保证代码的线程安全性.在Java中,synchronized修饰的代码块或方法不会被多个线程并发访 ...

  2. Java并发—–深入分析synchronized的实现原理

    记得刚刚开始学习Java的时候,一遇到多线程情况就是synchronized,相对于当时的我们来说synchronized是这么的神奇而又强大,那个时候我们赋予它一个名字“同步”,也成为了我们解决多线 ...

  3. Java并发分析—synchronized

    在计算机操作系统中,并发在宏观上是指在同一时间段内,同时有多道程序在运行. 一个程序可以对应一个进程或多个进程,进程有独立的存储空间.一个进程包含一个或多个线程.线程堆空间是共享的,栈空间是私有的.同 ...

  4. 27、Java并发性和多线程-CAS(比较和替换)

    以下内容转自http://ifeve.com/compare-and-swap/: CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术.简单来说,比较和替换是使用一个期 ...

  5. Java并发编程-synchronized

    多线程的同步机制对资源进行加锁,使得在同一个时间,只有一个线程可以进行操作,同步用以解决多个线程同时访问时可能出现的问题.同步机制可以使用synchronized关键字实现.synchronized关 ...

  6. java并发:AtomicInteger 以及CAS无锁算法【转载】

    1 AtomicInteger解析 众所周知,在多线程并发的情况下,对于成员变量,可能是线程不安全的: 一个很简单的例子,假设我存在两个线程,让一个整数自增1000次,那么最终的值应该是1000:但是 ...

  7. Java并发——关键字synchronized解析

    synchronized用法 在Java中,最简单粗暴的同步手段就是synchronized关键字,其同步的三种用法: ①.同步实例方法,锁是当前实例对象 ②.同步类方法,锁是当前类对象 ③.同步代码 ...

  8. Java并发,synchronized锁住的内容

    synchronized用在方法上锁住的是什么? 锁住的是当前对象的当前方法,会使得其他线程访问该对象的synchronized方法或者代码块阻塞,但并不会阻塞非synchronized方法. 脏读 ...

  9. java并发编程--Synchronized的理解

    synchronized实现锁的基础:Java中每一个对象都可以作为锁,具体表现为3种形式. (1)普通同步方法,锁是当前实例对象 (2)静态同步方法,锁是当前类的Class对象 (3)同步方法块,锁 ...

  10. Java并发编程 | Synchronized原理与使用

    Java提供了多种机制实现多线程之间有需要同步执行的场景需求.其中最基本的是Synchronized ,实现上使用对象监视器( Monitor ). Java中的每个对象都是与线程可以锁定或解锁的对象 ...

随机推荐

  1. 什么是 Cookie

    什么是 Cookie? Cookie 是一小段文本信息,伴随着用户请求和页面在 Web 服务器和浏览器之间传递.Cookie 包含每次用户访问站点时 Web 应用程序都可以读取的信息. 例如,如果在用 ...

  2. Ubuntu server中 samba的安装和简单配置

    samba是Linux系统上的一种文件共享协议,可以实现Windows系统访问Linux系统上的共享资源,现在介绍一下如何在Ubuntu 14.04上安装和配置samba 工具/原料   Ubuntu ...

  3. struts2官方 中文教程 系列十一:使用XML进行表单验证

    在本教程中,我们将讨论如何使用Struts 2的XML验证方法来验证表单字段中用户的输入.在前面的教程中,我们讨论了在Action类中使用validate方法验证用户的输入.使用单独的XML验证文件让 ...

  4. express与ejs,ejs在Linux上面的路径问题

    1.学习使用ejs模板(这个是ejs.js) var express = require('express'); var app = express(); app.set("view eng ...

  5. "Cannot open source file "Wire.h" " in Arduino Development

    0. Environment Windows 8 x64 Arduino 1.0.5 Visual studio 2012 Visual micro Arduino 1. Steps Add &quo ...

  6. 孤荷凌寒自学python第七十四天开始写Python的第一个爬虫4

    孤荷凌寒自学python第七十四天开始写Python的第一个爬虫4 (完整学习过程屏幕记录视频地址在文末) 今天在上一天的基础上继续完成对我的第一个代码程序的书写. 直接上代码.详细过程见文末屏幕录像 ...

  7. Spring实战第五章学习笔记————构建Spring Web应用程序

    Spring实战第五章学习笔记----构建Spring Web应用程序 Spring MVC基于模型-视图-控制器(Model-View-Controller)模式实现,它能够构建像Spring框架那 ...

  8. 剑指offer-数值的整数次方12

    class Solution: def Power(self, base, exponent): # write code here if base==0: return 0 if exponent= ...

  9. [leetcode-655-Print Binary Tree]

    Print a binary tree in an m*n 2D string array following these rules: The row number m should be equa ...

  10. URAL 1732 Ministry of Truth(KMP)

    Description In whiteblack on blackwhite is written the utterance that has been censored by the Minis ...