基础使用

基本上Java程序员都简单的了解synchronized的使用: 无非就是用在多线程环境下的同步。 看如下简单的例子:

publicclassUnsafeCounter{

privateint count=0;

publicint getAndIncrement(){

returnthis.count++;

}

}

上面是一个简单的非常常见的POJO类,在多线程环境下的测试代码:

publicclassRunUnsafeCounter{

privatestaticfinalUnsafeCounterunsafeCounter=newUnsafeCounter();

publicstaticvoidunsafeCounter()throwsInterruptedException{

inti=0;

ListthreadList=newArrayList<>(1025);

while(i<1000){

threadList.add(newThread(newRunnable(){

@Override

publicvoidrun(){

System.out.println(Thread.currentThread().getName()+" : "+unsafeCounter.getAndIncrement());;

}

}));

i++;

}

threadList.forEach(thread->thread.start());

for(Threadthread:threadList){

thread.join();

}

}

publicstaticvoidmain(String[]args){

for(inti=0;i<10;i++){

try{

RunUnsafeCounter.unsafeCounter();

System.out.println(unsafeCounter.getCount());

}catch(InterruptedExceptione){

e.printStackTrace();

}

}

}

}

上面的测试类中有一个静态的UnsafeCounter实例,然后生成了1000个线程调用非线程安全的getAndIncrement方法, 按照平常单线程环境的结果,这里的值应该是1000. 但是运行RunCounter类就会发现结果不一定是1000并且每一次的结果都不一定相同。 这是因为多个线程同时访问getAndIncrement这一个非线程安全的方法,可能中间某几个线程可能同时在运行这个方法,然后在进行++操作时,某个线程获取到了当前值,结果又切换到了其他线程也获取到了当前值,然后这两个线程的++得到了相同的结果。 也就导致了最终结果的不确定性。

再看如下使用synchronized已保证线程安全性的代码:

publicclassSafeCounter{

privateintcount=0;

publicsynchronizedintgetCount(){

returncount;

}

publicsynchronizedintgetAndIncrement(){

returnthis.count++;

}

}

上面的POJO类的getAndIncrement方法使用synchronized修饰,而且getCount方法也使用synchronized修饰。 测试例子:

publicclassRunSafeCounter{

privatestaticfinalSafeCountersafeCounter=newSafeCounter();

publicstaticvoidsafeCounter()throwsInterruptedException{

inti=0;

ListthreadList=newArrayList<>();

while(i<1000){

threadList.add(newThread(newRunnable(){

@Override

publicvoidrun(){

safeCounter.getAndIncrement();

}

}));

i++;

}

threadList.forEach(thread->thread.start());

for(Threadthread:threadList){

thread.join();

}

}

publicstaticvoidmain(String[]args){

try{

for(inti=0;i<10;i++){

RunSafeCounter.safeCounter();

System.out.println(safeCounter.getCount());

}

}catch(InterruptedExceptione){

e.printStackTrace();

}

}

}

而上面的代码在经过10*1000次循环过后获得结果是10000, 无论重复多少次都是。 并且也保证了线程的安全性。(PS: 在看完下面的内容过后判断SafeCounter中的Getter方法的getCount方法如果去掉synchronized修饰会不会还是一样的结果?)

规范说明

Java为多线程之间通信提供了非常多的机制,而其中, Synchronized是最基础最简单的一个。在JLS-17 对 Synchronized的定义大概意思如下(ps为我加的备注,非原文):

synchoronized使用监视器实现。 Java中每一个对象都和一个监视器关联,线程可以锁或则解锁监视器, 同一时间只有一个线程持有监视器的锁,其他任何想获取该监视器锁的线程都会被阻塞知道可以获得该锁(ps: 拥有锁的线程释放过后)。 一个线程可能对一个监视器锁多次(ps: 可重入),每一个解锁恢复一次锁操作(ps: 内部维护一个监视器锁的次数,每退出一个减少1直到为0就释放该监视器的锁)

synchoronized块计算一个对象的引用,然后开始在对象的监视器上执行锁操作并且不继续向下执行直到锁操作成功后。然后,synchoronized块的内容开始执行。 如果块中的内容执行完成(不管是正常还是突然(ps: 被外部关闭之类)),在该监视器上就会自动执行解锁操作。

