多线程的安全问题

Posted ljl150

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程的安全问题相关的知识,希望对你有一定的参考价值。

 

一,多线程安全问题分析

 1、线程安全问题出现的原因:

      (1)多个线程操作共享的数据;

      (2)线程任务操作共享数据的代码有多条(多个运算)。

       多线程,当CPU在执行的过程中,可能随时切换到其他的线程上执行。比如当线程1正在执行时,由于CPU的执行权被线程2抢走,于是线程1停止运行进入就绪队列,当线程2运行完,释放CPU的使用权,此时当线程1再次获得CPU的执行权时,由于线程2将某些共享数据的值已改变,所以此时线程1继续运行就会出现错误隐患。

2举例分析:

       假设有三个线程在抢票。当线程1抢到CPU执行权,先对系统票数进行判断,发现票数是大于0的,接着准备购票,但由于其他原因,该线程1被阻塞,CPU执行权被线程2抢到,CPU开始执行线程2,线程2同样先对票数进行判断,如果大于0,就进行购票,但由于其他原因,该线程2也被阻塞,CPU执行权被线程3抢到,CPU开始执行线程3,线程3同样先对票数进行判断,如果大于0,就进行购票,由于需求较大,系统的票全部被线程3购完,此时线程3执行完毕释放了cpu执行权。这时CPU执行权又被线程1抢到,CPU开始执行线程1代码,因为之前线程1已经对系统票数进行判断过,所以此时不会再继续判断,而是直接购票,但由于系统的票已全部被线程3购完,这时线程1再继续购买就会出现票数错误(如用户买的票号为0号票或-1号票,而现实中不存在0号票和-1号票)。所以这时候线程就出现了不安全隐患。而线程2也同理。

        注意:由于CPU的执行顺序是随机的(谁优先级大就执行谁),所以代码中加 Thread.sleep(10); 以模拟上述情况。

 1 class Demo implements Runnable{  //1.实现Runnable接口
 2     public int ticket=5;//系统的票数
 3     public void run() { //2.重写run方法
 4        while (true){
 5             if(ticket>0){
 6                try{ Thread.sleep(10);} catch(Exception e){ };  
 7                 //此处的异常不能抛,因为该run方法是重写的父类的方法。只能try!
 8                 System.out.println(Thread.currentThread().getName()+"ticket..."+ticket--);
 9             }
10         }
11 
12     }
13 }
14 public class TreadDemo {
15     public static void main(String[] args) {//main函数也是一个线程(主线程)
16         Demo d=new Demo();
17         Thread t1=new Thread(d);//创建一个线程
18         Thread t2=new Thread(d);
Thread t3=new Thread(d);
19 t1.start();//3.调用start方法d.run() 20 t2.start();
t3.start();
21 } 22 }

   运行结果:

技术图片

          一般火车票都是从1号开始售卖,而代码运行结果是从-1号开始售卖的,所以存在安全隐患。

二、多线程安全问题解决

        只要让一个线程在执行线程任务时,将多条操作共享数据的代码一次执行完,在执行过程中,不要让其他线程参与运算。那么如何在代码中体现呢?

            (1)通过同步代码块完成,使用关键字synchronized。 

                       同步代码块使用的锁是任意对象(由使用者自己来手动的指定)。

            (2)使用同步函数(方法)。

                      同步函数使用的锁是this,

                      静态同步函数使用的锁是字节码文件对象,类名.class.

         同步的前提:

           (1)必须要有两个或者两个以上的线程。

           (2)必须是多个线程使用同一个锁。

           (3)必须保证同步中只能有一个线程在运行。

