一、前言 

      本来计划将ConcurrentHashMap和HashMap对比着来说下,奈何看的源码有点懵逼,我在思考思考,等等有个清晰的思路在搞起来,我们先来谈一下synchronized,主要从用法,JVM两个方面来说一下;

二、用法

      要谈用法,首先要明白什么时候我们需要使用,在并发编程中照成线程安全问题主要有2点原因:1.共享资源;2.同时操作;这个时候我们就需要保证在同一时刻只能允许一个线程访问或者操作共享资源,Java中给我们提供锁,这种方式来实现同一时刻只有一个线程可以操作共享资源,另外同一时刻访问改资源的其他线程处于等待状态,这个锁也叫做互斥锁,保证同一时刻只有一个线程访问,同时保证了内存可见性;

接下来也引入我们的重点 synchronized:

1.修饰静态方法

synchronized修饰静态方法的时候,锁的是当前类的class对象锁,看如下代码

public class SyncClass implements Runnable {
static int i=0;
//锁的是当前类
public static synchronized void test(){
i++;
} public void run() {
for (int j=0;j<1000000;j++){
test();
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1=new Thread(new SyncClass());
Thread thread2=new Thread(new SyncClass()); thread1.start();
thread2.start(); thread1.join();
thread2.join(); System.out.println(i);
}
} public class SyncClass implements Runnable {
static int i=0;
//当前实例
public synchronized void test(){
i++;
} public void run() {
for (int j=0;j<1000000;j++){
test();
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1=new Thread(new SyncClass());
Thread thread2=new Thread(new SyncClass()); thread1.start();
thread2.start(); thread1.join();
thread2.join(); System.out.println(i);
}
}

上面的方法,执行一下会发现有很大的不同,这里我们进行分析,在SyncClass中,i变量属于共享变量,当在多线程情况下调用test()静态方法修饰的同步方法的时候,没有发生因为资源共享而导致的i输出的时候小于2000000,而当调用非静态的修饰的同步方法的时候,发生因为线程共享资源导致和自己预期不一样的值,所以这个时候我们就会发现,当synchronized修饰静态方法时候是锁的当前类,修饰非静态的类方法的时候是修饰的当前实例,当然这个还需要下面在证明一下。

2.修饰非静态的方法

synchronized修饰非静态方法的时候,锁的是当对象的实例,看如下代码

public class SyncClass implements Runnable {
static int i=0;
public synchronized void test(){
i++;
} public void run() {
for (int j=0;j<1000000;j++){
test();
}
}
public static void main(String[] args) throws InterruptedException {
SyncClass insatance=new SyncClass();
Thread thread1=new Thread(insatance);
Thread thread2=new Thread(insatance); thread1.start();
thread2.start(); thread1.join();
thread2.join(); System.out.println(i);
}
}

上面的方法,当我们传入instance的时候,没有发生因为共享资源而导致i输出的时候小于2000000,与上面另外一种传入类的修饰非静态的方法做比较,这个时候我们可以得出,当synchronized修饰非静态的方法的时候锁的对象是当前类的实例;

3.修饰代码块

这个就是提升锁的效率,没必要每次对方法进行同步操作,看如下代码

public class SyncClass implements Runnable {
static SyncClass instance=new SyncClass();
static int i=0;
public void test(){
i++;
} public void run() {
//给定的实例
synchronized (instance){
for (int j=0;j<1000000;j++){
test();
}
}
//当前实例
// synchronized (this){
// for (int j=0;j<1000000;j++){
// test();
// }
// }
//当前类
// synchronized (SyncClass.class){
// for (int j=0;j<1000000;j++){
// test();
// }
// }
}
public static void main(String[] args) throws InterruptedException {
SyncClass insatance=new SyncClass();
Thread thread1=new Thread(insatance);
Thread thread2=new Thread(insatance); thread1.start();
thread2.start(); thread1.join();
thread2.join(); System.out.println(i);
}
}

用法如上,主要有3种情况,我上面进行了展示,1.锁住的是特定的对象,2.锁住的是当前实例,3.锁住的当前类

三、推敲原理

     synchronized是通过互斥来保证并发的正确性的问题,synchronized经过编译后,会在同步块前后形成monitorenter和monitorexit这两个字节码,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置,当执行monitorenter指令的时,首先尝试获取对象的锁,如果当前对象没有被锁定,或者当前对象已经拥有对象的锁,那么就把锁的计数器加1,相应的在执行monitorexit的时候会将锁的计数器减1,当计数器为0的时候,锁就被释放,如果获取锁的对象失败,则当前线程就要阻塞等待,直到对象锁被另外一个线程锁释放为止----以上来自深入Java虚拟机一书;

这里我们思考下当我们拿到这个场景的时候如何去做,我们要搞清3个问题也就能实现上面场景了:

1.计数器问题;2.对象状态问题

这2个问题处理起来比较简单,就是在对象里面增加一个count属性去记录加锁和解锁以后的数量就可以,另外状态问题也随之处理完成,个数数量为0的时候处于未锁定,大于0的时候处于锁定状态;

3.线程问题

首先线程问题有两种状态,一种处于正在等待锁的状态,另外一种处于等待状态,分析清楚就很简单了,我们可以用2个队列来处理这个问题,队列里只要能记录线程Id就可以,保证能知道我们要唤醒那个线程就可以,当等待状态的队列为空的时候对象也处与为加锁状态,线程计数器也为0;

分析到这里我相信大家也比较清晰了,实现我不写了,这种问题考虑下就好了,思路为王,Java这个是通过C++去实现的,基本思路也是如此,我们重点主要来看下Java对象头包括那些,这个地方关系锁优化等等方面吧,只要明白这块的东西我相信很容易彻底掌握好synchronized;

Java对象头

synchronized用的锁是存在Java对象头里的,什么是Java的对象头,HotSpot虚拟机的对象的头分为两个部分,第一部分用于存储对象自身运行时的数据,包括哈希码,GC分代年龄等,官方称为Mark Word;另外一部分用于存储指向方法区的对象类型的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,这块在之前在介绍虚拟机的时候有说过;

Mark Word

Mark Word在32位的HotSpot虚拟机种对象处理未锁定状态下的默认存储结构:

Mark Word 被设计成为一个非固定的数据结构,以便存储更多有效的数据,它会根据对象本身的状态复用自己的存储空间,如32位JVM下,除了上述列出的Mark Word默认存储结构外,还有如下可能变化的结构:

分析完对象头以后,我们返回到上面考虑那个场景,不要去管轻量级锁和偏向锁,这些都是JVM做的优化,这个等等再谈,将重点放到重量级锁上面来,其实我们刚刚考虑对象的设计也就是重量级锁指向的指针monitor对象设计,当然我们考虑可能没有那全面,但是该有的重点都有了,当我们创建对象的时候都会创建与monitor的关联关系,当monitor创建以后生命周期与对象创建的生命周期是一样的,同生共死,相信到这里你已经能彻底明白重量级锁的实现原理了,要是在不明白去看下反编译的源码,在考虑考虑我设计时候考虑的3个问题,我想必然会融汇贯通;

四、锁优化

jdk1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。 
     锁主要存在4种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

1.自旋锁

线程之间的切换主要是靠CPU进行处理的,来回切换肯定会给服务器照成很大压力。可是我们有些时候锁的操作并不会持续很久,这个时候就没有必要进行线程的来回切换,针对于这种状况就引入了自旋锁;什么是自旋?就是让线程循环等待,看在一定时间内持有锁的线程是否释放,当然这个是建立在多核的基础上的,一个需要执行当前线程的操作,另外一个需要判断线程是否执行完成,自旋避免的线程之间切换带来的性能消耗,但是他需要占用处理器的时间,这个时候如果持有锁的线程执行时间很长的话,在性能则是一种浪费,所以自旋等待必须要有一个度;自旋锁在JDK 1.4.2中引入,默认关闭,但是可以使用-XX:+UseSpinning开启,在JDK1.6中默认开启。同时自旋的默认次数为10次,可以通过参数-XX:PreBlockSpin来调整;JDK1.6以后引入自适应自旋锁,变得智能化,可以根据运行状况自行进行判断;

2.锁消除

这个也是虚拟机自行判断的,主要是针对不存在竞争的资源进行的优化,判断的依据主要是依据是逃逸分析的数据支持,这里简单介绍一下,逃逸分析就是分析对象作用域,这个我想用点大白话说,就是不是在本类内部使用的对象,虚拟机做的就是如果一个变量无法被其他线程所访问到,那么这个变量就不会存在竞争,就可以对这个变量修饰的同步进行锁的消除;

3.锁粗化

这个也是虚拟机自行判断的,多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁;

4.轻量级锁

轻量级锁也还是为了解决重量级锁线程切换照成性能的问题,主要是通过CAS的操作实现,接下来主要分析执行流程:

加锁流程:

1).判断当前对象是否处于无锁状态,如果是无锁状态,JVM会在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方把这份拷贝加了一个Displaced前缀,即Displaced Mark Word);