synchoronized方法在调用它的时候自动执行锁操作。它的方法内容在成功获取到锁之前不会执行,如果是实例方法,它锁住了调用它的实例的监视器(方法中的this),如果是静态方法,它锁住了定义这个方法的类的Class对象的监视器, 如果块中的内容执行完成(不管是正常还是突然(ps: 被外部关闭之类)),在该监视器上就会自动执行解锁操作。

Java语言既不预防也不检查死锁(ps:这是程序员的事)

其他机制,比如volatile的读和写或则java.util.concurrent包提供了其他的可替代的同步方式。

一个synchronized块请求一个互斥锁,当拥有锁的线程在执行时,其他线程要获取这个锁必须等待。 它的语法如下:

SynchronizedStatement:

synchronized (Expression) Block

表达式的类型必须为引用类型,否则编译期报错。该方法块 首先计算表达式的值,然后执行其中的代码。** 如果计算表达式突然结束,那么代码块已同样的理由突然结束。 如果是null,就会抛出空指针异常。** 否则,就获取到表达式值锁代表的对象的监视器的锁,然后开始执行同步代码块。 如果代码块正常退出,监视器就会被解锁然后synchronized块也正常退出。 如果是已其他任何理由突然中断的话,监视器会被解锁并且同步代码块会已同样的方式结束。

一个synchronized方法在运行之前会先请求一个监视器(的锁)。对于一个静态方法,该类的Class对象关联的监视器将被获取。 对于一个实例方法, this所代表的实例的监视器将被获取。

同样,在JLS中也写清楚了每一个对象关联的监视器都有一个Wait Sets,显而易见的就是用来保存当前等待获取当前监视器锁的线程集合。该集合仅仅可以被Object.wait , Object.notify ,Object.notifyAll操纵。

synchoronized保证的互斥性与锁的对象

当然,对于synchoronized来讲,它的具体的规范可以阅读一下,但也没有必要在这里完全照搬过来。 在理解了上篇的Java内存模型并且仔细阅读了上面的JLS中synchoronized的定义过后,对于在程序中如何正确的使用其实应该有了个基本的概念。 我认为,使用synchoronized,最基本也是最重要的就是:

你为什么需要用synchoronized?

你锁的究竟是哪个对象?

为了做什么?

考虑如下代码:

publicclassSynchronizedCounter{

privateintc=0;

publicsynchronizedvoidincrement1(){

c++;

}

publicvoidincrement2(){

synchronized(this){

c++;

}

}

}

对于increment1方法,它是一个同步方法,并且是实例方法。 根据上面的定义,该方法会获取调用该方法的实例的监视器的锁; 而对于increment2,它是一个同步代码块,但获取一个对象的引用,然后尝试获取锁。 这里的引用是this,其实也就是该调用increment2的实例。 所以说, increment1和increment2其实是做了完全一样的事情。

代码:

classTest{

intcount;

synchronizedvoidbump(){

count++;

}

staticintclassCount;

staticsynchronizedvoidclassBump(){

classCount++;

}

}

classBumpTest{

intcount;

voidbump(){

synchronized(this){count++;}

}

staticintclassCount;

staticvoidclassBump(){

try{

synchronized(Class.forName("BumpTest")){

classCount++;

}

}catch(ClassNotFoundExceptione){}

}

}

也是拥有相同的效果。

在搞清楚锁的对象和时间周期过后,下面代码的安全性应该很容易看出来了:

publicclassLockObjectTest{

privatestaticintindex=0;

publicsynchronizedintgetAndIncrement1(){//这个锁的是实例的监视器

returnindex++;

}

publicstaticsynchronizedintgetAndIncrement2(){//这个锁的是LockObjectTest类的Class对象的监视器

returnindex++;

}

publicstaticvoidmain(String[]args){

inti=0;

ListthreadList=newArrayList<>(1000);

LockObjectTestlockObjectTest=newLockObjectTest();

while(i<10000){

i++;

threadList.add(newThread(newRunnable(){

@Override

publicvoidrun(){

lockObjectTest.getAndIncrement1();

}

}));

threadList.add(newThread(newRunnable(){

@Override

publicvoidrun(){

LockObjectTest.getAndIncrement2();

}

}));

}

threadList.forEach(thread->thread.start());

try{

for(Threadthread:threadList){

thread.join();

}

}catch(InterruptedExceptione){

e.printStackTrace();

}

System.out.println(LockObjectTest.index);

}

}

