如何理解Synchronized

news/2024/5/17 17:22:40 标签: java, 开发语言, 并发编程, synchronized, jvm

synchronized_0">synchronized用法

synchronizedjava提供的一种解决多线程并发问题的内置锁,是目前java中解决并发问题最常用的方法,也是最简单的方法。从语法上讲,synchronized的用法可以分为三种,分别为同步实例方法,同步静态方法和同步代码块

同步实例方法

当一个类中的普通方法被synchronized修饰时,相当于对this对象加锁,这个方法被声明为同步方法。此时,多个线程并发调用同一个对象实例中被synchronized修饰的方法是线程安全的。
修饰同步方法

java">    public synchronized void methodHandler(){
        //方法逻辑
    }

演示多线程调用同一个方法出现线程安全问题

 1. 创建成员变量count,初始值为0
 2. 创建方法increment()对count进行自增处理,该方法没有被synchronized修饰,多线程调用会出现线程安全问题。
 3. 创建execute()方法,实现多线程调用
java">    private Long count = 0L;
java">    public void incrementCount(){
        count++;
    }
java">    public Long execute() throws InterruptedException {
        Thread thread1 = new Thread(()->{
            IntStream.range(0,1000).forEach(i->incrementCount());
        });
        Thread thread2 =new Thread(()->{
            IntStream.range(0,1000).forEach(i->incrementCount());
        });
        //启动线程1 2
        thread1.start();
        thread2.start();
        //等待线程1和线程2执行完毕
        thread1.join();
        thread2.join();
        return count;
    }
java">        public static void main(String[] args) throws InterruptedException {
        SynchronizedTest synchronizedTest = new SynchronizedTest();
        System.out.println(synchronizedTest.execute());
    }

结果如图所示
在这里插入图片描述
通过多次运行测试,预期输出的值都是小于2000,产生了线程安全问题。
解决方法
在increment方法上添加synchronized关键字即可以解决线程安全问题

java">    public synchronized void incrementCount(){
        count++;
    }

在这里插入图片描述

同步静态方法

java的静态方法上添加synchronized关键字对其进行修饰,当一个类的某个静态方法被synchronized修饰时,相当于对这个类的Class对象加锁,而一个类只对应一个Class对象。此时,无论创建多少个当前类的对象被synchronized修饰的静态方法,这个方法都是线程安全的。
修饰静态方法

java">    public static synchronized void methodHandler(){
        //方法逻辑
    }

代码示例

java">  private static Long count = 2000L;

    public static void decrementCount(){
        count--;
    }

    public static Long execute() throws InterruptedException {
        Thread thread1 = new Thread(()->{
            IntStream.range(0,1000).forEach(i->decrementCount());
        });
        Thread thread2 =new Thread(()->{
            IntStream.range(0,1000).forEach(i->decrementCount());
        });
        //启动线程1 2
        thread1.start();
        thread2.start();
        //等待线程1和线程2执行完毕
        thread1.join();
        thread2.join();
        return count;
    }
java">    public static void main(String[] args) throws InterruptedException {
        System.out.println(execute());
    }

多次运行预期结果为0,实际大于0,说明在多线程调用之间产生了线程安全问题。
在这里插入图片描述
解决方法
在静态方法上添加synchronized关键字,如下所示:

java">    public static synchronized void decrementCount(){
        count--;
    }

在这里插入图片描述

同步代码块

synchronized关键字修饰的方法可以保证当前方法是线程安全的,但是如果修饰的方法临界区域较大,或者方法的业务逻辑过多,则可能影响程序的执行效率。此时最好的方法是将一个大的方法分成小的临界区代码。
比如下面的代码

java">	private static Long countA = 0L;
    private static Long countB = 0L;

    public static synchronized void incrementCount(){
        countA++;
        countB++;
    }

