深入学习Synchronized各种使用方法

在Java当中synchronized通常是用来标记一个方法或者代码块。在Java当中被synchronized标记的代码或者方法在同一个时刻只能够有一个线程执行被synchronized修饰的方法或者代码块。因此被synchronized修饰的方法或者代码块不会出现数据竞争的情况,也就是说被synchronized修饰的代码块是并发安全的。

Synchronized关键字

synchronized关键字通常使用在下面四个地方:

  • synchronized修饰实例方法。
  • synchronized修饰静态方法。
  • synchronized修饰实例方法的代码块。
  • synchronized修饰静态方法的代码块。

在实际情况当中我们需要仔细分析我们的需求选择合适的使用synchronized方法,在保证程序正确的情况下提升程序执行的效率。

Synchronized修饰实例方法

下面是一个用Synchronized修饰实例方法的代码示例:

public class SyncDemo {

  private int count;

  public synchronized void add() {
count++;
} public static void main(String[] args) throws InterruptedException {
SyncDemo syncDemo = new SyncDemo();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
syncDemo.add();
}
}); Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
syncDemo.add();
}
});
t1.start();
t2.start();
t1.join(); // 阻塞住线程等待线程 t1 执行完成
t2.join(); // 阻塞住线程等待线程 t2 执行完成
System.out.println(syncDemo.count);// 输出结果为 20000
}
}

在上面的代码当中的add方法只有一个简单的count++操作,因为这个方法是使用synchronized修饰的因此每一个时刻只能有一个线程执行add方法,因此上面打印的结果是20000。如果add方法没有使用synchronized修饰的话,那么线程t1和线程t2就可以同时执行add方法,这可能会导致最终count的结果小于20000,因为count++操作不具备原子性。

上面的分析还是比较明确的,但是我们还需要知道的是synchronized修饰的add方法一个时刻只能有一个线程执行的意思是对于一个SyncDemo类的对象来说一个时刻只能有一个线程进入。比如现在有两个SyncDemo的对象s1s2,一个时刻只能有一个线程进行s1add方法,一个时刻只能有一个线程进入s2add方法,但是同一个时刻可以有两个不同的线程执行s1s2add方法,也就说s1add方法和s2add是没有关系的,一个线程进入s1add方法并不会阻止另外的线程进入s2add方法,也就是说synchronized在修饰一个非静态方法的时候“锁”住的只是一个实例对象,并不会“锁”住其它的对象。其实这也很容易理解,一个实例对象是一个独立的个体别的对象不会影响他,他也不会影响别的对象。

Synchronized修饰静态方法

Synchronized修饰静态方法:

public class SyncDemo {

  private static int count;

  public static synchronized void add() {
count++; // 注意 count 也要用 static 修饰 否则编译通过不了
} public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
SyncDemo.add();
}
}); Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
SyncDemo.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(SyncDemo.count); // 输出结果为 20000
}
}

上面的代码最终输出的结果也是20000,但是与前一个程序不同的是。这里的add方法用static修饰的,在这种情况下真正的只能有一个线程进入到add代码块,因为用static修饰的话是所有对象公共的,因此和前面的那种情况不同,不存在两个不同的线程同一时刻执行add方法。

你仔细想想如果能够让两个不同的线程执行add代码块,那么count++的执行就不是原子的了。那为什么没有用static修饰的代码为什么可以呢?因为当没有用static修饰时,每一个对象的count都是不同的,内存地址不一样,因此在这种情况下count++这个操作仍然是原子的!

Sychronized修饰多个方法

synchronized修饰多个方法示例:

public class AddMinus {
public static int ans; public static synchronized void add() {
ans++;
} public static synchronized void minus() {
ans--;
} public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
AddMinus.add();
}
}); Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
AddMinus.minus();
}
}); t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(AddMinus.ans); // 输出结果为 0
}
}

在上面的代码当中我们用synchronized修饰了两个方法,addminus。这意味着在同一个时刻这两个函数只能够有一个被一个线程执行,也正是因为addminus函数在同一个时刻只能有一个函数被一个线程执行,这才会导致ans最终输出的结果等于0。

对于一个实例对象来说:

public class AddMinus {
public int ans; public synchronized void add() {
ans++;
} public synchronized void minus() {
ans--;
} public static void main(String[] args) throws InterruptedException {
AddMinus addMinus = new AddMinus();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
addMinus.add();
}
}); Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
addMinus.minus();
}
}); t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(addMinus.ans);
}
}