synchoronized保证的内存可见性

当线程A执行一个同步代码块过后,线程B进入同一个监视器锁的同步代码快的时候,所在线程A的操作(特别是对变量的改变)都保证可以被线程B看到(即不会因为重排序或则缓存之类的影响而看到错误的值)

内存可见性在单线程环境下从来没有出现过,因为这似乎就是一个智障问题:我前面给变量赋值了,后面肯定可以看到这个值。不然我们的代码岂不是问题重重?

而在多线程环境下之所以会出现这个问题还是由于编译器、运行时、CPU共同作用的结果。

编译器(不一定指javac,JIT)会对代码进行优化,一个非常常见的就是编译器循环优化,知乎RednaxelaFX的一个回答。 编译器在编译的时候可能就已经改变了代码中的变量声明或则赋值顺序-只要保证了语义一致性。 R大已经解释的非常清楚。

现代处理器的乱序执行和CPU上越来越多的缓存(L1,L2,L3 cache)都使得你最终跑在CPU上的代码和你所写的出入较大。 多线程环境下尤其需要考虑这种影响。 比如下面的代码:

intarith(intx,inty,intz){

intt1=x+y;

intt2=z*48;

intt3=t1&0xFFFF;

intt4=t2*t3;

returnt4;

}

由于t1和t2的赋值互不影响,所以他们的顺序完全可能已随机的次序跑在CPU上。

而内存可见性其实也是这个道理。 当你的程序跑在同一个线程的时候,后面的代码读取之前对变量的更改都会是在同一个“核心”的寄存器或则缓存上。 而如果是多线程环境,假设某一个线程更改了某个变量,然后放到了它的寄存器上。 而另外一个线程此时来读取这个变量,它是会从内存中读取还是从这个“核心”的缓存中读取还是从这个“核心”的寄存器上读取、又或则由于重排序这里的赋值还没有发生 是不能得到保证的。而唯一可以确定的是,它读取到的总会是某个线程在某个时间更改的数据,这被称为最低保证性(JMM规定了64位的数值(double,long)可以分成2个32位的进行计算,也就是说这两种数据类型没有最低保证性。它们的数据完全可能是随机的)。

如下代码:

publicclassNoVisibility{

privatestaticbooleanready;

privatestaticintnumber;

privatestaticclassReaderThreadextendsThread{

publicvoidrun(){

while(!ready)

Thread.yield();

System.out.println(number);

}

}

publicstaticvoidmain(String[]args){

newReaderThread().start();

number=42;

ready=true;

}

}

上面代码主线程和读线程访问共享变量ready和number,主线程开始读线程,然后把number设为42,把ready设置为true。 读线程等待ready为true后打印number. 但是这里,读线程可能会看到number是42也可能是0,或者说是永远不终止。主线程对于ready和number的写不能保证可以被其他线程看到。

synchronized可以保证内存可见性,也就是使用了synchronized关键字的方法或则语句都可以保证内存可见性(还有其他机制,如volatile)。具体的细节并发编程网有一篇非常好的文章

当线程A运行一个synchronized块,然后之后线程B进入同一个锁的synchronized块时,线程A释放锁之前可见的变量可以保证在线程B获取锁的时候可以看见。 换句话说,线程A做的事情线程B都知道。 而没有synchronized,则没有这样的保证。