在incrementCount方法中分别对countA和countB进行自增操作,对于countA和countB来说,面对的是两个不同的临界区资源。当某个线程进入incrementCount方法时,会对整个方法加锁,占用全部资源。即使在线程对countA进行自增操作而没有对countB进行自增操作时,也会占用countB的资源,其他线程只有等到当前线程执行完countA和countB的自增操作并释放synchronized锁后才能进入incrementCount方法。
所以,如果只将synchronized添加到方法上,其方法包含互不影响的多个临界区资源时,就会造成临界区资源的限制等待,影响程序的性能。为了提高程序的性能,可以将synchronized添加到方法体内,也就是synchronized修饰代码块。
synchronized修饰代码块可以分为两种情况,一种是对某个对象加锁,另一种是对类的class对象加锁。
对某个对象加锁

java">    public void methodHandler(){
        synchronized (obj){
        }
        //方法逻辑
    }

当obj为this时,相当于在普通方法上添加synchronized关键字。
对类的Class对象加锁

java">    public static void methodHandler(){
        synchronized (SynchronizedTest3.class){

        }
    }

上述方法相当于在类的静态方法上添加synchronized关键字。
可以将countA和countB当做两个互不影响的临界区资源,可以修改为下面的代码:

java">  private static Long countA = 0L;
    private static Long countB = 0L;

    private Object countALock = new Object();
    private Object countBLock = new Object();
    
    public void incrementCount(){
        synchronized (countALock){
            countA++;    
        }
        synchronized (countBLock){
            countB++;   
        }
    }

当线程进入incrementCount()方法后,正在执行countB的自增操作时,其他线程依然可以进入incrementCount方法中执行countA的自增操作,因此提高了程序的执行效率。同时incrementCount方法是线程安全的。

synchronized_156">synchronized底层原理

synchronized是基于JVM中的monitor锁实现的,jdk1.5版本之前的synchronized锁性能较低,但是从jdk1.6版本开始,对synchronized锁进行了大量的优化,引入了锁粗化,锁消除,偏向锁,轻量级锁,适应性自旋等技术来提升synchronized锁的性能。
synchronized修饰方式时,当前方法会比普通方法在常量池中多一个ACC_SYNCHRONIZED标识符,synchronized修饰方法的核心原理如下图所示:
在这里插入图片描述
JVM在执行程序时,会根据这个ACC_SYNCHRONIZED标识符完成方法的同步。如果调用了synchronized修饰的方法,则调用的指令会检查方法是否设置了ACC_SYNCHRONZIED标识符。
如果方法设置了SYNCHRONZIED标识符,则当前线程先获取monitor对象,在获取成功后执行同步代码逻辑,执行完毕释放monitor对象。同一时刻,只会有一个线程获取monitor对象成功,进入方法体执行方法逻辑。在当前线程执行方法逻辑之前。也就是当前县城释放monitor对象之前,其他线程无法获取同一个monitor对象。从而保证了同一时刻只能有一个线程进入被synchronized修饰的方法中执行方法体的逻辑。
synchronized修饰代码块时,synchronized关键字会被编译成monitorenter和monitorexit两条指令,monitorenter指令会被放在同步代码的前面,monitorexit指令会被放在同步代码的后面,synchronized修饰代码快的核心原理如下图:
在这里插入图片描述
由上图可以看出,当源码中使用了synchronized修饰代码块,源码中被编译字节码后,同步代码的逻辑前后分别被添加monitorenter和monitorexit指令,使得同一时刻只能一个线程进入monitorenter和monitorexit两条指令中间的同步代码块。
synchronized修饰方法和修饰代码块,在底层的实现上并没有本质区别,只是当synchronized修饰方法时,不需要JVM编译出的字节码完成加锁操作,是一种隐式的实现方式。而当synchronized修饰代码块时,是通过编译出的字节码生成的monitorenter和monitorexit指令完成的,在字节码层面上是一种现实的实现方式。
无论synchronized修饰方法,还是修饰代码块,底层都是通过JVM调用操作系统的Mutex锁实现的,当线程被阻塞时会被挂起,等待CPU重新调度,这会导致线程在操作系统的用户态和内核态之间切换,影响程序的执行性能。

Monitor锁原理

