一、引言。
(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)死锁问题。
- 下篇博客详细学习。