Java之线程同步(同步方法、同步代码块)(关键字synchronized)(案例分析)

以下内容都是本人总结的笔记,若有误感谢指出!

目录

一、引言。

(1)问题发现。

(2)解决思路(synchronized)

二、案例分析

(1)案例。

(2)思路分析。

(3)按平常的逻辑去写代码。

(I)支付方法。(pay()方法)

(II)测试方法。(张三线程、张三女朋友线程)

(III)测试结果。

(IIII)出错原因。

(IIIII)产生这种不安全的前提条件。

多个线程,同时执行。

多个线程需要共享资源。

​编辑

多个线程同时执行共享的代码。

(4)代码改进。(同步代码或同步代码块)

(I)支付方法。(pay()方法)

同步方法解决。

同步代码块解决。

(II)测试方法。(张三线程、张三女朋友线程)

(III)测试结果。

三、总结

(1)同步代码块。同步方法。

(2)关于对象的构成。(具体以后讨论)

(3)死锁问题。


一、引言。

(1)问题发现。
  • 多线程的并发执行可以提高程序的效率。但是多个线程访问共享资源时,会引发一些安全问题
  • 为了适应某些实际需求。限制共享资源在同一时刻只能被一个线程访问
  • 线程安全——现实中的案例,如张三和张三女朋友同时使用银行卡付款购物、购票系统中的多个售票窗口同时出售指定额度的票等等。具体案例在文章中详细介绍
