synchronized用法详解

目录

1.线程安全问题

2.使用同步机制管理共享数据

3.synchronized原理概述

3.1 同步代码块的实现原理

3.2 同步方法的原理

4.synchronized的使用

4.1同步代码块

4.2同步普通方法

4.3同步静态方法

5.synchronized的不可中断性与可重入性

5.1 不可中断性

5.2 可重入性

6.使用synchronized的注意事项

6.1注意synchronized同步块的粒度

6.2 对 String 加锁

6.3 对Integer、Long、Short等包装类加锁


1.线程安全问题

       线程允许程序控制流的多重分支同时存在于一个进程内,它们共享进程范围内的资源,比如内存和文件句柄,但每一个线程都有其自己的程序计数器、栈和本地变量。线程也称为是轻量级的进程,因为线程共享其所属进程的内存地址空间,因此同一进程内的所有线程访问相同的变量,从同一个堆中分配对象,这相对于进程间通信来说实现了良好的数据共享。这是多线程的好处。

       但是,因为线程共享相同的内存地址空间,且并发地运行,它们可能访问或修改其他线程正在使用的变量,当数据意外改变时,如果没有明确的同步来管理共享数据,可能会造成混乱,从而产生意外的结果,引发线程安全问题。造成线程安全问题的原因归结为:

  • 存在共享数据(也称临界资源);
  • 存在多条线程共同操作这些共享数据。

       比如一个简单的场景:取钱。用户输入取款金额,然后系统判断账户余额是否足够,如果足够则取款成功,否则取款失败。

       1、Account账户类,提供draw方法来完成取款操作

/**
 * @author yedashi
 * @version 1.0
 * @date 2022/5/13 9:34
 * @description
 */
public class Account {

    //  账户编号
    private String accountNo;

    // 余额
    private double balance;

    public Account() {}

    public Account(String accountNo, double balance) {
        this.accountNo = accountNo;
        this.balance = balance;
    }

    public String getAccountNo() {
        return accountNo;
    }

    public void setAccountNo(String accountNo) {
        this.accountNo = accountNo;
    }

    // 余额不能随便修改,所以只提供get方法
    public double getBalance() {
        return balance;
    }

    // 提供一个raw方法来完成取钱操作
    public void draw(double drawAccount) {
        //  账户余额大于所取的钱数
        if(balance >= drawAccount) {
            // 吐出钞票
            System.out.println(Thread.currentThread().getName() + " 取钱成功!吐出钞票:" + drawAccount);
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 修改余额
            balance -= drawAccount;
            System.out.println("余额为:" + balance);
        } else {
            System.out.println(Thread.currentThread().getName() + " 取钱失败!余额不足!");
        }
    }
}

      2、取钱线程类,模拟取款操作:

/**
 * @author yedashi
 * @version 1.0
 * @date 2022/5/13 9:41
 * @description
 */
public class DrawThread extends Thread {

    private Account account;
    // 当前线程所希望取到的钱数
    private double drawAccount;

    public DrawThread(String name, Account account, double drawAccount) {
        super(name);
        this.account = account;
        this.drawAccount = drawAccount;
    }

    @Override
    public void run() {
        account.draw(drawAccount);
    }
}

      3、测试,同时启动两个线程,对同一个账户进行取款操作:

package com.stone.crazy.java.ch16.se05;

/**
 * @author yedashi
 * @version 1.0
 * @date 2022/4/19 9:03
 * @description
 */
public class DrawTest {

    public static void main(String[] args) {
        Account account = new Account("1234567", 1000);
        new DrawThread("甲", account, 800).start();
        new DrawThread("乙", account, 800).start();
    }
}

      测试结果 如下:

       从结果得知,账户余额为1000,却总共取出了1600,明显错误了(多次运行,会得到不同的结果,甲乙两个线程取款的顺利也可能不同)。

2.使用同步机制管理共享数据

       针对上面的问题,Java提供了同步机制来协调多线程对共享数据的访问。对于Account类,只需要把draw方法声明为synchronized来保证线程的安全。

