线程安全问题和状态概述

1599-张同学

发表文章数:23

热门标签

首页 » Java » 正文

线程安全问题的概述

多线程访问共享数据才会产生线程安全问题。

线程安全问题的代码实现

public class RunnableImpltry implements Runnable{
    //定义一个多个线程共享的票源
    private int ticket =100;
    //设置多线程任务:卖票
    @Override
    public void run() {

        while (true){//死循环

            if(ticket>0) {//判断是否有票,有票:ticket--
                
                //提高安全问题出现的概率,让程序睡眠,会有不存在的票和重复票,产生了线程安全问题。
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //打印卖票
                System.out.println(Thread.currentThread().getName() + "-->正在卖第"+ticket+"张票");
                ticket--;
            }
        }
    }
}
=========================================================================================
package com.itheima.Thread.ThreadsAndSynchronization.demo06.ThreadSafe;
//创建一个实现类,传到三个线程中。
public class Trytest {
    public static void main(String[] args) {
        RunnableImpltry run =new RunnableImpltry();
        Thread t0= new Thread(run);
        Thread t1= new Thread(run);
        Thread t2= new Thread(run);
        t0.start();
        t1.start();
        t2.start();
    }
}

结果:
线程安全问题和状态概述

线程安全问题产生的原理

现在假设在抢第一张票(产生不存在票原理):

  • 上面代码中,t0 t1 t2 三个线程共同抢占CPU,假设t0
    抢到了,执行完if语句后,遇到sleep使程序睡眠了,失去了CPU的执行权;然后 t1 t2
    去抢CPU,假设t2抢到了,执行run方法,同样执行完if语句后,遇到sleep使程序睡眠了,失去了CPU的执行权;最后t1执行run方法
    同理执行行run方法并睡眠。
  • 过了一会儿,假设t2这个线程程序睡醒了,打印在卖第1张票,然后ticket–,ticket=0,判断ticket<0结束执行。
  • 过了一会儿,假设t1这个线程程序也睡醒了,因为ticket=0,打印在卖第0张票,然后ticket–,判断ticket<0结束执行。
  • 最后,t0这个线程程序睡醒了,因为ticket=-1,打印在卖第-1张票,然后ticket–,判断ticket<0结束执行。

产生重复票原理:
比如t2还没执行到ticket–的时候,t1就打印了

注意:线程安全问题是不能产生的,我们可以让一个线程在访问共享数据时,无论是否失去CPU的执行权,其他线程都只能等待,除非它执行完全。

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

卖票案例出现了线程安全问题
卖出了不存在的票和重复的票

解决线程安全问题的一种方案:使用同步代码块

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

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

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--;
                }
            }
        }
    }
}

线程安全问题和状态概述
注意:同步保证了只有一个程序在同步中执行共享数据,保证了安全,但是程序频繁地判断锁,释放锁,程序的效率会降低。

解决线程安全问题之使用同步方法

锁对象是谁? this**

使用步骤:
1.把访问了共享数据的代码抽取出来,放到一个方法中
2.在方法上添加synchronized修饰符

格式:定义方法的格式
修饰符 synchronized 返回值类型 方法名(参数列表){
可能会出现线程安全问题的代码(访问了共享数据的代码)

public class RunnableImpl implements Runnable{
    //定义一个多个线程共享的票源
    private static int ticket = 100;


    //设置线程任务:卖票
    @Override
    public void run() {
        System.out.println("this:"+this);//this:com.itheima.demo08.Synchronized.RunnableImpl@58ceff1
        //使用死循环,让卖票操作重复执行
        while(true){
            payTicketStatic();
        }
    }
    /*
        定义一个同步方法
        同步方法也会把方法内部的代码锁住
        只让一个线程执行
        同步方法的锁对象是谁?
        就是实现类对象 new RunnableImpl()
        也是就是this
     */
    public /*synchronized*/ void payTicket(){
        synchronized (this){ //==不注释上一行synchronized
            //先判断票是否存在
            if(ticket>0){
                //提高安全问题出现的概率,让程序睡眠
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //票存在,卖票 ticket--
                System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
                ticket--;
            }
        }

    }
}

解决线程安全问题之使用静态同步方法

锁对象是谁?RunnableImpl.class

public class RunnableImpl implements Runnable{
    //定义一个多个线程共享的票源
    private static int ticket = 100;//静态访问静态 这里ticket也要是静态的


    //设置线程任务:卖票
    @Override
    public void run() {
        System.out.println("this:"+this);//this:com.itheima.demo08.Synchronized.RunnableImpl@58ceff1
        //使用死循环,让卖票操作重复执行
        while(true){
            payTicketStatic();
        }
    }

    /*
        静态的同步方法
        锁对象是谁?
        不能是this
        this是创建对象之后产生的,静态方法优先于对象
        静态方法的锁对象是本类的class属性-->class文件对象(反射)
     */
    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--;
            }
        }

    }