java动态同步_java并发基础-Synchronized相关推荐

  1. java 等待几秒_Java并发编程synchronized相关面试题总结

    说说自己对于synchronized关键字的了解 synchronized关键字用于解决多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个 ...

  2. java队列加锁_java并发-----浅析ReentrantLock加锁,解锁过程,公平锁非公平锁,AQS入门,CLH同步队列...

    前言 为什么需要去了解AQS,AQS,AbstractQueuedSynchronizer,即队列同步器.它是构建锁或者其他同步组件的基础框架(如ReentrantLock.ReentrantRead ...

  3. java 内存同步_Java中的硬件事务性内存,或者为什么同步将再次变得很棒

    java 内存同步 总览 硬件事务内存有可能允许多个线程同时以推测方式访问相同的数据结构,并使缓存一致性协议确定是否发生冲突. HTM旨在为您提供细粒度锁定的可伸缩性,粗粒度锁定的简单性以及几乎没有锁 ...

  4. java 代码同步_Java同步代码块 转

    Java 同步块(synchronized block)用来标记方法或者代码块是同步的.Java同步块用来避免竞争.本文介绍以下内容: Java同步关键字(synchronzied) 实例方法同步 静 ...

  5. java动态方法_Java 动态方法调用

    Java 动态方法调用 在Java中,如果方法重写只是一种名字空间的编写,那么它最多是让人感到有趣,但没有实际价值,但情况并非如此.方法重写构造成了Java最大的一个概念基础:动态方法调度(dynam ...

  6. 零基础java自学就业_java零基础到就业需要多长时间呢?

    展开全部 先以肯定的语气说明一下自学e68a84e8a2ad62616964757a686964616f31333433663030Java,多久可以找到工作: 按照目前Java的体系来说,Java的 ...

  7. java动态语言_java动态类型语言支持(三)

    invokedynamic指令 在前面java动态类型语言支持(一)(二)中我们有提到invokedynamic指令和java.lang.invoke包中的MethodHandle机制,在某种程度上他 ...

  8. java 动态字符串_Java动态编译执行一串字符串,类似于Javascript里的eval函数

    Javascript里的eval函数能动态执行一串js脚本. 那Java里怎么做到呢. 有两种方法: 一种是使用可以执行js脚本的Java类 ScriptEngineManagerpublic sta ...

  9. java动态代理_Java动态代理

    java动态代理 代理是一种设计模式. 当我们想添加或修改现有类的某些功能时,我们创建并使用代理对象. 使用代理对象而不是原始对象. 通常,代理对象具有与原始对象相同的方法,并且在Java代理类中,通 ...

最新文章

  1. 小shell脚本---查找目录下面包含string的文件
  2. 兀键和6键怎么判断_湖南槽钢经销商告诉您,槽钢的优劣状况应该怎么判断,注意这6点...
  3. A Dynamic Algorithm for Local Community Detection in Graphs--阅读笔记
  4. HDU 3954 Level up(线段树)
  5. LeetCode 数据库 182. 查找重复的电子邮箱
  6. java中方法的命名_Java方法中的参数太多,第5部分:方法命名
  7. windows10中屏幕键盘 vs 触摸键盘
  8. 数据库上云如何顺利进行?6位专家直播助你一臂之力
  9. linux docker自动启动命令行,docker容器内服务随容器自启动
  10. ZBrush中的Clip剪切笔刷怎么快速运用
  11. HTMLCSS 第二天 笔记
  12. VirtualBox安装MACOSX 10.13虚拟机的增强功能
  13. 如何ping网站的IP地址
  14. 计算机毕业设计(附源码)python幼儿园管理系统
  15. IDEA设置背景图片
  16. 最新html取消dynsrc属性无效,Html属性标签 - osc_5aj0jo70的个人空间 - OSCHINA - 中文开源技术交流社区...
  17. 2016版excel_Excel 超级表格无法插入切片器(灰色)解决
  18. 中考计算机考试作文,准备中考的作文4篇
  19. 序列的Z变换和逆Z变化
  20. 几款三维模型OSGb转换3dtile格式的软件

热门文章

  1. Python 爬虫: 抓取花瓣网图片
  2. hadoop 集群启动时 Address already in use 解决方法
  3. 浅谈Base64编码[转]
  4. 为什么你一直在写bug?原因找到了
  5. 什么是大数据「实时流计算」?深度解析它的4大应用及4个特点
  6. 企业数字化转型与中台建设全攻略:什么阶段进行?采用哪些方法?
  7. linux band0 手动重启,band,call,apply的区别以及手动封装
  8. python怎么创建函数_Python创建与调用函数
  9. 学完计组后,我马上在「我的世界」造了台显示器,你敢信?
  10. 你知道荷兰旗问题吗?