当前位置: 首页 > article >正文

并发编程系列(三) -synchronized关键字介绍

最近对 Java 并发编程技术知识进行了重新整理,再次献上文章合集索引,感兴趣的小伙伴可以直接点击如下地址快速阅读。

  • 并发编程系列(一) -多线程技术快速入门
  • 并发编程系列(二) -Thread类介绍
  • 并发编程系列(三) -synchronized关键字介绍
  • 并发编程系列(四) -volatile关键字介绍
  • 并发编程系列(五) -死锁产生原因介绍
  • 并发编程系列(六) -线程等待与唤醒机制介绍
  • 并发编程系列(七) -本地线程ThreadLocal类介绍
  • 并发编程系列(八) -可重入锁ReentrantLock类介绍(上)
  • 并发编程系列(九) -可重入锁ReentrantLock类介绍(下)
  • 并发编程系列(十) -读写锁ReadWriteLock类介绍
  • 并发编程系列(十一) -超高并发读写锁StampedLock类介绍
  • 并发编程系列(十二) -生产者和消费者模型介绍
  • 并发编程系列(十三) -阻塞队列BlockingQueue介绍
  • 并发编程系列(十四) -线程组介绍
  • 并发编程系列(十五) -线程池技术介绍
  • 并发编程系列(十六) -常用线程并发同步技术介绍
  • 并发编程系列(十七) -常用并发原子操作技术介绍
  • 并发编程系列(十八) -AQS设计原理解读

一、简介

在之前的线程系列文章中,我们介绍了线程创建的几种方式以及常用的方法介绍。

今天我们接着聊聊多线程线程安全的问题,以及解决办法。

实际上,在多线程环境中,难免会出现多个线程对一个对象的实例变量进行同时访问和操作,如果编程处理不当,会产生脏读现象。

二、线程安全问题介绍

我们先来看一个简单的线程安全问题的例子!

public class DataEntity {

    private int count = 0;

    public void addCount(){
        count++;
    }

    public int getCount(){
        return count;
    }
}
public class MyThread extends Thread {

    private DataEntity entity;

    public MyThread(DataEntity entity) {
        this.entity = entity;
    }

    @Override
    public void run() {
        for (int j = 0; j < 1000000; j++) {
            entity.addCount();
        }
    }
}
public class MyThreadTest {

    public static void main(String[] args) {
        // 初始化数据实体
        DataEntity entity = new DataEntity();
        //使用多线程编程对数据进行计算
        for (int i = 0; i < 10; i++) {
            MyThread thread = new MyThread(entity);
            thread.start();
        }

        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("result: " + entity.getCount());
    }
}

多次运行结果如下:

第一次运行:result: 9788554
第二次运行:result: 9861461
第三次运行:result: 6412249
...

上面的代码中,总共开启了 10 个线程,每个线程都累加了 1000000 次,如果结果正确的话,自然而然总数就应该是 10 * 1000000 = 10000000。

但是多次运行结果都不是这个数,而且每次运行结果都不一样,为什么会出现这个结果呢

简单的说,这是主内存和线程的工作内存数据不一致,以及多线程执行时无序,共同造成的结果

我们先简单的了解一下 Java 的内存模型,后期我们在介绍里面的原理!

如上图所示,线程 A 和线程 B 之间,如果要完成数据通信的话,需要经历以下几个步骤:

  • 1.线程 A 从主内存中将共享变量读入线程 A 的工作内存后并进行操作,之后将数据重新写回到主内存中;
  • 2.线程 B 从主存中读取最新的共享变量,然后存入自己的工作内存中,再进行操作,数据操作完之后再重新写入到主内存中;

如果线程 A 更新后数据并没有及时写回到主存,而此时线程 B 从主内存中读到的数据,可能就是过期的数据,于是就会出现“脏读”现象。

因此在多线程环境下,如果不进行一定干预处理,可能就会出现像上文介绍的那样,采用多线程编程时,程序的实际运行结果与预期会不一致,就会产生非常严重的问题。

针对多线程编程中,程序运行不安全的问题,Java 提供了synchronized关键字来解决这个问题,当多个线程同时访问共享资源时,会保证线程依次排队操作共享变量,从而保证程序的实际运行结果与预期一致。

