Java synchronized与CAS使用方式详解

目录
  • 引言
  • synchronized
  • synchronized的三种使用方式
  • synchronized的底层原理
  • JDK1.6对synchronized的优化
  • synchronized的等待唤醒机制
  • CAS

引言

上一篇文章中我们说过,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() {@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 synchronized与CAS使用方式详解的文章就介绍到这了,更多相关Java synchronized与CAS内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java的锁机制:synchronized和CAS详解

    目录 一 为什么要用锁 二 synchronized怎么实现的 三 CAS来者何人 四synchronized和CAS孰优孰劣 轻量级锁 重量级锁 总结 提到Java的知识点一定会有多线程,JDK版本不断的更迭很多新的概念和方法也都响应提出,但是多线程和线程安全一直是一个重要的关注点.比如说我们一入门就学习的synchronized怎么个实现和原理,还有总是被提到的CAS是啥,他和synchronized关系是啥?这里大概会让你对这些东西有一个认识. 一 为什么要用锁 我们使用多线程肯定是为了提

  • java synchronized的用法及原理详解

    目录 为什么要用synchronized 使用方式 字节码语义 对象锁(monitor) 锁升级过程 为什么要用synchronized 相信大家对于这个问题一定都有自己的答案,这里我还是要啰嗦一下,我们来看下面这段车站售票的代码: /** * 车站开两个窗口同时售票 */ public class TicketDemo { public static void main(String[] args) { TrainStation station = new TrainStation(); //

  • classloader类加载器_基于java类的加载方式详解

    基础概念 Classloader 类加载器,用来加载 Java 类到 Java 虚拟机中.与普通程序不同的是.Java程序(class文件)并不是本地的可执行程序.当运行Java程序时,首先运行JVM(Java虚拟机),然后再把Java class加载到JVM里头运行,负责加载Java class的这部分就叫做Class Loader. JVM本身包含了一个ClassLoader称为Bootstrap ClassLoader,和JVM一样,BootstrapClassLoader是用本地代码实现

  • Java数据库连接_jdbc-odbc桥连接方式(详解)

    jdbc-odbc桥连接方式操作数据库SU(Course) 步骤: 1.配置数据源 控制面板下搜索管理工具->ODBC数据源(32位)->添加->选择sql server(填写名称mytest,服务器local或者.)->下一步->更改默认的数据库为SU->下一步->测试数据源至成功 用户数据源会多一条mytest,至此配置数据源成功. 2.在程序中连接数据源 打开eclipse,编写程序. public class Demo_1 { public static

  • 看过就懂的java零拷贝及实现方式详解

    目录 前言 1.什么是零拷贝 2. 传统 IO 的执行流程 3. 零拷贝相关的知识点回顾 3.1 内核空间和用户空间 3.2 什么是用户态.内核态 3.3 什么是上下文切换 3.4 虚拟内存 3.5 DMA技术 4. 零拷贝实现的几种方式 4.1 mmap+write实现的零拷贝 4.2 sendfile实现的零拷贝 4.3 sendfile+DMA scatter/gather实现的零拷贝 5. java提供的零拷贝方式 5.1 Java NIO对mmap的支持 5.2 Java NIO对se

  • Java synchronized锁升级jol过程详解

    jol(java object layout)需要的依赖 <dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.10</version> </dependency> 一.synchronized锁对象的升级(膨胀)过程主要如下: 1.膨胀过程:无锁(锁对象初始化时)-> 偏向

  • Java SpringAOP技术之注解方式详解

    目录 1.配置xml扫描注解 2.配置注解 3.配置文件中开启自动代理 4.通知类型注解 5.测试类 6.结果 总结 1.配置xml扫描注解 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchem

  • Java开发中synchronized的定义及用法详解

    概念 是利用锁的机制来实现同步的. 互斥性:即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程中的协调机制,这样在同一时间只有一个线程对需同步的代码块(复合操作)进行访问.互斥性我们也往往称为操作的原子性. 可见性:必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作从而引起不一致. 用法 修饰静态方法: //同步静态方法 public synchronized

  • Java多线程通信实现方式详解

    这篇文章主要介绍了Java多线程通信实现方式详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 线程通信的方式: 1.共享变量 线程间通信可以通过发送信号,发送信号的一个简单方式是在共享对象的变量里设置信号值.线程A在一个同步块里设置boolean型成员变量hasDataToProcess为true,线程B也在同步代码块里读取hasDataToProcess这个成员变量.这个简单的例子使用了一个持有信号的对象,并提供了set和get方法. pu

  • java打jar包的几种方式详解

    一.制作只含有字节码文件的jar包 我们先来看只含有字节码文件,即只含有class文件的jar包怎么制作,这是最简单的形式 1.最简单的jar包--直接输出hello 最终生成的jar包结构 META-INF Hello.class 方法步骤 (1)用记事本写一个Hello.java的文件 class Hello{     public static void main(String[] agrs){         System.out.println("hello");     }

  • Java中遍历ConcurrentHashMap的四种方式详解

    这篇文章主要介绍了Java中遍历ConcurrentHashMap的四种方式详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 方式一:在for-each循环中使用entries来遍历 System.out.println("方式一:在for-each循环中使用entries来遍历");for (Map.Entry<String, String> entry: map.entrySet()) { System.out.pr

随机推荐

其他