• JAVA多线程(5)


    JAVA多线程(5)

    线程安全问题概述

    卖票问题分析

    • 单窗口卖票

    img

    一个窗口(单线程)卖100张票没有问题
    单线程程序是不会出现线程安全问题的

    • 多个窗口卖不同的票

    img

    3个窗口一起卖票,卖的票不同,也不会出现问题
    多线程程序,没有访问共享数据,不会产生问题

    • 多个窗口卖相同的票

    img

    3个窗口卖的票是一样的,就会出现安全问题
    多线程访问了共享的数据,会产生线程安全问题

    线程安全问题代码实现

    模拟卖票案例

    创建3个线程,同时开启,对共享的票进行出售

    public class Demo01Ticket {
     public static void main(String[] args) {
     //创建Runnable接口的实现类对象
     RunnableImpl run = new RunnableImpl();
     //创建Thread类对象,构造方法中传递Runnable接口的实现类对象
     Thread t0 = new Thread(run);
     Thread t1 = new Thread(run);
     Thread t2 = new Thread(run);
     //调用start方法开启多线程
            t0.start();
            t1.start();
            t2.start();
     }
    }
    public class RunnableImpl implements Runnable{
     //定义一个多个线程共享的票源
     private int ticket = 100;
     //设置线程任务:卖票
     @Override
     public void run() {
     //使用死循环,让卖票操作重复执行
     while(true){
     //先判断票是否存在
     if(ticket>0){
     //提高安全问题出现的概率,让程序睡眠
     try {
     Thread.sleep(10);
     } catch (InterruptedException e) {
     e.printStackTrace();
     }
     //票存在,卖票 ticket--
     System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
                    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
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    线程安全问题原理分析

    img

    线程安全问题产生原理图

    分析:线程安全问题正常是不允许产生的,我们可以让一个线程在访问共享数据的时候,无论是否失去了cpu的执行权;让其他的线程只能等待,等待当前线程卖完票,其他线程在进行卖票。

    解决线程安全问题办法1-synchronized同步代码块

    同步代码块:synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。

    使用synchronized同步代码块格式:

    synchronized(锁对象){
    可能会出现线程安全问题的代码(访问了共享数据的代码)
    }

    代码实现如下:

    public class Demo01Ticket {
     public static void main(String[] args) {
     //创建Runnable接口的实现类对象
     RunnableImpl run = new RunnableImpl();
     //创建Thread类对象,构造方法中传递Runnable接口的实现类对象
     Thread t0 = new Thread(run);
     Thread t1 = new Thread(run);
     Thread t2 = new Thread(run);
     //调用start方法开启多线程
            t0.start();
            t1.start();
            t2.start();
     }
    }
    public class RunnableImpl implements Runnable{
     //定义一个多个线程共享的票源
     private int ticket = 100;
     //创建一个锁对象
     Object obj = new Object();
     //设置线程任务:卖票
     @Override
     public void run() {
     //使用死循环,让卖票操作重复执行
     while(true){
     //同步代码块
     synchronized (obj){
     //先判断票是否存在
     if(ticket>0){
     //提高安全问题出现的概率,让程序睡眠
     try {
     Thread.sleep(10);
     } catch (InterruptedException e) {
     e.printStackTrace();
     }
     //票存在,卖票 ticket--
     System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
                        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
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    ⚠️注意:

    1. 代码块中的锁对象,可以使用任意的对象。
    2. 但是必须保证多个线程使用的锁对象是同一个。
    3. 锁对象作用:把同步代码块锁住,只让一个线程在同步代码块中执行。

    同步技术原理分析

    同步技术原理:

    使用了一个锁对象,这个锁对象叫同步锁,也叫对象锁,也叫对象监视器

    3个线程一起抢夺cpu的执行权,谁抢到了谁执行run方法进行卖票。

    • t0抢到了cpu的执行权,执行run方法,遇到synchronized代码块这时t0会检查synchronized代码块是否有锁对象

    发现有,就会获取到锁对象,进入到同步中执行

    • t1抢到了cpu的执行权,执行run方法,遇到synchronized代码块这时t1会检查synchronized代码块是否有锁对象

    发现没有,t1就会进入到阻塞状态,会一直等待t0线程归还锁对象,t0线程执行完同步中的代码,会把锁对象归 还给同步代码块t1才能获取到锁对象进入到同步中执行

    总结:同步中的线程,没有执行完毕不会释放锁,同步外的线程没有锁进不去同步。

    解决线程安全问题办法2-synchronized普通同步方法

    同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。

    格式:

    public synchronized void payTicket(){
    可能会出现线程安全问题的代码(访问了共享数据的代码)
    }

    代码实现:

     public /**synchronized*/ void payTicket(){
     synchronized (this){
     //先判断票是否存在
     if(ticket>0){
     //提高安全问题出现的概率,让程序睡眠
     try {
     Thread.sleep(10);
     } catch (InterruptedException e) {
     e.printStackTrace();
     }
     //票存在,卖票 ticket--
     System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
                    ticket--;
     }
     }
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    分析:

    定义一个同步方法,同步方法也会把方法内部的代码锁住,只让一个线程执行。

    同步方法的锁对象是谁?

    就是实现类对象 new RunnableImpl(),也是就是this,所以同步方法是锁定的this对象。

    解决线程安全问题办法3-synchronized静态同步方法

    同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。
    格式:

    public static synchronized void payTicket(){
    可能会出现线程安全问题的代码(访问了共享数据的代码)
    }

    代码实现:

     public static /**synchronized*/ void payTicketStatic(){
     synchronized (RunnableImpl.class){
     //先判断票是否存在
     if(ticket>0){
     //提高安全问题出现的概率,让程序睡眠
     try {
     Thread.sleep(10);
     } catch (InterruptedException e) {
     e.printStackTrace();
     }
     //票存在,卖票 ticket--
     System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
                    ticket--;
     }
     }
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    分析:

    静态的同步方法锁对象是谁?

    不能是this,this是创建对象之后产生的,静态方法优先于对象

    静态方法的锁对象是本类的class属性–>class文件对象(反射)。

    解决线程安全问题办法4-Lock锁

    Lock接口中的方法:

    • public void lock() :加同步锁。
    • public void unlock() :释放同步锁

    使用步骤:

    1. 在成员位置创建一个ReentrantLock对象
    2. 在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁
    3. 在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁

    代码实现:

    public class RunnableImpl implements Runnable{
     //定义一个多个线程共享的票源
     private int ticket = 100;
     //1.在成员位置创建一个ReentrantLock对象
     Lock l = new ReentrantLock();
     //设置线程任务:卖票
     @Override
     public void run() {
     //使用死循环,让卖票操作重复执行
     while(true){
     //2.在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁
     l.lock();
     try {
     //先判断票是否存在
     if(ticket>0) {
     //提高安全问题出现的概率,让程序睡眠
     Thread.sleep(10);
     //票存在,卖票 ticket--
     System.out.println(Thread.currentThread().getName() + "-->正在卖第" + ticket + "张票");
                        ticket--;
     }
     } catch (InterruptedException e) {
     e.printStackTrace();
     }finally {
     l.unlock();
     //3.在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁
     //无论程序是否异常,都会把锁释放
     }
     }
     }
    
    • 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

    分析

    java.util.concurrent.locks.Lock接口

    Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。相比Synchronized,ReentrantLock类提供了一些高级功能,主要有以下3项:

    1.等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。通过lock.lockInterruptibly()来实现这个机制。

    2.公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。

    公平锁、非公平锁的创建方式:

    //创建一个非公平锁,默认是非公平锁
    Lock lock = new ReentrantLock();
    Lock lock = new ReentrantLock(false);
     //创建一个公平锁,构造传参true
    Lock lock = new ReentrantLock(true);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    3.锁绑定多个条件,一个ReentrantLock对象可以同时绑定多个对象。ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。

    ReentrantLock和Synchronized的区别

    相同点:

    1. 它们都是加锁方式同步;
    2. 都是重入锁;
    3. 阻塞式的同步;也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的(操作系统需要在用户态与内核态之间来回切换,代价很高,不过可以通过对锁优化进行改善);

    不同点:

    ck对象可以同时绑定多个对象。ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。

    ReentrantLock和Synchronized的区别

    相同点:

    1. 它们都是加锁方式同步;
    2. 都是重入锁;
    3. 阻塞式的同步;也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的(操作系统需要在用户态与内核态之间来回切换,代价很高,不过可以通过对锁优化进行改善);

    不同点:

    img

  • 相关阅读:
    RK3568平台开发系列讲解(NPU篇)让 NPU 跑起来
    【机器学习项目实战10例】(二):利用LightGBM实现天气变化的时间序列预测
    C++入门-day03
    Kafka(三)- Kafka 命令行操作
    h0215.闭区间问题
    SpringMVC:拦截器(动力)
    Win11升级包下载后如何删除
    设计模式之【工厂模式、适配器模式】
    3.1 Android eBPF代码仓解读
    【个人博客公网访问】使用Cpolar+Emlog在Ubuntu上轻松搭建个人博客公网访问
  • 原文地址:https://blog.csdn.net/weixin_46251037/article/details/134496890