- ubuntu12.04环境下使用kvm ioctl接口实现最简单的虚拟机
- Ubuntu 通过无线网络安装Ubuntu Server启动系统后连接无线网络的方法
- 在Ubuntu上搭建网桥的方法
- ubuntu 虚拟机上网方式及相关配置详解
CFSDN坚持开源创造价值,我们致力于搭建一个资源共享平台,让每一个IT人在这里找到属于你的精彩世界.
这篇CFSDN的博客文章java synchronized的用法及原理详解由作者收集整理,如果你对这篇文章有兴趣,记得点赞哟.
相信大家对于这个问题一定都有自己的答案,这里我还是要嗦一下,我们来看下面这段车站售票的代码:
/*** 车站开两个窗口同时售票*/public class TicketDemo { public static void main(String[] args) { TrainStation station = new TrainStation(); // 开启两个线程同时进行售票 new Thread(station, "A").start(); new Thread(station, "B").start(); }}class TrainStation implements Runnable { private volatile int ticket = 10; @Override public void run() { while (ticket > 0) { System.out.println("线程" + Thread.currentThread().getName() + "售出" + ticket + "号票"); ticket = ticket - 1; } }}
上面这段代码是没有做考虑线程安全问题的,执行这段代码可能会出现下面的运行结果:
可以看出,两个线程都买出了10号票,这在实际业务场景中是绝对不能出现的。(你去坐火车有个大哥说你占了他的座,让你滚,还说你是票贩子,你气不气) 。
那因为有这种问题的存在,我们应该怎么解决呢?synchronized就是为了解决这种多线程共享数据安全问题的.
。
synchronized的使用方式主要以下三种.
同步代码块 。
public static void main(String[] args) { String str = "hello world"; synchronized (str) { System.out.println(str); }}
同步实例方法 。
class TrainStation implements Runnable { private volatile int ticket = 100; // 关键字直接写在实例方法签名上 public synchronized void sale() { while (ticket > 0) { System.out.println("线程" + Thread.currentThread().getName() + "售出" + ticket + "号票"); ticket = ticket - 1; } } @Override public void run() { sale(); }}
同步静态方法 。
class TrainStation implements Runnable { // 注意这里ticket变量声明为static的,因为静态方法只能访问静态变量 private volatile static int ticket = 100; // 也可以直接放在静态方法的签名上 public static synchronized void sale() { while (ticket > 0) { System.out.println("线程" + Thread.currentThread().getName() + "售出" + ticket + "号票"); ticket = ticket - 1; } } @Override public void run() { sale(); }}
。
通过程序运行,我们发现通过synchronized关键字确实可以保证线程安全,那计算机到底是怎么保证的呢?这个关键字背后到底做了些什么?我们可以看一下java代码编译后的class文件。首先来看同步代码块编译后的class。通过javap -v 名称可以查看字节码文件:
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=4, args_size=1 0: ldc #2 // String hello world 2: astore_1 3: aload_1 4: dup 5: astore_2 6: monitorenter // 监视器进入 7: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 10: aload_1 11: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 14: aload_2 15: monitorexit // 监视器退出 16: goto 24 19: astore_3 20: aload_2 21: monitorexit 22: aload_3 23: athrow 24: return
注意看第6行和第15行,这两个指令是增加synchronized代码块之后才会出现的,monitor是一个对象的监视器,monitorenter代表这段指令的执行要先拿到对象的监视器之后,才能接着往下执行,而monitorexit代表执行完synchronized代码块之后要从对象监视器中退出,也就是要释放。所以这个对象监视器也就是我们所说的锁,获取锁就是获取这个对象监视器的所有权.
接下来我们在看看synchronized修饰实例方法时的字节码文件是什么样的.
public synchronized void sale(); descriptor: ()V //方法标识ACC_PUBLIC代表public修饰,ACC_SYNCHRONIZED指明该方法为同步方法 flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=3, locals=1, args_size=1 0: aload_0 1: getfield #2 // Field ticket:I // 省略其他无关字节码
可以看到synchronized修饰实例方法上之后不会再有monitorenter和monitorexit指令,而是直接在这个方法上增加一个ACC_SYNCHRONIZED的flag。当程序在运行时,调用sale()方法时,会检查该方法是否有ACC_SYNCHRONIZED访问标识,如果有,则表明该方法是同步方法,这时候还行线程会先尝试去获取该方法对应的监视器(monitor)对象,如果获取成功,则继续执行该sale()方法,在执行期间,任何其他线程都不能再获取该方法监视器的使用权,知道该方法执行完毕或者抛出异常,才会释放,其他线程可以重新获得该监视器.
那么synchronized修饰静态方法的字节码文件是什么样呢?
public static synchronized void sale(); descriptor: ()V flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED Code: stack=3, locals=0, args_size=0 0: getstatic #2 // Field ticket:I // 省略其他无关字节码
可以看出synchronized修饰静态方法和实例方法没有区别,都是增加一个ACC_SYNCHRONIZED的flag,静态方法只是比实例方法多一个ACC_STATIC标识代表这个方法是静态的.
以上的同步代码块,同步方法中都提到对象监视器这个概念,那么三种同步方式使用的对象监视器具体是哪个对象呢?
同步代码块的对象监视器就是使用的我们synchronized(str)中的str,也就是我们括号中指定的对象。而我们在开发中增加同步代码块的目的是为了多个线程同一时间只能有一个线程持有监视器,所以这个对象的指定一定要是多个线程共享的对象,不能直接在括号中new一个对象,这样不能做到互斥,也就不能保证安全.
同步实例方法的对象监视器是当前这个实例,也就是this.
同步静态方法的对象监视器是当前这个静态方法所在类的Class对象,我们都知道Java中每个类在运行过程中也会用一个对象表示,就是这个类的对象,每个类有且仅有一个.
。
上面说了线程要进入同步代码块需要先获取到对象监视器,也就是对象锁,那在开始说之前我们先来了解下在Java中一个对象都由哪些东西组成.
这里先问大家一个问题,Object obj = new Object()这段代码在JVM中是怎样的一个内存分布?
想必了解过JVM知识的同学应该都知道,new Object()会在堆内存中创建一个对象,Object obj是栈内存中的一个引用,这个引用指向堆中的对象。那么怎么知道堆内存中的对象到底由哪些内容组成呢?这里给大家介绍一个工具叫JOL(Java Object Layout)Java对象布局。可以通过maven在项目中直接引入.
<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.9</version></dependency>
引入之后在代码中可以打印出对象的内存分布.
public static void main(String[] args) { Object obj = new Object(); // parseInstance将对象解析,toPrintable让解析后的结果可输出 System.out.println(ClassLayout.parseInstance(obj).toPrintable());}
输出后的结果如下:
java.lang.Object object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 。
从结果上可以看出,这个obj对象主要分4部分,每部分的SIZE=4代表4个字节,前三行是对象头object header,最后一行的4个字节是为了保证一个对象的大小能是8的整数倍.
我们再来看看对于一个加了锁的对象,打印出来有什么不一样?
public static void main(String[] args) { Object obj = new Object(); synchronized (obj){ System.out.println(ClassLayout.parseInstance(obj).toPrintable()); }}
java.lang.Object object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 58 f7 19 01 (01011000 11110111 00011001 00000001) (18478936) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 。
可以很明显的看到,最前面的8个字节发生了变化,也就是Mark Word变了。所以给对象加锁,实际就是改变对象的Mark Word.
Mark Word中的这8个字节具有不同的含义,为了让这64个bit能表示更多信息,JVM将最后2位设置为标记位,不同标记位下的Mark word含义如下:
其中最后两位的锁标记位,不同值代表不同含义.
。
biased_lock | lock | 状态 |
---|---|---|
0 | 00 | 无锁态(NEW) |
0 | 01 | 偏向锁 |
1 | 01 | 偏向锁 |
0 | 00 | 轻量级锁 |
0 | 10 | 重量级锁 |
0 | 11 | GC标记 |
biased_lock标记该对象是否启用偏向锁,1代表启用偏向锁,0代表未启用.
age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因.
identity_hashcode:25位的对象标识Hash码,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中.
thread:持有偏向锁的线程ID.
epoch:偏向时间戳.
ptr_to_lock_record:指向栈中锁记录的指针.
ptr_to_heavyweight_monitor:指向管程Monitor的指针.
。
既然会有无锁,偏向锁,轻量级锁,重量级锁,那么这些锁是怎么样一个升级过程呢,我们来看一下.
新建 。
从前面讲到对象头的结构和我们上面打印出来的对象内存分布,可以看出新创建的一个对象,它的标记位是00,偏向锁标记(biased_lock)也是0,表示该对象是无锁态.
偏向锁 。
偏向锁是指当一段同步代码被同一个线程所访问时,不存在其他线程的竞争时,那么该线程在以后访问时便会自动获得锁,从而降低获取锁带来的消耗,提高性能.
当一个线程访问同步代码块并获取锁时,会在 Mark Word 里存储线程 ID。在线程进入和退出同步块时不再通过 CAS 操作来加锁和解锁,而是检测 Mark Word 里是否存储着指向当前线程的偏向锁。轻量级锁的获取及释放依赖多次 CAS 原子指令,而偏向锁只需要在置换 ThreadID 的时候依赖一次 CAS 原子指令即可.
轻量级锁 。
轻量级锁是指当锁是偏向锁的时候,有其他线程来竞争,但是该锁正在被其他线程访问,那么就会升级为轻量级锁。或者还有一种情况就是关闭JVM的偏向锁开关,那么一开始锁对象就会被标记位轻量级锁.
轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放.
在进入同步代码时,如果对象锁状态符合升级轻量级锁的条件,虚拟机会在当前想要竞争锁的线程的栈帧中开辟一个Lock Record空间,并将锁对象的Mark Word拷贝到Lock Record空间中.
然后虚拟机会使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record中的owner指针指向对象的Mark Word.
如果操作成功,则表示当前线程获得锁,如果失败则表示其他线程持有该锁,当前线程会尝试使用自旋的方式来重新获取.
轻量级锁解锁时,会使用CAS操作将Lock Record替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁.
重量级锁 。
重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。是依赖于底层操作系统的Mutex实现,Mutex也叫互斥锁。也就是说重量级锁会让锁从用户态切换到内核态,将线程的调度交给操作系统,性能相比会很低.
整个锁升级的过程通过下面这张图能更全面的展示.
到此这篇关于java synchronized的用法及原理详解的文章就介绍到这了,更多相关java synchronized内容请搜索我以前的文章或继续浏览下面的相关文章希望大家以后多多支持我! 。
原文链接:https://www.cnblogs.com/heiz123/p/15205145.html 。
最后此篇关于java synchronized的用法及原理详解的文章就讲到这里了,如果你想了解更多关于java synchronized的用法及原理详解的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
最近我在用 RestSharp消耗我的 Restful 资源。并期望在服务器和客户端之间与 JSon 交换数据。下面是我的 C# 代码。 var client = new RestSharp.Rest
我正在阅读 Bartosz Milewski 的一篇文章,其中他定义了以下函数: instance Applicative Chan where pure x = Chan (repeat x)
‘…' 其实是go的一种语法糖。 它的第一个用法主要是用于函数有多个不定参数的情况,可以接受多个不确定数量的参数。 第二个用法是slice可以被打散进行传递。 实例:
前言 在算face_track_id map有感: 开始验证 data={"state":[1,1,2,2,1,2,2,2],"pop":[&quo
本文实例讲述了php访问数组最后一个元素的函数end()用法。分享给大家供大家参考。具体分析如下: end()函数在PHP中用于检索数组中的最后一个元素。end()函数需要一个数组作为其唯一参数,
我使用的是 jdk1.8.0_92。我的虚拟机如下所示。 $java -version java version "1.8.0_92" Java(TM) SE Runtime Environment
我的情况是我需要将所有匹配 http://mywebsite.com/portfolio/[anyname] 的请求定向到 http://mywebsite.com/portfolio.php?用户名
我正在尝试在 NLTK 中使用语音标记并使用了以下命令: >>> text = nltk.word_tokenize("And now for something completely differe
#include typedef QList IntList; qRegisterMetaType("IntList"); error C2909: 'qRegisterMetaType':
来自 here我知道 BN_CTX 是一个保存 BIGNUM 临时变量的结构。这些 BIGNUM 变量什么时候会进入 BN_CTX 的 BN_POOL?如果我有一个 bignum_ctx BN_CTX
尝试为 ABPersonRef 创建对象例子:ABpersonRef 引用; 已包含Addressbook和AddressBookUI框架即使这样,当我编译时,它仍显示“ABPersonRef”未声明
我无法使用 GetAltTabInfo。可能是一个愚蠢的错误,但这有什么问题呢? HWND taskSwitcher = FindWindow(L"TaskSwitcherWnd", L"Task S
JSLint4Java 是 JSLint 的 Java 包装器。我需要这样的东西在我的 GWT 项目中使用,但使用 JSLint4Java 的唯一方法似乎是从命令行或通过 ANT 任务。有谁知道是否有
我有一个持久化实体对象的方法 persistData() 。我有另一个方法 findData() ,它对同一实体类执行 find() 操作以获取持久的主键值。当我在实体类的@PostPersist中调
下面是我的代码。请查看。 1. bool isUnavailable = db.Deploys.Where(p => p.HostEnvironmentId == Guid.Parse(h
这个问题已经有答案了: Why can't a Generic Type Parameter have a lower bound in Java? (6 个回答) 已关闭 9 年前。 我试图理解为什
我正在尝试使用 scala 编译器 Y 警告,但我认为我做得不对。在下面的示例中,nums 未使用,因此我希望 -Ywarn-value-discard 打印一个警告。有两个 if 条件,一个嵌套在另
用户被要求从某个给定的集合中选择一个 ID。我检查该 ID 是否存在于我的集合中,如果不存在,我会抛出 IndexOutOfBoundsException 并稍后捕获它。我实际上可以使用该异常来达到这
我正在尝试减少从 OSM 路径数据生成的形状文件。我正在使用 VTS 的 DouglasPeuckerSimplifier 实现。我想为特定 GTFS(通用交通提要规范)构建路线图的 geojson。
我明白了?!是排除某个模式,例如 a(?!b) 表示如果“a”后面没有“b”,它将匹配“a”。我的问题是,假设我有一个包含以下内容的文件: a cat is a cat, a dog is a dog
我是一名优秀的程序员,十分优秀!