线程的安全问题

线程的同步:用来解决线程的安全问题
程序中如果写多个线程不一定会出现线程的安全问题,如果存在共享数据,线程才会出现安全问题

例子: 三个窗口卖票,三个窗口的总票数为100张

 public void run() {
        while(true){
            if(tickte>0){
                try {
                    Thread.sleep(100);//如果我们不加这一行,出现票号为0或票号为-1的概率非常小(是可能出现的,因为CPU也可能在这里切换成另一个线程)
                } catch (InterruptedException e) {//不能说出现的概率小就不去解决问题
                    e.printStackTrace();
                }
                System.out.println(tickte+":"+Thread.currentThread().getName());
                tickte--;
            }else{
                break;
            }
        }
    }

比如现在票还剩一张,线程一先去执行,进入if被阻塞
此时线程二和线程三有非常大的概率也会进来(此时票数是1),也都被阻塞了
最先进入执行状态的线程输出1
在ticket–之后第二个就输出0,最后一个就输出-1。这是出现了错票的安全问题

重票的出现概率比较大的情况:

 public void run() {
        while(true){
            if(tickte>0){
                System.out.println(tickte+":"+Thread.currentThread().getName());
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                tickte--;
            }else{
                break;
            }
        }

比如某个线程输出了100,然后被阻塞
另外一个线程进来也是100,出现重票

问题分析:

  • 问题:卖票的过程中出现重票和错票–>线程安全问题
  • 原因:当某个线程操作车票的过程当中,还未操作完成,别的线程就进入处理代码进行操作
  • 这个卖票问题的共享数据是ticket
  • 解决:当一个线程在操作共享数据时,其他线程不能参与进来,直到这个线程操作完(即使当前线程出现阻塞其他线程也不能进来操作共享数据),其他线程才能操作
  • 在java中通过同步机制来解决线程的安全问题
    采用实现Runnable接口的解决方法:
    方式一:同步代码块
    方式二:同步方法

    下面的程序演示使用方式一进行解决

同步代码块:
synchronized(同步监视器){
//需要被同步的代码
}

说明:
1.需要被同步的代码:操作共享数据的代码
2.共享数据:多个线程共同操作的变量(数据)
3.同步监视器:俗称:锁,谁拿到锁谁操作代码。任何一个类的对象(包括我们自己定义的类的对象)都能来充当锁。锁不能是基本数据类型
要求:多个线程必须要共用同一把锁,要保证锁的唯一性
总结: 局限性:同步的外面会有并行,但同步的里面只有一个线程执行,所以同步代码块中相当于是单线程,效率低

package test0;

public class ThreadDemo {
    public static void main(String[] args) {
        MyWindow m=new MyWindow();//因为我们只new过这一个 MyWindow 对象,所以obj只有一个,满足唯一一把锁
        Thread t1=new Thread(m);
        Thread t2=new Thread(m);
        Thread t3=new Thread(m);
        t1.setName("线程一");
        t2.setName("线程二");
        t3.setName("线程三");
        t1.start();
        t2.start();
        t3.start();
    }
}
class MyWindow implements Runnable{
    Object obj=new Object();
    private int tickte =100;//此时不用再加static,三个线程共用ticket(因为是同一个对象给了他们的构造器,除非你再new一个Mywindow,把m1给构造器)
    @Override
    public void run() {
        //Object obj=new Object();不能放在这个地方,这会使得一个线程各自一把锁,不安全。总共三把锁
        while(true){
            synchronized(obj){//不能写成这样synchronized(new Object()),这样三把锁都不止了
                if(tickte>0){
                    System.out.println(tickte+":"+Thread.currentThread().getName());

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();//即使线程sleep了,别的线程也要等着.直到这个线程操作完成出去了(这个线程还可以回来再抢)
                    }
                    tickte--;
                }else{
                    break;
                }
            }
        }
    }
}

更简单的方法(针对于实现Runnable接口的方式):使用当前对象来充当锁

package test0;

public class ThreadDemo {
    public static void main(String[] args) {
        MyWindow m=new MyWindow();       
        Thread t1=new Thread(m);
        Thread t2=new Thread(m);
        Thread t3=new Thread(m);
        t1.setName("线程一");
        t2.setName("线程二");
        t3.setName("线程三");
        t1.start();
        t2.start();
        t3.start();
    }
}

class MyWindow implements Runnable{
    //Object obj=new Object();
    private int tickte =100;
    
    @Override
    public void run() {
        //Object obj=new Object();不能放在这个地方,这会使得一个线程各自一把锁,不安全。总共三把锁
        while(true){
            synchronized(this){//this是m,调用的run方法是在MyWindow中定义的,MyWindow的对象为this,即m。是唯一的。
                if(tickte>0){
                    System.out.println(tickte+":"+Thread.currentThread().getName());
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();//即使线程sleep了,别的线程也要等着.直到这个线程操作完成出去了(还可以回来再抢)
                    }
                    tickte--;
                }else{
                    break;
                }
            }
        }
    }
}

继承Thread类的解决方式:
方式一:用同步代码块解决
方式二:同步方法
下面的程序是采用方式一进行解决

package test0;

