【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

相关文章

SpringBoot内嵌Tomcat临时目录问题及解决

《SpringBoot内嵌Tomcat临时目录问题及解决》:本文主要介绍SpringBoot内嵌Tomcat临时目录问题及解决,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,... 目录SprinjavascriptgBoot内嵌Tomcat临时目录问题1.背景2.方案3.代码中配置t

SpringBoot使用GZIP压缩反回数据问题

《SpringBoot使用GZIP压缩反回数据问题》:本文主要介绍SpringBoot使用GZIP压缩反回数据问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录SpringBoot使用GZIP压缩反回数据1、初识gzip2、gzip是什么,可以干什么?3、Spr

Java程序进程起来了但是不打印日志的原因分析

《Java程序进程起来了但是不打印日志的原因分析》:本文主要介绍Java程序进程起来了但是不打印日志的原因分析,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Java程序进程起来了但是不打印日志的原因1、日志配置问题2、日志文件权限问题3、日志文件路径问题4、程序

Spring 基于XML配置 bean管理 Bean-IOC的方法

《Spring基于XML配置bean管理Bean-IOC的方法》:本文主要介绍Spring基于XML配置bean管理Bean-IOC的方法,本文给大家介绍的非常详细,对大家的学习或工作具有一... 目录一. spring学习的核心内容二. 基于 XML 配置 bean1. 通过类型来获取 bean2. 通过

Spring Boot 集成 Quartz并使用Cron 表达式实现定时任务

《SpringBoot集成Quartz并使用Cron表达式实现定时任务》本篇文章介绍了如何在SpringBoot中集成Quartz进行定时任务调度,并通过Cron表达式控制任务... 目录前言1. 添加 Quartz 依赖2. 创建 Quartz 任务3. 配置 Quartz 任务调度4. 启动 Sprin

springboot上传zip包并解压至服务器nginx目录方式

《springboot上传zip包并解压至服务器nginx目录方式》:本文主要介绍springboot上传zip包并解压至服务器nginx目录方式,具有很好的参考价值,希望对大家有所帮助,如有错误... 目录springboot上传zip包并解压至服务器nginx目录1.首先需要引入zip相关jar包2.然

Java数组初始化的五种方式

《Java数组初始化的五种方式》数组是Java中最基础且常用的数据结构之一,其初始化方式多样且各具特点,本文详细讲解Java数组初始化的五种方式,分析其适用场景、优劣势对比及注意事项,帮助避免常见陷阱... 目录1. 静态初始化:简洁但固定代码示例核心特点适用场景注意事项2. 动态初始化:灵活但需手动管理代

Java使用SLF4J记录不同级别日志的示例详解

《Java使用SLF4J记录不同级别日志的示例详解》SLF4J是一个简单的日志门面,它允许在运行时选择不同的日志实现,这篇文章主要为大家详细介绍了如何使用SLF4J记录不同级别日志,感兴趣的可以了解下... 目录一、SLF4J简介二、添加依赖三、配置Logback四、记录不同级别的日志五、总结一、SLF4J

将Java项目提交到云服务器的流程步骤

《将Java项目提交到云服务器的流程步骤》所谓将项目提交到云服务器即将你的项目打成一个jar包然后提交到云服务器即可,因此我们需要准备服务器环境为:Linux+JDK+MariDB(MySQL)+Gi... 目录1. 安装 jdk1.1 查看 jdk 版本1.2 下载 jdk2. 安装 mariadb(my

SpringBoot中配置Redis连接池的完整指南

《SpringBoot中配置Redis连接池的完整指南》这篇文章主要为大家详细介绍了SpringBoot中配置Redis连接池的完整指南,文中的示例代码讲解详细,具有一定的借鉴价值,感兴趣的小伙伴可以... 目录一、添加依赖二、配置 Redis 连接池三、测试 Redis 操作四、完整示例代码(一)pom.