需要明确的几个问题:

  • synchronized关键字可以作为函数的修饰符,也可作为函数内的语句,也就是平时说的同步方法和同步语句块。如果 再细的分类,synchronized可作用于instance变量object reference(对象引用)、static函数和class literals(类名称字面常量)身上。
  • 无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁――而且同步方法很可能还会被其他线程的对象访问。
  • 每个对象只有一个锁(lock)与之相关联。
  • 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。

synchronized关键字的作用域有二种:

  1. 某个对象实例内,synchronized aMethod(){}可以防止多个线程同时访问这个对象的synchronized方法(如果一个对象有多个synchronized方法,只要一个线 程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法)。这时,不同的对象实例的 synchronized方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法;
  2. 某个类的范围,synchronized static aStaticMethod{}防止多个线程同时访问这个类中的synchronized static 方法。它可以对类的所有对象实例起作用。

1、使用在方法上synchronized aMethod(){...}

  使用相同的 object

public  class synchTest {
private String a= "";
private List<String> b= new ArrayList<>(); // 方法一
public void job() {
System.out.println("job .....");
synchronized (b){
System.out.println("job 使用锁中 ....");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
}
System.out.println("job end.....");
}
// 方法二
public synchronized void job2(){
System.out.println("job2 .....");
synchronized (b){
System.out.println("job22 使用锁中 ...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
}
System.out.println("job2 end.....");
} public static void main(String[] args) {
final synchTest rs = new synchTest();
new Thread() {
public void run() {
rs.job();
}
}.start();
new Thread() {
public void run() {
rs.job2();
}
}.start();
}
} 结果:
job .....
job 使用锁中 ....
job2 .....
job end.....
job22 使用锁中 ...
job2 end.....

使用不同的object

public  class synchTest {
private String a= "";
private List<String> b= new ArrayList<>(); // 方法一
public void job() {
System.out.println("job .....");
synchronized (a){
System.out.println("job 使用锁中 ....");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
}
System.out.println("job end.....");
}
// 方法二
public synchronized void job2(){
System.out.println("job2 .....");
synchronized (b){
System.out.println("job22 使用锁中 ...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
}
System.out.println("job2 end.....");
} public static void main(String[] args) {
final synchTest rs = new synchTest();
new Thread() {
public void run() {
rs.job();
}
}.start();
new Thread() {
public void run() {
rs.job2();
}
}.start();
}
} 结果:
job .....
job 使用锁中 ....
job2 .....
job22 使用锁中 ...
job end.....
job2 end.....

使用this关键词

public  class synchTest {
private String a= "";
private List<String> b= new ArrayList<>(); // 方法一
public void job() {
System.out.println("job .....");
synchronized (this){
System.out.println("job 使用锁中 ....");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
}
System.out.println("job end.....");
}
// 方法二
public synchronized void job2(){
System.out.println("job2 .....");
synchronized (b){
System.out.println("job22 使用锁中 ...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
}
System.out.println("job2 end.....");
} public static void main(String[] args) {
final synchTest rs = new synchTest();
new Thread() {
public void run() {
rs.job();
}
}.start();
new Thread() {
public void run() {
rs.job2();
}
}.start();
}
} 结果:
job .....
job 使用锁中 ....
job end.....
job2 .....
job22 使用锁中 ...
job2 end.....

结论:

  •   synchronized(Object) object相同的情况下,修饰的内容会同步,等上一个执行完才能执行下一个方法的内容
  •        synchronized(Object) object不相同的情况下,修饰内容不会同步,两个方法可以一起执行
  •         this这个比较特殊,如果先执行修饰this这个方法的内容,会同步,否则 不会同步(可以测试下) 【慎用 this同步块,会锁对象】

2、使用在方法内部 synchronized(Oject){...}

public  class synchTest {
// 方法一
public synchronized void job() {
System.out.println("job .....");
System.out.println("job 使用锁中 ....");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
System.out.println("job end.....");
}
// 方法二
public synchronized void job2(){
System.out.println("job2 .....");
System.out.println("job22 使用锁中 ...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
System.out.println("job2 end.....");
} public static void main(String[] args) {
final synchTest rs = new synchTest();
new Thread() {
public void run() {
rs.job();
}
}.start();
new Thread() {
public void run() {
rs.job2();
}
}.start();
}
} 结果:
job .....
job 使用锁中 ....
job end.....
job2 .....
job22 使用锁中 ...
job2 end.....

结论:

