文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

浅谈 Synchronized 的几种用法,超多干货!

2024-11-29 18:08

关注

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

02、线程安全问题回顾

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

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 之间,如果要完成数据通信的话,需要经历以下几个步骤:

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

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

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

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

public class DataEntity {

    private int count = 0;

    
    public synchronized void addCount(){
        count++;
    }

    public int getCount(){
        return count;
    }
}

多次运行结果如下:

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

运行结果与预期一致!

03、synchronized 使用详解

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

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

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

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

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

3.1、修饰一个方法

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

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

示例如下:

public class DataEntity {

    private int count;

    
    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;

    
    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;

    
    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();

    
    public void addCount(){
        synchronized (lock){
            // todo...
        }
    }
}

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

public class DataEntity {
    
    
    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;  
    }  
}

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

04、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线程需等待对象锁完全释放之后再获取,这就是锁重入。

04、小结

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

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

来源:潘志的研发笔记内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