/**
 * @author yedashi
 * @version 1.0
 * @date 2022/5/13 9:34
 * @description
 */
public class Account {

    //  账户编号
    private String accountNo;

    // 余额
    private double balance;

    public Account() {}

    public Account(String accountNo, double balance) {
        this.accountNo = accountNo;
        this.balance = balance;
    }

    public String getAccountNo() {
        return accountNo;
    }

    public void setAccountNo(String accountNo) {
        this.accountNo = accountNo;
    }

    // 余额不能随便修改,所以只提供get方法
    public double getBalance() {
        return balance;
    }

    // 提供一个raw方法来完成取钱操作
    public synchronized void draw(double drawAccount) {
        //  账户余额大于所取的钱数
        if(balance >= drawAccount) {
            // 吐出钞票
            System.out.println(Thread.currentThread().getName() + " 取钱成功!吐出钞票:" + drawAccount);
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 修改余额
            balance -= drawAccount;
            System.out.println("余额为:" + balance);
        } else {
            System.out.println(Thread.currentThread().getName() + " 取钱失败!余额不足!");
        }
    }
}

      再次进行测试,结果如下:

       无论运行多少次,结果都一样正确。对比上面的结果,也可以发现,线程按顺利访问draw方法,按照代码的先后顺序执行,一个线程执行完draw方法后,另一个线程才接着执行draw方法后,这样可以保证多个线程不会同时访问同一个共享数据。

3.synchronized原理概述

        synchronized是Java的一个关键字。来自官方的解释:Synchronized方法支持一种简单的策略,用于防止线程干扰和内存一致性错误:如果一个对象对多个线程可见,则对该对象变量的所有读或写操作都通过Synchronized方法完成。

       Synchronized保证同一时刻有且只有一条线程在操作共享数据,其他线程必须等待该线程处理完数据后再对共享数据进行操作。此时便产生了互斥锁,互斥锁的特性如下:

  • 互斥性:即在同一时刻只允许一个线程持有某个对象锁,通过这种特性来实现多线程协调机制,这样在同一时刻只有一个线程对所需要的同步的代码块(复合操作)进行访问。互斥性也成为了操作的原子性。
  • 可见性:必须确保在锁释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程可见(即在获得锁时应获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作,从而引起数据不一致。

        Synchronized是最基本的互斥手段,保证同一时刻最多只有1个线程执行被Synchronized修饰的方法 / 代码,其他线程 必须等待当前线程执行完该方法 / 代码块后才能执行该方法 / 代码块。

3.1 同步代码块的实现原理

        Synchronized是由JVM实现的一种实现互斥同步的一种方式,Synchronized同步代码块时,如果查看编译后的字节码,会发现,被Synchronized修饰过的程序块,在编译前后被编译器生成了monitorenter和monitorexit两个字节码指令,其中,monitorexit指令出现了两次。monitorenter指向同步代码块的开始位置,monitorexit指明同步代码块的结束位置。如图:

        先看monitorenter指令。每个对象都是一个监视器锁(monitor)(不加 synchronized 的对象不会关联监视器),在虛拟机执行到monitorenter指令时,首先要尝试获取对象的锁,获取monitor的所有权:

       (1)如果monitor的进入数为0,表示这个对象没有被锁定,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者;
       (2)如果线程已经占有该monitor,说明当前线程已经拥有了这个对象的锁,只是重新进入,则进入monitor的进入数加1;
       (3)如果其他线程已经占用了monitor,则获取monitor的所有权失败,该线程进入阻塞状态等待,直到monitor的进入数为0,再重新尝试获取monitor的所有权;

        执行monitorexit指令的线程必须是对象锁所对应的monitor的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者锁,就被释放了其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
       monitorexit指令出现了两次,第1次为同步正常退出释放锁,第2次为发生异步退出释放锁,也就是说获得锁的线程可以通过正常控制路径退出,或者在同步代码块中抛出异常来释放锁。

  • 35
    点赞
  • 155
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值