【Java笔记】多线程2: 加锁小练习(卖票+交替打印+哲学家就餐)

本文主要是介绍【Java笔记】多线程2: 加锁小练习(卖票+交替打印+哲学家就餐),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 0. 稍微回顾点基础
    • 0.1 如何起多线程
      • 0.1.1 继承Thread
      • 0.1.2 实现Runnable接口
      • 0.1.3 Callable接口+Future接口
    • 0.2 如何加锁
      • 0.2.1 synchronized
    • 0.2.2 Lock的实现类
  • 1. 案例:卖电影票
    • 1.1 案例实现:Runnable接口
      • 1.1.1 synchronized 同步代码块
      • 1.1.2 synchronized 同步方法
      • 1.1.3 Lock手动上锁
    • 1.2 案例实现:Thread
  • 2. 案例:两个线程交替打印0-100
  • 3. 面试官:你来写个死锁吧(哲学家就餐问题)
    • 3.* 有关死锁
      • 预防死锁
      • 解除死锁

练习下Thread,Runnable,synchronized, Lock,稍微记录下

0. 稍微回顾点基础

0.1 如何起多线程

一般就是三种方式:

  1. Thread的类,
  2. Runnable接口
  3. Callable+FutureTask接口

0.1.1 继承Thread

主要就是重写run方法,实例化后调用Thread.start()开启线程

public class MyThread extends Thread{@Overridepublic void run() {super.run();for (int i = 0; i < 1000; i++) {System.out.println(getName()+": Hello MyThread!");}}
}
// ----------------------------------------
public class Main {public static void main(String[] args) {MyThread t1 = new MyThread();t1.setName("T1");MyThread t2 = new MyThread();t2.setName("T2");t1.start();t2.start();}
}

0.1.2 实现Runnable接口

实现run方法,并实例化作为参数传入Thread实例
最后也是通过调用Thread.start()开启线程

public class MyRun implements Runnable{@Overridepublic void run() {for (int i = 0; i < 1000; i++) {Thread t = Thread.currentThread(); // 获取当前线程对象System.out.println(t.getName()+"Hello Runnable");// System.out.println(Thread.currentThread().getName()+"Hello Runnable");}}
}
// ---------------------------
public class Main {public static void main(String[] args) {// 创建MyRun对象,表示多线程要执行的任务MyRun myRun = new MyRun();// 创建线程对象Thread t1 = new Thread(myRun);Thread t2 = new Thread(myRun);// 启动线程t1.start();t2.start();}
}

0.1.3 Callable接口+Future接口

最特别的就是可以获取多线程的运行结果,也就是run有返回值

public class MyCallable implements Callable<Integer> {@Overridepublic Integer call() throws Exception {// 求1-100和并返回int sum = 0;for (int i = 0; i < 100; i++) {sum += i;}return sum;}
}
// ---------------------------
public class Main {public static void main(String[] args) throws ExecutionException, InterruptedException {MyCallable mc = new MyCallable();FutureTask<Integer> ft = new FutureTask<>(mc);Thread t1 = new Thread(ft);t1.start();int sum = ft.get();System.out.println(sum);}
}

0.2 如何加锁

主要就是synchronized修饰符或者Lock接口的实现类

0.2.1 synchronized

大概有需要注意几点:

  • synchronized是基于悲观锁的,当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。
  • 锁对象:锁对象一定要唯一。
    • 可以设为本类的class对象,因为一个类的class对象是唯一的,哪怕有很多实例;
    • 也可以在类内增加一个static final的Object实例,也不一定是Object实例,只要是static保证所有类实例都相同,final保证不可更改,也就是一起确保了锁对象的唯一性。
    • 锁膨胀:无锁->偏向锁->轻量级锁->重量级锁

有两种用法:

  1. synchronized 同步代码块
synchronized(锁对象){...
}
  1. synchronized 同步方法
修饰符 synchronized 返回值类型 方法名(方法参数) {...}

注意一下,同步方法会锁住方法里的所有代码,并且锁对象不能自己指定:

