一,多线程安全问题分析
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,锁住的是当前类的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、加入了同步安全依然存在
首先查看同步代码块的位置是否加在了需要被同步的代码上。如果同步代码的位置没有错误,这时就再看同步代码块上使用的锁对象是否是同一个。多个线程是否在共享同一把锁
【代码演示】:模拟两个人去取银行取钱,假设银行总总资产为100。
1 class Person implements Runnable{ 2 int bankmoney=100; 3 Object obj=new Object(); 4 @Override 5 public void run() { 6 while (true){ 7 synchronized (obj){ 8 if(bankmoney>=0){ 9 System.out.println(Thread.currentThread().getName()+"取了一次钱,银行剩余钱数"+bankmoney--); 10 }else { 11 System.out.println("钱已取完"); 12 break; 13 } 14 } 15 } 16 } 17 } 18 public class Bank { 19 public static void main(String[] args) { 20 Person p1=new Person(); 21 Thread t1=new Thread(p1); 22 t1.setName("A"); 23 t1.start(); 24 Person p2=new Person(); 25 Thread t2=new Thread(p2); 26 t2.setName("B"); 27 t2.start(); 28 } 29 }
【代码演示】
1 class Res{ 2 String name=null; 3 String sex=null; 4 } 5 class Write extends Thread { 6 Res res; 7 public Write(Res res){ 8 this.res=res; 9 } 10 @Override 11 public void run() { 12 int count=0; 13 while (true){ 14 synchronized (res){ 15 if(count==0){ 16 res.name="小红"; 17 res.sex="女"; 18 count++; 19 }else { 20 res.name="小军"; 21 res.sex="男"; 22 count++; 23 } 24 count=count%2; 25 } 26 //如果不加锁,就可能会出现名字赋值完性别还没赋值就被其他读线程抢走的情况。如果这时读取就会出现信息误差, 27 //因为姓名是现赋的,但名字还是赋值之前的。也就是说会出现,打印出来 小红:男 和小军:女的错误信息 28 } 29 } 30 } 31 class Read extends Thread { 32 Res res; 33 public Read(Res res){ 34 this.res=res; 35 } 36 @Override 37 public void run() { 38 while (true){ 39 synchronized (res){ 40 System.out.println(res.name+":"+res.sex); 41 } 42 //因为在读的过程中也可能出现,把名字读取后,资源被另一个线程抢走,当在此抢到资源时,性别已被重新赋值, 43 // 而导致读取失败,所以在这块也要加锁 44 } 45 } 46 } 47 public class ReadWriteDemo { 48 public static void main(String[] args) { 49 Res res=new Res(); 50 new Write(res).start(); 51 new Read(res).start(); 52 } 53 }
1.如何解决多线程之间线程安全问题?
答:使用内置锁(同步代码synchronized)或显示锁(lock)。
2.为什么使用同步代码块或使用显示锁可以解决线程安全问题?
答:使用同步代码块可以让一个线程在执行线程任务时,将多条操作共享数据的代码一次执行完,代码执行完后在释放锁,然后其他线程才能进行执行。这样就解决了线程不安全的情况。
3.什么是线程之间同步?
答:多个线程共享一个资源,当一个线程在操作共享资源时,其他线程是不会影响的。