解决线程安全问题之使用Lock锁

java.util.concurrent.locks.Lock接口
Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。
Lock接口中的方法:
void lock()获取锁。
void unlock() 释放锁。
java.util.concurrent.locks.ReentrantLock implements Lock接口

使用步骤:
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();

            //先判断票是否存在
            if(ticket>0){
                //提高安全问题出现的概率,让程序睡眠
                try {
                    Thread.sleep(10);
                    //票存在,卖票 ticket--
                    System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
                    ticket--;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    //3.在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁
                    l.unlock();//无论程序是否异常,都会把锁释放
                }
            }
        }
    }

线程状态概述

线程安全问题和状态概述
线程安全问题和状态概述

等待唤醒案例

Wating状态在API中介绍为:一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态
线程安全问题和状态概述
等待唤醒案例:线程之间的通信
创建一个顾客线程(消费者):告知老板要的包子的种类和数量,调用wait方法,放弃cpu的执行,进入到WAITING状态(无限等待)
创建一个老板线程(生产者):花了5秒做包子,做好包子之后,调用notify方法,唤醒顾客吃包子

注意:
顾客和老板线程必须使用同步代码块包裹起来,保证等待和唤醒只能有一个在执行
同步使用的锁对象必须保证唯一
只有锁对象才能调用wait和notify方法

Obejct类中的方法

  • void wait()
    在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。
  • void notify()
    唤醒在此对象监视器上等待的单个线程。
    会继续执行wait方法之后的代码
public class Demo01WaitAndNotify {
    public static void main(String[] args) {
        //创建锁对象,保证唯一
        Object obj = new Object();
        // 创建一个顾客线程(消费者)
        new Thread(){
            @Override
            public void run() {
               //一直等着买包子
               while(true){
                   //保证等待和唤醒的线程只能有一个执行,需要使用同步技术
                   synchronized (obj){
                       System.out.println("告知老板要的包子的种类和数量");
                       //调用wait方法,放弃cpu的执行,进入到WAITING状态(无限等待)
                       try {
                           obj.wait();
                       } catch (InterruptedException e) {
                           e.printStackTrace();
                       }
                       //唤醒之后执行的代码
                       System.out.println("包子已经做好了,开吃!");
                       System.out.println("---------------------------------------");
                   }
               }
            }
        }.start();

        //创建一个老板线程(生产者)
        new Thread(){
            @Override
            public void run() {
                //一直做包子
                while (true){
                    //花了5秒做包子
                    try {
                        Thread.sleep(5000);//花5秒钟做包子
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    //保证等待和唤醒的线程只能有一个执行,需要使用同步技术
                    synchronized (obj){
                        System.out.println("老板5秒钟之后做好包子,告知顾客,可以吃包子了");
                        //做好包子之后,调用notify方法,唤醒顾客吃包子
                        obj.notify();
                    }
                }
            }
        }.start();
    }
}

Object类中wait带参方法和notifyAll方法

进入到TimeWaiting(计时等待)有两种方式:
1.使用sleep(long m)方法,在毫秒值结束之后,线程睡醒进入到Runnable/Blocked状态
2.使用wait(long m)方法,wait方法如果在毫秒值结束之后,还没有被notify唤醒,就会自动醒来,线程睡醒进入到Runnable/Blocked状态

  obj.wait(5000);

唤醒的方法:

  • void notify() 唤醒在此对象监视器上等待的单个线程。

  • void notifyAll() 唤醒在此对象监视器上等待的所有线程。

obj.notify();//如果有多个等待线程,随机唤醒一个
obj.notifyAll();//唤醒所有等待的线程
标签:

未经允许不得转载:作者:1599-张同学, 转载或复制请以 超链接形式 并注明出处 拜师资源博客
原文地址:《线程安全问题和状态概述》 发布于2021-10-14

分享到:
赞(0) 打赏

评论 抢沙发

评论前必须登录!

  注册



长按图片转发给朋友

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏

Vieu3.3主题
专业打造轻量级个人企业风格博客主题!专注于前端开发,全站响应式布局自适应模板。

登录

忘记密码 ?

您也可以使用第三方帐号快捷登录

Q Q 登 录
微 博 登 录