当前位置: 首页 > article >正文

[001-02-001]. 第07-03节:理解线程的安全问题

我的后端学习大纲

我的Java学习大纲


当多个线程共享一份数据的时候,不同的线程对数据进行操作,就可能会导致线程安全问题,比如卖票过程中出现了错票和重复票的问题:

1、卖票问题分析:

1.1.理想状态:

在这里插入图片描述

1.2.问题状态:

  • 问题原因:当多条语句在操作同一个线程共享的数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误
private int tick = 100;
public void run(){
	while(true){
		if(tick>0){
			try{
				//这里可能就多个线程进入,进行了各种操作。。。。导致线程安全
				Thread.sleep(10);
			}catch(InterruptedException e){ 
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName()+“售出车票,tick号为: "+tick--);
		} 
	}
 }

1.3.改善后状态:

  • 对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行
    在这里插入图片描述

2、线程安全问题的解决

2.1.解决线程安全办法:

  • 对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行,在java中,我们通过同步机制,来解决线程安全问题

2.2.实现方式:

a.方式一:同步代码块:

  • 1.操作共享数据的代码,就是需要被同步的代码;所谓的共享数据就是多个线程共同操作的变量。比如卖票中的ticket数量就是变量
  • 2.同步监视器就是的意思;任何一个类对象都可以充当锁,一定要保证多个线程要共用同一把锁,即用的是同一个对象;
    • 实现Runnable接口创建的多线程的方式中,可以考虑使用this充当同步监视器;
    • 继承Thread类创建多继承的方式中,要慎用this,看看this代表的是不是一样的意义。可以考虑当前类
synchronized(同步监视器){
	//需要被同步的代码(操作共享数据的那部分代码就是需要被同步的代码)
}
  • 3.继承方式中问题的改进:
    在这里插入图片描述
  • 5.接口实现方式中程序的改进 :
    在这里插入图片描述
  • 6.同步监视器的要求:即为锁的要求:多个线程必须要用同一把锁
  • 7.同步方式解决了线程的安全问题,但是降低了效率,在操作同步代码的时候,相当于一个单线程

b.方式二:同步方法

  • 1.如果操作共享数据的代码完整的声明在了一个完整的方法中,我们不妨就考虑此方法声明式同步的解决实现方式的线程问题:
class ImporeTicket2 implements Runnable{
    private int ticket = 10;//每个对象都有100张票,属性不共享
    @Override
    public void run() {
        while(true){
            show();
        }
    }

    //卖票的完整方法
    //在同步方法中,锁就是this
    private synchronized void show(){
        if (ticket >0){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":卖票,票号是::" + ticket);
            ticket--;
        }
    }
}
package com.atguigu.java;
/**
 * @author: jianqun
 * @email: 1033586391@qq.com
 * @creat: 2022-04-09-13:50
 * @Description:
 */
public class ThreadTest007  {
    public static void main(String[] args) {
        ImporeTicket2 impporeTicket = new ImporeTicket2();
        Thread thread1 = new Thread(impporeTicket);
        Thread thread2 = new Thread(impporeTicket);
        Thread thread3 = new Thread(impporeTicket);

        thread1.setName("窗口1");
        thread2.setName("窗口2");
        thread3.setName("窗口3");

        thread1.start();
        thread2.start();
        thread3.start();
    }
}
  • 3.解决继承方式的线程问题:这里的锁是当前类,即Bwindow2.class
class Bwindow2 extends Thread{

    private static int ticket = 10;
    private static Object obj = new Object();
    @Override
    public void run() {
        while(true){
            show2();
        }
    }

	//private  synchronized void show2(){}这个更改是错误的,代表的是不同对象的不同方法,要定义成静态的方法才可以
    private static synchronized void show2(){//这里的锁是当前类
        if (ticket >0){
            System.out.println(Thread.currentThread().getName() + ":卖票,票号是::" + ticket);
            ticket--;
        }
    }
}

package com.atguigu.java;

/**
 * @author: jianqun
 * @email: 1033586391@qq.com
 * @creat: 2022-04-09-9:09
 * @Description: 卖票问题
 */


public class ThreadTest008 {
    public static void main(String[] args) {
        Bwindow bwindow1 = new Bwindow();
        Bwindow bwindow2 = new Bwindow();
        Bwindow bwindow3 = new Bwindow();

        bwindow1.setName("窗口1");
        bwindow2.setName("窗口2");
        bwindow3.setName("窗口3");

        bwindow1.start();
        bwindow2.start();
        bwindow3.start();

    }
}
  • 3.同步方法的总结:
    • 同步方法仍然是涉及到了同步监视器,但是不需要我们自己手动显示声明
    • 非静态同步方法:锁是:this
    • 静态的同步方法:锁是当前类的本身

c.方式三:线程同步方式-Lock锁方式 来解决线程安全问题

1.Lock(锁)

  • 从JDK 5开始,Java提供了更强大的线程同步机制,通过显式定义同步锁对象来实现同步同步锁使用Lock对象充当
  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
  • ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁

2.语法:

  • 注意:如果同步代码有异常,要将unlock()写入finally语句块
 //Lock(锁)
class A{
	private final ReentrantLock lock = new ReenTrantLock();
	public void m(){
		lock.lock();
		try{
			//保证线程安全的代码; 
		}finally{
			lock.unlock(); 
		} 
	} 
}

3.卖票问题代码演示

package com.atguigu.java;

/**
 * @author: jianqun
 * @email: 1033586391@qq.com
 * @creat: 2022-04-09-15:57
 * @Description:
 */
import java.util.concurrent.locks.ReentrantLock;

class Window implements Runnable{
    private int ticket = 10;
    //1.实例化ReentrantLock
    private ReentrantLock lock = new ReentrantLock();
    @Override
    public void run() {
        while(true){
            try{
                //2.调用锁定方法lock()
                lock.lock();
                if(ticket > 0){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
                    ticket--;
                }else{
                    break;
                }
            }finally {
                //3.调用解锁方法:unlock()
                lock.unlock();
            }
        }
    }
}
public class ThreadTest011 {
    public static void main(String[] args) {
        Window w = new Window();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

3、使用同步机制将单例模式中的懒汉式改写为线程安全的

在这里插入图片描述

package com.atguigu.java;

public class ThreadTest009 {

}
class Bank{
    private Bank(){}
    private static Bank instance = null;
    public static Bank getInstance(){
        //方式一:效率稍差
       // 当有多个线程的时候,因为一上来就是锁,多个线程都在排队,然后当拿到锁之后,进入判断instance是否是null,
       // 当不是null的时候,就直接返回了。这样一堆线程都在排队,但是拿到锁的时候,就又啥都不干,这样就浪费了时间,降低了效率
       
		//synchronized (Bank.class) {
        //  if(instance == null){
        //      instance = new Bank();
        //  }
        //  return instance;
        // }

        //方式二:效率更高
        if(instance == null){
            synchronized (Bank.class) {
                if(instance == null){
                    instance = new Bank();
                }
            }
        }
        return instance;
    }
}

4、面试题目:

4.1.synchronized 与 Lock的异同

a.相同:

  • 二者都可以解决线程安全问题

b.不同:

  • synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器
  • Lock需要手动的启动同步(lock())同时结束同步也需要手动的实现(unlock())

4.2.synchronized 与 Lock 的对比:

  • Lock是显式锁(手动开启和关闭锁,别忘记关闭锁)synchronized是隐式锁,出了作用域自动释放
  • Lock只有代码块锁,synchronized有代码块锁和方法锁
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)

4.3.优先使用顺序:

  • Lock --> 同步代码块(已经进入了方法体,分配了相应资源)--> 同步方法(在方法体之外)

5、线程的安全练习:

银行有一个账户。有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额
分析:是一个多线程问题(两个储户),通过判断是否出现共享数据来判断是否出现线程安全问题。当有共享数据(账户余额是共享数据)的时候,就使用三种方式来实现线程同步机制

//账户类
class Account{
    private double balance;
    public Account(double balance) {
        this.balance = balance;
    }

    //存钱:
    // 在未控制多线程同步的时候分析:
    //     当乙用户存钱的时候,执行了 balance+=amt,然后未及时打印输出余额,
    //     然后呢甲用户又存了钱,然后甲又进入了睡眠状态
    //     过了一会乙和甲都睡醒了,但是都是存了1000元,可以打印的是存了2000元,这样就出现了线程安全的问题。