1,通过同步代码块完成

 格式: synchronized(对象)

             {

                需要被同步的代码

             }

       通过分析可知,run()方法中的代码是线程运行的代码,但只有操作共享数据的代码才是需要被同步的代码。所以一般不建议把同步加在run方法,如果把同步加在了run方法上,导致任何一个线程在调用start方法开启之后,JVM去调用run方法的时候,首先都要先获取同步的锁对象,只有获取到了同步的锁对象之后,才能去执行run方法。而我们在run中书写的被多线程操作的代码,永远只会有一个线程在里面执行。只有这个线程把这个run执行完,出去之后,把锁释放了,其他某个线程才能进入到这个run执行,这时候代码的运行跟单线程类似。所以只有操作共享数据的代码才是需要被同步的代码

 1 class Demo implements Runnable{
 2     public int ticket=5;   //此处ticket(票)是共享数据
 3     Object obj=new Object();
 4     public void run() {   
 5         while (true){
 6            synchronized (obj){
 7                if(ticket>0){
 8                    try{ Thread.sleep(10);} catch(Exception e){ };  
 9                    //此处的异常不能抛,因为该run方法是重写的父类的方法。只能try! 
10                    System.out.println(Thread.currentThread().getName()+"ticket..."+ticket--);
11                }
12            }
13 
14         }
15 
16     }
17 }

运行结果:

技术图片

      通过结果可知,安全问题已解决。

      要注意运行结果没有第三个线程,并不是说第三个线程没有启动,它启动了,只是因为票数太少,在它抢到CPU执行权时,票已经被买光。。。

分析过程:

 技术图片

 

 

2,使用同步函数(方法)。

就是将关键字synchronized放到修饰符位置上。

   public  synchronized  返回值类型  方法名()

   {  

          需要同步的代码

   }

           通过分析可知,若该方法中的所有代码都是操作共享数据的,则可以直接将关键字synchronized放到该方法修饰符位置上。若该方法中仅有部分代码是操作共享数据的,则将这些操作共享数据的代码重新封装在一个函数中,然后将关键字synchronized放到新函数(方法)修饰符位置上。

 1 class Demo implements Runnable{
 2     public int ticket=5;
 3     public   void run() { //因为run()方法中仅有部分代码是操作共享数据
 4        while (true){
 5           show();   //this.show();
 6         }
 7 
 8     }
 9     public synchronized  void show() {//所以将这些操作共享数据的代码重新封装在一个函数中。   同步函数使用的锁是this。
10         if(ticket>0){
11             try{ Thread.sleep(10);} catch(Exception e){ };  
12             //此处的异常不能抛,因为该run方法是重写的父类的方法。只能try!
13             System.out.println(Thread.currentThread().getName()+"ticket..."+ticket--);
14         }
15     }
16 }

 

2,使静态同步函数(方法)。

          如果同步函数被关键字static修饰后,则使用的锁不再是this。因为静态方法中不可以定义this,当静态进入内存时,内存中还没有本类对象,但是有该类对应的字节码文件对象(类名.class),该对象的类型是Class。所以静态的同步方法使用的锁是该方法所在类的字节码文件对象。(类名.class)

 

   public  static  synchronized  返回值类型  方法名()

   {  

          需要同步的代码

   }

三,解决线程问题要注意的问题

 1、同步的好处和弊端

  好处:可以保证多线程操作共享数据时的安全问题

  弊端:较消耗资源(要加锁),降低了程序的执行效率(每次要判断锁)。

    2、同步的前提

  要同步,必须有多个线程,多线程在操作共享的数据,同时操作共享数据的语句不止一条。

           (1)必须要有两个或者两个以上的线程。

           (2)必须是多个线程使用同一个锁。

           (3)必须保证同步中只能有一个线程在运行。

    3、加入了同步安全依然存在

  首先查看同步代码块的位置是否加在了需要被同步的代码上。如果同步代码的位置没有错误,这时就再看同步代码块上使用的锁对象是否是同一个。多个线程是否在共享同一把锁

 

以上是关于多线程的安全问题的主要内容,如果未能解决你的问题,请参考以下文章

线程同步-使用ReaderWriterLockSlim类

多线程安全问题

多线程安全问题

多个用户访问同一段代码

多线程带来的风险——线程安全

多线程带来的风险——线程安全