2).JVM利用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指正,如果成功表示竞争到锁,则将锁标志位变成00(表示此对象处于轻量级锁状态),执行同步操作;

3).判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀为重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态;

释放锁流程:

1).如果对象的Mark Word还是指向的线程的锁,那么就取出在获取轻量级锁保存在Displaced Mark Word中的数据;

2).用CAS操作将取出的数据替换当前对象的Mark Word中,如果成功,则说明释放锁成功;

3). 如果CAS操作替换失败,说明有其他线程尝试获取该锁,则需要在释放锁的同时需要唤醒被挂起的线程;

这样子介绍我感觉大家还是会有疑惑,现在我们A和B线程为例,再来说一下这个流程:

1).当A和B线程同时进入无所状态的时候,这个时候都会进行复制Mark Word操作;

2).当进行CAS操作的时候只能保证有一个线程能执行成功,假设A线程执行成功这个时候A线程就会执行同步方法体,当B线程执行CAS操作的时候就会发现指向的已经不同,Mark Word变为轻量级锁的状态,这个时候CAS操作失败,B线程进入自旋获取锁的状态;

3).B线程获取自旋锁失败,这个时候Mark Word变为重量级锁,线程阻塞;当A线程执行完成同步方法体,然后在执行CAS操作的时候也是执行失败,这个时候A线程就进入等待状态;这个时候大家就回归重量级锁的状态;