public class ThreadDemo {
    public static void main(String[] args) {
        window w1=new window();
        window w2=new window();
        window w3=new window();
        w1.setName("窗口一");
        w2.setName("窗口二");
        w3.setName("窗口三");
        w1.start();
        w2.start();
        w3.start();
    }
}
class window extends Thread{
    private static int ticket=100;//不要写成private int ticket=100;static是让所有对象共享
    
    private  static Object obj=new Object();//不要写成private Object obj=new Object();否则锁不唯一,这个代码有三把锁,因为new了三次
    @Override
    public void run() {
        while (true){
           synchronized(obj){
               if(ticket>0){
                   try {
                       sleep(100);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
                   System.out.println(getName()+":卖票,票号为:"+ticket);
                   ticket--;
               }else{
                   break;
               }
           }

        }
    }
}

如果使用继承的方式
不能把obj写this,会让锁不唯一
可以这么干:
把obj改成window.class

  • 说明:在继承Thread类创建多线程的方式中,慎用this创建同步监视器(具体问题具体分析,主要看this是否唯一)
  • 可以考虑使用当前类来充当同步监视器,反正一定要保证锁唯一
  • 同步代码块中不能包含代码多了(效率低,变成单线程。甚至可能会错。),也不能少了,把需要被同步的代码包起来即可
  • 包多了可能会错的理解:比如下面把while(true)也包了,那就一个线程把出票这个任务全包了,安全是安全,但实现功能错了
package test0;

public class ThreadDemo {
    public static void main(String[] args) {
        window w1=new window();
        window w2=new window();
        window w3=new window();
        w1.setName("窗口一");
        w2.setName("窗口二");
        w3.setName("窗口三");
        w1.start();
        w2.start();
        w3.start();
    }
}
class window extends Thread{
    private static int ticket=100;//不要写成private int ticket=100;static是让所有对象共享

    @Override
    public void run() {
        while (true){
            synchronized(window.class){//是用当前类去充当锁,类也是对象。类也是对象会在反射中详细说。类可以作为一个对象出现,是Class的一个对象
                if(ticket>0){//类只会加载一次(即window.class只会加载一次),所以window.class是唯一的
                    try {
                        sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(getName()+":卖票,票号为:"+ticket);
                    ticket--;
                }else{
                    break;
                }
            }

        }
    }
}

同步方法(在方法的声明中写上synchronized):

也会涉及到:共享数据和同步监视器的问题
理解:
如果操作共享数据的代码正好完整地声明在一个方法中,我们不妨将此方法声明为同步的。
就叫做同步方法
同理:同步方法中包的东西既不能多,也不能少。所以不能把run变成同步方法(除非run中全是操作共享数据的代码)

实现Runnable接口使用同步方法解决线程安全问题

package test0;

public class ThreadDemo {
    public static void main(String[] args) {
        MyWindow m=new MyWindow();
        Thread t1=new Thread(m);
        Thread t2=new Thread(m);
        Thread t3=new Thread(m);

        t1.setName("线程一");
        t2.setName("线程二");
        t3.setName("线程三");

        t1.start();
        t2.start();
        t3.start();
    }
}
class MyWindow implements Runnable{
    private int tickte =100;//此时不用再加static,三个线程共用ticket(因为是同一个对象给了他们的构造器,除非你再new一个Mywindow,把m1给构造器)
    @Override
    public void run() {
        while(true){
            show();
        }
    }
    private synchronized void show(){//同步方法的方法体和同步方法块效果是一样的
        if(tickte>0){//同步方法中也是有同步监视器的,只是用的是默认的,这个东西是this,又因为本程序中this唯一,所以没问题
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(tickte+":"+Thread.currentThread().getName());
            tickte--;
        }//因为break要用在循环中,就把break干掉了
    }
}
//注意:本程序需要手动关闭,可以在while中加条件进行break

使用同步方法解决继承Thread类的线程安全问题
同步方法仍然涉及到同步监视器,只是不需要我们显示声明,用的是默认的
非静态的同步方法的同步监视器是this
静态的同步方法的同步监视器是当前类本身

package test0;

public class ThreadDemo {
    public static void main(String[] args) {
        window w1=new window();
        window w2=new window();
        window w3=new window();

        w1.setName("窗口一");
        w2.setName("窗口二");
        w3.setName("窗口三");

        w1.start();
        w2.start();
        w3.start();
    }
}
class window extends Thread{
    private static int ticket=100;//不要写成private int ticket=100;static是让所有对象共享
  
    @Override
    public void run() {
        while (true){
            show();//非静态可以直接调用静态,静态调非静态必须用对象来调用,不能直接调用
            if(!(ticket>0)){
                break;
            }
        }
    }
    private static synchronized void show(){//private synchronized void show()直接这么做不安全,同步监视器为this,以本程序进行分析,同步监视器有三个w1,w2,w3
        if(ticket>0){//同步监视器此时不可能是this,因为静态方法不能调this。此时的同步监视器是当前的类window.class
            try {   //因为当前的类是唯一的,所以锁唯一,安全
                sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+":卖票,票号为:"+ticket);//写成static后直接写会报错,因为他不是静态的,不能直接调用
            ticket--;
        }
    }
}




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值