上面的代码没有使用static关键字,因此我们需要new出一个实例对象才能够调用addminus方法,但是同样对于AddMinus的实例对象来说同一个时刻只能有一个线程在执行add或者minus方法,因此上面代码的输出同样是0。

Synchronized修饰实例方法代码块

Synchronized修饰实例方法代码块

public class CodeBlock {

  private int count;

  public void add() {
System.out.println("进入了 add 方法");
synchronized (this) {
count++;
}
} public void minus() {
System.out.println("进入了 minus 方法");
synchronized (this) {
count--;
}
} public static void main(String[] args) throws InterruptedException {
CodeBlock codeBlock = new CodeBlock();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
codeBlock.add();
}
}); Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
codeBlock.minus();
}
}); t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(codeBlock.count); // 输出结果为 0
}
}

有时候我们并不需要用synchronized去修饰代码块,因为这样并发度就比较低了,一个方法一个时刻只能有一个线程在执行。因此我们可以选择用synchronized去修饰代码块,只让某个代码块一个时刻只能有一个线程执行,除了这个代码块之外的代码还是可以并行的。

比如上面的代码当中addminus方法没有使用synchronized进行修饰,因此一个时刻可以有多个线程执行这个两个方法。在上面的synchronized代码块当中我们使用了this对象作为锁对象,只有拿到这个锁对象的线程才能够进入代码块执行,而在同一个时刻只能有一个线程能够获得锁对象。也就是说add函数和minus函数用synchronized修饰的两个代码块同一个时刻只能有一个代码块的代码能够被一个线程执行,因此上面的结果同样是0。

这里说的锁对象是this也就CodeBlock类的一个实例对象,因为它锁住的是一个实例对象,因此当实例对象不一样的时候他们之间是没有关系的,也就是说不同实例用synchronized修饰的代码块是没有关系的,他们之间是可以并发的。

Synchronized修饰静态代码块

public class CodeBlock {

  private static int count;

  public static void add() {
System.out.println("进入了 add 方法");
synchronized (CodeBlock.class) {
count++;
}
} public static void minus() {
System.out.println("进入了 minus 方法");
synchronized (CodeBlock.class) {
count--;
}
} public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
CodeBlock.add();
}
}); Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
CodeBlock.minus();
}
}); t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(CodeBlock.count);
}
}

上面的代码是使用synchronized修饰静态代码块,上面代码的锁对象是CodeBlock.class,这个时候他不再是锁住一个对象了,而是一个类了,这个时候的并发度就变小了,上一份代码当锁对象是CodeBlock的实例对象时并发度更大一些,因为当锁对象是实例对象的时候,只有实例对象内部是不能够并发的,实例之间是可以并发的。但是当锁对象是CodeBlock.class的时候,实例对象之间时不能够并发的,因为这个时候的锁对象是一个类。

应该用什么对象作为锁对象

在前面的代码当中我们分别使用了实例对象和类的class对象作为锁对象,事实上你可以使用任何对象作为锁对象,但是不推荐使用字符串和基本类型的包装类作为锁对象,这是因为字符串对象和基本类型的包装对象会有缓存的问题。字符串有字符串常量池,整数有小整数池。因此在使用这些对象的时候他们可能最终都指向同一个对象,因为指向的都是同一个对象,线程获得锁对象的难度就会增加,程序的并发度就会降低。

比如在下面的示例代码当中就是由于锁对象是同一个对象而导致并发度下降:

import java.util.concurrent.TimeUnit;

public class Test {

