synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。

synchronized的四种使用方式

  1. 修饰代码块:被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用于调用对象
  2. 修饰方法:被修饰的方法称为同步方法,其作用的范围是整个方法,作用于调用对象

注意:synchronized修饰方法时必须是显式调用,如果没有显式调用,例如子类重写该方法时没有显式加上synchronized,则不会有加锁效果。

  1. 修饰静态方法:其作用的范围是整个静态方法,作用于所有对象
  2. 修饰类:其作用的范围是synchronized后面括号括起来的部分(例如:test.class),作用于所有对象

对象锁和类锁是否会互相影响么?

  • 对象锁:Java的所有对象都含有1个互斥锁,这个锁由JVM自动获取和释放。线程进入synchronized方法的时候获取该对象的锁,当然如果已经有线程获取了这个对象的锁,那么当前线程会等待;synchronized方法正常返回或者抛异常而终止,JVM会自动释放对象锁。这里也体现了用synchronized来加锁的1个好处,方法抛异常的时候,锁仍然可以由JVM来自动释放。

  • 类锁:对象锁是用来控制实例方法之间的同步,类锁是用来控制静态方法(或静态变量互斥体)之间的同步。其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的。java类可能会有很多个对象,但是只有1个Class对象,也就是说类的不同实例之间共享该类的Class对象。Class对象其实也仅仅是1个java对象,只不过有点特殊而已。由于每个java对象都有1个互斥锁,而类的静态方法是需要Class对象。所以所谓的类锁,不过是Class对象的锁而已。

类锁和对象锁不是同1个东西,一个是类的Class对象的锁,一个是类的实例的锁。也就是说:1个线程访问静态synchronized的时候,允许另一个线程访问对象的实例synchronized方法。反过来也是成立的,因为他们需要的锁是不同的。

对应的实验代码如下:

@Slf4j
public class SynchronizedExample { // 修饰一个代码块
public void test1(int j) {
synchronized (this) {
for (int i = 0; i < 10; i++) {
log.info("test1 {} - {}", j, i);
}
}
} // 修饰一个方法
public synchronized void test2(int j) {
for (int i = 0; i < 10; i++) {
log.info("test2 {} - {}", j, i);
}
} // 修饰一个类
public static void test3(int j) {
synchronized (SynchronizedExample.class) {
for (int i = 0; i < 10; i++) {
log.info("test3 {} - {}", j, i);
}
}
} // 修饰一个静态方法
public static synchronized void test4(int j) {
for (int i = 0; i < 10; i++) {
log.info("test4 {} - {}", j, i);
}
} public static void main(String[] args) {
SynchronizedExample example1 = new SynchronizedExample();
SynchronizedExample example2 = new SynchronizedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> {
example1.test2(1);
});
executorService.execute(() -> {
example2.test2(2);
});
}
}

在JDK1.6之前,synchronized一直被称呼为重量级锁(重量级锁就是采用互斥量来控制对资源的访问)。通过反编译成字节码指令可以看到,synchronized会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令。根据虚拟机规范的要求,在执行monitorenter指令时,首先要尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,相应的,在执行monitorexit指令时会将锁计算器减1,当计数器为0时,锁就被释放,然后notify通知所有等待的线程。

Java的线程是映射到操作系统的原生线程上的,如果要阻塞或唤醒一个线程,都需要操作系统来帮忙完成,这就需要用户态和内核态切换,大量的状态转换需要耗费很多处理器的时间。

synchronized的优化

在JDK1.6中对锁的实现引入了大量的优化:

  1. 锁粗化(Lock Coarsening):将多个连续的锁扩展成一个范围更大的锁,用以减少频繁互斥同步导致的性能损耗。
  2. 锁消除(Lock Elimination):JVM即时编译器在运行时,通过逃逸分析,如果判断一段代码中,堆上的所有数据不会逃逸出去从来被其他线程访问到,就可以去除这个锁。
  3. 偏向锁(Biased Locking):目的是消除数据无竞争情况下的同步原语。使用CAS记录获取它的线程。下一次同一个线程进入则偏向该线程,无需任何同步操作。
  4. 适应性自旋(Adaptive Spinning):为了避免线程频繁挂起、恢复的状态切换消耗。线程会进入自旋状态。JDK1.6引入了自适应自旋。自旋时间根据之前锁自旋时间和线程状态,动态变化,可以能减少自旋的时间。
  5. 轻量级锁(Lightweight Locking):在没有多线程竞争的情况下避免重量级互斥锁,只需要依靠一条CAS原子指令就可以完成锁的获取及释放。

