• 【多线程 (二)】线程安全问题、同步代码块、同步方法、Lock锁、死锁


    线程安全问题

    前言

    之前我们讲了多线程的基础知识,但是在我们解决实际问题中会遇到一些错误,这些错误是怎么产生的呢?又该如何解决呢?今天我们来学习线程安全问题,下面来看一个生活中常见的卖票问题,我们用多线程实现。

    2.1多线程模拟卖票出现的问题

    • 案例需求
      某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票

    • 实现步骤

      • 定义一个类SellTicket实现Runnable接口,里面定义一个成员变量:private int tickets = 100;
      • 在SellTicket类中重写run()方法实现卖票,代码步骤如下
      • 判断票数大于0,就卖票,并告知是哪个窗口卖的
      • 卖了票之后,总票数要减1
      • 票卖没了,线程停止
      • 定义一个测试类SellTicketDemo,里面有main方法,代码步骤如下
      • 创建SellTicket类的对象
      • 创建三个Thread类的对象,把SellTicket对象作为构造方法的参数,并给出对应的窗口名称
      • 启动线程
    • 代码实现

    public class SellTicket implements Runnable {
        private int ticket = 100;
        @Override
        public void run() {
            while(true){
                if(ticket==0){
                    break;
                }else{
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    ticket--;
                    System.out.println(Thread.currentThread().getName()+"在卖票,还剩下"+ticket+"张票");
    
                }
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    public class Demo {
        public static void main(String[] args) {
            SellTicket st = new SellTicket();
            Thread t1 = new Thread(st,"窗口一");
            Thread t2 = new Thread(st,"窗口二");
            t1.start();
            t2.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 运行结果
      在这里插入图片描述
      在这里插入图片描述

    2.2卖票案例中出现的问题分析

    从运行结果我们可以看出,当线程二买了一张票后,剩余99张票,之后线程一买了一张票后也打印剩余99张票,还会打印出负数票的情况,显然,这是不符合生活常识的,看代码,我们明明在每次线程结束都会使剩余票数减一,也会判断当剩余票数为零时,跳出,线程会处于销毁状态,但是为什么还会出现这些情况呢?我们来分析一下

    • 打印相同票数问题
      在阅读分析前,希望读者熟悉一下我上述的代码,代码中在测试类我创建了两个线程对象,然后开启两个线程,这时Java虚拟机会帮我们调用实现类中的run()方法,我们都知道,在任意时刻,每个线程都有可能抢夺CPU 的使用权,我们先假设线程一刚开始抢夺到了CPU的使用权,并开始执行 run()方法,当执行到 sleep()方法时,线程一休眠,线程二此时抢到了CPU的使用权,并开始执行 run()方法,此时线程一和线程二都在执行run()方法,线程二执行到 sleep()方法时,开始休眠,线程一此时抢夺到CPU的使用权,线程一继续执行run()方法,执行到 ticket- -;此时票数剩余99,而线程一和线程二共享的一个数据,线程二的 ticket 属性从 100 变成 99,当线程一准备执行打印方法时,线程二又抢夺到CPU的使用权,继续执行到 ticket- -;此时线程一和线程二中的 ticket属性都为 98,线程二执行打方法,打印 98,线程一抢到CPU的使用权,执行打印方法打印 98,这也就是为什么会出现打印相同票数的原因了。

    • 打印负数票的问题
      同样,当线程一线程二执行了好多次以后,此时,线程一和线程二都回到原来的起点开始抢夺CPU使用权,前提是ticket此时等于1,当线程一抢夺到CPU的执行权时,开始执行run()方法体中的代码,执行到 ticket- -;,此时的 ticket 等于 0,当线程一准备打印剩余票数时,CPU执行权又被线程二抢夺,线程二开始执行run()方法,此时线程二的 ticket 属性值等于 0,当线程二执行到 ticket - -;时,此时线程一和线程二的 ticket 属性值都会自减一,此时 ticket 属性值从 0 变成 -1,接着线程一抢夺到CPU使用权,打印 -1,线程二随后抢到CPU执行权,也打印 -1,这就是为什么会出现负数票的原因了。

    2.3同步代码块解决数据安全问题

    • 首先我们总结一下上述安全问题出现的条件
      • 1.是多线程环境
      • 共享数据
      • 多条语句操作共享数据
    • 那我们如何解决多线程安全问题呢?
      • 基本思想:让程序没有安全问题的环境
    • 怎么实现呢?
      • 把多条语句操作共享数据的代码给起来,让任意时刻只能有一个线程执行即可。
      • Java 提供了同步代码块的方式来解决。
    • 同步代码块格式:
    synchronized(任意对象) { 
    	多条语句操作共享数据的代码 
    }
    
    • 1
    • 2
    • 3

    synchronized(任意对象):就相当于给代码加锁了,任意对象就可以看成是一把锁。

    • 同步的好处弊端
      • 好处:解决了多线程的数据安全问题。
      • 弊端:当线程很多时,因为每个线程都会去判断 同步上的,这是很耗费资源的,无形中会降低程序的运行效率
    • 代码演示
    public class Ticket2 implements Runnable {
        private int ticket = 100;
        private Object obj= new Object();
    
        @Override
        public void run() {
            while (true) {
                synchronized (obj) {//多个线程必须使用同一把锁
                    if (ticket == 0) {
                        //卖完了
                        break;
                    } else {
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        ticket--;
                        System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticket + "张票");
                    }
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    public class Demo {
        public static void main(String[] args) {
            Ticket2 ticket = new Ticket2();
            Thread t1 = new Thread(ticket);
            Thread t2 = new Thread(ticket);
            Thread t3 = new Thread(ticket);
            t1.setName("窗口一");
            t2.setName("窗口二");
            t3.setName("窗口三");
    
            t1.start();
            t2.start();
            t3.start();
    
            //多线程在执行每一行代码的时候,它 cpu 的执行权都有可能被抢走
            //线程一第一次抢到执行权,但在打印99之前还没有打印呢它cpu的执行权被二跟三抢走了
    
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 运行结果
      在这里插入图片描述
      在这里插入图片描述
      补充:打印票数不是按照降序顺序打印也是由于在任意时刻,每个线程都是有可能抢到CPU的使用权造成的,这种情况也可以通过加锁的方式解决。

    2.4同步方法解决数据安全问题

    在解决安全问题时也可以通过同步方法的方式,原理也是通过加锁

    • 同步方法的格式
      • 同步方法:就是把 synchronized关键字加到方法上
    修饰符 synchronized 返回值类型 方法名(方法参数) {
    	 方法体; 
    }
    
    • 1
    • 2
    • 3
    • 同步方法的锁对象是什么呢?

      • this
    • 同步静态方法的锁对象是什么呢?

      • 类名.class
    • 代码演示

    public class MyRunnable implements Runnable { 
    	private static int ticketCount = 100; 
    	@Override 
    	public void run() {
    		 while(true){ 
    		 	if("窗口一".equals(Thread.currentThread().getName())){
    		 		 //同步方法 
    		 		 boolean result = synchronizedMthod(); 
    		 		 if(result){ 
    		 		 	break; 
    		 		 } 
    		 	}
    		 	if("窗口二".equals(Thread.currentThread().getName())){
    		 		//同步代码块 
    		 		synchronized (MyRunnable.class){
    		 			if(ticketCount == 0){ 
    		 				break; 
    		 			}else{
    		 				try {Thread.sleep(10);
    		 				} catch (InterruptedException e) {
    		 				 e.printStackTrace(); 
    		 				}
    		 				ticketCount‐‐;
    		 				System.out.println(Thread.currentThread().getName() + "在卖票,还剩 下" + ticketCount + "张票"); 
    		 			} 
    		 		} 
    		 	}
    		 } 
    	}
    	private static synchronized boolean synchronizedMthod() {
    		if(ticketCount == 0){ 
    			return true; 
    		}else{
    			try {
    				Thread.sleep(10); 
    				} catch (InterruptedException e) { 
    					e.printStackTrace(); 
    				}
    				ticketCount‐‐;
    				System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticketCount + "张票"); 
    				return false; 
    			} 
    		} 
    	}
    public class Demo { 
    	public static void main(String[] args) { 
    		MyRunnable mr = new MyRunnable(); 
    		Thread t1 = new Thread(mr); 
    		Thread t2 = new Thread(mr); 
    		t1.setName("窗口一"); 
    		t2.setName("窗口二"); 
    		t1.start(); 
    		t2.start(); 
    	} 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    2.5Lock锁

    虽然我们可以理解同步代码块同步方法锁对象问题,但是我们并没有直接看到哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock,Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化

    • ReentrantLock构造方法
    方法名说明
    ReentrantLock()创建一个 ReentrantLock的实例
    • 加锁解锁方法
    方法名说明
    void lock()获得锁
    void unlock()释放锁
    • 代码演示
    public class Ticket implements Runnable { 
    	//票的数量 
    	private int ticket = 100; 
    	private Object obj = new Object(); 
    	private ReentrantLock lock = new ReentrantLock(); 
    	@Override 
    	public void run() { 
    		while (true) { 
    		//synchronized (obj){//多个线程必须使用同一把锁. 
    			try {
    				lock.lock(); 
    				if (ticket <= 0) { 
    					//卖完了 
    					break; 
    				} else { 
    					Thread.sleep(100); 
    					ticket‐‐;
    					 System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticket + "张票");
    				} 
    			} catch (InterruptedException e) { 
    				e.printStackTrace(); 
    			} finally { 
    				lock.unlock(); 
    			}
    			// } 
    		} 
    	} 
    }
    public class Demo { 
    	public static void main(String[] args) { 
    		Ticket ticket = new Ticket(); 
    		Thread t1 = new Thread(ticket); 
    		Thread t2 = new Thread(ticket); 
    		Thread t3 = new Thread(ticket); 
    		t1.setName("窗口一"); 
    		t2.setName("窗口二"); 
    		t3.setName("窗口三"); 
    		t1.start(); 
    		t2.start();
    		t3.start(); 
    	} 
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    2.6死锁

    • 概述
      线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。
      举个例子:
      在这里插入图片描述
      如图:小明和小红是两个线程,它们的目的都是前往淑芳阁。

    • 正常情况
      在这里插入图片描述
      白色路障代表通用的,小明先抢夺到CPU的使用权,此时锁默认是打开的,等小明经过路障后,将锁关闭,此时就算小红就算抢到CPU的执行权,也只能等到小明到达淑芳阁后打开锁,小红才能去淑芳阁,这是比较正常的情况。

    • 死锁情况
      在这里插入图片描述
      另一种情况如图,小明和小红都有自己的锁,白色代表小明的锁,黑色代表小红的锁,刚开始两个锁都是默认打开的,小红抢到CPU的执行权,此时因为小明还没开始,所以黑色锁默认打开,小红直接进去,然后因为小红已经开始执行,黑色锁关闭,正当小红要经过小明的白色锁时,小明抢到了CPU的执行权,然后经过自己的锁,并关闭自己的白色锁,此时两个锁处于关闭状态,所以小明跟小红都被困在里边了。这就是我们所说的死锁

    • 什么情况会产生死锁

      • 1.资源有限
      • 2.同步嵌套
    • 代码演示

    public class Demo {
        public static void main(String[] args) {
            Object objA = new Object();
            Object objB = new Object();
             new Thread(()->{
                while(true){
                    synchronized (objA){
                        //线程一
                        synchronized (objB){
                            System.out.println("小明正在走路");
                        }
                    }
                }
             }).start();
    
             new Thread(()->{
                 while(true){
                     synchronized (objB){
                         //线程二
                         synchronized (objA){
                             System.out.println("小红正在走路");
                         }
                     }
                 }
             }).start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 运行结果
      在这里插入图片描述

    总结

    本篇文章通过模拟现实中的卖票案例引出了多线程中可能存在的问题,并讲述了通过同步代码块以及同步方法加锁的方式解决卖票案例中的问题,也简单介绍了JDK5之后,可以通过实例Lock锁的实现类 ReentrantLock让我们能够更清晰的看到在哪里上锁,又在哪里释放锁,也分析了线程死锁问题产生的原因,希望大家多多支持,你们的支持,是我更新的动力!在这里插入图片描述

  • 相关阅读:
    计算机网络第4章-通用转发和SDN
    【go学习笔记】Go errors 最佳实践
    经典卷积神经网络 - VGG
    Dash 2.9.0版本重磅新功能一览
    Vue扩展组件mixins,extends,composition api,slots
    力扣 8049. 判断能否在给定时间到达单元格
    Angular知识点系列(3)-每天10个小知识
    CSAPP-Lab05 Cache Lab 深入解析
    云IDE测试案例
    Java泛型的总结
  • 原文地址:https://blog.csdn.net/hihielite/article/details/128022301