synchronized底层是基于Monitor锁实现的,而monitor锁是基于操作系统的Mutex锁实现的,Mutex锁是操作系统级别的重量级锁,其性能较低。
java中,创建出来的任何一个对象在JVM中都会关联一个Monitor对象,当Monitor对象被一个java对象持有后,这个Monitor对象将处于锁定状态,synchronized在JVM底层本质上都是基于进入和退出Monitor对象来实现同步方法和同步代码快的。
在HotSpot JVM中,Montior是由ObjectMonitor实现的,ObjectMonitor存在两个集合,分别为_waitSet和_EntryList。每个在竞争锁时未获取到锁的线程都会被封装成ObjectWaiter对象,而_waitSet和_EntryList集合就用来存储这些ObjectWaiter对象。
另外,ObjectMonitor中的_owner用来指向获取到ObjectMonitor对象的线程。当一个线程获取到ObjectMonitor对象时,这个ObjectMonitor对象就存储在当前对象的对象头中的MarkWord中(实际上存储的是指向ObjectMonitor对象的指针)。所以,在java中可以使用任意对象作为synchronized锁对象。
当多个线程同时访问一个被synchronized修饰的方法或代码块时,synchronized加锁与解锁在JVM底层的实现大致分为如下几个步骤:

  1. 进入_EntryList集合,当某个线程获取到Monitor对象后,这个线程就会进入_Owner区域,同时,会把Monitor对象中的_owner变量复制为当前线程,并把Monitor对象中的_count变量加1
  2. 当线程调用wait()方法时,当前线程会释放持有的Monitor对象,并且把Monitor对象中的_owner变量设置为null,_count变量减1.同时,当前线程会进入_WaitSet集合中等待被再次唤醒。
  3. 如果获取到Monitor对象的线程执行完毕,则也会释放Monitor对象,当Monitor对象中的_owner变量设置为null,_count变量值减1.

http://www.niftyadmin.cn/n/174085.html

相关文章

Vue学习计划五:了解Vue路由功能

Vue.js 是一个流行的 JavaScript 框架,用于构建现代的 Web 应用程序。Vue.js 提供了许多功能,其中包括路由功能,它使得构建单页应用程序(SPA)变得更加容易。Vue.js 的路由功能通过 Vue Router 实现,Vue Rou…

超详细-安装vCenterv Server Appliance 7.0

目录 介绍: 第一阶段安装: 第二阶段安装: 最近在玩虚拟化记录下过程~~~~ 介绍: VMware vCenterServer 提供了一个可伸缩、可扩展的平台,为 虚拟化管理奠定了基础。 VMware vCenter Server(以前称为 VMw…

多线程进阶学习07------线程中断与等待唤醒

线程的中断协商机制 什么是中断 一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。所以,Thread.stop、Thread.suspend、Thread. resume都已经被废弃了。 在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。 因此,Jav…

2023年PMP考生|考前必练全真模拟题分享,附答案解析

“日日行,不怕千万里;常常做,不怕千万事。”每日五题,备考无压力! 1、项目经理被指派领导一个新组建的敏捷团队,职能经理将任务分配给团队成员,但团队成员并没有感到被授权,项目经理…

23年PMP考试会使用第七版教材吗?

大家都知道了,今年的考纲是改版了的,为啥要改版呢,因为《PMBOK指南》更新到第七版了,考纲自然也要更新,据PMI的市场调查,近年来,项目管理行业新趋势在第六版和旧考纲中未收纳,为了确…

《扬帆优配》国际金价创出阶段性新高 内盘市场风险整体可控

近日世界金价创出阶段性新纪录,是受欧美商场避险心情大涨驱动所造成的。世界商场微观面危险加大,则是避险心情升温的主要原因 近日,海外商场避险心情大幅升温,世界金价假势创出2014.9美元/盎司的阶段性新纪录。在外盘带动下&#…

Windows LIBCurl支持http2.0

今天windows下 vs2019环境环境记录 libcurl 在7.43版本就支持http2.0 文章地址 我本地的版本是7.77 libcurl支持http2.0需要nghttp2库 我下的v1.52.0版本 下载解压以后,使用x64 Native Tools Command Prompt for VS 2019命令行工具 cd到改目录下 执行cmake C:\…

04_C++指针

指针的定义#include <iostream> using namespace std; int main() {int a 10; //定义整型变量aint* p;//指针变量赋值p &a; //指针指向变量a的地址cout << &a << endl; //打印数据a的地址cout << p << endl; //打印指针变量pcout <…