在JDK1.6之后,synchronized不再是重量级锁,锁的状态变成以下四种状态:

无锁->偏向锁->轻量级锁->重量级锁

锁的状态

自适应自旋锁

大部分时候,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。如果能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。这项技术就是所谓的自旋锁。

自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数仍然没有获取到锁,则该线程应该被挂起。在JDK1.6中引入了自适应的自旋锁,自适应意味着自旋的时间不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

所谓自旋,不是获取不到就阻塞,而是在原地等待一会儿,再次尝试(当然次数或者时长有限),他是以牺牲CPU为代价来换取内核状态切换带来的开销。借助于适应性自旋,可以在CPU时间片的损耗和内核状态的切换开销之间相对的找到一个平衡,进而能够提高性能

偏向锁

大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的MarkWord里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下MarkWord中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程,如果失败则进行轻量锁的升级。

轻量级锁

如果说偏向锁是只允许一个线程获得锁,那么轻量级锁就是允许多个线程获得锁,但是只允许他们顺序拿锁,不允许出现竞争,也就是拿锁失败的情况,轻量级锁的步骤如下:

  1. 线程1在执行同步代码块之前,JVM会先在当前线程的栈帧中创建一个空间用来存储锁记录,然后再把对象头中的MarkWord复制到该锁记录中,官方称之为DisplacedMarkWord。然后线程尝试使用CAS将对象头中的MarkWord替换为指向锁记录的指针。如果成功,则获得锁,进入步骤3)。如果失败执行步骤2)
  2. 线程自旋,自旋成功则获得锁,进入步骤3)。自旋失败,则膨胀成为重量级锁,并把锁标志位变为10,线程阻塞进入步骤3)
  3. 锁的持有线程执行同步代码,执行完CAS替换MarkWord成功释放锁,如果CAS成功则流程结束,CAS失败执行步骤4)
  4. CAS执行失败说明期间有线程尝试获得锁并自旋失败,轻量级锁升级为了重量级锁,此时释放锁之后,还要唤醒等待的线程

重量级锁

自旋的线程在自旋过程中,成功获得资源(即之前获的资源的线程执行完成并释放了共享资源),则整个状态依然处于轻量级锁的状态,如果自旋失败则进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己,需要从用户态切换到内核态实现。(当竞争竞争激烈时,线程直接进入阻塞状态。不过在高版本的JVM中不会立刻进入阻塞状态而是会自旋一小会儿看是否能获取锁如果不能则进入阻塞状态。)

总结

可以简单总结是如下场景:

  1. 只有一个线程进入加锁区,锁状态是偏向锁
  2. 多个线程交替进入加锁区,锁状态可能是轻量级锁
  3. 多线程同时进入加锁区,锁状态可能是重量级锁

最后,限于笔者经验水平有限,欢迎读者就文中的观点提出宝贵的建议和意见。如果想获得更多的学习资源或者想和更多的技术爱好者一起交流,可以关注我的公众号『全菜工程师小辉』后台回复关键词领取学习资料、进入后端技术交流群和程序员副业群。同时也可以加入程序员副业群Q群:735764906 一起交流。

