欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

线程安全

程序员文章站 2022-05-04 20:51:30
...

线程安全

              什么是线程安全

                         两个线程或两个以上的线程在同时操作同一个共享资源时仍然能够得到正确的结果则称为线程安全。

                        这个解释感觉很难理解我给大家举个栗子吧:

                         例如两个人在打糍粑一个人用锤子敲打一个人在用手翻动下面的糯米,(两个人看作是两个线程,下面的糯米看作是共享资源)线程安全我们就可以理解为当打糯米的人抬起锤子的时候,另一个人翻动下面的糯米,他们两个人相互配合,锤子不会锤到翻动糯米那个人的手,而线程不安全就可以理解为他们配合的不好,锤子锤到了翻动糯米那个人的手。

                接下来你们看看这个代码,线程不安全的


public class Tets1 {
    public static void main(String[] args){
           //创建一个Runnable接口实现类的对象
            MyRunnable myRunnable = new MyRunnable();
            //创建线程1   传入myRunnable对象为参数,并且为这个线程取一个名字叫做“苍井空”
            Thread thread1 = new Thread(myRunnable,"苍井空");
          //创建线程2  传入myRunnable对象为参数,并且为这个线程取一个名字叫做“小泽玛利亚”
            Thread thread2 = new Thread(myRunnable,"小泽玛利亚");

            //开启线程1
            thread1.start();
            //开启线程2
            thread2.start();
    }
}
class MyRunnable implements Runnable{
    //一个100斤肉  这个count就是我们线程的共享数据,因为我们只创建了一个count对象
     //并且这个count为成员变量,所以只有一个count被两个线程共同使用
    int count = 100;
    @Override
    public void run() {
        //死循环卖肉
        while (true){
            //这try  和  catch为抛出异常如果看不懂可以忽略
            try {
         //由于循环次数较少,cpu处理过快问题不明显,所以调用sleep方法让线程休眠20毫秒,让出cpu资源,使的问题明显一点
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //当肉小于或者等于0斤时候就会执行else里面的语句
            if(count>0){
                //进来if里面就说明肉还大于0斤 就减去一斤肉
                count--;
                //                   打印该线程名字和count的值
                System.out.println(Thread.currentThread().getName()+"卖了一斤肉还剩 "+count+" 斤");
            }else {
                //如果执行else里面的语句就说明count等于或者小于0了  就输出肉卖完了
                System.out.println("肉卖完了,收工了");
                //然后跳出循环
                break;
            }
        }

    }
}

              代码运行结果

                        代码运行出现了两个bug

                         bug1:

线程安全
线程不安全带来的问题

                       bug2

线程安全
线程不安全带来的问题

                        bug出现原因:

线程安全

                          当名字为苍井空的那个线程进入if判断还没来得及执行了count--- 如果这时count的值为1。此时CPU的资源被名字为小泽玛利亚的那条线程抢去了,因为名为苍井空的那条线程还没来得及执行count---所以count这个值还是为1.所以名字为小泽玛利亚的那条线程还是可以进入if判断,并且执行了count--语句,此时count的值为0,这时CPU的资源又被苍井空那条线程抢去了,他执行了count--此时count的值为-1.然后苍井空那条线程执行了输出语句打印了-1,这时cpu的资源又被小泽玛利亚的那条线程抢去了。他执行了打印语句,因为此时的count值为1.所以他也输出了负一,所以他们就直接跳过0了输出两个负一。

                                       从以上代码我们看见了线程不安全带来的问题那么,如何解决这个问题呢接下来为大家介绍解决多线程安全问题的3个方法。

      


解决线程安全问题

                           我想一下是什么原因引起了上面的这个问题,是因为我们多线程同时操作同一个共享数据引起了这问题,那么我们如何解决呢?解决问题的办法非常简单,我们让操作共享资源的线程代码不一起运行就可以了。

                    方式一:同步代码块

                            同步代码块格式

                            注意:锁对象必须唯一,也就是操作同一个共享资源的那一个锁对象必须是同一个

                             我可以把这个锁对象看作是一把钥匙,钥匙只有一把,而synchronized代码我们可以看做是锁,操作同一个共享数据的每一个线程都有一把锁,并且他们的锁是一样的,都能够被那一把钥匙打开,可是钥匙只有一把,一次只能打开一把锁,只有关闭了这一把锁,拿出钥匙,才能打开另一把锁。

synchronized(锁对象){
    //操作共享资源的代码
}

                         同步代码块原理

                                  能够保证同一时间只有一个线程执行同步代码块中的代码。

                        锁对象说明

                                 锁对象可以是任何类型对象

                                 锁对象必须被所有线程共享

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

public class Tets1 {
    public static void main(String[] args){
           //创建一个Runnable接口实现类的对象
            MyRunnable myRunnable = new MyRunnable();
            //创建线程1   传入myRunnable对象为参数,并且为这个线程取一个名字叫做“苍井空”
            Thread thread1 = new Thread(myRunnable,"苍井空");
          //创建线程2  传入myRunnable对象为参数,并且为这个线程取一个名字叫做“小泽玛利亚”
            Thread thread2 = new Thread(myRunnable,"小泽玛利亚");
            //开启线程1
            thread1.start();
            //开启线程2
            thread2.start();
    }
}
class MyRunnable implements Runnable{
    //如果我们这是创建了一个MyRunnable对象,并且把这一个对象传入了几个Thread的构造方法里面,开启了线程
    //因为在这个对象里面count是成员变量,所以count是被几个线程共同操作的数据,是共享数据
    int count = 100;  
    @Override
    public void run() {
        while (true){
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //这个同步代码块里面的就是操作共享数据的代码  
            //因为我们只创建了一个MyRunnable对象,并且把这个对象作为参数传入了Thread构造方法里面,所以this相当于每一个线程来说是唯一的
            synchronized (this) {
                if (count > 0) {
                    count--;
                    System.out.println(Thread.currentThread().getName() + "卖了一斤肉还剩 " + count + " 斤");
                } else {
                    System.out.println("肉卖完了,收工了");
                    break;
                }
            }
        }

    }
}

