大数据成神之路-Java高级特性加强(Synchronized关键字)

请戳GitHub原文: github.com/wangzhiwubi…java

大数据成神之路系列:

请戳GitHub原文: github.com/wangzhiwubi…git

Java高级特性加强-集合程序员

Java高级特性加强-多线程github

Java高级特性加强-Synchronized面试

Java高级特性加强-volatile数据库

Java高级特性加强-并发集合框架编程

Java高级特性加强-分布式安全

Java高级特性加强-Zookeeperbash

Java高级特性加强-JVM网络

Java高级特性加强-NIO

公众号

  • 全网惟一一个从0开始帮助Java开发者转作大数据领域的公众号~

  • 公众号大数据技术与架构或者搜索import_bigdata关注,大数据学习路线最新更新,已经有不少小伙伴加入了~

Java高级特性加强-Synchronized

本部分网络上有大量的资源能够参考,在这里作了部分整理,感谢前辈的付出,每节文章末尾有引用列表,源码推荐看JDK1.8之后的版本,注意甄别~ ####多线程 ###集合框架 ###NIO ###Java并发容器


Synchronized关键字

参考文章目录: 感谢各位大大的劳动成果~深表敬意~ blog.csdn.net/qq_34337272… blog.csdn.net/qq_34337272… www.jianshu.com/p/d53bf830f… www.jianshu.com/p/c5058b6fe…


简介

Java并发编程这个领域中synchronized关键字一直都是元老级的角色,好久以前不少人都会称它为“重量级锁”。可是,在JavaSE 1.6以后进行了主要包括为了减小得到锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各类优化以后变得在某些状况下并非那么重了。

变量安全性

“非线程安全”问题存在于“实例变量”中,若是是方法内部的私有变量,则不存在“非线程安全”问题,所得结果也就是“线程安全”的了。

若是两个线程同时操做对象中的实例变量,则会出现“非线程安全”,解决办法就是在方法前加上synchronized关键字便可。

Synchronized的使用

修饰代码块

/**
 * 同步线程
 */
class SyncThread implements Runnable {
   private static int count;
 
   public SyncThread() {
      count = 0;
   }
 
   public  void run() {
      synchronized(this) {
         for (int i = 0; i < 5; i++) {
            try {
               System.out.println(Thread.currentThread().getName() + ":" + (count++));
               Thread.sleep(100);
            } catch (InterruptedException e) {
               e.printStackTrace();
            }
         }
      }
   }
 
   public int getCount() {
      return count;
   }
}
SyncThread的调用:
SyncThread syncThread = new SyncThread();
Thread thread1 = new Thread(syncThread, "SyncThread1");
Thread thread2 = new Thread(syncThread, "SyncThread2");
thread1.start();
thread2.start();

结果以下:

SyncThread1:0
SyncThread1:1
SyncThread1:2
SyncThread1:3
SyncThread1:4
SyncThread2:5
SyncThread2:6
SyncThread2:7
SyncThread2:8
SyncThread2:9
复制代码

当两个并发线程(thread1和thread2)访问同一个对象(syncThread)中的synchronized代码块时,在同一时刻只能有一个线程获得执行,另外一个线程受阻塞,必须等待当前线程执行完这个代码块之后才能执行该代码块。Thread1和thread2是互斥的,由于在执行synchronized代码块时会锁定当前的对象,只有执行完该代码块才能释放该对象锁,下一个线程才能执行并锁定该对象。 咱们再把SyncThread的调用稍微改一下:

Thread thread1 = new Thread(new SyncThread(), "SyncThread1");
Thread thread2 = new Thread(new SyncThread(), "SyncThread2");
thread1.start();
thread2.start();
复制代码

结果以下:

SyncThread1:0
SyncThread2:1
SyncThread1:2
SyncThread2:3
SyncThread1:4
SyncThread2:5
SyncThread2:6
SyncThread1:7
SyncThread1:8
SyncThread2:9
复制代码