详解Java多线程锁之synchronized的更多相关文章

  1. 异常处理器详解 Java多线程异常处理机制 多线程中篇(四)

    在Thread中有异常处理器相关的方法 在ThreadGroup中也有相关的异常处理方法 示例 未检查异常 对于未检查异常,将会直接宕掉,主线程则继续运行,程序会继续运行 在主线程中能不能捕获呢? 我 ...

  2. 【Java学习笔记之三十四】超详解Java多线程基础

    前言 多线程并发编程是Java编程中重要的一块内容,也是面试重点覆盖区域,所以学好多线程并发编程对我们来说极其重要,下面跟我一起开启本次的学习之旅吧. 正文 线程与进程 1 线程:进程中负责程序执行的 ...

  3. 【java】详解java多线程

    目录结构: contents structure [+] 线程的创建与启动 继承Thread类创建线程类 实现Runnable接口创建线程类 使用Callable和Future创建线程 线程的生命周期 ...

  4. 详解Java多线程编程中LockSupport

    LockSupport是用来创建锁和其他同步类的基本线程阻塞原语. LockSupport中的park() 和 unpark() 的作用分别是阻塞线程和解除阻塞线程,而且park()和unpark() ...

  5. 看一遍就懂,详解java多线程——volatile

    多线程一直以来都是面试必考点,而volatile.synchronized也是必问点,这里我试图用容易理解的方式来解释一下volatile. 来看一下它的最大特点和作用: 一 使变量在多个线程间可见 ...

  6. 详解Java多线程编程中LockSupport类的线程阻塞用法

    LockSupport类是Java6(JSR166-JUC)引入的一个类,提供了基本的线程同步原语.LockSupport实际上是调用了Unsafe类里的函数,归结到Unsafe里,只有两个函数: p ...

  7. Java多线程6:Synchronized锁代码块(this和任意对象)

    一.Synchronized(this)锁代码块 用关键字synchronized修饰方法在有些情况下是有弊端的,若是执行该方法所需的时间比较长,线程1执行该方法的时候,线程2就必须等待.这种情况下就 ...

  8. Java多线程5:Synchronized锁机制

    一.前言 在多线程中,有时会出现多个线程对同一个对象的变量进行并发访问的情形,如果不做正确的同步处理,那么产生的后果就是“脏读”,也就是获取到的数据其实是被修改过的. 二.引入Synchronized ...

  9. synchronized与static synchronized 的差别、synchronized在JVM底层的实现原理及Java多线程锁理解

    本Blog分为例如以下部分: 第一部分:synchronized与static synchronized 的差别 第二部分:JVM底层又是怎样实现synchronized的 第三部分:Java多线程锁 ...

随机推荐

  1. 【Linux命令】nohup命令

    nohup命令 不挂断的运行,它会使您的脚本被忽略SIGHUP,并重定向stdout/stderr到一个文件nohup.out,以便在您注销后该命令可以在后台继续运行. 1)语法 nohup Comm ...

  2. 【转载】Windows api数据类型

    最近在接触windows api函数,看到了很多之前没有看到过的数据类型,发现“个人图书馆”中有个帖子说的挺详细的,特地搬运过来 Windows 数据类型 Delphi 数据类型 描述 LPSTR P ...

  3. Badboy录制模式

    参考: http://leafwf.blog.51cto.com/872759/1109940 http://www.51testing.com/html/00/130600-1367743.html ...

  4. Leetcode 957.N天后的牢房

    Leetcode 957.N天后的牢房 8间牢房排成一排,每间牢房不是有人住就是空着. 每天,无论牢房是被占用或空置,都会根据以下规则进行更改: 如果一间牢房的两个相邻的房间都被占用或都是空的,那么该 ...

  5. codeforces 454 D. Little Pony and Harmony Chest(状压dp)

    题目链接:http://codeforces.com/contest/454/problem/D 题意:给定一个序列a, 求一序列b,要求∑|ai−bi|最小.并且b中任意两数的最大公约数为1. 题解 ...

  6. Maximum Product UVA - 11059

    Given a sequence of integers S = {S1, S2, . . . , Sn}, you should determine what is the value of the ...

  7. hdu 2844 Coins 多重背包(模板) *

    Coins                                                                             Time Limit: 2000/1 ...

  8. Java深拷贝与序列化

    对基本类型的变量进行拷贝非常简单,直接赋值给另外一个对象即可: int b = 50; int a = b; // 基本类型赋值 对于引用类型的变量(例如 String),情况稍微复杂一些,因为直接等 ...

  9. 列表ListView和ListActivity以及使用SimpleAdapter

    创建listview有2种方式 1>直接使用ListView进行创建 2>让Activity继承ListActivity 下面是listview常用的一些属性 案例: public cla ...

  10. 如何让基于.NET 2.0的应用在高版本的系统上运行?

    我们的WinForm项目是基于.NET 2.0开发的,在部署时,发现有些机器没有.NET 2.0,但是即使这些机器有装.NET 2.0 以上的版本,也无法运行我们的程序.这就比较蛋疼了. 我们查了一下 ...