  • 对象实例内,synchronized aMethod(){}可以防止多个线程同时访问这个对象的synchronized方法,在对象内容所有的synchronized 的方法都会同步,必须等上一个方法执行完才能执行下一个方法

2、使用在方法内部 synchronized(Oject){...}、synchronized aMethod(){...}混用

public  class synchTest {
public String a = "";
// 方法一
public void job() {
System.out.println("job .....");
synchronized (a){
System.out.println("job 使用锁中 ....");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
}
System.out.println("job end.....");
}
// 方法二
public synchronized void job2(){
System.out.println("job2 .....");
System.out.println("job22 使用锁中 ...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
System.out.println("job2 end.....");
} public static void main(String[] args) {
final synchTest rs = new synchTest();
new Thread() {
public void run() {
rs.job();
}
}.start();
new Thread() {
public void run() {
rs.job2();
}
}.start();
}
}
结果:
job .....
job 使用锁中 ....
job2 .....
job22 使用锁中 ...
job end.....
job2 end.....

结论:

  对象实例内 synchronized aMethod(){} 与synchronized(Object)  不会相互同步

二、原理

  Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”。

  

  synchronized用的锁是存在Java对象头里的。

  JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。代码块同步是使用monitorenter和monitorexit指令实现的,monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。

  根据虚拟机规范的要求,在执行monitorenter指令时,首先要去尝试获取对象的锁,如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1;

  相应地,在执行monitorexit指令时会将锁计数器减1,当计数器被减到0时,锁就释放了。

  如果获取对象锁失败了,那当前线程就要阻塞等待,直到对象锁被另一个线程释放为止。

注意两点:

  1、synchronized同步快对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;