不是说一个线程执行synchronized代码块时其它的线程受阻塞吗?为何上面的例子中thread1和thread2同时在执行。这是由于synchronized只锁定对象,每一个对象只有一个锁(lock)与之相关联,而上面的代码等同于下面这段代码:

SyncThread syncThread1 = new SyncThread();
SyncThread syncThread2 = new SyncThread();
Thread thread1 = new Thread(syncThread1, "SyncThread1");
Thread thread2 = new Thread(syncThread2, "SyncThread2");
thread1.start();
thread2.start();
复制代码

这时建立了两个SyncThread的对象syncThread1和syncThread2,线程thread1执行的是syncThread1对象中的synchronized代码(run),而线程thread2执行的是syncThread2对象中的synchronized代码(run);咱们知道synchronized锁定的是对象,这时会有两把锁分别锁定syncThread1对象和syncThread2对象,而这两把锁是互不干扰的,不造成互斥,因此两个线程能够同时执行。

修饰一个方法 Synchronized修饰一个方法很简单,就是在方法的前面加synchronized,public synchronized void method(){//todo}; synchronized修饰方法和修饰一个代码块相似,只是做用范围不同,修饰代码块是大括号括起来的范围,而修饰方法范围是整个函数。

public synchronized void run() {
   for (int i = 0; i < 5; i ++) {
      try {
         System.out.println(Thread.currentThread().getName() + ":" + (count++));
         Thread.sleep(100);
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
   }
复制代码

修饰一个静态的方法 Synchronized也可修饰一个静态方法,用法以下:

public synchronized static void method() {
   // todo
}
复制代码

咱们知道静态方法是属于类的而不属于对象的。一样的,synchronized修饰的静态方法锁定的是这个类的全部对象.

修饰一个类 Synchronized还可做用于一个类,用法以下:

class ClassName {
   public void method() {
      synchronized(ClassName.class) {
         // todo
      }
   }
}
复制代码

总结:

34110231aa12f351a94b5384a1245a59.png
A. 不管synchronized关键字加在方法上仍是对象上,若是它做用的对象是非静态的,则它取得的锁是对象;若是synchronized做用的对象是一个静态方法或一个类,则它取得的锁是对类,该类全部的对象同一把锁。 B. 每一个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就能够运行它所控制的那段代码。 C. 实现同步是要很大的系统开销做为代价的,甚至可能形成死锁,因此尽可能避免无谓的同步控制。

Synchronized的原理

对象锁(monitor)机制

如今咱们来看看synchronized的具体底层实现。先写一个简单的demo:

public class SynchronizedDemo {
    public static void main(String[] args) {
        synchronized (SynchronizedDemo.class) {
        }
        method();
    }

    private static void method() {
    }
}
复制代码

上面的代码中有一个同步代码块,锁住的是类对象,而且还有一个同步静态方法,锁住的依然是该类的类对象。编译以后,切换到SynchronizedDemo.class的同级目录以后,而后用javap -v SynchronizedDemo.class查看字节码文件:

98cdb1130796f19ed87ac94054035d7c.png
synchronized关键字基于上述两个指令实现了锁的获取和释放过程,解释器执行monitorenter时会进入到InterpreterRuntime.cpp的InterpreterRuntime::monitorenter函数,具体实现以下:
0ffb2d827a6b326cd8ad5b40b444eb71.png
执行同步代码块后首先要先执行monitorenter指令,退出的时候monitorexit指令。经过分析以后能够看出,使用Synchronized进行同步,其关键就是必需要对对象的监视器monitor进行获取,当线程获取monitor后才能继续往下执行,不然就只能等待。而这个获取的过程是互斥的,即同一时刻只有一个线程可以获取到monitor。上面的demo中在执行完同步代码块以后紧接着再会去执行一个静态同步方法,而这个方法锁的对象依然就这个类对象,那么这个正在执行的线程还须要获取该锁吗?答案是没必要的,从上图中就能够看出来,执行静态同步方法的时候就只有一条monitorexit指令,并无monitorenter获取锁的指令。这就是锁的重入性,即在同一锁程中,线程不须要再次获取同一把锁。Synchronized先天具备重入性。每一个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一。

synchronized的happens-before关系
什么是happens-before

概念 happens-before的概念最初由Leslie Lamport在其一篇影响深远的论文(《Time,Clocks and the Ordering of Events in a Distributed System》)中提出,有兴趣的能够google一下。JSR-133使用happens-before的概念来指定两个操做之间的执行顺序。因为这两个操做能够在一个线程以内,也能够是在不一样线程之间。 所以,JMM能够经过happens-before关系向程序员提供跨线程的内存可见性保证(若是A线程的写操做a与B线程的读操做b之间存在happens-before关系,尽管a操做和b操做在不一样的线程中执行,但JMM向程序员保证a操做将对b操做可见)。具体的定义为: 1)若是一个操做happens-before另外一个操做,那么第一个操做的执行结果将对第二个操做可见,并且第一个操做的执行顺序排在第二个操做以前。 2)两个操做之间存在happens-before关系,并不意味着Java平台的具体实现必需要按照happens-before关系指定的顺序来执行。若是重排序以后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM容许这种重排序)。 上面的1)是JMM对程序员的承诺。从程序员的角度来讲,能够这样理解happens-before关系:若是A happens-before B,那么Java内存模型将向程序员保证——A操做的结果将对B可见,且A的执行顺序排在B以前。注意,这只是Java内存模型向程序员作出的保证! 上面的2)是JMM对编译器和处理器重排序的约束原则。正如前面所言,JMM实际上是在遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。JMM这么作的缘由是:程序员对于这两个操做是否真的被重排序并不关心,程序员关心的是程序执行时的语义不能被改变(即执行结果不能被改变)。所以,happens-before关系本质上和as-if-serial语义是一回事。

