目录
多线程基础
Synchronized
使用多线程模拟售票系统
线程同步机制
具体同步方法-Synchronized
分析同步原理
互斥锁
基本介绍
使用互斥锁来解决售票问题
注意事项和细节
线程的死锁
基本介绍
应用案例
释放锁
下面操作会释放锁
释放锁的分析
下面操作不会释放锁
章节作业
1、编程题
2、编程题
多线程基础
Synchronized
使用多线程模拟售票系统
[售票系统],编程模拟三个售票窗口售票,分别使用继承Thread和实现Runnable方式,并分析有什么问题
package com17.ticket; /** * @author 甲柒 * @version 1.0 * @title SellTicket * @package com17.ticket * @time 2023/3/27 11:41 * 使用多线程,模拟三个窗口同时售票 100张 */public class SellTicket { public static void main(String[] args) {// //测试// SellTicket01 sellTicket01 = new SellTicket01();// SellTicket01 sellTicket02 = new SellTicket01();// SellTicket01 sellTicket03 = new SellTicket01();//// //这里会出现票数超卖现象// sellTicket01.start();//启动售票线程// sellTicket02.start();//启动售票线程// sellTicket03.start();//启动售票线程 System.out.println("=====使用Runnable接口方式实现====="); SellTicket02 sellTicket02 = new SellTicket02(); //这里也会出现票数超卖现象 new Thread(sellTicket02).start();//第1个线程-窗口 new Thread(sellTicket02).start();//第2个线程-窗口 new Thread(sellTicket02).start();//第3个线程-窗口 }} //使用Thread方式class SellTicket01 extends Thread { private static int ticketNum = 100;//让多个线程共享ticketNum @Override public void run() { while (true) { if (ticketNum <= 0) { System.out.println("售票结束~~~"); } //休眠50毫秒 try { Thread.sleep(50); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票" + " 剩余票数 " + (--ticketNum)); } }} //实现接口方式class SellTicket02 implements Runnable { private static int ticketNum = 100;//让多个线程共享ticketNum @Override public void run() { while (true) { if (ticketNum <= 0) { System.out.println("售票结束~~~"); } //休眠50毫秒 try { Thread.sleep(50); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票" + " 剩余票数 " + (--ticketNum)); } }}
票数会超卖
线程同步机制
- 在多线程编程,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何时刻,最多有一个线程访问,以保证数据的完整性。
- 也可以这样理解:线程同步,即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作。
具体同步方法-Synchronized
1、同步代码块
synchronized(对象){//得到对象的锁,才能操作同步代码 //需要被同步代码}
2、synchronized还可以放在方法声明中,表示整个方法-为同步方法
public synchronized void m(String name){ //需要被同步的代码}
3、如何理解:
就好像某人去厕所前先把门关上(上锁),完事后再出来(解锁),那么其他人就可以在使用厕所了
4、使用synchronized解决售票问题
package com17.syn;/** * @author 甲柒 * @version 1.0 * @title SellTicket * @package com17.syn * @time 2023/3/29 16:41 * 使用多线程,模拟三个窗口同时售票 100张 */public class SellTicket { public static void main(String[] args) {// //测试// SellTicket01 sellTicket01 = new SellTicket01();// SellTicket01 sellTicket02 = new SellTicket01();// SellTicket01 sellTicket03 = new SellTicket01();//// //这里会出现票数超卖现象// sellTicket01.start();//启动售票线程// sellTicket02.start();//启动售票线程// sellTicket03.start();//启动售票线程// System.out.println("=====使用Runnable接口方式实现=====");// SellTicket02 sellTicket02 = new SellTicket02();//// //这里也会出现票数超卖现象// new Thread(sellTicket02).start();//第1个线程-窗口// new Thread(sellTicket02).start();//第2个线程-窗口// new Thread(sellTicket02).start();//第3个线程-窗口 //测试 SellTicket03 sellTicket03 = new SellTicket03(); new Thread(sellTicket03).start();//第1个线程-窗口 new Thread(sellTicket03).start();//第2个线程-窗口 new Thread(sellTicket03).start();//第3个线程-窗口 }}//使用Thread方式class SellTicket01 extends Thread { private static int ticketNum = 100;//让多个线程共享ticketNum @Override public void run() { while (true) { if (ticketNum <= 0) { System.out.println("售票结束~~~"); } //休眠50毫秒 try { Thread.sleep(50); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票" + " 剩余票数 " + (--ticketNum)); } }}//实现接口方式class SellTicket02 implements Runnable { private static int ticketNum = 100;//让多个线程共享ticketNum @Override public void run() { while (true) { if (ticketNum <= 0) { System.out.println("售票结束~~~"); } //休眠50毫秒 try { Thread.sleep(50); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票" + " 剩余票数 " + (--ticketNum)); } }}//实现接口方式,使用synchronized实现线程同步class SellTicket03 implements Runnable { private static int ticketNum = 100;//让多个线程共享ticketNum private boolean loop = true;//控制run方法变量 public synchronized void sell() {//同步方法,在同一时刻,只能有一个线程来执行run方法 if (ticketNum <= 0) { System.out.println("售票结束~~~"); loop = false; return; } //休眠50毫秒 try { Thread.sleep(50); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票" + " 剩余票数 " + (--ticketNum)); } @Override public void run() { while (loop) { sell();//sell方法是一个同步方法 } }}
分析同步原理
互斥锁
基本介绍
- Java语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性
- 每个对象都对应于一个可称为”互斥锁”的标记,这个标记用来保证在任意时刻,只能有一个线程访问该对象
- 关键字synchronized来与对象的互斥锁联系。当某个对象用synchronized修饰时,表明该对象在任意时刻只能由一个线程访问
- 同步的局限性:导致程序的执行效率要降低
- 同步方法(非静态的)的锁可以是this,也可以是其他对象(要求是同一个对象)
- 同步方法(静态的)的锁为当前类本身
使用互斥锁来解决售票问题
package com17.syn;/** * @author 甲柒 * @version 1.0 * @title SellTicket * @package com17.syn * @time 2023/3/29 16:41 * 使用多线程,模拟三个窗口同时售票 100张 */public class SellTicket { public static void main(String[] args) {// //测试// SellTicket01 sellTicket01 = new SellTicket01();// SellTicket01 sellTicket02 = new SellTicket01();// SellTicket01 sellTicket03 = new SellTicket01();//// //这里会出现票数超卖现象// sellTicket01.start();//启动售票线程// sellTicket02.start();//启动售票线程// sellTicket03.start();//启动售票线程// System.out.println("=====使用Runnable接口方式实现=====");// SellTicket02 sellTicket02 = new SellTicket02();//// //这里也会出现票数超卖现象// new Thread(sellTicket02).start();//第1个线程-窗口// new Thread(sellTicket02).start();//第2个线程-窗口// new Thread(sellTicket02).start();//第3个线程-窗口 //测试 SellTicket03 sellTicket03 = new SellTicket03(); new Thread(sellTicket03).start();//第1个线程-窗口 new Thread(sellTicket03).start();//第2个线程-窗口 new Thread(sellTicket03).start();//第3个线程-窗口 }}//使用Thread方式//new SellTicket01.start();//new SellTicket01.start();class SellTicket01 extends Thread { private static int ticketNum = 100;//让多个线程共享ticketNum// public void m1(){// synchronized (this){// System.out.println("hello");// }// } @Override public void run() { while (true) { if (ticketNum <= 0) { System.out.println("售票结束~~~"); } //休眠50毫秒 try { Thread.sleep(50); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票" + " 剩余票数 " + (--ticketNum)); } }}//实现接口方式class SellTicket02 implements Runnable { private static int ticketNum = 100;//让多个线程共享ticketNum @Override public void run() { while (true) { if (ticketNum <= 0) { System.out.println("售票结束~~~"); } //休眠50毫秒 try { Thread.sleep(50); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票" + " 剩余票数 " + (--ticketNum)); } }}//实现接口方式,使用synchronized实现线程同步class SellTicket03 implements Runnable { private static int ticketNum = 100;//让多个线程共享ticketNum Object object = new Object(); private boolean loop = true;//控制run方法变量 //同步方法(静态的)的锁为当前类本身 //解读 //1. public synchronized static void m1() {} 锁是加在 SellTicket03.class //2. 如果在静态方法中,实现一个同步代码块 /* synchronized (SellTicket03.class){ System.out.println("m2"); } */ public synchronized static void m1() { } public static void m2() { synchronized (SellTicket03.class) { System.out.println("m2"); } } //说明 //1. public synchronized void sell() {} 就是一种同步方法 //2. 这时锁在 this对象 //3. 也可以在代码块上写 synchronized ,同步代码块,互斥锁还是在this对象 public /*synchronized*/ void sell() {//同步方法,在同一时刻,只能有一个线程来执行run方法 synchronized (/*this*/ object) { if (ticketNum <= 0) { System.out.println("售票结束~~~"); loop = false; return; } //休眠50毫秒 try { Thread.sleep(50); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票" + " 剩余票数 " + (--ticketNum)); } } @Override public void run() { while (loop) { sell();//sell方法是一个同步方法 } }}
注意事项和细节
- 同步方法如果没有使用static修饰:默认锁对象为this
- 如果方法使用static修饰,默认锁对象:当前类.class
- 实现的落地步骤:
- 需要先分析上锁的代码
- 选择同步代码块或同步方法
- 要求多个线程的锁对象为同一个
线程的死锁
基本介绍
多个线程都占用了对方的锁资源,但是不肯相让,导致了死锁,在编程时一定要避免死锁的发生
应用案例
package com17.syn;/** * @author 甲柒 * @version 1.0 * @title DeadLock_ * @package com17.syn * @time 2023/3/29 17:49 * 模拟线程死锁 */public class DeadLock_ { public static void main(String[] args) { //模拟死锁现象 DeadLockDemo A = new DeadLockDemo(true); A.setName("A线程"); DeadLockDemo B = new DeadLockDemo(false); B.setName("B线程"); A.start(); B.start(); }}//线程class DeadLockDemo extends Thread { static Object o1 = new Object();//保证多线程,共享一个对象,这里使用static static Object o2 = new Object(); boolean flag; public DeadLockDemo(boolean flag) {//构造器 this.flag = flag; } @Override public void run() { //下面业务逻辑的分析 //1. 如果flag 为 True,线程就会先得到 持有 o1 对象锁,然后尝试去获取 o2 对象锁 //2. 如果线程A 得不到 o2 对象锁,就会Blocked //3. 如果flag 为 False,线程就会先得到 持有 o2 对象锁,然后尝试去获取 o1 对象锁 //4. 如果线程B 得不到 o1 对象锁,就会Blocked if (flag) { synchronized (o1) {//对象互斥锁,下面就是同步代码 System.out.println(Thread.currentThread().getName() + " />
释放锁
下面操作会释放锁
- 当前线程的同步方法、同步代码块执行结束
案例:上厕所,完事出来 - 当前线程在同步代码块、同步方法中遇到break、return
案例:没有正常的完事,经理叫他修改bug,不得已出来 - 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束
案例:没有正常的完事,发现忘带纸,不得已出来 - 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁
案例:没有正常完事,觉得需要酝酿一下,所以出来等会再进去
释放锁的分析
下面操作不会释放锁
- 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行,不会释放锁
案例:上厕所,太困了,在坑位上眯了一会 - 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁
提示:尽量避免使用suspend()和resume()来控制线程,方法不再推荐使用
章节作业
1、编程题
- 在main方法中启动两个线程
- 第1个线程循环随机打印100以内的整数
- 直到带2个线程从键盘读取了"Q"命令
代码如下
package com17.exercise;import java.util.Scanner;/** * @author 甲柒 * @version 1.0 * @title Exercise01 * @package com17.exercise * @time 2023/3/29 18:50 */public class Exercise01 { public static void main(String[] args) { A a = new A(); B b = new B(a); a.start(); b.start(); }}//创建A线程类class A extends Thread { private boolean loop = true; @Override public void run() { //输出1-100 数字 while (loop) { System.out.println((int) (Math.random() * 100 + 1)); //休眠 try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } System.out.println("A线程退出..."); } public void setLoop(boolean loop) {//可以修改loop变量 this.loop = loop; }}//直到带2个线程从键盘读取了"Q"命令class B extends Thread { private A a; private Scanner scanner = new Scanner(System.in); public B(A a) {//构造器中,直接传入A类对象 this.a = a; } @Override public void run() { while (true) { //接收到用户的输入 System.out.println("请输入你的指令(Q)表示退出:"); char key = scanner.next().toUpperCase().charAt(0); if (key == 'Q') { //以通知的方式结束A线程 a.setLoop(false); System.out.println("b线程退出~~~"); break; } } }}
2、编程题
- 有2个用户分别从同一个卡上取钱(总额:10000)
- 每次都取1000,当余额不足时,就不能取款了
- 不能出现超取现象=》线程同步问题
代码如下
package com17.exercise;/** * @author 甲柒 * @version 1.0 * @title Exercise02 * @package com17.exercise * @time 2023/3/29 19:14 */public class Exercise02 { public static void main(String[] args) { T t = new T(); Thread thread1 = new Thread(t); thread1.setName("t1"); Thread thread2 = new Thread(t); thread2.setName("t2"); thread1.start(); thread2.start(); }}//编程收款的线程//1.因为这里涉及到多个线程共享资源,所以我们使用实现Runnable方式//2.每次取出1000class T implements Runnable { private int money = 10000; @Override public void run() { while (true) { //解读 //1. 这里使用synchronized实现了线程同步 //2. 当多个线程执行到这里时,就会去争夺this对象锁 //3. 哪个线程争夺到(获取)this对象锁,就执行synchronized代码块,执行完后,会释放this对象锁 //4. 争夺不到this对象锁,就blocked,准备继续争夺 //5.this对象锁是非公平锁 synchronized (this) {// //判断余额是否够 if (money < 1000) { System.out.println("余额不足~~~~"); break; } money -= 1000; System.out.println(Thread.currentThread().getName() + " 取出了1000 当前余额=" + money); try { //休眠1s Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } } }}