返回顶部
首页 > 资讯 > 后端开发 > Python >深入了解Java中Synchronized的各种使用方法
  • 505
分享到

深入了解Java中Synchronized的各种使用方法

Java Synchronized用法Java SynchronizedJava Synchronized使用 2022-11-13 14:11:03 505人浏览 安东尼

Python 官方文档:入门教程 => 点击学习

摘要

目录Synchronized关键字Synchronized修饰实例方法Synchronized修饰静态方法Sychronized修饰多个方法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的对象s1和s2,一个时刻只能有一个线程进行s1的add方法,一个时刻只能有一个线程进入s2的add方法,但是同一个时刻可以有两个不同的线程执行s1和s2的add方法,也就说s1的add方法和s2的add是没有关系的,一个线程进入s1的add方法并不会阻止另外的线程进入s2的add方法,也就是说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修饰了两个方法,add和minus。这意味着在同一个时刻这两个函数只能够有一个被一个线程执行,也正是因为add和minus函数在同一个时刻只能有一个函数被一个线程执行,这才会导致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出一个实例对象才能够调用add和minus方法,但是同样对于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去修饰代码块,只让某个代码块一个时刻只能有一个线程执行,除了这个代码块之外的代码还是可以并行的。

比如上面的代码当中add和minus方法没有使用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代码块内部可能进行重排序。

以上就是深入了解Java中Synchronized的各种使用方法的详细内容,更多关于Java Synchronized用法的资料请关注编程网其它相关文章!

--结束END--

本文标题: 深入了解Java中Synchronized的各种使用方法