具体规则

具体规则以下:

  1. 程序顺序规则:一个线程中的每一个操做,happens-before于该线程中的任意后续操做。
  2. 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  3. volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  4. 传递性:若是A happens-before B,且B happens-before C,那么A happens-before C。
  5. start()规则:若是线程A执行操做ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操做happens-before于线程B中的任意操做。
  6. join()规则:若是线程A执行操做ThreadB.join()并成功返回,那么线程B中的任意操做happens-before于线程A从ThreadB.join()操做成功返回。
  7. 程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。
  8. 对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。
synchronized的happens-before关系

Synchronized的happens-before规则,即监视器锁规则:对同一个监视器的解锁,happens-before于对该监视器的加锁。继续来看代码:

public class MonitorDemo {
    private int a = 0;

    public synchronized void writer() {     // 1
        a++;                                // 2
    }                                       // 3

    public synchronized void reader() {    // 4
        int i = a;                         // 5
    }                                      // 6
}
复制代码

该代码的happens-before关系如图所示:

b3d7851276b01f579cac06a858d67df7.png
在图中每个箭头链接的两个节点就表明之间的happens-before关系,黑色的是经过程序顺序规则推导出来,红色的为监视器锁规则推导而出:线程A释放锁happens-before线程B加锁,蓝色的则是经过程序顺序规则和监视器锁规则推测出来happens-befor关系,经过传递性规则进一步推导的happens-before关系。如今咱们来重点关注2 happens-before 5,经过这个关系咱们能够得出什么? 根据happens-before的定义中的一条:若是A happens-before B,则A的执行结果对B可见,而且A的执行顺序先于B。线程A先对共享变量A进行加一,由2 happens-before 5关系可知线程A的执行结果对线程B可见即线程B所读取到的a的值为1。

synchronized的优化

