java中线程安全问题及解决方法、线程状态、线程间通信(线程等待唤醒机制)

线程安全

概述:

多线程访问了共享数据,此时会产生冲突(如:在多个线程中执行售卖货物的业务,要求是某个货被某个线程售卖后,其他线程应该不再可以售卖此个货,但是默认被某个线程售卖后,其他线程还是会售卖此货物,这里不合理,不过有解决的方法),这里的冲突指线程安全问题,这个问题是可以避免(始终保证一个线程在执行任务,当前线程任务执行完后才可以执行其他线程的任务),下面通过卖电影票实现线程安全问题:

实现Runnable接口的方式创建一个卖票线程类:

// 实现Runnbale接口的方式创建一个多线程实现类:
public class RunnableTmplClass implements Runnable {
    
    
    // 定义一个多个线程共享的数据(票):
    private int ticket = 10;
    // 重写run方法设置卖票任务:
    @Override
    public void run() {
    
    
        // 判断票是否存在,存在的话在售卖:
        for (int i = 0; i <= ticket; i++) {
    
    
            if (ticket > 0) {
    
    
                System.out.println("线程:" + Thread.currentThread().getName() + "正在售卖第:" + ticket + "号票。");// 没有处理线程安全前,这里打印的结果是:某个票可能被多次售卖,某个票可能一次也没有售卖
                ticket--;
            }
        }
    }
}

创建三个线程实例售卖票:

// 创建3个线程售卖票:
public class SellTicketTest {
    
    
    public static void main(String[] args) {
    
    
        // 创建Runnable接口的实现类对象:
        RunnableTmplClass rt = new RunnableTmplClass();
        // 创建三个线程:
        Thread t1 = new Thread(rt);
        Thread t2 = new Thread(rt);
        Thread t3 = new Thread(rt);
        // 开启三个线程:
        t1.start();
        t2.start();
        t3.start();
    }
}

打印结果:
请添加图片描述
执行原理:请添加图片描述
解决线程安全问题

解决线程安全问题可以使用线程同步,线程同步一共有三种方式:同步代码块、同步方法、锁机制

同步代码块解决线程安全问题:

// 解决线程安全的第一种方式:使用同步代码块(把同步代码块锁住,只让一个线程在同步代码块中执行)
// 注意:锁对象可以是任意对象、但是多个线程保证应该使用同一个锁对象
public class RunnableTmplClass implements Runnable {
    
    
    private int ticket = 10;
    // 在run外面创建一个公共的锁对象(同步锁,对象监视器):
    Object obj = new Object();
    @Override
    public void run() {
    
    
        for (int i = 0; i <= ticket; i++) {
    
    
            // 创建同步代码块:将会出现线程安全的代码放到同步代码块中即可:
            synchronized (obj) {
    
    
                if (ticket > 0) {
    
    
                    System.out.println("线程:" + Thread.currentThread().getName() + "正在售卖第:" + ticket + "号票。");// 没有处理线程安全前,这里打印的结果是:某个票可能被多次售卖,某个票可能一次也没有售卖
                    ticket--;
                }
            };
        }
    }
}

请添加图片描述
同步方法解决线程安全:

// 解决线程安全的第二种方式:使用同步方法,把访问了共享数据的代码抽取出来放到一个方法中,此方法前面添加一个修饰符:synchronized,方法体里面写访问了共享数据的代码:
// 注意:还可以在方法体内写synchronized代码块解决线程安全问题
public class RunnableTmplMethod implements Runnable {
    
    
    private /*static*/ int ticket = 10;
    @Override
    public void run() {
    
    
        for (int i = 0; i <= ticket; i++) {
    
    
            cellTicket();
            // cellTicketSecond();
        }
    }
    // 定义一个同步方法,第一种方式:
    public /*static ,这里static可以加上,提前定义的变量也是静态的才可以访问得到变量*/ synchronized void cellTicket() {
    
    
        if (ticket > 0) {
    
    
            System.out.println("线程:" + Thread.currentThread().getName() + "正在售卖第:" + ticket + "号票。");// 没有处理线程安全前,这里打印的结果是:某个票可能被多次售卖,某个票可能一次也没有售卖
            ticket--;
        }
    };
    // 定义一个同步方法,第二种方式:
    public void cellTicketSecond() {
    
    
        synchronized (this) {
    
    
            if (ticket > 0) {
    
    
                System.out.println("线程:" + Thread.currentThread().getName() + "正在售卖第:" + ticket + "号票。");// 没有处理线程安全前,这里打印的结果是:某个票可能被多次售卖,某个票可能一次也没有售卖
                ticket--;
            }
        };
    };
}