  public void testFunction() throws InterruptedException {
synchronized ("HELLO WORLD") {
System.out.println(Thread.currentThread().getName() + "\tI am in synchronized code block");
TimeUnit.SECONDS.sleep(5);
}
} public static void main(String[] args) {
Test t1 = new Test();
Test t2 = new Test();
Thread thread1 = new Thread(() -> {
try {
t1.testFunction();
} catch (InterruptedException e) {
e.printStackTrace();
}
}); Thread thread2 = new Thread(() -> {
try {
t2.testFunction();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread1.start();
thread2.start();
}
}

在上面的代码当中我们使用两个不同的线程执行两个不同的对象内部的testFunction函数,按道理来说这两个线程是可以同时执行的,因为执行的是两个不同的实例对象的同步代码块。但是上面代码的执行首先一个线程会进入同步代码块然后打印输出,等待5秒之后,这个线程退出同步代码块另外一个线程才会再进入同步代码块,这就说明了两个线程不是同时执行的,其中一个线程需要等待另外一个线程执行完成才执行。这正是因为两个Test对象当中使用的"HELLO WORLD"字符串在内存当中是同一个对象,是存储在字符串常量池中的对象,这才导致了锁对象的竞争。

下面的代码执行的结果也是一样的,一个线程需要等待另外一个线程执行完成才能够继续执行,这是因为在Java当中如果整数数据在[-128, 127]之间的话使用的是小整数池当中的对象,使用的也是同一个对象,这样可以减少频繁的内存申请和回收,对内存更加友好。

import java.util.concurrent.TimeUnit;

public class Test {

  public void testFunction() throws InterruptedException {
synchronized (Integer.valueOf(1)) {
System.out.println(Thread.currentThread().getName() + "\tI am in synchronized code block");
TimeUnit.SECONDS.sleep(5);
}
} public static void main(String[] args) {
Test t1 = new Test();
Test t2 = new Test();
Thread thread1 = new Thread(() -> {
try {
t1.testFunction();
} catch (InterruptedException e) {
e.printStackTrace();
}
}); Thread thread2 = new Thread(() -> {
try {
t2.testFunction();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread1.start();
thread2.start();
}
}

Synchronized与可见性和重排序

可见性

  • 当一个线程进入到synchronized同步代码块的时候,将会刷新所有对该线程的可见的变量,也就是说如果其他线程修改了某个变量,而且线程需要在Synchronized代码块当中使用,那就会重新刷新这个变量到内存当中,保证这个变量对于执行同步代码块的线程是可见的。

  • 当一个线程从同步代码块退出的时候,也会将线程的工作内存同步到内存当中,保证在同步代码块当中修改的变量对其他线程可见。

重排序

Java编译器和JVM当发现能够让程序执行的更快的时候是可能对程序的指令进行重排序处理的,也就是通过调换程序指令执行的顺序让程序执行的更快。

但是重排序很可能让并发程序产生问题,比如说当一个在synchronized代码块当中的写操作被重排序到synchronized同步代码块外部了这显然是有问题的。

在JVM的实现当中是不允许synchronized代码块内部的指令和他前面和后面的指令进行重排序的,但是在synchronized内部的指令是可能与synchronized内部的指令进行重排序的,比较著名的就是DCL单例模式,他就是在synchronized代码块当中存在重排序的,如果你对DCL单例模式还不是很熟悉,你可以阅读这篇文章DCL单例模式部分。

总结

在本篇文章当中主要介绍了各种synchronized的使用方法,总结如下:

  • Synchronized修饰实例方法,这种情况不同的对象之间是可以并发的。
  • Synchronized修饰实例方法,这种情况下不同的对象是不能并发的,但是不同的类之间可以进行并发。
  • Sychronized修饰多个方法,这多个方法在统一时刻只能有一个方法被执行,而且只能有一个线程能够执行。
  • Synchronized修饰实例方法代码块,同一个时刻只能有一个线程执行代码块。
  • Synchronized修饰静态代码块,同一个时刻只能有一个线程执行这个代码块,而且不同的对象之间不能够进行并发。
  • 应该用什么对象作为锁对象,建议不要使用字符串和基本类型的包装类作为锁对象,因为Java对这些进行优化,很可能多个对象使用的是同一个锁对象,这会大大降低程序的并发度。
  • 程序在进入和离开Synchronized代码块的时候都会将线程的工作内存刷新到内存当中,以保证数据的可见性,这一点和volatile关键字很像,同时Synchronized代码块中的指令不会和Synchronized代码块之间和之后的指令进行重排序,但是Synchronized代码块内部可能进行重排序。

更多精彩内容合集可访问项目:https://github.com/Chang-LeHung/CSCore

关注公众号:一无是处的研究僧,了解更多计算机(Java、Python、计算机系统基础、算法与数据结构)知识。

深入Synchronized各种使用方法的更多相关文章

  1. java synchronized修饰普通方法,修饰静态方法,修饰代码块,修饰线程run方法 比较

    synchronized用于多线程设计,有了synchronized关键字,多线程程序的运行结果将变得可以控制.synchronized关键字用于保护共享数据. synchronized实现同步的机制 ...

  2. synchronized的使用方法

    [转自] http://blog.csdn.net/witsmakemen/article/details/6966116 记下来,很重要. Java语言的关键字,当它用来修饰一个方法或者一个代码块的 ...

  3. synchronized修饰的方法之间相互调用

    1:synchronized修饰的方法之间相互调用,执行结果为There hello  ..因为两个方法(main,hello)的synchronized形成了互斥锁.  所以当main方法执行完之后 ...

  4. synchronized修饰普通方法,修饰静态方法,修饰代码块,修饰线程run方法 比较

    synchronized用于多线程设计,有了synchronized关键字,多线程程序的运行结果将变得可以控制.synchronized关键字用于保护共享数据. synchronized实现同步的机制 ...

  5. synchronized修饰普通方法和静态方法

    首先,要知道,synchronized关键字修饰普通方法时,获得的锁是对象锁,也就是this.而修饰静态方法时,锁是类锁,也就是类名.class. synchronized修饰普通方法 Synchro ...

  6. synchronized 修饰static方法

    •1.依靠对象锁锁定 初始化一个对象时,自动有一个 对象锁. synchronized {普通方法}依靠对象锁工作,多线程访问synchronized方法,一旦某个进程抢得锁之后,其他的进程只有排队对 ...

  7. 被synchronized修饰的方法调用了没有被synchronized修饰的方法,是否是线程安全

    1 被synchronized修饰的方法调用了没有被synchronized修饰的方法,是否线程安全? /** * (1)被synchronized修饰的方法调用了没有被synchronized修饰的 ...

  8. synchronized(修饰方法和代码块)

    synchronized(修饰方法和代码块) 1. 含义 synchronized 是同步锁,用来实现互斥同步. 在 Java 中,关键字 synchronized 可以保证在同一个时刻,只有一个线程 ...

  9. 【synchronized锁】通过synchronized锁 反编译查看字节码指令分析synchronized关键字修饰方法与代码块的区别

    前提: 首先要铺垫几个前置的知识: Java中的锁如sychronize锁是对象锁,Java对象头中具有标识位,当对象锁升级为重量级锁时,重量级锁的标识位会指向监视器monitor, 而每个Java对 ...

随机推荐

  1. SpringBoot实现基于token的登录验证

    一.SpringBoot实现基于token的登录验证 基于token的登录验证实现原理:客户端通过用户名和密码调用登录接口,当验证数据库中存在该用户后,将用户的信息按照token的生成规则,生成一个字 ...

  2. monit 配置详解(monitrc)

    monitrc是Monit的主配置文件(控制文件). monitrc的内容主要分为全局(golbal)和服务(services)两个部分. 默认情况下monitrc文件在/etc/monit目录下. ...

  3. Java中如何快捷的创建不可变集合

    在Java 9中又新增了一些API来帮助便捷的创建不可变集合,以减少代码复杂度. 本期配套视频:Java 9 新特性:快速定义不可变集合 常规写法 以往我们创建一些不可变集合的时候,通常是这样写的: ...

  4. docker安装mysql,开启主从

    docker pull mysql:5.7 创建目录/mydata/mysql/log /mydata/mysql/conf /mydata/mysql/data docker run -itd -- ...

  5. 基于.NetCore开发博客项目 StarBlog - (10) 图片瀑布流

    系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...

  6. JVM学习笔记-从底层了解程序运行(一)

    1:JVM基础知识 什么是JVM 1. java虚拟机,跨语言的平台,实现java跨平台 2. 可以实现多种语言跨平台,只要该语言可以编译成.class文件 3. 解释执行.class文件 java是 ...

  7. HTML入门,基础知识

    初识HTML HTML: 超文本标记语言 一.HTML的基本结构 根控制标记(头) ​ 头控制标记(头) ​ 标题 标题标记 ​ 头控制标记(尾) ​ 网页显示区域(一般要实现的代码都在这里写) &l ...

  8. 腾讯云数据库公有云市场稳居TOP 2!

    7月4日,国际权威机构IDC发布的<2021年下半年中国关系型数据库软件市场跟踪报告>显示,腾讯云数据库在关系型数据库软件市场(公有云模式)中,位列第二. IDC报告显示,2021下半年中 ...

  9. JavaScript知识梳理

    JS内功修炼 专业术语 类,封装,继承, 专业术语 babel 块级作用域 函数 扩展对象的功能性 解构 set和map js的类 改进的数组功能 Promise与异步编程 代理和反射 用模块封装代码 ...

  10. 如果一个promise永不resolve,会内存泄漏吗

    答:跟内存泄漏没有直接关系gc的策略不会改变,如果该promise没有被人引用,就会被gc掉.如果仍被引用,就不会被gc掉.即使一个promise,resolve或者reject了,但是它还被人引用, ...