                  方式二:同步方法

                                   同步方法格式

                                          修饰符  synchronized    返回值类型     方法名(参数列表){     }

                                   同步方法原理

                                         能够保证同一时间只有一个线程执行方法体里面的代码

                       

               下面是同步方法解决安全问题代码     

public class Tets1 {
    public static void main(String[] args){
           //创建一个Runnable接口实现类的对象
            MyRunnable myRunnable = new MyRunnable();
            //创建线程1   传入myRunnable对象为参数,并且为这个线程取一个名字叫做“苍井空”
            Thread thread1 = new Thread(myRunnable,"苍井空");
          //创建线程2  传入myRunnable对象为参数,并且为这个线程取一个名字叫做“小泽玛利亚”
            Thread thread2 = new Thread(myRunnable,"小泽玛利亚");
            //开启线程1
            thread1.start();
            //开启线程2
            thread2.start();
    }
}
class MyRunnable implements Runnable{
    //如果我们这是创建了一个MyRunnable对象,并且把这一个对象传入了几个Thread的构造方法里面,开启了线程
    //因为在这个对象里面count是成员变量,所以count是被几个线程共同操作的数据,是共享数据
    int count = 100;
    @Override
    public void run() {
        while (true){
            //在循环里面 调用同步方法
           sellMeat();
        }
    }
    //同步方法   synchronized关键字   线程不能同时进入这个方法,每一次只能够一个线程进入
    public synchronized void sellMeat(){
        try {
            Thread.sleep(20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (count > 0) {
            count--;
            System.out.println(Thread.currentThread().getName() + "卖了一斤肉还剩 " + count + " 斤");
        } else {
            System.out.println("肉卖完了,收工了");
        }
    }
}

                    我们知道同步代码块是依靠钥匙(锁对象),的方法让同步代码块里面一次只进入一个线程,因为钥匙(锁对象)是唯一的哪一个线程得到他就能够进入同步代码块,别的线程进不去。

                    那么同步方法是依靠什么办法来来保证只有一个线程进入方法呢?

                                 其实同步方法也有一个锁对象,只是这锁对象是Java虚拟机自己设置的。

                                  静态同步方法的锁对象为:该类的类名.class  对象

                                                   class对象介绍:每一个类都会编译生成他对应的class文件,当程序运行时Java虚拟机会默认帮我们创建每一个类的class对象,所以每一个类的class对象都时唯一的。

                                  非静态同步方法的锁对象为:该类的对象  this

 

              方式三:Lock接口

                       解决线程安全问题的方式三,我们可以通过jdk1.5出现的新特性来解决,使用lock接口。

                       lock锁介绍

                              lock锁是一个接口所以我们只有创建他的实现类,创建方式如下      

 //lock接口      创建他实现类ReentrantLock的对象  
  Lock lock = new ReentrantLock();

                       lock锁接口的常用方法

void   lock() 获取锁
void    unlock() 释放锁

                       lock接口注意事项

                                    获取锁的方法和释放锁的代码要成对出现

                        lock锁原理

                                lock锁的原理和同步方法同步代码块的原理差不多,要操作一个共享数据的线程它们只能使用同一个lock对象,才能实现线程安全,lock锁就好比一把钥匙,lock方法就好比拿到这一把钥匙,unlock方法就好比把这一把钥匙放开让别的线程去抢,因为钥匙只有一把(操作共享数据的线程只能使用同一个lock对象),使用每次只能一个线程抢到钥匙,只能进去一个线程,只有等这个线程执行了unlock方法释放了钥匙别的线程才能够拿到钥匙,运行操作共享数据的代码。

               lock锁实现线程安全代码如下

public class Tets1 {
    public static void main(String[] args){
           //创建一个Runnable接口实现类的对象
            MyRunnable myRunnable = new MyRunnable();
            //创建线程1   传入myRunnable对象为参数,并且为这个线程取一个名字叫做“苍井空”
            Thread thread1 = new Thread(myRunnable,"苍井空");
          //创建线程2  传入myRunnable对象为参数,并且为这个线程取一个名字叫做“小泽玛利亚”
            Thread thread2 = new Thread(myRunnable,"小泽玛利亚");
            //开启线程1
            thread1.start();
            //开启线程2
            thread2.start();
    }
}
class MyRunnable implements Runnable{
    //如果我们这是创建了一个MyRunnable对象,并且把这一个对象传入了几个Thread的构造方法里面,开启了线程
    //因为在这个对象里面count是成员变量,所以count是被几个线程共同操作的数据,是共享数据
    int count = 100;
    //创建lock对象   这个对象必须是多个线程中唯一的对象才能实现线程安全
    Lock lock = new ReentrantLock();
    @Override
    public void run() {
        while (true){
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //获取锁(拿到钥匙)      有获取锁就有释放锁  方法必须成对出现
            lock.lock();
            //因为count是共享数据  所以把要操作的数据放到lock方法和unlook方法中间
            if (count > 0) {
                count--;
                System.out.println(Thread.currentThread().getName() + "卖了一斤肉还剩 " + count + " 斤");
            } else {
                System.out.println("肉卖完了,收工了");
            }
            //释放锁(分开钥匙让别的线程去抢)
            lock.unlock();
        }
    }

}