lock锁解决线程安全问题:

使用lock锁解决线程安全问题比synchronized先进些。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

// 解决线程安全的第三种方式:使用lock锁
// 使用步骤:1.在成员位置创建一个ReentrantLock对象 2.在可能会出现安全问题的代码前调用lock方法获取锁 3.在可能会出现安全问题的代码后调用unlock方法释放锁
public class RunnableTmplLock implements Runnable {
    
    
    private int ticket = 10;
    // 创建锁对象:
    Lock lk = new ReentrantLock();
    @Override
    public void run() {
    
    
        // for (int i = 0; i <= ticket; i++) {
    
    
        //     // 调用锁:
        //     lk.lock();
        //     if (ticket > 0) {
    
    
        //         System.out.println("线程:" + Thread.currentThread().getName() + "正在售卖第:" + ticket + "号票。");// 没有处理线程安全前,这里打印的结果是:某个票可能被多次售卖,某个票可能一次也没有售卖
        //         ticket--;
        //     };
        //     // 释放锁:
        //    lk.unlock();
        // }

        // 下面方式为比较安全的,无论是否报错,都会释放锁
        for (int i = 0; i <= ticket; i++) {
    
    
            // 调用锁:
            lk.lock();
            if (ticket > 0) {
    
    
                try {
    
    
                    System.out.println("线程:" + Thread.currentThread().getName() + "正在售卖第:" + ticket + "号票。");// 没有处理线程安全前,这里打印的结果是:某个票可能被多次售卖,某个票可能一次也没有售卖
                    ticket--;
                } finally {
    
    
                    lk.unlock();
                }
            };
        }
    }
}

线程状态

线程状态概述:

线程的状态这里可以理解为状态的生命周期,也就是指从状态的创建到状态的结束的过程。状态依次可分为6种状态:new Therad()新建阶段—Runnable运行阶段(这里还有一种没有抢占到CPU的情况,称为blocked阻塞状态,阻塞状态和运行状态是可以相互切换的)—Terminated死亡状态—Timed_waiting休眠状态—waiting无限等待状态
请添加图片描述
休眠状态案例:

public class SleepStatic implements Runnable {
    
    
    private int ticket = 10;
    @Override
    public void run() {
    
    
        for (int i = 0; i <= ticket; i++) {
    
    
            if (ticket > 0) {
    
    
                try {
    
    
                    // 调用Thread类的sleep对程序进入休眠状态休眠2000毫秒在进入其他状态
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
    
    
                    throw new RuntimeException(e);
                }
                System.out.println("线程:" + Thread.currentThread().getName() + "正在售卖第:" + ticket + "号票。");
                ticket--;
            }
        }
    }
}

无限等待状态(线程之间的通信):

一个正在无限期等待另一个线程执行一个特别的唤醒动作的线程处于这一状态。

无限等待状态的执行过程:有一个消费者线程需要某种需求,此时消费者会调用wait方法进入无限等待状态,生产者线程开始处理需求,当结果被生产者处理好后可以调用notify方法通知唤醒消费者可以进行消费了。

import java.util.Random;