本文链接: https://lsjlt.com/news/172018.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • 深入了解Java中Synchronized的各种使用方法
    目录Synchronized关键字Synchronized修饰实例方法Synchronized修饰静态方法Sychronized修饰多个方法Synchronized修饰实例方法代码块...
    99+
    2022-11-13
    Java Synchronized用法 Java Synchronized Java Synchronized使用
  • Java中synchronized的几种使用方法
    目录用法简介1、修饰普通方法2、修饰静态方法修饰普通方法 VS 修饰静态方法3、修饰代码块this VS class总结前言: 在 Java 语言中,保证线程安全性的主要手段是加锁,...
    99+
    2024-04-02
  • 深入了解HTML中display属性的各种的属性值及用法
    学习HTML中display属性的多种属性值及其使用方法,需要具体代码示例 在HTML中,display属性用于控制元素的显示方式。通过不同的display属性值,我们可以改变元素的布局方式和显示效果。在本文中,我们将学习dis...
    99+
    2024-02-02
    html 属性值 弹性布局
  • java并发编程之深入理解Synchronized的使用
    1.为什么要使用synchronized 在并发编程中存在线程安全问题,主要原因有:1.存在共享数据 2.多线程共同操作共享数据。关键字synchronized可以保证在同一时刻,只...
    99+
    2024-04-02
  • 深入了解git rebase的使用方法
    Git是目前最流行的版本控制工具之一,它带来了一些改变,包括支持多个分支,并且有助于管理代码版本更新。当我们在团队中合作开发时,往往会遇到一些时候需要合并分支,而这时Git Rebase的使用就显得极为重要。下面我们来一起了解一下Git R...
    99+
    2023-10-22
  • 深入解析MySQL中的各种锁机制
    MySQL 各种锁详解一、引言在并发访问中,数据库需要使用锁来保护数据的一致性和完整性。MySQL 提供了多种类型的锁,包括共享锁、排他锁、意向共享锁、意向排他锁等。本文将使用具体的代码示例介绍并解析这些锁的使用方式和特点。二、共享锁(Sh...
    99+
    2023-12-21
    MySQL - 事务 - 行锁 - 表锁
  • 深入详解Java中synchronized锁升级的套路
    目录锁原理偏向锁轻量级锁重量级锁锁升级无锁偏向锁轻量级锁重量级锁锁粗化&锁消除锁粗化/锁膨胀锁消除synchronized锁是啥?锁其实就是一个对象,随便哪一个都可以,Jav...
    99+
    2023-05-15
    Java synchronized锁升级 Java synchronized锁 Java synchronized
  • Java中关键字synchronized的使用方法详解
    synchronized是Java里的一个关键字,起到的一个效果是“监视器锁”~~,它的功能就是保证操作的原子性,同时禁止指令重排序和保证内存的可见性! public clas...
    99+
    2024-04-02
  • 深入理解java各种集合的线程安全
    线程安全首先要明白线程的工作原理,jvm有一个main memory,而每个线程有自己的workingmemory,一个线程对一个variable进行操作时,都要在自己的workingmemory里面建立一个copy,操作完之后再写入mai...
    99+
    2017-12-26
    java入门 java 线程安全
  • 深入了解Golang中的run方法
    Go是一种快速,可靠和开源的编程语言。Go语言通过其高效的并发性和垃圾回收器以及C的速度,用于构建高效和可扩展的网络服务器和系统编程。让我们深入了解Golang中的run方法。run()方法是golang中重要的一种方法,可以用于创建新的协...
    99+
    2023-05-14
  • 深入了解Java中循环结构的使用
    目录1.Java 循环结构概述2. while 循环2.1 while 循环结构简介2.2 while 循环语法格式2.3 while 循环代码实例3. do…whil...
    99+
    2022-11-13
    Java 循环结构 Java 循环
  • 揭秘CSS基本选择器:深入解析各种选择器的使用方法
    CSS(Cascading Style Sheets)是一种用于描述网页样式的语言。在CSS中,选择器是用来选择需要应用样式的元素的一种方式。选择器的使用方法有很多种,每一种都有其特点和适用场景。本文将深入解析各种CSS基本选择器的用法,帮...
    99+
    2023-12-26
    CSS选择器 深入解析 基本选择器
  • 深入了解git checkout命令的使用方法
    Git是一种流行的版本控制系统,它允许开发人员跟踪和控制代码的更改。Git有许多命令供使用,其中之一就是git checkout。git checkout命令可以用于切换分支、还原更改以及更改工作目录中文件的状态等。在这篇文章中,我们将深入...
    99+
    2023-10-22
  • 深入了解SpringBoot中@ControllerAdvice的介绍及三种用法
    目录浅析@ControllerAdvice1.处理全局异常2.预设全局数据3.请求参数预处理浅析@ControllerAdvice 首先,ControllerAdvice本质上是一个...
    99+
    2023-02-06
    SpringBoot @ControllerAdvice用法 @ControllerAdvice用法 SpringBoot @ControllerAdvice
  • 深入探讨PHP isset()函数的各种用法
    在PHP中,isset()函数是一个非常常用的函数,该函数用于检查变量是否设置并且非NULL。该函数可以用于检查变量、数组成员、对象属性是否被设置。在本文中,我们将深入探讨isset()函数的各种用法。基本用法最常用的形式是检查单个变量。例...
    99+
    2023-05-14
    php
  • 详解Java中String类的各种用法
    目录一、创建字符串二、字符、字节与字符串的转换1.字符与字符串的转换2.字节与字符串的转换三、字符串的比较1.字符串常量池2.字符串内容比较四、字符串查找五、字符串替换六、字符串拆分...
    99+
    2024-04-02
  • 深入研究Vue选择器:掌握Vue中各种选择器的使用方法
    深入解析Vue选择器:学习使用Vue中的各种选择器 Vue.js是一款流行的JavaScript框架,它被广泛应用于构建用户界面。在Vue中,选择器是我们常用的工具,它能够帮助我们找到特定的元素,并对其进行操作。本文将深入解析V...
    99+
    2024-01-15
    学习 使用 Vue选择器
  • 深入了解Java File对象的使用
    目录1.File对象 2.创建文件3.文件的相关操作1.File对象  java封装的一个操作文件及文件夹(目录)的对象。可以操作磁盘上的任何一个文件和文件夹。 2.创建文件 方式一...
    99+
    2022-11-13
    Java File对象 Java File
  • 深入了解Java内部类的用法
    目录1.内部类分类和概念2.局部内部类3.匿名内部类(重要)基于接口的匿名内部类基于类的匿名内部类一些细节匿名内部类的最佳实践4.成员内部类5.静态内部类1.内部类分类和概念 jav...
    99+
    2024-04-02
  • Java中Synchronized的用法解析
    简单介绍 synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:   1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来...
    99+
    2024-04-02
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作