Java并发编程:深入了解synchronized关键字

news/2024/5/17 15:21:49 标签: synchronized, java, 并发编程

目录

 

synchronized%E7%AE%80%E4%BB%8B-toc" style="margin-left:40px;">1:synchronized简介

synchronized%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F-toc" style="margin-left:40px;">2:synchronized使用方式

synchronized%E5%8F%AF%E9%87%8D%E5%85%A5%E6%80%A7-toc" style="margin-left:40px;">3:synchronized可重入性

synchronized%E5%8F%AF%E8%A7%81%E6%80%A7-toc" style="margin-left:40px;">4:synchronized可见性

synchronized%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86-toc" style="margin-left:40px;">5:synchronized实现原理

synchronized%E7%9A%84%E7%BC%BA%E9%99%B7-toc" style="margin-left:40px;">6:synchronized的缺陷

synchronized%E7%9A%84%E4%BD%BF%E7%94%A8%E6%B3%A8%E6%84%8F-toc" style="margin-left:40px;">7:synchronized的使用注意


synchronized%E7%AE%80%E4%BB%8B">1:synchronized简介

在多线程环境中对同一资源同时操作可能会导致结果的不确定性。java内置了synchronized关键字来解决这种问题。synchronized通常也被称为重量级锁,尽管从JDK1.5新增了Lock,但synchronized凭借java自带的高封装性依旧被广泛的使用。synchronized可以把任何一个非null的对象作为锁synchronized有两种主要用法:第一种是对象锁,包括方法锁(默认锁对象为this当前的实例对象)和同步代码块锁(自己指定的锁对象)。第二种是类锁,指synchronized修饰静态的方法或指定锁为Class对象。

synchronized%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F">2:synchronized使用方式

首先,我先写一段代码

public class SynchronizedMethod1 implements Runnable {
    static SynchronizedMethod1 synchronizedMethod = new SynchronizedMethod1();
    static int i = 0;
    @Override
    public void run() {
        for (int j = 0; j < 100000; j++) {
            i++;
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(synchronizedMethod);
        Thread t2 = new Thread(synchronizedMethod);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("最终的结果是:" + i);
    }
}

这段代码大家应该可以看出来是有问题的,没有保证对共享变量i的原子操作,导致的结果就是:无法确定i输出的值。

下面来演示synchronized的几种用法,如何保证数据的准确性。

方式一: 同步代码块

public class SynchronizedMethod2 implements Runnable {
    static SynchronizedMethod2 instance = new SynchronizedMethod2();
    static int i = 0;