经过上面的讨论如今咱们对Synchronized应该有所印象了,它最大的特征就是在同一时刻只有一个线程可以得到对象的监视器(monitor),从而进入到同步代码块或者同步方法之中,即表现为互斥性(排它性)。这种方式确定效率低下,每次只能经过一个线程,既然每次只能经过一个,这种形式不能改变的话,那么咱们能不能让每次经过的速度变快一点了。打个比方,去收银台付款,以前的方式是,你们都去排队,而后去纸币付款收银员找零,有的时候付款的时候在包里拿出钱包再去拿出钱,这个过程是比较耗时的,而后,支付宝解放了你们去钱包找钱的过程,如今只须要扫描下就能够完成付款了,也省去了收银员跟你找零的时间的了。一样是须要排队,但整个付款的时间大大缩短,是否是总体的效率变高速率变快了?这种优化方式一样能够引伸到锁优化上,缩短获取锁的时间。

CAS操做

这里作一个介绍,CAS为后续锁的章节作一个铺垫O(∩_∩)O~

推荐文章:www.jianshu.com/p/24ffe531e… 什么是CAS? 使用锁时,线程获取锁是一种悲观锁策略,即假设每一次执行临界区代码都会产生冲突,因此当前线程获取到锁的时候同时也会阻塞其余线程获取该锁。而CAS操做(又称为无锁操做)是一种乐观锁策略,它假设全部线程访问共享资源的时候不会出现冲突,既然不会出现冲突天然而然就不会阻塞其余线程的操做。所以,线程就不会出现阻塞停顿的状态。那么,若是出现冲突了怎么办?无锁操做是使用CAS(compare and swap)又叫作比较交换来鉴别线程是否出现冲突,出现冲突就重试当前操做直到没有冲突为止。

CAS的操做过程 CAS比较交换的过程能够通俗的理解为CAS(V,O,N),包含三个值分别为:V 内存地址存放的实际值;O 预期的值(旧值);N 更新的新值。当V和O相同时,也就是说旧值和内存中实际的值相同代表该值没有被其余线程更改过,即该旧值O就是目前来讲最新的值了,天然而然能够将新值N赋值给V。反之,V和O不相同,代表该值已经被其余线程改过了则该旧值O不是最新版本的值了,因此不能将新值N赋给V,返回V便可。当多个线程使用CAS操做一个变量是,只有一个线程会成功,并成功更新,其他会失败。失败的线程会从新尝试,固然也能够选择挂起线程 CAS的实现须要硬件指令集的支撑,在JDK1.5后虚拟机才可使用处理器提供的CMPXCHG指令实现。 CAS的应用场景 在J.U.C包中利用CAS实现类有不少,能够说是支撑起整个concurrency包的实现,在Lock实现中会有CAS改变state变量,在atomic包中的实现类也几乎都是用CAS实现,关于这些具体的实现场景在以后会详细聊聊,如今有个印象就行了(微笑脸)。 CAS的问题

  1. ABA问题 由于CAS会检查旧值有没有变化,这里存在这样一个有意思的问题。好比一个旧值A变为了成B,而后再变成A,恰好在作CAS时检查发现旧值并无变化依然为A,可是实际上的确发生了变化。解决方案能够沿袭数据库中经常使用的乐观锁方式,添加一个版本号能够解决。原来的变化路径A->B->A就变成了1A->2B->3C。java这么优秀的语言,固然在java 1.5后的atomic包中提供了AtomicStampedReference来解决ABA问题,解决思路就是这样的。
  2. 自旋时间过长 使用CAS时非阻塞同步,也就是说不会将线程挂起,会自旋(无非就是一个死循环)进行下一次尝试,若是这里自旋时间过长对性能是很大的消耗。若是JVM能支持处理器提供的pause指令,那么在效率上会有必定的提高。
  3. 只能保证一个共享变量的原子操做 当对一个共享变量执行操做时CAS能保证其原子性,若是对多个共享变量进行操做,CAS就不能保证其原子性。有一个解决方案是利用对象整合多个共享变量,即一个类中的成员变量就是这几个共享变量。而后将这个对象作CAS操做就能够保证其原子性。atomic中提供了AtomicReference来保证引用对象之间的原子性。

GitHub: github.com/wangzhiwubi…

关注公众号,内推,面试,资源下载,关注更多大数据技术~
                   预计更新500+篇文章,已经更新50+篇~ 
复制代码
相关文章
相关标签/搜索