5.偏向锁

偏向锁主要是为了处理在没有线程竞争的时候没必要走向轻量级锁,为了减少轻量级锁的CAS操作,接下来看下具体的处理流程:

获取锁

1).检测Mark Word是否为可偏向状态,如果为否则锁标记为01;

2).如果为偏向锁,则检查线程ID是否为当前ID,如果是则执行同步代码;

3).如果不是则进行轻量级锁的流程;

释放锁

偏向锁只有在竞争状态的情况下才会释放锁;

五、结束语

      上面文章主要参考深入理解Java虚拟机,如果有不明白的地方可以找我,QQ群:438836709  ;下一篇预告:volatile;

面试(二)---synchronized的更多相关文章

  1. 二十二 synchronized同步方法

    一 Synchronized锁: 1 synchronized取得的锁都是对象锁,而不是把一段代码或方法加锁. synchronized是给该方法的实例对象加锁.如果多个线程访问的是同一个对象  的s ...

  2. [Java面试二]Java基础知识精华部分.

    一:java概述(快速浏览): 1991 年Sun公司的James Gosling等人开始开发名称为 Oak 的语言,希望用于控制嵌入在有线电视交换盒.PDA等的微处理器: 1994年将Oak语言更名 ...

  3. java面试二

    技术交流群: 233513714 126.什么是ORM?答:对象关系映射(Object-Relational Mapping,简称ORM)是一种为了解决程序的面向对象模型与数据库的关系模型互不匹配问题 ...

  4. JAVA基础面试(二)

    11.是否可以从一个static方法内部发出对非static方法的调用? 不可以.因为非static方法是要与对象关联在一起的,必须创建一个对象后,才可以在该对象上进行方法调用,而static方法调用 ...

  5. Java多线程之二(Synchronized)

    常用API method 注释 run() run()方法是我们创建线程时必须要实现的方法,但是实际上该方法只是一个普通方法,直接调用并没有开启线程的作用. start() start()方法作用为使 ...

  6. Android面试二之Fragment

    基本概念 Fragment,简称碎片,是Android 3.0(API 11)提出的,为了兼容低版本,support-v4库中也开发了一套Fragment API,最低兼容Android 1.6. F ...

  7. 【java并发编程艺术学习】(四)第二章 java并发机制的底层实现原理 学习记录(二) synchronized

    章节介绍 本章节主要学习 Java SE 1.6 中为了减少获得锁 和 释放锁 时带来的性能消耗 而引入的偏向锁 和 轻量级锁,以及锁的存储结构 和 升级过程. synchronized实现同步的基础 ...

  8. Java面试之synchronized 和 static synchronized

    ​面试题: 答案: 不能 不能 不能 不能 能 正文 概述 通过分析这两个用法的分析,我们可以理解java中锁的概念.一个是实例锁(锁在某一个实例对象上,如果该类是单例,那么该锁也具有全局锁的概念), ...

  9. java笔试面试二

    http://www.cnblogs.com/lanxuezaipiao/p/3371224.html