    @Override
    public void run() {
         synchronized (this){
            for (int j = 0; j < 100000; j++) {
                i++;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("最终的结果是:" + i);
    }

}

this指的是当前的实例对象。

方式二: 同步方法

public class SynchronizedMethod3 implements Runnable {
    static SynchronizedMethod3 instance = new SynchronizedMethod3();
    static int i = 0;

    @Override
    public void run() {
        add();
    }

    public synchronized void add() {
        for (int j = 0; j < 100000; j++) {
            i++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("最终的结果是:" + i);
    }
}

第二种方式也能够保证数据的准确。

下面我再对上述代码进行改动。实例化两个对象,分别给线程使用。这时会发现,上述的两种方式都尽管都使用了synchronized,但依然无法保证数据的准确。原因是因为我们使用的是synchronized的对象锁,对于不同的实例对象,线程只能够对自己引用的对象进行加锁。

public class SynchronizedMethod3 implements Runnable {
    static SynchronizedMethod3 instance1 = new SynchronizedMethod3();
    static SynchronizedMethod3 instance2 = new SynchronizedMethod3();
    static int i = 0;

    @Override
    public void run() {
        add();
    }

    public synchronized void add() {
        for (int j = 0; j < 100000; j++) {
            i++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(instance1);
        Thread t2 = new Thread(instance2);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("最终的结果是:" + i);
    }
}

演示下面用法的时候先讲一个概念:Java类可能有很多个对象,但只有1个Class对象。这个很重要,要考的!!!比如我new一个Student对象A,一个Student对象B,这是两个对象,但它们只有一个Class对象,这个Class对象由JVM实现。

方式三: 静态方法锁

与第二种方式相比就是在同步方法上多加了static

 

public class SynchronizedMethod4 implements Runnable{
    static SynchronizedMethod4 instance1 = new SynchronizedMethod4();
    static SynchronizedMethod4 instance2 = new SynchronizedMethod4();
    static int i = 0;

    @Override
    public void run() {
        try {
            add();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static synchronized void add() throws InterruptedException {
        System.out.println(Thread.currentThread().getName());
        for (int j = 0; j < 100000; j++) {
            i++;
        }
        Thread.sleep(2000);
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(instance1);
        Thread t2 = new Thread(instance2);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("最终的结果是:" + i);
    }

当作用在静态方法时锁住的便是对象对应的Class实例,因为Class数据存在于永久带,因此静态方法锁相当于该类的一个全局锁。所以就算new了多个对象,但一样能够锁住。

方式四: synchronized锁为Class对象

 

public class SynchronizedMethod5 implements Runnable {
    static SynchronizedMethod5 instance1 = new SynchronizedMethod5();
    static SynchronizedMethod5 instance2 = new SynchronizedMethod5();
    static int i = 0;

    @Override
    public void run() {
        synchronized (SynchronizedMethod5.class) {
            for (int j = 0; j < 100000; j++) {
                i++;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(instance1);
        Thread t2 = new Thread(instance2);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("最终的结果是:" + i);
    }

}

在方法四中,我们的锁对象是*.class,无论实例化多少对象,这些对象也只有一个class对象,所以,这种方式也是可以锁住的。

synchronized%E5%8F%AF%E9%87%8D%E5%85%A5%E6%80%A7">3:synchronized可重入性

synchronized和Lock(ReentrantLock)都是可重入锁,可重入锁又叫做递归锁。在这里只说明synchronized的可重入性,不对二者进行比较。

可重入性定义:同一线程在外层函数获取到锁后,内层函数可以直接再次获取到锁

synchronized的可重入性有两点好处:避免死锁,提升封装性。

这是什么意思呢?我们用如下代码来说明:

public class SynchronizedMethod6 {
    static SynchronizedMethod6 instance = new SynchronizedMethod6();

    public void method1() {
        synchronized (SynchronizedMethod6.class) {
            System.out.println("method1执行成功");
            method2();
        }
    }

    public void method2() {
        synchronized (SynchronizedMethod6.class) {
            System.out.println("method2执行成功");
        }
    }

    public static void main(String[] args) {
        instance.method1();
    }
}

我们假设synchronized不具有重入性,在调用method2时,由于当前持有的锁没有释放,又无法获取到method2中的锁,这时就会导致死锁。

由于synchronizedjava内置的锁,自带重入性,所以封装性更强。

原理:加锁次数计数器。

每个对象都有一把锁,JVM负责跟踪对象被加锁的次数。线程第一次获取对象锁的时候,计数变为1.当这个线程在此对象上再次获得锁的时候,计数会递增。当任务离开的时候,计数递减,当计数递减为0的时候,锁完全被释放。

 

synchronized%E5%8F%AF%E8%A7%81%E6%80%A7">4:synchronized可见性

既然说到线程之间的可见性,那么必须要先了解java的内存模型JMM(这篇博客中简单简单讲解了一下java的内存模型:https://blog.csdn.net/love1793912554/article/details/88618453)。在这里不再详细讲述JMM。对于synchronized来说,在释放锁的时候,线程会把操作的数据刷新到主内存中去,由此就可以保证线程之间的数据可见性。

synchronized%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86">5:synchronized实现原理

synchronizedjava内置的关键字,由此可见它的重要性。但它无法直接通过源码来分析。下面我用反编译字节码信息来分析一下synchronized的实现原理。

先用javac将方法一中的.java文件编译成.class文件,执行javap -v SynchronizedMethod1.class,javap是jdk自带的工具,想要仔细了解可以参考(https://blog.csdn.net/junsure2012/article/details/7099222)。

执行同步代码块需要先获取对象的监视器monitor。monitorenter对应多个monitorexit,释放锁的情况可能有多种,正常释放锁,异常释放锁,所以monitorexit相比较于monitorenter会多。

synchronized%E7%9A%84%E7%BC%BA%E9%99%B7">6:synchronized的缺陷

锁的释放情况少。synchronized释放锁只有两种情况,一种是正常流程运行结束,另一种是发生了异常。如果说一个线程正在执行IO操作(当然,不建议在锁中进行耗时的IO操作,只是举例),那么另一个线程就只能焦急的等待。

试图获得锁时不能设置超时时间。排队获取锁的线程会一直存在,不会因为暂时的堵塞而撤退。与之对应的tryLock(long time, TimeUnit unit) 在指定的时间内不能获得锁就会主动放弃。

不能中断一个正在试图获得锁的线程。

不够灵活:每个锁仅有单一的条件,加锁和释放锁的时机单一。

无法知道是否成功获取到锁。

synchronized%E7%9A%84%E4%BD%BF%E7%94%A8%E6%B3%A8%E6%84%8F">7:synchronized的使用注意

1:锁的对象不能为空,锁是存在于对象头中的,对象都没有,如何加锁。

2:锁的作用域不宜过大,简单点说就是加锁的代码块不能过多,如果代码块都存在与锁中,代 码的执行就变成了串行执行。这就无法体现多线程的效率了。

3:避免死锁。在一个线程中避免获取不同的锁。

 

记录自己的学习和成长,如果写的有什么不对的地方,还请大家多多指正。

 


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

相关文章

如何有效处理数据并发操作问题

本篇文章以我在真实项目中遇到的数据并发问题作为背景&#xff0c;讲解问题出现的原因及解决的办法&#xff0c;以及从中得到的反思。并发中踩过很多坑&#xff0c;可能还有不足的地方&#xff0c;但会一直学习成长&#xff0c;现在将学习到的东西记录下来&#xff0c;&#xf…

Javascript模拟c#方法重载

每次看John Resig的blog&#xff0c;都会有很大的收获&#xff01;这次和大家一起分享一个Javascript模拟c#方法重载的例子。 functionloadMethod(object, name, fn){ varold object[ name ]; if( old ) object[ name ] function(){ …

使用Lock,需要注意的事项?阿里代码

下面代码是上个星期阿里社区推送的一道技术题中的代码。借助这个代码&#xff0c;检验一下自己。 3秒钟&#xff0c;看出什么问题了吗&#xff1f; private final static Lock lock new ReentrantLock(); public static void main(String[] args) {try {lock.tryLock();} cat…

npm scripts 使用指南

Node 开发离不开 npm&#xff0c;而脚本功能是 npm 最强大、最常用的功能之一。 本文介绍如何使用 npm 脚本&#xff08;npm scripts&#xff09;。一、什么是 npm 脚本&#xff1f; npm 允许在package.json文件里面&#xff0c;使用scripts字段定义脚本命令。{// ..."scr…

小程序技巧(跳转到指定节点,锚点效果,非scroll-view实现)

背景&#xff1a; 本人后端开发&#xff0c;最近在援助小程序开发工作&#xff0c;需要实现一下类型于下图中的功能。通过点击事件跳转到指定的位置。 在HTML中实现很简单。在小程序中实现一般需要借助srcoll-view组件&#xff0c;而srcoll-view在实现效果时&#xff0c;需要指…

【Azure Services Platform Step by Step-第4篇】SQL Data Services 编程基础

在上一篇中&#xff0c;我们通过SSDS Explorer详细了解了SDS的工作和操作流程。 这一篇&#xff0c;我们会详细讲解如何使用程序员的方法来操作SDS。 SDS提供SOAP和REST两种接口&#xff0c;这里我们是用RESTC#的方法来讲解。SOAP与之殊途同归&#xff0c;请有兴趣的同学自己查…

还在为小程序生成图片苦恼吗?JSON格式帮你生成,小白也能做

小程序开发中&#xff0c;生成图片的需求是不可避免的&#xff0c;Canvas的使用还算不上优雅简洁。有没有什么开源的组件可以帮助我们快速实现图片的生成呢&#xff1f;当然是有的啦。写者在Github上发现了一个开源组件Painter&#xff0c;十分好用。地址&#xff1a;https://g…

MySQL查询语句的45道练习

一、设有一数据库&#xff0c;包括四个表&#xff1a;学生表&#xff08;Student&#xff09;、课程表&#xff08;Course&#xff09;、成绩表&#xff08;Score&#xff09;以及教师信息表&#xff08;Teacher&#xff09;。四个表的结构分别如表1-1的表&#xff08;一&#…