多线程——深入剖析 Synchronized

多线程——Synchronized 详解


在上一篇文章中我们详细了解了线程安全的概念,也掌握了在 Java 语言中线程安全的三种实现机制:

  • 互斥同步:悲观并发策略,主要有 Synchronized(块结构) 和 J.U.C.Lock(非块结构)
  • 非阻塞同步:乐观并发策略,主要是 CAS 和 原子类
  • 无同步方案:消除数据的争用,主要是 ThreadLocal

在本篇文章中我们来详细探讨保证线程安全中最常见也是最主要的并发正确性保障手段——互斥同步中的 Synchronized 关键字的细节

一、Synchronzized 的三种使用方式

在详细理解 Synchronized 的底层实现原理之前,我们先需要知道 Synchronized 怎么用,Synchronized 关键字主要有以下三种应用方式,下面将分别介绍:

  • 修饰实例方法:修饰类中的普通方法,对当前实例对象加锁,进入同步代码块之前需要获得当前实例的锁

  • 修饰类方法:所谓类方法就是使用 static 关键字修饰的静态方法,不属于某一个具体实例,而是属于类;对当前的类对象(即 Class对象)加锁,进入同步代码块之前需要获得当前 Class 对象的锁

  • 修饰代码块:可以指定加锁对象,对指定对象加锁,进入同步代码块之前需要获得指定对象的锁;如果未指定加锁对象,就会判断加锁块的方法是静态还是非静态,判断锁定类对象还是实例对象

1、Synchronzized 作用于实例方法

当 Synchronzized 修饰实例方法时,就会是对调用该方法的实例对象加锁,注意是实例方法,也就是普通的方法,未被 static 修饰的方法。示例如下:

public class AccountSync implements Runnable{
   
    //共享资源
    public static int i = 0;

    //synchronized 修饰实例方法
    public synchronized void increase(){
   
        i++;
//        System.out.println(Thread.currentThread().getName() + " i的值:" + i);
    }

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

    public static void main(String[] args) throws InterruptedException {
   
        AccountSync instance = new AccountSync();
        //两个线程争用同一个对象
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        //执行 join 操作,确保两个线程都能够运行
        t1.join();
        t2.join();

        //打印结果————200000
        System.out.println(i);
    }
}

在上述代码中,我们可以开启若干个线程通过同一个对象操作共享资源 i ,由于 i++ 操作在执行指令的层面并不是原子操作(前面我们讲过原子性的指令只有 8 个),所以如果一个线程在读取旧值和写回新值之间被中断,第二个线程也会读取到旧值并自增,这样两个线程都在旧值的基础上自增,就会导致旧值经过两个线程自增了 2 次,但是数值却只自增了 1 ,造成了线程安全失败;因此 increase 方法必须使用 Synchronzized 方法修饰,以保证线程安全

在此时我们注意到,两个线程是同时争用实例对象 instance 的,也就是两个线程都会尝试获得实例对象 instance 的锁 ,当其中一个线程拿到 instance 的锁之后,它可以访问这个对象的其他任意 Synchronzized 方法;与之对应的是,当一个线程拿到 instance 的锁,其他线程是不能访问这个对象的其他 Synchronzized 方法的,毕竟一个对象只有一把锁,当一个线程获得了该对象的锁之后,其他线程就无法获取到该对象的锁,当然也就无法访问该对象的其他 Synchronzized 方法,但是其他线程还是可以访问该实例对象的其他非 synchronized 方法,因为访问普通方法不需要对象的锁支持的

如果一个线程 A 访问的是实例对象 obj1 的加锁方法 increase,另一个线程 B 访问的是实例对象 obj2 的加锁方法 increase,这样是允许的,因为两个线程执行 increase 方法所需要的对象锁是不一样的,不需要竞争,此时如果两个线程操作的数据并非共享的,线程安全是有保障的;不过如果两个线程操作的是共享数据,那么线程安全就无法保障了。示例代码如下:

public class AccountSyncBad implements Runnable{
   
    //共享资源
    public static int i = 0;

    //synchronized 修饰实例方法
    public synchronized void increase(){
   
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值