随机推荐

  1. bzoj千题计划252:bzoj1095: [ZJOI2007]Hide 捉迷藏

    http://www.lydsy.com/JudgeOnline/problem.php?id=1095 点分树+堆 请去看 http://www.cnblogs.com/TheRoadToTheGo ...

  2. surging教学视频资源汇总

    surging是什么 surging 是一个分布式微服务框架,提供高性能RPC远程服务调用,采用Zookeeper.Consul作为surging服务的注册中心,集成了哈希,随机,轮询.压力最小优先作 ...

  3. c++中模板是什么?为什么要定义模板?

    一.c++中模板是什么? 首先: int Max(int x, int y) { return x > y ? x : y; } float Max(float a,float b) { ret ...

  4. html5shiv.js和respond.min.js的作用

    html5shiv:解决ie9以下浏览器对html5新增标签的不识别,并导致CSS不起作用的问题. respond.min:让不支持css3 Media Query的浏览器包括IE6-IE8等其他浏览 ...

  5. Docker学习笔记 - 在运行中的容器内启动新进程

    docker psdoker top dc1 # 容器情况# 在运行中的容器内启动新进程docker exec [-d] [-i] [-t] 容器名 [command] [args]docker ex ...

  6. 用Jmeter实现mysql数据库的增删查改

    主要是参考虫师的“使用JMeter创建数据库(Mysql)测试”. 1.打开Jmeter,点击测试计划 链接:https://pan.baidu.com/s/1ZtaZ6IC_0DRjSlXkjslY ...

  7. Android智能手机上的音频浅析

    手机可以说是现在人日常生活中最离不开的电子设备了.它自诞生以来,从模拟的发展到数字的,从1G发展到目前的4G以及不久将来的5G,从最初的只有唯一的功能(打电话)发展到目前的全功能,从功能机(featu ...

  8. 服务器批量管理软件ansible安装以及配置

    1.yum安装(管理主机以及被管理主机都需要安装) yum install epel-release yum install ansible 2.配置管理主机 vim /etc/ansible/hos ...

  9. Python_fullstack_test1

    1.执行Python脚本的两种方式 使用交互式的带提示符的解释器或使用源文件 2.简述位.字节的关系 位是计算机中最小计量单位,用bit表示 字节是计算机中最小存储单位,用Byte表示 1字节=8位, ...

  10. 愿奴胁下生双翼——— 详解cookie和session

    cookie和session都是基于web服务器的,不同的是cookie存储在客户端而session存储在服务器. 当用户浏览网站时,web服务器会在浏览器上存储一些当前用户的相关信息,在本地Web客 ...