• 线程安全和线程安全的解决方案


    第一节 什么是线程安全

    如果多个线程同时运行,而这些线程同时具有修改某段代码的能力。程序不管是单线程执行还是多线程执行,结果都一样,并且变量与预期一样,就是线程安全。

    线程安全问题都是全局变量或者静态变量导致的,如果每个线程中对全局变量、静态变量只有读操作,而没有写操作,通常这个全局变量是线程安全的;如果多个线程同时执行写操作,就需要考虑到线程同步问题,否则就是出现线程安全问题。

    第二节 模拟线程安全问题

    举一个例子,假设火车票卖票,有3个窗口同时卖票,库存10张,那么三个独立窗口,各自都会判断余票必须大于0,否则买票不成功,我们就假设每次只卖1张。

    public class Ticket implements Runnable{
    	
    	private int ticket=10;
    
    	@Override
    	public void run() {
    		do{
    			if(ticket>0){
    				try {
    					Thread.sleep(1000);
    				} catch (InterruptedException e) {
    					e.printStackTrace();
    				}
    				ticket--;
    				System.out.println("售出1张 success");//每次成功售出都打印一下
    				System.out.println("窗口:"+Thread.currentThread().getName()+",余票:"+ticket);
    			}
    		}while(ticket>0);
    		
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    启动卖票,很显然余票已经显示负数了,存在超售问题。
    在这里插入图片描述
    而当我们只启动一个线程(一个窗口),可以正常售卖,不会超售。
    在这里插入图片描述
    当判断ticket大于0时,多个窗口都能同时满足条件,于是大家走执行了扣减1张的操作,于是当余票为1时,三个窗口都扣减了1次,就变成了-2张。

    第三节 线程同步

    当我们使用多线程访问同一资源的时候,且多个线程都有写操作(扣减或者增加),就容易出现线程安全问题。要解决这个多线程并发访问同一资源的安全问题,Java提供了同步锁机制来解决synchronized。
    实现逻辑:
    窗口1线程进入操作的时候,其它窗口等待,当窗口1结束,其它窗口可以进行操作。也就是当某线程操作共享资源的时候,其它线程不能修改资源,等待修改同步完成,才能获取该资源进行执行,保证数据的同步性,解决了线程不安全问题。
    为了保证每个线程都可以进行原子性的操作,Java引入了同步锁机制。

    • 同步锁代码块
    • 同步锁方法
    • 锁机制

    1. 同步锁代码块

    synchronized 只在方法的某一部分进行操作,表示多个线程在执行这段代码时,需要等待锁释放,对象锁。
    格式

    synchronized(同步锁){
       //共享资源的写操作,对共享资源进行判断和修改
    }
    
    • 1
    • 2
    • 3

    同步锁: 对象的同步锁只是一个概念,可以理解为在对象上标记一个锁。

    1. 锁对象,可以是任意类型。
    2. 多个线程要使用同一把锁。(任何时候,最多只允许一个线程获取到同步锁,拿到同步锁的可以执行代码块,其它线程需要等待同步锁的释放再竞争锁,执行同步锁代码)
    package com.lengcz.tools;
    
    public class Ticket implements Runnable{
    	
    	private int ticket=10;
    	
    	private Object lock=new Object();
    
    	@Override
    	public void run() {
    		do{
    			synchronized(lock){
    				if(ticket>0){
    					try {
    						Thread.sleep(1000);
    					} catch (InterruptedException e) {
    						e.printStackTrace();
    					}
    					ticket--;
    					System.out.println("售出1张 success");//每次成功售出都打印一下
    					System.out.println("窗口:"+Thread.currentThread().getName()+",余票:"+ticket);
    				}
    			}
    		}while(ticket>0);
    		
    	}
    }
    
    • 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

    在这里插入图片描述

    2. 同步方法

    2.1 同步方法锁的应用

    使用synchronized修饰方法,就叫同步方法,多个线程同时执行同步方法时,获取锁的线程可以这个方法的执行权限,其它线程只能等待这个线程完成同步方法的执行,才能争抢同步方法的执行权限,任何时候都最多只有一个线程有执行同步锁方法的权限。

    public synchronized void method(xxx){
     //共享资源的写操作,对共享资源进行判断和修改
    }
    
    • 1
    • 2
    • 3
    package com.lengcz.tools;
    
    public class Ticket implements Runnable{
    	
    	private int ticket=10;
    	
    
    	@Override
    	public void run() {
    		sell();
    	}
    	
    	public synchronized void sell(){
    		do{
    				if(ticket>0){
    					try {
    						Thread.sleep(1000);
    					} catch (InterruptedException e) {
    						e.printStackTrace();
    					}
    					ticket--;
    					System.out.println("售出1张 success");//每次成功售出都打印一下
    					System.out.println("窗口:"+Thread.currentThread().getName()+",余票:"+ticket);
    				}
    		}while(ticket>0);
    	}
    }
    
    • 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

    在这里插入图片描述

    2.2 同步方法锁到底锁的是什么

    2.2.1 非静态方法

    我们让窗口1执行Ticket 1的售票作业,窗口4 执行Ticket 2售卖作业,这两个售票作业没有关系,资源不共享。
    在这里插入图片描述

    运行代码,我们发现两个售票窗口都在同时执行各自的卖票作业,如果同步方法锁独享方法资源,那么卖出20张票需要20秒,而实际上它们各自卖票,所以t1和t2并没有竟然同一个资源,那么它们实际上是
    synchronized(this),即synchronized(ticket1)和synchronized(ticket2)。
    结论:

    • 两个线程访问同一个非静态同步方法,如果这个同步方法的this不是同一个,那么它们没有竞争关系,各自都能独立不干扰的执行这个同步方法。

    在这里插入图片描述

    2.2.2 静态方法锁 static
    public synchronized static void method(xxx){
    //code
    }
    
    • 1
    • 2
    • 3

    静态方法的同步锁到底是锁什么?
    我们改造一下卖票demo,只让进入静态同步方法的线程打印一下自己的身份(线程名)和执行时间。

    import java.util.Date;
    
    public class Ticket2 implements Runnable{
    
    	@Override
    	public void run() {
    		try {
    			do{
    				sell();
    			}while(true);
    		} catch (InterruptedException e) {
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    		}
    	}
    	
    	public synchronized static  void sell() throws InterruptedException{
    		Thread.sleep(1000);
    		System.out.println("sell窗口:"+Thread.currentThread().getName()+",获取到执行资源:"+DateUtils.parseDate(new Date()));
    	}
    	public synchronized static  void sell2() throws InterruptedException{
    		Thread.sleep(1000);
    		System.out.println("sell2窗口:"+Thread.currentThread().getName()+",获取到执行资源:"+DateUtils.parseDate(new Date()));
    	}
    }
    
    • 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
    public class Ticket3 implements Runnable{
    
    	@Override
    	public void run() {
    		try {
    			do{
    //				sell();
    				Ticket2.sell2();
    			}while(true);
    		} catch (InterruptedException e) {
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    		}
    	}
    	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在这里插入图片描述
    执行demo

    在这里插入图片描述

    线程1和线程4同时竞争都竞争sell方法(静态同步方法),而观察日志,发现1和4并不能同时执行,说明线程1和线程4访问该方法时,存在竞争关系,但是我们给t1和t4传入的是不同的Ticket2对象,显然不可能是对象锁,那么它们用的就是synchronized(Ticket2.class) ,锁的是该类的字节码对象。
    在这里插入图片描述
    线程1和线程5对比,两者并没有竞争同一个方法,也没有竞争通过一个Ticket对象,线程1调用sell方法,线程5则调用sell2方法,两者没有交集。但是我们观察上面的打印日志,可以看到,线程1和线程5并没有同时执行,它们各自执行时,都独占了资源。这说明它们这个锁是针对的Ticket2.class对象的,synchronized(Ticket2.class),该类的不同静态同步方法也存在竞争关系。
    在这里插入图片描述
    在这里插入图片描述

    2.2.3 同步方法和静态同步方法

    同步方法锁this,而静态同步方法锁类这个类本身,该类的静态同步方法存在锁竞争关系。

    3. Lock锁

    3.1 Lock锁的使用

    java.util.concurrent.locks.Lock锁提供了比synchronized更细化的锁定操作,同步方法块和同步方法具有的Lock都有,此外,Lock更体现面对对象。
    Lock锁也称为同步锁。

    Lock.lock();#上锁
    Lock.unlock();# 释放锁
    
    • 1
    • 2

    使用示例

    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class LockDemo {	
    	Lock lock=new ReentrantLock();
    	
    	public void method(){
    		lock.lock();
    		/**
    		 * 需要加锁保护的代码
    		 */
    		lock.unlock();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    我们使用Lock锁修改卖票操作。

    import java.util.Date;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class Ticket implements Runnable{
    	
    	private  int ticket=10;
    	
    	Lock lock=new ReentrantLock();
    	
    
    	@Override
    	public void run() {
    		String name = Thread.currentThread().getName();
    		try {
    			do {
    				lock.lock();// 上锁
    				if (ticket > 0) {
    					Thread.sleep(1000);//模拟业务处理需要1秒
    					System.out.println("窗口:" + name + ", 正在运行....");
    				
    					ticket--;
    					System.out.println("售出1张 success,time:" + DateUtils.parseDate(new Date()));// 每次成功售出都打印一下
    					System.out.println("窗口:" + name + ",余票:" + ticket);
    
    					lock.unlock();// 释放锁					
    				}
    				Thread.sleep(1);//休息1ms是为了避免该线程while循环立即再次lock,让出资源给其它线程
    			} while (ticket > 0);
    		} catch (Exception e) {
    			System.out.println("窗口:" + name + ", error ....");
    			e.printStackTrace();
    		}
    
    	}
    	
    }
    
    • 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

    启动t1,t2,t3线程进行卖票作业,发现被Lock锁的代码同样达到了synchronized锁的效果。
    在这里插入图片描述

    3.2 Lock死锁问题

    由于使用Lock锁,需要手动lock和unlock,因此如果在业务处理发生异常时,unlock可能无法被执行。

    import java.util.Date;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class Ticket implements Runnable{
    	
    	private  int ticket=10;
    	
    	Lock lock=new ReentrantLock();
    	
    	@Override
    	public void run() {
    		String name = Thread.currentThread().getName();
    		try {
    			do {
    				lock.lock();// 上锁
    				if (ticket > 0) {
    					Thread.sleep(1000);//模拟业务处理需要1秒
    					System.out.println("窗口:" + name + ", 正在运行....");
    					if(name.equals("3")){
    						int a=1/0; //模拟某线程处理业务时发生异常,直接进入catch,不能执行unlock
    					}
    								
    					ticket--;
    					System.out.println("售出1张 success,time:" + DateUtils.parseDate(new Date()));// 每次成功售出都打印一下
    					System.out.println("窗口:" + name + ",余票:" + ticket);
    
    					lock.unlock();// 释放锁					
    				}
    				Thread.sleep(1);//休息1ms是为了避免该线程while循环立即再次lock,让出资源给其它线程
    			} while (ticket > 0);
    		} catch (Exception e) {
    			System.out.println("窗口:" + name + ", error ....");
    			e.printStackTrace();
    		}
    
    	}	
    }
    
    • 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

    上面窗口3在卖票时,正在卖票,但是发生了异常,Lock锁无法进行unlock,这个锁也就无法释放了,导致死锁问题。
    在这里插入图片描述
    所以在使用Lock锁时,一定要确保锁可以被释放。

  • 相关阅读:
    2022鹏城杯
    C++ 类、方法的同一声明不同实现的方式
    基于SSM的小区物业管理系统设计与实现
    请编码实现动物世界的继承关系……定义一个体育活动类(Sports)作为基类……编写一个程序,并满足如下要求……
    修改图片尺寸的几个简单方法
    毫末速度:中国自动驾驶落地最快的1000天
    LeetCode【100】单词拆分
    【无标题】
    MySQL的DML操作表记录
    在Windows中安装MinGW-w64最新版本(目前12.1.0)
  • 原文地址:https://blog.csdn.net/u011628753/article/details/126882194