我们对上面示例中的DataEntity.addCount()方法进行改造,再看看效果如下。

public class DataEntity {

    private int count = 0;

    /**
     * 在方法上加上 synchronized 关键字
     */
    public synchronized void addCount(){
        count++;
    }

    public int getCount(){
        return count;
    }
}

多次运行结果如下:

第一次运行:result: 10000000
第二次运行:result: 10000000
第三次运行:result: 10000000
...

运行结果与预期一致!

三、synchronized 使用详解

synchronized作为 Java 中的关键字,在多线程编程中,有着非常重要的地位,也是新手了解并发编程的基础,从功能角度看,它有以下几个比较重要的特性:

  • 原子性:即一个或多个操作要么全部执行成功,要么全部执行失败。synchronized关键字可以保证只有一个线程拿到锁,访问共享资源
  • 可见性:即一个线程对共享变量进行修改后,其他线程可以立刻看到。执行synchronized时,线程获取锁之后,一定从主内存中读取数据,释放锁之前,一定会将数据写回主内存,从而保证内存数据可见性
  • 有序性:即保证程序的执行顺序会按照代码的先后顺序执行。synchronized关键字,可以保证每个线程依次排队操作共享变量

synchronized也被称为同步锁,它可以把任意一个非 NULL 的对象当成锁,只有拿到锁的线程能进入方法体,并且只有一个线程能进入,其他的线程必须等待锁释放了才能进入,它属于独占式的悲观锁,同时也属于可重入锁。

关于锁的知识,我们后面在介绍,大家先了解一下就行。

从实际的使用角度来看,synchronized修饰的对象有以下几种:

  • 修饰一个方法:被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象
  • 修饰一个静态的方法:其作用的范围是整个静态方法,作用的对象是这个类的所有对象
  • 修饰一个代码块:被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象,使用上比较灵活

下面我们一起来看看它们的具体用法。

3.1、修饰一个方法

synchronized修饰一个方法时,多个线程访问同一个对象,哪个线程持有该方法所属对象的锁,就拥有执行权限,否则就只能等待。

如果多线程访问的不是同一个对象,不会起到保证线程同步的作用

示例如下:

public class DataEntity {

    private int count;