    // 以下进行了线程安全控制:
    public synchronized void  deposits(double amt){
        if (amt > 0){
            balance+=amt;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "存储成功,余额:"+ balance);
        }
    }
}
//储户类存钱,可以看成是两个线程
class Customer extends Thread{
    private Account acct;//定义一个Account类型的属性

    //提供一个有参构造器,用于对属性的实例化
    public Customer(Account acct){
        this.acct = acct;
    }

    //run方法中重写存钱的逻辑
    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            //存钱的方法
             acct.deposits(1000);
        }
    }
}
package com.atguigu.java;
public class ThreadTest12   {
    public static void main(String[] args) {
        //对Account中double类型的balance属性进行初始化
        Account account = new Account(0);

        //对Customer中的Account类型的属性acc进行实例化,通过实例化出来两个用户
        Customer customer1 = new Customer(account);
        Customer customer2 = new Customer(account);
        customer1.setName("张三");
        customer2.setName("李四");

        customer1.start();
        customer2.start();
    }
}

5、线程同步中的死锁问题:

5.1.死锁定义:

  • 1.不同的线程分别占用对方需要的同步资源不放弃都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
  • 2.出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续

5.2.举例分析死锁

在这里插入图片描述

package com.atguigu.java;

/**
 * @author: jianqun
 * @email: 1033586391@qq.com
 * @creat: 2022-04-09-14:39
 * @Description:
 */
public class ThreadTest010 {
    public static void main(String[] args) {
        StringBuffer stringBuffer1 = new StringBuffer();
        StringBuffer stringBuffer2 = new StringBuffer();


        //继承方式创建匿名方式实现多线程的同步
        new Thread(){
            @Override
            public void run() {
                synchronized (stringBuffer1){
                    stringBuffer1.append("a");
                    stringBuffer2.append("1");

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    synchronized (stringBuffer2){
                        stringBuffer1.append("b");
                        stringBuffer2.append("2");

                        System.out.println(stringBuffer1);
                        System.out.println(stringBuffer2);
                    }
                }
            }
        }.start();

        //接口实现方式创建匿名内部类实现线程同步
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (stringBuffer2){

                    stringBuffer1.append("c");
                    stringBuffer2.append("3");

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    synchronized (stringBuffer1){
                        stringBuffer1.append("d");
                        stringBuffer2.append("4");

                        System.out.println(stringBuffer1);
                        System.out.println(stringBuffer2);
                    }
                }
            }
        }){}.start();
    }
}

5.3.死锁解决方法

  • 1.专门的算法、原则
  • 2.尽量减少同步资源的定义
  • 3.尽量避免嵌套同步


http://www.kler.cn/a/301996.html

相关文章:

  • apk反编译修改教程系列-----apk应用反编译中AndroidManifest.xml详细代码释义解析 包含各种权限 代码含义【二】
  • 【Java基础知识系列】之Java类的初始化顺序
  • html数据类型
  • MDK 5 各个历史版本下载地址
  • 数据产品:深度探索与案例剖析
  • fastadmin多个表crud连表操作步骤
  • 空间物联网中的大规模接入:挑战、机遇和未来方向
  • 基于 onsemi NCV78343 NCV78964的汽车矩阵式大灯方案
  • Linux下进程间的通信--共享内存
  • 计算机视觉的应用33-基于双向LSTM和注意力机制融合模型的车辆轨迹预测应用实战
  • 五分钟让你学会threeJS
  • git 远程分支同步本地落后的有冲突的分支
  • Redis常用操作及springboot整合redis
  • web基础之文件上传
  • Kotlin 中的 `flatMap` 方法详解
  • wifiip地址可以随便改吗?wifi的ip地址怎么改变
  • Brave编译指南2024 Windows篇:安装Git(四)
  • FloodFill算法
  • 语言模型微调:提升语言Agent性能的新方向
  • HarmonyOS开发之使用Picker(从相册选择图片),并且通过Swiper组件实现图片预览
  • Day11笔记-字典基本使用系统功能字典推导式
  • 自定义spring security的安全表达式
  • Numpy中random.seed()函数的使用
  • librdkafka Windows编译
  • 【python因果推断库9】工具变量回归与使用 pymc 验证工具变量2
  • Mac强制删除文件,碰上“拖拽到废纸篓”无法删除时怎么办?