// 创建一个消费者线程:告知生产者线程需要的结果种类和数量,调用wait方法,放弃cpu执行权,进入Waiting无限等待状态
// 创建一个生产者线程:处理消费者需要的结果,当结果处理好后,调用notify方法唤醒消费者来消费结果。
// 注意: 消费者线程和生产者线程必须使用同步代码块包裹起来,保证等待和唤醒只能有一个执行;同步使用的锁对象必须是唯一的,只有锁对象才能调用wait和notify方法
public class WaitNotifyStatic {
    
    
    public static void main(String[] args) {
    
    
        // 创建一个随机数变量
        final int[] num = {
    
    0};
        // 创建一个唯一的锁对象:
        Object obj = new Object();

        // 1.创建一个消费者线程:
        new Thread() {
    
    
            @Override
            public void run() {
    
    
                // 使用同步技术保证消费者线程和生产者线程只能有一个执行:
                synchronized (obj) {
    
    
                    // 向生产者发起业务需求:
                    System.out.println("A:消费者要求生产者处理业务,生成一个随机数:");
                    try {
    
    
                        // 调用wait方法进入无限休眠状态(这里会抛异常,使用try/catch处理即可):
                        System.out.println("B:消费者调用wait方法开启无限等待状态中");
                        obj.wait(); // 里面可以传入一个毫秒值,此时和sleep大概一样,当传入的时间小于生产者处理业务所用时间时,此时消费者会提前醒来进入runnable/blocked状态
                    } catch (InterruptedException e) {
    
    
                        throw new RuntimeException(e);
                    }
                    // 生产者唤醒消费者后执行的代码:
                    System.out.println("E:消费者被生产者唤醒开始消费生产者所处理的结果:" + num[0]);
                };
            };
        }.start(); // 调用start方法执行消费者线程

        // 2.创建一个生产者线程:
        new Thread() {
    
    
            @Override
            public void run() {
    
    
                try {
    
    
                    // 生产者需要花费5秒钟处理业务(同样会抛异常,使用try/catch处理即可):
                    Thread.sleep(5000);
                    Random randoms = new Random();
                    num[0] = randoms.nextInt(100);
                    System.out.println("C:生产者花费了一段时间处理结果,生成一个随机数:" + num[0]);
                } catch (InterruptedException e) {
    
    
                    throw new RuntimeException(e);
                }
                // 在同步代码块中唤醒消费者,继续执行消费者中wait之后的代码:
                synchronized (obj) {
    
    
                    System.out.println("D:生产者调用notify方法唤醒消费者:");
                    obj.notify(); // 当有多个消费者线程时,notify只能随机唤醒一个等待消费者线程,而obj.notifyAll可以唤醒所有等待线程
                };
            };
        }.start(); // 调用start方法执行消费者线程
    }
    // 提示:依次打印了a-e的结果,且执行完B后等待了一段时间后再开始执行后面代码
}

等待唤醒机制(线程间通信)

线程间通信概念: 多个线程处理同一个资源(这里往往指某个整套业务),但是处理的动作(线程的任务)却不同。在多个线程并发执行时,默认情况下cpu是随机切换线程的,但是有的时候我们需要让他们有规律的执行,此时多个线程之间就要进行一些协调通信,来完成多个线程处理某个整套业务。

保证线程间通信是有效利用资源: 多个线程间处理同一份数据时,避免对同一共享变量的争夺,我们可以通过某种手段(比如判断某个数据是否存在,存在时执行哪个线程,不存在时执行哪个线程)使各个线程能有效的利用资源,这种手段被称为等待唤醒机制。

等待唤醒中的方法:也就是上面用到的wait、notify、notifyAll。

当调用wait方法后,当前线程进入wait set中,等待别的线程执行notify将线程从wait set中释放出来从新进入到调度队列ready queue中,进入wait set中 此时不会消耗CPU,也不会去竞争锁。

notify: 选取所通知对象的wait set中等待时间最久的一个线程释放。

notifyAll:释放所通知对象的wait set中的全部线程。

注意:哪怕只通知了一个等待的线程,被通知的线程也不会立即恢复执行,因为它中断的地方是在同步块内,而此刻它已经不持有锁,所以它需要再次尝试获取锁(可能面临其他线程的竞争),获取锁成功后才能在当初调用wait方法之后的地方恢复执行。

下面是一个用户买包子的案例:

包子类:

// 1.创建一个包子类:
public class BaoZiClass {
    
    
    // 包子皮:
    String pi;
    // 包子馅:
    String xian;
    // 包子状态:是否有包子,用于线程间通信状态的判断
    boolean flag = false;
}

包子铺类:

