多线程的安全问题
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、加入了同步安全依然存在
首先查看同步代码块的位置是否加在了需要被同步的代码上。如果同步代码的位置没有错误,这时就再看同步代码块上使用的锁对象是否是同一个。多个线程是否在共享同一把锁
以上是关于多线程的安全问题的主要内容,如果未能解决你的问题,请参考以下文章