  • 非静态方法:this,即当前方法的调用者。此时也叫方法锁
  • 静态方法:当前类的字节码文件。此时也叫类锁

0.2.2 Lock的实现类

比较常用的就是ReentranLock
需要自己手动上锁,一般会用try{要上锁的代码}finally{解锁}来保证所以定会被释放

Lock lock=new ReentrantLock();
lock.lock();
try{...
}finally{lock.unlock();
}

下面来看点案例练练手

1. 案例:卖电影票

1000张电影票,在两个窗口领取,每次领取一张,假设被刺领取的时间为3000ms
用多线程模拟卖票过程,并打印剩余电影票的数量

1.1 案例实现:Runnable接口

1.1.1 synchronized 同步代码块

Runnable实现类

// 实现Runnable接口+synchronized同步代码块
public class TicketRunnable1 implements Runnable {static int tickets = 1000;
//    定义一个锁对象,也可以用当前类的class对象private static Object lock = new Object();@Overridepublic void run() {while (tickets > 0) {synchronized (lock) {if (tickets > 0) {try {Thread.sleep(50);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(Thread.currentThread().getName() + " 卖出1张票,剩余电影票:" + --tickets);}}}}
}

1.1.2 synchronized 同步方法

就是把上面同步代码块抽出一个函数,并且不用自己制定锁对象

// 实现Runnable接口+synchronized同步方法
public class TicketRunnable2 implements Runnable{static int tickets = 1000;// 静态@Overridepublic void run() {while(tickets > 0){sell();}}private synchronized void sell() {if (tickets > 0){try{Thread.sleep(50);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(Thread.currentThread().getName() + " 卖出1张票,剩余电影票:" + --tickets);}}
}

开两个线程测试

public class TestRunnable {// 1000张电影票,在两个窗口领取,假设被领取的时间为3000ms// 时间可以调小点,比如50ms,给cpu上点强度,可以更好体现出线程交替互斥的过程// 用多线程模拟卖票过程,并打印剩余电影票的数量public static void main(String[] args) {
//        TicketRunnable1 = new TicketRunnable1();
//        Thread window1 = new Thread(ticketRunnable1, "Window1");
//        Thread window2 = new Thread(ticketRunnable1, "Window2");TicketRunnable2 ticketRunnable2 = new TicketRunnable2();Thread window1 = new Thread(ticketRunnable2, "Window1");Thread window2 = new Thread(ticketRunnable2, "Window2");window1.start();window2.start();}
}

运行结果
在这里插入图片描述
没啥问题。
如果你想实例化两个runnable实现类也没问题,因为锁对象是唯一,比如下面这样

public class TestRunnable {// 1000张电影票,在两个窗口领取,假设被领取的时间为3000ms// 时间可以调小点,比如50ms,给cpu上点强度,可以更好体现出线程交替互斥的过程// 用多线程模拟卖票过程,并打印剩余电影票的数量public static void main(String[] args) {TicketRunnable1 ticketRunnable1 = new TicketRunnable1();TicketRunnable1 ticketRunnable11 = new TicketRunnable1();Thread window1 = new Thread(ticketRunnable1, "Window1");Thread window2 = new Thread(ticketRunnable11, "Window2");window1.start();window2.start();}
}

1.1.3 Lock手动上锁

// 实现Runnable接口+Lock
public class TicketRunnable3 implements Runnable{static int tickets = 1000;@Overridepublic void run() {ReentrantLock lock = new ReentrantLock();while(tickets > 0){lock.lock();try {Thread.sleep(50);if (tickets > 0){System.out.println(Thread.currentThread().getName() + " 卖出1张票,剩余电影票:" + --tickets);}} catch (InterruptedException e) {throw new RuntimeException(e);} finally {lock.unlock();}}}
}

测试结果
在这里插入图片描述

1.2 案例实现:Thread

先写个Thread的继承类,这里就用同步方法写了,其他锁的办法也差不多,就不多讲咯

public class MyThread1 extends Thread{static int tickets = 1000;@Overridepublic void run() {super.run();while (tickets > 0){sell();}}private static synchronized void sell() {if (tickets > 0){try{Thread.sleep(50);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(Thread.currentThread().getName() + " 卖出1张票,剩余电影票:" + --tickets);}}
}

需要注意的是,这里synchronized的方法sell()必须是静态的,
因为静态的同步方法的锁对象是当前类的字节码文件对象,
而非静态的同步方法的锁对象是this,也就是调用类的实例,这里我们需要实例化两个对象window1和window2,他们的锁对象是不同的,因此会出现不同步的问题,如下图
在这里插入图片描述
票卖的只剩-1了,明显线程间没有同步

2. 案例:两个线程交替打印0-100

这是之前牛客上看到个面试手撕题,用wait()notifyAll()让线程等待与唤醒就行
先给段代码,大家可以思考一下对不对

public class Printer implements Runnable {private static int number = 0;int printId;public Printer(int printId) {this.printId = printId;}@Overridepublic void run() {while (number <= 100) {printNum();}}private synchronized void printNum() {if (number % 2 != printId) {// 不是当前线程就等待try {this.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}// 被唤醒后if (number <= 100) {System.out.println(Thread.currentThread().getName() + ": " + number++);this.notifyAll();}}
}

测试一下

public class Test {public static void main(String[] args) {Printer printer1 = new Printer(0);Printer printer2 = new Printer(1);Thread thread1 = new Thread(printer1);Thread thread2 = new Thread(printer2);thread1.start();;thread2.start();}
}

在这里插入图片描述
很明显,寄了。主要问题就出在代码里用的synchronized同步方法,因为我们是给两个线程一个printId(0或1)以便通过number的奇偶来判断该哪个线程打印,显然,我们需要实例化两个Runnable实现类的对象,前面也说了synchronized非静态同步方法的锁对象是this,也就是实例本身,因此这里其实是没有同步了。
那,是不是直接把这个要同步的printNum方法给static了就行了呢?
很遗憾,也不行,因为printNum方法需要用printId来判断是不是当前线程的回合,而printId是需要通过构造函数初始化的,不可以是static,所以这样直接用同步方法行不通。
换成同步代码块,加一个唯一的锁对象就行了

public class Printer implements Runnable {private static int number = 0;int printId;private static final Object lock = new Object();public Printer(int printId) {this.printId = printId;}@Overridepublic void run() {while (number <= 100) {synchronized (lock){if (number % 2 != printId) {// 不是当前线程就等待try {lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}// 被唤醒后if (number <= 100) {System.out.println(Thread.currentThread().getName() + ": " + number++);lock.notifyAll();}}}}
}

当然这个锁对象用当前类的字节码文件也可以

synchronized (Printer.class){...Printer.class.wait();...Printer.class.notifyAll();
}

结果没毛病
在这里插入图片描述
当然,lock手动加锁就没这些麻烦,记得unlock就好

3. 面试官:你来写个死锁吧(哲学家就餐问题)

这也是今天在牛客上看到的(第一次听到这种请求)
死锁,简单来说就是两个线程互相等对方释放锁资源,然后你等我我等你你等我我等你你等我我等你你等我我等你你等我我等你你等我我等你你等我我等你你等我我等你你等我我等你你等我我等你你等我我等你你等我我等你你等我我等你你等我我等你你等我我等你你等我我等你…一直等下去

public class Test {static Object lock1 = new Object();static Object lock2 = new Object();public static void main(String[] args) {Thread thread1 = new Thread(() -> {synchronized (lock1){System.out.println(Thread.currentThread().getName() + " get lock1!");try {Thread.sleep(2000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(Thread.currentThread().getName() + " wait lock2...");synchronized (lock2){System.out.println(Thread.currentThread().getName() + " get lock2!");}}});Thread thread2 = new Thread(() -> {synchronized (lock2){System.out.println(Thread.currentThread().getName() + " get lock2!");try {Thread.sleep(2000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(Thread.currentThread().getName() + " wait lock1...");synchronized (lock1){System.out.println(Thread.currentThread().getName() + " get lock1!");}}});thread1.start();thread2.start();}
}

启动一下
在这里插入图片描述
就一直在wait力。哲学家就餐问题来说,这里两个线程就是两个哲学家,两个锁就是两支筷子,只有凑够两支筷子才能吃上饭。

3.* 有关死锁

预防死锁

上面这个问题出现的很大一部分原因就是锁的嵌套,实际写代码的时候也要尽量避免这种情况来预防死锁。此外也有银行家算法之类的来评估预防死锁,但今天就先不展开了。

解除死锁

一般就是资源剥夺法,撤销进程法,进程回退法

  • 撤销进程:强制结束一个或多个进程并回收它们的资源,以打破死锁(比如直接kill掉);
  • 进程回退:将一个或多个进程回退到某一安全状态,这些状态之前未涉及死锁。通过回退并重新计算资源分配,系统尝试解决死锁问题;
  • 资源剥夺:挂起/激活机制。挂起一些进程,剥夺它们的资源以解除死锁,待条件满足时,再激活进程。
    这里【进程回退】时其实也涉及到了【资源剥夺】

这篇关于【Java笔记】多线程2: 加锁小练习(卖票+交替打印+哲学家就餐)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/920215

相关文章

Java Stream流与使用操作指南

《JavaStream流与使用操作指南》Stream不是数据结构,而是一种高级的数据处理工具,允许你以声明式的方式处理数据集合,类似于SQL语句操作数据库,本文给大家介绍JavaStream流与使用... 目录一、什么是stream流二、创建stream流1.单列集合创建stream流2.双列集合创建str

springboot集成easypoi导出word换行处理过程

《springboot集成easypoi导出word换行处理过程》SpringBoot集成Easypoi导出Word时,换行符n失效显示为空格,解决方法包括生成段落或替换模板中n为回车,同时需确... 目录项目场景问题描述解决方案第一种:生成段落的方式第二种:替换模板的情况,换行符替换成回车总结项目场景s

SpringBoot集成redisson实现延时队列教程

《SpringBoot集成redisson实现延时队列教程》文章介绍了使用Redisson实现延迟队列的完整步骤,包括依赖导入、Redis配置、工具类封装、业务枚举定义、执行器实现、Bean创建、消费... 目录1、先给项目导入Redisson依赖2、配置redis3、创建 RedissonConfig 配

SpringBoot中@Value注入静态变量方式

《SpringBoot中@Value注入静态变量方式》SpringBoot中静态变量无法直接用@Value注入,需通过setter方法,@Value(${})从属性文件获取值,@Value(#{})用... 目录项目场景解决方案注解说明1、@Value("${}")使用示例2、@Value("#{}"php

SpringBoot分段处理List集合多线程批量插入数据方式

《SpringBoot分段处理List集合多线程批量插入数据方式》文章介绍如何处理大数据量List批量插入数据库的优化方案:通过拆分List并分配独立线程处理,结合Spring线程池与异步方法提升效率... 目录项目场景解决方案1.实体类2.Mapper3.spring容器注入线程池bejsan对象4.创建

线上Java OOM问题定位与解决方案超详细解析

《线上JavaOOM问题定位与解决方案超详细解析》OOM是JVM抛出的错误,表示内存分配失败,:本文主要介绍线上JavaOOM问题定位与解决方案的相关资料,文中通过代码介绍的非常详细,需要的朋... 目录一、OOM问题核心认知1.1 OOM定义与技术定位1.2 OOM常见类型及技术特征二、OOM问题定位工具

基于 Cursor 开发 Spring Boot 项目详细攻略

《基于Cursor开发SpringBoot项目详细攻略》Cursor是集成GPT4、Claude3.5等LLM的VSCode类AI编程工具,支持SpringBoot项目开发全流程,涵盖环境配... 目录cursor是什么?基于 Cursor 开发 Spring Boot 项目完整指南1. 环境准备2. 创建

Spring Security简介、使用与最佳实践

《SpringSecurity简介、使用与最佳实践》SpringSecurity是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架,本文给大家介绍SpringSec... 目录一、如何理解 Spring Security?—— 核心思想二、如何在 Java 项目中使用?——

SpringBoot+RustFS 实现文件切片极速上传的实例代码

《SpringBoot+RustFS实现文件切片极速上传的实例代码》本文介绍利用SpringBoot和RustFS构建高性能文件切片上传系统,实现大文件秒传、断点续传和分片上传等功能,具有一定的参考... 目录一、为什么选择 RustFS + SpringBoot?二、环境准备与部署2.1 安装 RustF

springboot中使用okhttp3的小结

《springboot中使用okhttp3的小结》OkHttp3是一个JavaHTTP客户端,可以处理各种请求类型,比如GET、POST、PUT等,并且支持高效的HTTP连接池、请求和响应缓存、以及异... 在 Spring Boot 项目中使用 OkHttp3 进行 HTTP 请求是一个高效且流行的方式。