// 2.创建一个包子铺类,用来生产包子的线程:当包子实例flag有包子时,包子铺调用wait方法进入等待状态;当flag没有包子时,包子铺生产包子,当包子生产好后修改flag状态为有并唤醒吃货线程吃包子
// 注意:包子铺线程和包子线程之间是互斥关系,两者只能有一个同时执行,因此需要使用同步技术

// 因为包子铺是一个线程,所以可以继承Thread类,并重写里面的run方法设置线程任务:
public class BaoZiPuClass extends Thread{
    
    
    // 定义一个包子变量:
    private BaoZiClass bz;

    // 使用包子铺带参数构造方法,为包子变量赋值:
    public BaoZiPuClass(BaoZiClass bz) {
    
    
        this.bz = bz;
    }

    // 重写run方法生产包子:
    @Override
    public void run() {
    
    
        // 定义一个序号,用于生产两种包子:
        int count = 0;

        // 使用循环让包子铺一直生产包子:
        while (true) {
    
    
            // 使用同步代码块解决线程安全问题:
            synchronized (bz) {
    
    
                // 如果有包子,调用wait方法进入等待,否则生产包子:
                if (bz.flag == true) {
    
    
                    try {
    
    
                        bz.wait();
                    } catch (InterruptedException e) {
    
    
                        throw new RuntimeException(e);
                    }
                };

                // 生产两种类型的包子:
                if (count % 2 == 0) {
    
    
                    bz.pi = "薄皮";
                    bz.xian = "粉丝馅";
                } else {
    
    
                    bz.pi = "厚皮";
                    bz.xian = "猪肉线";
                };

                System.out.println("A:包子铺正在生产第"+count+"个包子:"+bz.pi+","+bz.xian);
                count++;

                // 为了模拟真实环境,设置生产包子花费的时间为2秒钟:
                try {
    
    
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
    
    
                    throw new RuntimeException(e);
                }

                // 当休眠2s后包子生产好了,修改包子的状态为有:
                bz.flag = true;

                // 唤醒吃货线程:
                System.out.println("B:包子铺已经生产好包子了,开始唤醒吃货吃包子:");
                bz.notify();
            };
        }
    };
}

吃货类:

// 3.创建一个吃货类(消费者)线程:对包子的状态进行判断,有包子的话吃,没有的话调用await方法等待。
public class ChiHuoClass extends Thread {
    
    
    // 设置一个包子变量:
    private BaoZiClass bz;
    // 使用构造方法为包子变量赋值:
    public ChiHuoClass(BaoZiClass bz) {
    
    
        this.bz = bz;
    };

    // 重写run方法设置吃包子的任务:
    @Override
    public void run() {
    
    
        // 使用死循环一直吃包子:
        while(true){
    
    
            // 使用同步代码块解决线程安全问题,这里锁对象和包子铺中的锁对象为同一个对象:
            synchronized (bz) {
    
    
                // 如果没有包子,调用wait进入等待状态
                if (bz.flag == false) {
    
    
                    try {
    
    
                        bz.wait();
                    } catch (InterruptedException e) {
    
    
                        throw new RuntimeException(e);
                    }
                }
                // 开吃包子:
                System.out.println("C:吃货正在吃:"+bz.pi+","+bz.xian+"的包子");

                // 修改包子状态:
                bz.flag = false;

                // 唤醒包子铺生产包子:
                System.out.println("D:吃货唤醒包子铺生产包子:");
                bz.notify();

                System.out.println("-----------------------------");
            }
        }
    };
}

测试吃货吃包子:

// 4.测试吃货买包子及包子铺生产包子等业务:
public class TestClass {
    
    
    public static void main(String[] args) {
    
    
        // 创建一个包子:
        BaoZiClass bz = new BaoZiClass();

        // 创建一个包子铺,并开启线程:
        new BaoZiPuClass(bz).start();

        // 创建一个吃货,并开启线程:
        new ChiHuoClass(bz).start();

        // 提示打印结果为:包子铺生产一个包子,吃货就吃掉一个包子
    }
}

提示:本文图片等素材来源于网络,若有侵权,请发邮件至邮箱:[email protected]联系笔者删除。
笔者:苦海

猜你喜欢

转载自blog.csdn.net/weixin_46758988/article/details/128523965