  2、同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。

  

(图摘自:https://blog.csdn.net/javazejian/article/details/72828483)

Synchronized在jvm字节码上的体现

我们以之前的例子为例,使用javac编译代码,然后使用javap进行反编译。

反编译后部分片段如下图:

对于使用synchronized修饰的方法,反编译后字节码中会有ACC_SYNCHRONIZED关键字。

而synchronized修饰的代码块中,在代码块的前后会有monitorenter、monitorexit关键字,此处的字节码中有两个monitorexit是因为我们有try-catch语句块,有两个出口。

Synchronized与等待唤醒

等待唤醒是指调用对象的wait、notify、notifyAll方法。调用这三个方法时,对象必须被synchronized修饰,因为这三个方法在执行时,必须获得当前对象的监视器monitor对象。

另外,与sleep方法不同的是wait方法调用完成后,线程将被暂停,但wait方法将会释放当前持有的监视器锁(monitor),直到有线程调用notify/notifyAll方法后方能继续执行。而sleep方法只让线程休眠并不释放锁。notify/notifyAll方法调用后,并不会马上释放监视器锁,而是在相应的synchronized代码块或synchronized方法执行结束后才自动释放锁。

自旋锁与自适应自旋锁

  • 引入自旋锁的原因:互斥同步对性能最大的影响是阻塞的实现,因为挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性能带来很大的压力。同时虚拟机的开发团队也注意到在许多应用上面,共享数据的锁定状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。
  • 自旋锁:让该线程执行一段无意义的忙循环(自旋)等待一段时间,不会被立即挂起(自旋不放弃处理器额执行时间),看持有锁的线程是否会很快释放锁。自旋锁在JDK 1.4.2中引入,默认关闭,但是可以使用-XX:+UseSpinning开开启;在JDK1.6中默认开启。
  • 自旋锁的缺点:自旋等待不能替代阻塞,虽然它可以避免线程切换带来的开销,但是它占用了处理器的时间。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好;反之,自旋的线程就会白白消耗掉处理器的资源,它不会做任何有意义的工作,这样反而会带来性能上的浪费。所以说,自旋等待的时间(自旋的次数)必须要有一个限度,例如让其循环10次,如果自旋超过了定义的时间仍然没有获取到锁,则应该被挂起(进入阻塞状态)。通过参数-XX:PreBlockSpin可以调整自旋次数,默认的自旋次数为10。
  • 自适应的自旋锁:JDK1.6引入自适应的自旋锁,自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定:如果在同一个锁的对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。简单来说,就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。
  • 自旋锁使用场景:从轻量级锁获取的流程中我们知道,当线程在获取轻量级锁的过程中执行CAS操作失败时,是要通过自旋来获取重量级锁的。(见前面“轻量级锁”)

总结

  • synchronized特点:保证内存可见性、操作原子性
  • synchronized影响性能的原因:
    • 1、加锁解锁操作需要额外操作;
    • 2、互斥同步对性能最大的影响是阻塞的实现,因为阻塞涉及到的挂起线程和恢复线程的操作都需要转入内核态中完成(用户态与内核态的切换的性能代价是比较大的)
  • synchronized锁:对象头中的Mark Word根据锁标志位的不同而被复用
    • 偏向锁:在只有一个线程执行同步块时提高性能。Mark Word存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单比较ThreadID。特点:只有等到线程竞争出现才释放偏向锁,持有偏向锁的线程不会主动释放偏向锁。之后的线程竞争偏向锁,会先检查持有偏向锁的线程是否存活,如果不存货,则对象变为无锁状态,重新偏向;如果仍存活,则偏向锁升级为轻量级锁,此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁
    • 轻量级锁:在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,尝试拷贝锁对象目前的Mark Word到栈帧的Lock Record,若拷贝成功:虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向对象的Mark Word。若拷贝失败:若当前只有一个等待线程,则可通过自旋稍微等待一下,可能持有轻量级锁的线程很快就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁
    • 重量级锁:指向互斥量(mutex),底层通过操作系统的mutex lock实现。等待锁的线程会被阻塞,由于Linux下Java线程与操作系统内核态线程一一映射,所以涉及到用户态和内核态的切换、操作系统内核态中的线程的阻塞和恢复。

 

Java synchronized的使用与原理的更多相关文章

  1. Java synchronized 关键字的实现原理

    数据同步需要依赖锁,那锁的同步又依赖谁?synchronized给出的答案是在软件层面依赖JVM,而Lock给出的方案是在硬件层面依赖特殊的CPU指令,大家可能会进一步追问:JVM底层又是如何实现sy ...

  2. java多线程系列(五)---synchronized ReentrantLock volatile Atomic 原理分析

    java多线程系列(五)---synchronized ReentrantLock volatile Atomic 原理分析 前言:如有不正确的地方,还望指正. 目录 认识cpu.核心与线程 java ...

  3. Java线程:概念与原理

    Java线程:概念与原理 一.操作系统中线程和进程的概念 现在的操作系统是多任务操作系统.多线程是实现多任务的一种方式. 进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程 ...

  4. Java并发之底层实现原理学习笔记

    本篇博文将介绍java并发底层的实现原理,我们知道java实现的并发操作最后肯定是由我们的CPU完成的,中间经历了将java源码编译成.class文件,然后进行加载,然后虚拟机执行引擎进行执行,解释为 ...

  5. Java 连接池的工作原理(转)

    原文:Java 连接池的工作原理 什么是连接? 连接,是我们的编程语言与数据库交互的一种方式.我们经常会听到这么一句话“数据库连接很昂贵“. 有人接受这种说法,却不知道它的真正含义.因此,下面我将解释 ...

  6. java并发包&线程池原理分析&锁的深度化

          java并发包&线程池原理分析&锁的深度化 并发包 同步容器类 Vector与ArrayList区别 1.ArrayList是最常用的List实现类,内部是通过数组实现的, ...

  7. java synchronized实现可见性对比volatile

    问题: 大家可以先看看这个问题,看看这个是否有问题呢? 那里有问题呢? public class ThreadSafeCache { int result; public int getResult( ...

  8. Java虚拟机类装载的原理及实现(转)

    Java虚拟机类装载的原理及实现(转) 一.引言 Java虚拟机(JVM)的类装载就是指将包含在类文件中的字节码装载到JVM中, 并使其成为JVM一部分的过程.JVM的类动态装载技术能够在运行时刻动态 ...

  9. Java volatile 关键字底层实现原理解析

    本文转载自Java volatile 关键字底层实现原理解析 导语 在Java多线程并发编程中,volatile关键词扮演着重要角色,它是轻量级的synchronized,在多处理器开发中保证了共享变 ...

  10. Java 多线程与并发【原理第二部分笔记】

    Java 多线程与并发[原理第二部分笔记] 什么是Java内存模型中的happens-before Java内存模型,即JMM,本身是一种抽象的概念,并不是真实存在的,他描述的是一组规则或者说是一种规 ...

随机推荐

  1. tortoisegit 还原远程分支到某个版本

    v2还原到v1 1.强制还原(git reset) 如果使用这种方式还原到v1,将丢失还原到v1到v2之间的所有提交及日志. 1.1显示日志 有save1.save2两条提交记录. 1.2 重置版本( ...

  2. 从零开始配置 vim(3)—— 键盘映射进阶

    严格意义上来说,快捷键的绑定应该是键盘映射,将某些键映射为另一些键. 在上篇我们介绍了基本的键盘映射操作,知道了如何 :map.:imap.:vmap.:nmap这些命令来映射键盘快捷键.它们很方便, ...

  3. 从零开始的知识图谱生活,构建一个百科知识图谱,完成基于Deepdive的知识抽取、基于ES的简单语义搜索、基于 REfO 的简单KBQA

    从零开始的知识图谱生活,构建一个百科知识图谱,完成基于Deepdive的知识抽取.基于ES的简单语义搜索.基于 REfO 的简单KBQA 个人入门知识图谱过程中的学习笔记,算是半教程类的,指引初学者对 ...

  4. Umi配置路由

    一.Umi路由的概念 在 Umi 中,你可以在 config/config.js 文件中使用 routes 属性来配置路由.routes 属性是一个数组,每个元素都表示一个路由对象.每个路由对象都包含 ...

  5. 2024-02-03:用go语言,你有 k 个背包。给你一个下标从 0 开始的整数数组 weights, 其中 weights[i] 是第 i 个珠子的重量。同时给你整数 k, 请你按照如下规则将所有

    2024-02-03:用go语言,你有 k 个背包.给你一个下标从 0 开始的整数数组 weights, 其中 weights[i] 是第 i 个珠子的重量.同时给你整数 k, 请你按照如下规则将所有 ...

  6. 硬件、OS、数据库各种兼容性傻傻分不清?

    服务器硬件型号.OS操作系统版本.Oracle数据库版本.. 各种型号各种版本,排列组合的话也是N多种组合,如何确认这些可以兼容并且得到官方认证呢? 其实很简单,各司其职,各自去确认各自的. 我们知道 ...

  7. 如何使用MBP制作Win启动盘

    最近有一个需求,想给家人的一台笔记本安装一套win 10的操作系统,但是我手头上现在没有对应的启动U盘. 由于工作原因,很多年没用win了,工作电脑也都是MBP,根本没有之前使用win时熟悉的Ultr ...

  8. webpack学习笔记(一)安装与试用

    由于初次接触 webpack(官网),对很多方面都不是很理解,在查找部分资料后记录一下自己的见解(本文实践基于webpack4). 1. 个人见解 简单来说,webpack就是js的 打包 工具.个人 ...

  9. 【Unity3D】动态路障导航

    1 NavMeshObstacle组件 ​ 导航系统.分离路面导航中路障都是静态的,程序运行过程中烘焙的导航网格一直不变,本文将进一步讲解动态路障场景下导航的实现. ​ 对于动态路障游戏对象,除了要设 ...

  10. Vue+ElementUI实现用户管理前后分离实战二:API接口篇

    项目介绍 上一篇介绍了前端相关实现代码和效果,本篇则介绍后端接口API如何实现. :) 上一篇地址: https://blog.csdn.net/IndexMan/article/details/11 ...