    /**
     * 在方法上加上 synchronized 关键字
     */
    public synchronized void addCount(){
        for (int i = 0; i < 3; i++) {
            try {
                System.out.println(Thread.currentThread().getName() + ":" + (count++));
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public int getCount() {
        return count;
    }
}
public class MyThreadA extends Thread {

    private DataEntity entity;

    public MyThreadA(DataEntity entity) {
        this.entity = entity;
    }

    @Override
    public void run() {
        entity.addCount();
    }
}
public class MyThreadB extends Thread {

    private DataEntity entity;

    public MyThreadB(DataEntity entity) {
        this.entity = entity;
    }

    @Override
    public void run() {
        entity.addCount();
    }
}
public class MyThreadTest {

    public static void main(String[] args) {
        // 初始化数据实体
        DataEntity entity = new DataEntity();

        MyThreadA threadA = new MyThreadA(entity);
        threadA.start();

        MyThreadB threadB = new MyThreadB(entity);
        threadB.start();


        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("result: " + entity.getCount());
    }
}

运行结果如下:

Thread-0:0
Thread-0:1
Thread-0:2
Thread-1:3
Thread-1:4
Thread-1:5
result: 6

当两个线程共同操作一个对象时,此时每个线程都会依次排队执行。

假如两个线程操作的不是一个对象,此时没有任何效果,示例如下:

public class MyThreadTest {

    public static void main(String[] args) {
        DataEntity entity1 = new DataEntity();
        MyThreadA threadA = new MyThreadA(entity1);
        threadA.start();

        DataEntity entity2 = new DataEntity();
        MyThreadA threadB = new MyThreadA(entity2);
        threadB.start();


        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("result: " + entity1.getCount());
        System.out.println("result: " + entity2.getCount());
    }
}

运行结果如下:

Thread-0:0
Thread-1:0
Thread-0:1
Thread-1:1
Thread-0:2
Thread-1:2
result: 3
result: 3

从结果上可以看出,当synchronized修饰一个方法,当多个线程访问同一个对象的方法,每个线程会依次排队;如果访问的不是一个对象,线程不会进行排队,像正常执行一样。

3.2、修饰一个静态的方法

synchronized修改一个静态的方法时,代表的是对当前.java文件对应的 Class 类加锁,不区分对象实例。

示例如下:

public class DataEntity {

    private static int count;

    /**
     * 在静态方法上加上 synchronized 关键字
     */
    public synchronized static void addCount(){
        for (int i = 0; i < 3; i++) {
            try {
                System.out.println(Thread.currentThread().getName() + ":" + (count++));
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static int getCount() {
        return count;
    }
}
public class MyThreadA extends Thread {

    @Override
    public void run() {
        DataEntity.addCount();
    }
}
public class MyThreadB extends Thread {

    @Override
    public void run() {
        DataEntity.addCount();
    }
}
public class MyThreadTest {

    public static void main(String[] args) {

        MyThreadA threadA = new MyThreadA();
        threadA.start();

        MyThreadB threadB = new MyThreadB();
        threadB.start();


        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("result: " + DataEntity.getCount());
    }
}

运行结果如下:

Thread-0:0
Thread-0:1
Thread-0:2
Thread-1:3
Thread-1:4
Thread-1:5
result: 6

静态同步方法和非静态同步方法持有的是不同的锁,前者是类锁,后者是对象锁,类锁可以理解为这个类的所有对象。

3.3、修饰一个代码块

synchronized用于修饰一个代码块时,只会控制代码块内的执行顺序,其他试图访问该对象的线程将被阻塞,编程比较灵活,在实际开发中用的应用比较广泛。

示例如下

public class DataEntity {

    private int count;

    /**
     * 在方法上加上 synchronized 关键字
     */
    public void addCount(){
        synchronized (this){
            for (int i = 0; i < 3; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() + ":" + (count++));
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public int getCount() {
        return count;
    }
}
public class MyThreadTest {

    public static void main(String[] args) {
        // 初始化数据实体
        DataEntity entity = new DataEntity();

        MyThreadA threadA = new MyThreadA(entity);
        threadA.start();

        MyThreadB threadB = new MyThreadB(entity);
        threadB.start();


        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("result: " + entity.getCount());
    }
}

运行结果如下:

Thread-0:0
Thread-0:1
Thread-0:2
Thread-1:3
Thread-1:4
Thread-1:5
result: 6

其中synchronized (this)中的this,表示的是当前类实例的对象,效果等同于public synchronized void addCount()

除此之外,synchronized()还可以修饰任意实例对象,作用的范围就是具体的实例对象。

比如,修饰个自定义的类实例对象,作用的范围是拥有lock对象,其实也等价于synchronized (this)

public class DataEntity {

    private Object lock = new Object();

    /**
     * synchronized 可以修饰任意实例对象
     */
    public void addCount(){
        synchronized (lock){
            // todo...
        }
    }
}

当然也可以用于修饰类,表示类锁,效果等同于public synchronized static void addCount()

public class DataEntity {
    
    /**
     * synchronized 可以修饰类,表示类锁
     */
    public void addCount(){
        synchronized (DataEntity.class){
            // todo...
        }
    }
}

synchronized修饰代码块,比较经典的应用案例,就是单例设计模式中的双重校验锁实现。

public class Singleton {  

    private volatile static Singleton singleton;  
    
    private Singleton (){}  
    
    public static Singleton getSingleton() {  
        if (singleton == null) {  
            synchronized (Singleton.class) {  
                if (singleton == null) {  
                    singleton = new Singleton();  
                }  
            }  
        }  
        return singleton;  
    }  
}

采用代码块的实现方式,编程会更加灵活,可以显著的提升并发查询的效率。

四、synchronized 锁重入介绍

synchronized关键字拥有锁重入的功能,所谓锁重入的意思就是:当一个线程得到一个对象锁后,再次请求此对象锁时可以再次得到该对象的锁,而无需等待。

我们看个例子就能明白。

public class DataEntity {

    private int count = 0;

    
    public synchronized void addCount1(){
        System.out.println(Thread.currentThread().getName() + ":" + (count++));
        addCount2();
    }

    public synchronized void addCount2(){
        System.out.println(Thread.currentThread().getName() + ":" + (count++));
        addCount3();
    }

    public synchronized void addCount3(){
        System.out.println(Thread.currentThread().getName() + ":" + (count++));

    }

    public int getCount() {
        return count;
    }
}
public class MyThreadA extends Thread {

    private DataEntity entity;

    public MyThreadA(DataEntity entity) {
        this.entity = entity;
    }

    @Override
    public void run() {
        entity.addCount1();
    }
}
public class MyThreadB extends Thread {

    private DataEntity entity;

    public MyThreadB(DataEntity entity) {
        this.entity = entity;
    }

    @Override
    public void run() {
        entity.addCount1();
    }
}
public class MyThreadTest {

    public static void main(String[] args) {
        // 初始化数据实体
        DataEntity entity = new DataEntity();

        MyThreadA threadA = new MyThreadA(entity);
        threadA.start();

        MyThreadB threadB = new MyThreadB(entity);
        threadB.start();


        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("result: " + entity.getCount());
    }
}

运行结果如下:

Thread-0:0
Thread-0:1
Thread-0:2
Thread-1:3
Thread-1:4
Thread-1:5
result: 6

从结果上看线程没有交替执行,线程Thread-0获取到锁之后,再次调用其它带有synchronized关键字的方法时,可以快速进入,而Thread-1线程需等待对象锁完全释放之后再获取,这就是锁重入。

五、小结

从上文中我们可以得知,在多线程环境下,恰当的使用synchronized关键字可以保证线程同步,使程序的运行结果与预期一致。

  • 1.当synchronized修饰一个方法时,作用的范围是整个方法,作用的对象是调用这个方法的对象;
  • 2…当synchronized修饰一个静态方法时,作用的范围是整个静态方法,作用的对象是这个类的所有对象;
  • 3.当synchronized修饰一个代码块时,作用的范围是代码块,作用的对象是修饰的内容,如果是类,则这个类的所有对象都会受到控制;如果是任意对象实例子,则控制的是具体的对象实例,谁拥有这个对象锁,就能进入方法体

synchronized是一种同步锁,属于独占式,使用它进行线程同步,JVM 性能开销很大,大量的使用未必会带来好处。

关于更深入的原理知识,我们会在 JVM 系列中进行详解。文章内容难免有所遗漏,欢迎网友留言指出。

六、参考

1、五月的仓颉 - synchronized锁机制

2、你听 - 让你彻底理解Synchronized

写到最后

不会有人刷到这里还想白嫖吧?点赞对我真的非常重要!在线求赞。加个关注我会非常感激!


http://www.kler.cn/a/464959.html

相关文章:

  • 麒麟服务器安装kafka--亲测
  • NLP CH3复习
  • 【Cesium】三、实现开场动画效果
  • Conda 安装 Jupyter Notebook
  • 景区自助售卡机与定点酒店的合作双赢之策-景区酒店方案
  • React-Router 一站式攻略:从入门到精通,掌握路由搭建与权限管控
  • docker 部署nginx
  • 掌握 Dockerfile:格式、解析器指令、环境变量替换
  • uwsgi中指定了uid为nginx,通过subprocess调用conda时候仍尝试读取/root/.config/conda/.condarc
  • Tailwind CSS 实战:响应式布局最佳实践
  • Python从0到100(八十一):神经网络-Fashion MNIST数据集取得最高的识别准确率
  • CSS 学习之 padding 与图形绘制
  • 实训四 :磁盘管理
  • .net core 依赖注入 Dependency Injection, DI
  • Spring Certified Professional 2024 (2V0-72.22)
  • C#二维数组详解
  • 【项目】智能BI洞察引擎 测试报告
  • Adobe ColdFusion 关键安全漏洞紧急修复
  • 解决virtualbox克隆ubuntu虚拟机之后IP重复的问题
  • 第十七周:Fast R-CNN论文阅读
  • 深入理解委托:C# 编程中的强大工具
  • C# 设计模式(结构型模式):外观模式
  • leetcode hot 100 跳跃游戏
  • CANoe中ETH报文浅析
  • 深入理解 MVCC 与 BufferPool 缓存机制
  • Android GameActivity(NativeActivity)读写文件