(2)解决思路(synchronized
  • 为了实现多个线程处理同一个资源,在Java中提供了"同步机制"。
  • 当多个线程使用同一个共享资源时,可以将处理共同资源的代码放在一个使用"synchronized"关键字修饰的代码块中。这个代码块称为"同步代码块"。
  • 当然也有"同步方法"。也是使用"synchronized"关键字修饰声明。

具体详细的细节文章中详细介绍

二、案例分析

(1)案例。
  • 人物:张三与张三女朋友。
  • 俩人同时对一个银行账户进行消费。
  • 其中张三买1杯奶茶: 消费20元。张三女朋友买1条裙子: 消费100元。
  • 当前的银行账户余额: 100元。

  • 按照实际情况,这种时候是无法满足需求的。因为只能有一个人成功。
  • 要么张三女朋友买了100元的裙子,剩余0元,张三支付失败!或者张三买了20元的奶茶,剩余80元,张三女朋友支付失败!(张三买奶茶、张三女朋友不能买裙子或者张三女朋友买裙子、张三不能买奶茶)

下面直接写程序进行模拟上述情况

(2)思路分析。
  • 支付时就是买东西。所以设计时,张三是一个线程张三女朋友也是一个线程
  • 他们是并发的、同时的。
(3)按平常的逻辑去写代码。
  • 通过Thread.sleep(100)(线程睡眠)模拟支付的交易时间。
  • 简单的逻辑判断。支付金额<银行账户余额,即可支付成功!

  • 关键字volatile。用处就是使整个代码执行时,这个变量的值都是直接用主存里面的,也就是同步。不会使用cpu提供的高速缓存区域(3个)。具体下次博客讲解。
  • new Thread(Runnable target)。里面参数传入的是一个任务类,也就是Runnable接口的实现子类。这里使用匿名内部类。不过改进后选择使用Lambda表达式,因为更简单实用。

(I)支付方法。(pay()方法)
/**
 * @Title: PayTask
 * @Author HeYouLong
 * @Package PACKAGE_NAME
 * @Date 2024/10/11 下午8:49
 * @description:消费任务
 */
public class PayTask {
    //账户余额
    //关键字volatile的用处就是使整个代码执行时,这个变量的值都是用主存里面的,也就是同步,不会使用cpu提供的高速缓存区域(3个)
    private volatile int balancer = 100;
    //private boolean flag;
    /*
    * 支付方法
    * */
    public void pay(int money) throws InterruptedException {
        //支付金额<银行余额,才能支付
        if(money <= balancer) {
            //线程睡眠——>模拟支付的交易时间
            Thread.sleep(100);
            //购买后
            balancer -= money;
            System.out.println(Thread.currentThread().getName()+"支付"+money+"元成功,余额:"+balancer);
        }else {
            System.out.println(Thread.currentThread().getName()+"支付"+money+"失败,余额:"+balancer);
        }
    }

}
(II)测试方法。(张三线程、张三女朋友线程)
/**
 * @Title: Test02
 * @Author HeYouLong
 * @Package PACKAGE_NAME
 * @Date 2024/10/11 下午9:02
 * @description: 测试类
 */
public class Test02 {
    public static void main(String[] args) {
        //因为是同资源。相同的银行卡账号,所以只实例化一个对象
        PayTask payTask = new PayTask();
        //模拟张三线程、张三女朋友线程
        Thread thread01 = new Thread(() -> {
            //内部就是在执行重写run()方法
            System.out.println("张三购买奶茶...");
            try {
                payTask.pay(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"张三");
        Thread thread02 = new Thread(() -> {
            //内部就是在执行重写run()方法
            System.out.println("张三女朋友购买裙子...");
            try {
                payTask.pay(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        },"张三女朋友");

        //启动线程
        //这两个线程并发的,看谁先抢到时间片执行支付方法
        thread01.start();
        thread02.start();
    }
}
(III)测试结果。

(还有其它出现错误测试结果,但这里只讲解一个。其它错误的情况自己类推即可)

(IIII)出错原因。
  • 这部分代码因为线程等待(Thread.sleep())问题(线程并发执行)让整个代码其实内部执行时是分割的。也就是"张三线程"执行一段,"张三女朋友线程"执行一段。

简单如下介绍一下

  • 两个线程是共享资源的。(同一个payTask对象)
  • 其次将代码的执行过程解析。"张三线程"进入支付方法。执行到睡眠,睡眠后执行100-20=80元。因为关键字volatile,账户余额全部同步都变成80。也因为"张三女朋友线程"也因为花费的金额100元<=账户余额100,也进入该支付方法。此时"张三女朋友线程"抢到执行,也同样执行到80-100=-20元。导致结果,后面的时间片两个线程都输出"支付成功,余额-20元"。
  • 需要改进。也就是用到前面讲的"同步方法"与"同步代码块"。
  • 总结原因。就是在修改余额的时候,另外一个线程侵入进来了。
(IIIII)产生这种不安全的前提条件。
  • 多个线程,同时执行。
  • 解决方法:在两个线程的start()方法之间添加代码:"Thread.sleep(1000)"。让主线程休息1s,等"张三线程"执行完毕后,再让"张三女朋友线程"执行支付操作。
        thread01.start();
        Thread.sleep(1000);
        thread02.start();
  • 多个线程需要共享资源。
  • 解决方法:创建两个对象。这样相当于他们有两张银行卡,去分别支付各自的费用。
        PayTask payTask = new PayTask();
        PayTask payTask1 = new PayTask();
Thread thread01 = new Thread(() -> {
            //内部就是在执行重写run()方法
            System.out.println("张三购买奶茶...");
            try {
                payTask.pay(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"张三");
        Thread thread02 = new Thread(() -> {
            //内部就是在执行重写run()方法
            System.out.println("张三女朋友购买裙子...");
            try {
                payTask1.pay(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        },"张三女朋友");
  • 多个线程同时执行共享的代码。
  • 最重要的问题不是在共享资源的问题。而是他们两个线程在执行pay()方法时,内部是被拆分的,所以出现了问题。
  • 前面的两种情况现实生活是无法避免的。如同时进行消费,消费同一张银行卡。
  • 解决方法:关键字"synchronized"。也就是"同步方法"与"同步代码块"来解决。具体如下。这也是并发编程三大原则之一,"原子性"。(“不分割”)
  • 具体的关于锁、"锁对象"这里就不深入学习和讲解了。后面再讨论。
(4)代码改进。(同步代码或同步代码块)

解决方案如下。


//同步方法  锁 this
public synchronized 返回类型 方法名(){
    
}
//锁  类.class对象
public synchronized  static  返回类型 方法名(){
    
}
//同步代码块
public  返回类型 方法名(){
    //...代码1
    synchronized(对象){
        //代码2
    }
    //...代码3
}
(I)支付方法。(pay()方法)
  • 同步方法解决。
/*
    * 支付方法
    * 同步方法
    * */
    public synchronized void pay(int money) throws InterruptedException {
        //支付金额<银行余额,才能支付
        if(money <= balancer) {
            //线程睡眠——>模拟支付的交易时间
            Thread.sleep(100);
            //购买后
            balancer -= money;
            System.out.println(Thread.currentThread().getName()+"支付"+money+"元成功,余额:"+balancer);
        }else {
            System.out.println(Thread.currentThread().getName()+"支付"+money+"失败,余额:"+balancer);
        }
    }
  • 同步代码块解决。

这里使用的锁对象是类对象。因为是共享资源,调用的是同一个对象,它里面的对象数据都是唯一的Object 类型的lock)

public class PayTask {
    //账户余额
    private volatile int balancer = 100;
    //锁对象
    private Object lock = new Object();
    /*
    * 支付方法
    * 同步方法
    * */
    public void pay(int money) throws InterruptedException {
        synchronized (lock) {
            //支付金额<银行余额,才能支付
            if(money <= balancer) {
                //线程睡眠——>模拟支付的交易时间
                Thread.sleep(100);
                //购买后
                balancer -= money;
                System.out.println(Thread.currentThread().getName()+"支付"+money+"元成功,余额:"+balancer);
            }else {
                System.out.println(Thread.currentThread().getName()+"支付"+money+"失败,余额:"+balancer);
            }
        }
    }

}
(II)测试方法。(张三线程、张三女朋友线程)
/**
 * @Title: Test02
 * @Author HeYouLong
 * @Package PACKAGE_NAME
 * @Date 2024/10/11 下午9:02
 * @description: 测试类
 */
public class Test02 {
    public static void main(String[] args) throws InterruptedException {
        //因为是同资源。相同的银行卡账号,所以只实例化一个对象
        PayTask payTask = new PayTask();
        /*PayTask payTask1 = new PayTask();*/
        //模拟张三线程、张三女朋友线程
        Thread thread01 = new Thread(() -> {
            //内部就是在执行重写run()方法
            System.out.println("张三购买奶茶...");
            try {
                payTask.pay(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"张三");
        Thread thread02 = new Thread(() -> {
            //内部就是在执行重写run()方法
            System.out.println("张三女朋友购买裙子...");
            try {
                /*payTask1.pay(100);*/
                payTask.pay(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        },"张三女朋友");

        //启动线程
        //这两个线程并发的,看谁先抢到时间片执行支付方法
        thread01.start();
        /*Thread.sleep(1000);*/
        thread02.start();
    }
}
(III)测试结果。
  • 测试得到的结果都是随机的。因为是两个线程的并发执行。
  • "张三线程"与"张三女朋友线程"互相抢时间片。然后抢锁,执行完再释放锁对象。

三、总结

(1)同步代码块。同步方法。
  • 优点:保证并发编程的原子性,保证了线程安全
  • 缺点:它的执行效率降低,可能造成死锁问题
  • 同步代码块的执行效率要比同步方法快一点
(2)关于对象的构成。(具体以后讨论)
  • 我们这里讨论的锁是"锁对象"。

(3)死锁问题。
  • 下篇博客详细学习。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

岁岁岁平安

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值