多线程系列(三) -synchronized 关键字使用详解

本文详细讨论了多线程环境中的线程安全问题,通过实例分析了脏读现象,并介绍了Java的内存模型。重点讲解了`synchronized`关键字如何确保线程同步,包括其在方法、静态方法和代码块上的应用,以及锁重入的概念。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、简介

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

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

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

二、线程安全问题介绍

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

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().
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值