一、问题的提出
以买票系统为例:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 class Ticket implements Runnable 2 { 3 public int sum=10; 4 public void run() 5 { 6 while(true) 7 { 8 if(sum>0) 9 { 10 System.out.println(Thread.currentThread().getName()+":"+sum--); 11 } 12 } 13 } 14 } 15 public class Demo 16 { 17 public static void main(String args[]) 18 { 19 Ticket t=new Ticket(); 20 new Thread(t).start(); 21 new Thread(t).start(); 22 new Thread(t).start(); 23 new Thread(t).start(); 24 } 25 }
这个代码有问题。仔细分析可以知道,如果四个线程同时进入了run方法中,假设当时sum==1,则第一个线程可以进入if块中,但是如果CPU突然切换到了其他线程,那么第一个线程将会等待CPU执行权,但是并没有改变sum的值,此时sum仍然是1;同理,假设极端情况发生了,即第2、3个线程均进入了if块,而且均在改变sum值之前就并指运行,等待CPU执行权,那么第四个线程改变完sum的值称为0之后,其余三个线程会将sum的值变为-1,-2,-3(但是输出只能到-2),很明显的,问题发生了,虽然几率不大,但是一旦发生就是致命的问题。
使用Thread.sleep()方法可以暂停线程的执行,通过输出即可检验。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 class Ticket implements Runnable 2 { 3 public int sum=10; 4 public void run() 5 { 6 while(true) 7 { 8 if(sum>0) 9 { 10 try 11 { 12 Thread.sleep(50); 13 } 14 catch(InterruptedException e){} 15 System.out.println(Thread.currentThread().getName()+":"+sum--); 16 } 17 18 } 19 } 20 } 21 public class Demo 22 { 23 public static void main(String args[]) 24 { 25 Ticket t=new Ticket(); 26 new Thread(t).start(); 27 new Thread(t).start(); 28 new Thread(t).start(); 29 new Thread(t).start(); 30 } 31 }
运行结果:
注意,本例中还出现了另一个线程安全性问题:第二条和第三条同时卖出了9号票,这是因为sum--还没来得及自减CPU就切换到了其他线程。
注意使用sleep方法产生的异常只能捕获不能抛出。
二、线程安全性问题造成的原因是什么?
1.多个线程在操作共享的数据。四个线程操作共享的ticket数据
2.操作共享数据的线程代码有多条。
当一个线程在执行操作共享数据的多条代码过程中其他线程参与了运算就会导致线程安全问题的产生。
通过以上得分以方法,我们可以发现并解决大多数的线程安全问题。
在本例中,由于操作共享数据ticket的线程有多条,而且每个线程操作共享数据的代码有三条(除去try-catch):
1 if(sum>0) 2 { 3 try 4 { 5 Thread.sleep(50); 6 } 7 catch(InterruptedException e){} 8 System.out.println(Thread.currentThread().getName()+":"+sum--); 9 }
因此出现了线程安全性问题。
解决线程安全性问题的分析:
关键问题:两个语句被分开读了。
解决思路:将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候其他线程是不可以参与运算的,必须要当前线程把这些代码都执行完毕后其他线程才可以参与运算。
三、解决线程安全性问题的方法一:同步代码块
使用同步代码块的格式:
synchronized(对象 )
{
需要被同步的代码。
}
需要说明的是,这里的对象类型是任意的,但是要保证各个线程所使用的对象是统一个对象,可以将此对象定义为Ticket的成员,还可以是其它现有的对象,如this或者字节码文件对象等。
现在将代码改成如下格式:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 class Ticket implements Runnable 2 { 3 Object obj=new Object(); 4 private int sum=10; 5 public void run() 6 { 7 while(true) 8 { 9 try 10 { 11 Thread.sleep(50); 12 } 13 catch(InterruptedException e){} 14 synchronized(obj) 15 { 16 if(sum>0) 17 { 18 try 19 { 20 Thread.sleep(50); 21 } 22 catch(InterruptedException e){} 23 System.out.println(Thread.currentThread().getName()+":"+sum--); 24 } 25 } 26 } 27 } 28 } 29 public class Demo 30 { 31 public static void main(String args[]) 32 { 33 Ticket t=new Ticket(); 34 new Thread(t).start(); 35 new Thread(t).start(); 36 new Thread(t).start(); 37 new Thread(t).start(); 38 39 } 40 }
除了改成同步代码块之外,还需要改动其它:在if与while之间加入sleep方法,这样做的目的是为了便于观察多个线程协调运作的情况,否则容易出现单个线程将任务完成的情况,出现这样的原因就是判断同步锁的消耗大于进行下一次循环的消耗,因此,将进行下一次循环所用的时间延长即可轻易解决掉这个问题。
运行结果:
我们发现已经没有线程安全性问题了(即使增加if中sleep的时间)。
四、分析同步的好处和弊端以及其他问题的解决方案。
先分析一下同步的原理:
同步代码块需要的对象相当于一把锁,这把锁称为同步锁。我们可以和火车上的卫生间相比较:当我们需要上卫生间的时候,会先看一下指示灯是否是绿灯,如果是绿灯则表示没有人(这相当于判断锁的过程),这时候我们可以打开门(拿到锁),干活(执行同步代码块代码)。如果这时候外面的人想进来,就会发现灯变红了(这是因为锁的控制权在蹲坑的人手里),所以他们进不来(拿不到锁),等到卫生间里的人出来了(释放锁),外面的人才能进去(拿到锁),这样就保证了卫生间里的人只有一个(保证执行同步代码块的线程只有一个)。
同步的好处:
解决了线程安全性问题。
同步的弊端:
进入同步代码块中的线程不会一直持有CPU执行权,CPU切换到其他线程,判断锁之后又进不去同步代码块,相当于做了无用功。
这样就相对降低了执行效率。
如果我们加了同步代码块之后仍然出现了线程安全性问题,原因是什么?
同步中必须有多个线程并使用同一把锁,这是同步的前提。如果出现了即使加上锁仍然出现了线程安全性问题,很有可能是多个线程用的不是同一把锁。
举例:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 class Ticket implements Runnable 2 { 3 4 private int sum=10; 5 public void run() 6 { 7 Object obj=new Object(); 8 while(true) 9 { 10 try 11 { 12 Thread.sleep(50); 13 } 14 catch(InterruptedException e){} 15 synchronized(obj) 16 { 17 if(sum>0) 18 { 19 try 20 { 21 Thread.sleep(50); 22 } 23 catch(InterruptedException e){} 24 System.out.println(Thread.currentThread().getName()+":"+sum--); 25 } 26 } 27 } 28 } 29 } 30 public class Demo 31 { 32 public static void main(String args[]) 33 { 34 Ticket t=new Ticket(); 35 new Thread(t).start(); 36 new Thread(t).start(); 37 new Thread(t).start(); 38 new Thread(t).start(); 39 40 } 41 }
将同步锁的定义放在了run方法中,这样每个线程都有了自己的锁,线程安全性问题依旧:
五、解决线程安全性问题的方法二:同步函数
使用格式:
在同步方法的修饰符中添加synchronized关键字即可
先介绍一个小案例:
储户存钱问题:
需求:有两个储户,每个都到银行存钱,每次存100,共存三次。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 class Bank 2 { 3 private int sum; 4 public void add(int num) 5 { 6 sum+=num; 7 System.out.println(Thread.currentThread().getName()+":Bank有钱"+sum); 8 } 9 } 10 class Cus implements Runnable 11 { 12 private Bank bank; 13 public Cus(){} 14 public Cus(Bank bank) 15 { 16 this.bank=bank; 17 } 18 public void run() 19 { 20 for(int i=1;i<=3;i++) 21 { 22 System.out.println(Thread.currentThread().getName()+":存入100"); 23 this.bank.add(100); 24 } 25 } 26 } 27 public class Demo 28 { 29 public static void main(String args[]) 30 { 31 Bank bank=new Bank(); 32 new Thread(new Cus(bank)).start(); 33 new Thread(new Cus(bank)).start(); 34 } 35 }
我们运行代码很多次,没有发现线程异常,虽然没有发现异常但是并不代表以后不会发生。假设其中一个线程在sum+=num之后进入堵塞状态,那么肯定就会发生两个线程打印出的银行账目相同的情况。
现在我们模拟sum+=num之后线程堵塞的情况,我们可以通过使用sleep方法实现。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 class Bank 2 { 3 private int sum; 4 public void add(int num) 5 { 6 sum+=num; 7 try 8 { 9 Thread.sleep(50); 10 } 11 catch (InterruptedException e) 12 { 13 14 } 15 System.out.println(Thread.currentThread().getName()+":Bank有钱"+sum); 16 } 17 } 18 class Cus implements Runnable 19 { 20 private Bank bank; 21 public Cus(){} 22 public Cus(Bank bank) 23 { 24 this.bank=bank; 25 } 26 public void run() 27 { 28 for(int i=1;i<=3;i++) 29 { 30 System.out.println(Thread.currentThread().getName()+":存入100"); 31 this.bank.add(100); 32 } 33 } 34 } 35 public class Demo 36 { 37 public static void main(String args[]) 38 { 39 Bank bank=new Bank(); 40 new Thread(new Cus(bank)).start(); 41 new Thread(new Cus(bank)).start(); 42 } 43 }
运行结果:
很明显出现了线程安全性问题。
使用同步代码块可以解决这个问题,但是我们可以使用同步方法解决这个问题,因为add方法本身就是一个单独的封装体。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 class Bank 2 { 3 private int sum; 4 public synchronized void add(int num) 5 { 6 sum+=num; 7 try 8 { 9 Thread.sleep(50); 10 } 11 catch (InterruptedException e) 12 { 13 14 } 15 System.out.println(Thread.currentThread().getName()+":Bank有钱"+sum); 16 } 17 } 18 class Cus implements Runnable 19 { 20 private Bank bank; 21 public Cus(){} 22 public Cus(Bank bank) 23 { 24 this.bank=bank; 25 } 26 public void run() 27 { 28 for(int i=1;i<=3;i++) 29 { 30 System.out.println(Thread.currentThread().getName()+":存入100"); 31 this.bank.add(100); 32 } 33 } 34 } 35 public class Demo 36 { 37 public static void main(String args[]) 38 { 39 Bank bank=new Bank(); 40 new Thread(new Cus(bank)).start(); 41 new Thread(new Cus(bank)).start(); 42 } 43 }
现象:
我们可以发现存入了200但是银行只有100,问题解决了一半。
经过分析我们可以知道问题出在
1 System.out.println(Thread.currentThread().getName()+":存入100"); 2 this.bank.add(100);
这两句代码没有同步,也就是说一个线程在执行到第一行代码的时候CPU切换到了其他线程。
我们可以将这两个代码同步起来解决这个问题。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 /** 2 线程安全性问题使用同步方法解决。 3 */ 4 class Bank 5 { 6 private int sum; 7 public synchronized void add(int num) 8 { 9 sum+=num; 10 try 11 { 12 Thread.sleep(50); 13 } 14 catch (InterruptedException e) 15 { 16 17 } 18 System.out.println(Thread.currentThread().getName()+":Bank有钱"+sum); 19 } 20 } 21 class Cus implements Runnable 22 { 23 private Bank bank; 24 public Cus(){} 25 public Cus(Bank bank) 26 { 27 this.bank=bank; 28 } 29 public void run() 30 { 31 for(int i=1;i<=3;i++) 32 { 33 synchronized(bank) 34 { 35 System.out.println(Thread.currentThread().getName()+":存入100"); 36 this.bank.add(100); 37 } 38 } 39 } 40 } 41 public class Demo 42 { 43 public static void main(String args[]) 44 { 45 Bank bank=new Bank(); 46 new Thread(new Cus(bank)).start(); 47 new Thread(new Cus(bank)).start(); 48 } 49 }
现象:
我们发现线程0一直执行,直到循环结束才轮到线程1,运行很多次仍然是这样,出现这样的原因就是判断锁需要的时间过大
我们可以使用sleep方法解决这个问题,以达到交替显示的效果。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 class Bank 2 { 3 private int sum; 4 public synchronized void add(int num) 5 { 6 sum+=num; 7 try 8 { 9 Thread.sleep(50); 10 } 11 catch (InterruptedException e) 12 { 13 14 } 15 System.out.println(Thread.currentThread().getName()+":Bank有钱"+sum); 16 } 17 } 18 class Cus implements Runnable 19 { 20 private Bank bank; 21 public Cus(){} 22 public Cus(Bank bank) 23 { 24 this.bank=bank; 25 } 26 public void run() 27 { 28 for(int i=1;i<=3;i++) 29 { 30 synchronized(bank) 31 { 32 System.out.println(Thread.currentThread().getName()+":存入100"); 33 this.bank.add(100); 34 } 35 try 36 { 37 Thread.sleep(50); 38 } 39 catch (InterruptedException e) 40 { 41 42 } 43 } 44 } 45 } 46 public class Demo 47 { 48 public static void main(String args[]) 49 { 50 Bank bank=new Bank(); 51 new Thread(new Cus(bank)).start(); 52 new Thread(new Cus(bank)).start(); 53 } 54 }
效果:
经过多次改进代码,我们达到了理想的效果,但是我们应当注意,锁的嵌套有可能会出现死锁,要慎用。
现在将买票系统的同步代码块改成同步函数:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 /* 2 将买票系统的同步代码块改造成同步方法的形式。 3 */ 4 class Ticket implements Runnable 5 { 6 Object obj=new Object(); 7 private int sum=10; 8 public void run() 9 { 10 while(true) 11 { 12 try 13 { 14 Thread.sleep(50); 15 } 16 catch(InterruptedException e){} 17 show(); 18 } 19 } 20 public synchronized void show() 21 { 22 if(sum>0) 23 { 24 try 25 { 26 Thread.sleep(50); 27 } 28 catch(InterruptedException e){} 29 System.out.println(Thread.currentThread().getName()+":"+sum--); 30 } 31 } 32 } 33 public class Demo 34 { 35 public static void main(String args[]) 36 { 37 Ticket t=new Ticket(); 38 new Thread(t).start(); 39 new Thread(t).start(); 40 new Thread(t).start(); 41 new Thread(t).start(); 42 43 } 44 }
结果:
我们可以看到并没有发生线程安全性问题,改造成功。
现在开始分析同步函数使用的锁是什么锁。
答案:this锁,即本对象。
验证:
为了便于比较和保持代码简洁,现在使用两个线程买票。
两个线程分别使用同步代码块和同步方法,如果没有出现线程安全性问题,则证明同步代码块和同步方法使用的是同一把锁。
同步代码块使用Object锁,同时票的数量改为100。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 class Ticket implements Runnable 2 { 3 Object obj=new Object(); 4 boolean flag; 5 private int sum=100; 6 public void run() 7 { 8 if(flag==true) 9 { 10 while(true) 11 { 12 synchronized(obj) 13 { 14 if(sum>0) 15 { 16 try 17 { 18 Thread.sleep(10); 19 } 20 catch (InterruptedException e) 21 { 22 } 23 System.out.println(Thread.currentThread().getName()+":object--"+sum--); 24 } 25 } 26 } 27 } 28 else 29 { 30 while(true) 31 show(); 32 } 33 } 34 public synchronized void show() 35 { 36 if(sum>0) 37 { 38 try 39 { 40 Thread.sleep(10); 41 } 42 catch (InterruptedException e) 43 { 44 } 45 System.out.println(Thread.currentThread().getName()+":function--"+sum--); 46 } 47 } 48 } 49 public class Demo 50 { 51 public static void main(String args[]) 52 { 53 Ticket t=new Ticket(); 54 t.flag=false; 55 new Thread(t).start(); 56 57 58 t.flag=true; 59 new Thread(t).start(); 60 } 61 }
现象就是所有的线程都经由同步代码块而没有经过同步方法。
原因就是主方法一口气执行完了,就将flag的值改为了true,这时候两个线程都还没有启动,当启动的时候,发现flag的值为true所以都走了同步代码块。
我们应当找出一种方法,让线程0在启动之后并且进入死循环才让标志变量改变,我们只需要在改变标志变量之前等待一段时间即可,等待的目的就是让线程0启动。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 /* 2 将买票系统的同步代码块改造成同步方法的形式。 3 */ 4 class Ticket implements Runnable 5 { 6 Object obj=new Object(); 7 boolean flag; 8 private int sum=100; 9 public void run() 10 { 11 if(flag==true) 12 { 13 while(true) 14 { 15 synchronized(obj) 16 { 17 if(sum>0) 18 { 19 try 20 { 21 Thread.sleep(10); 22 } 23 catch (InterruptedException e) 24 { 25 } 26 System.out.println(Thread.currentThread().getName()+":object--"+sum--); 27 } 28 } 29 } 30 } 31 else 32 { 33 while(true) 34 show(); 35 } 36 } 37 public synchronized void show() 38 { 39 if(sum>0) 40 { 41 try 42 { 43 Thread.sleep(10); 44 } 45 catch (InterruptedException e) 46 { 47 } 48 System.out.println(Thread.currentThread().getName()+":function--"+sum--); 49 } 50 } 51 } 52 public class Demo 53 { 54 public static void main(String args[]) 55 { 56 Ticket t=new Ticket(); 57 t.flag=false; 58 new Thread(t).start(); 59 60 try//加入等待时间,让线程0启动 61 { 62 Thread.sleep(500); 63 } 64 catch (InterruptedException e) 65 { 66 } 67 t.flag=true; 68 new Thread(t).start(); 69 } 70 }
执行结果:
我们发现了线程安全性问题的存在(通过设置延长时间大大增加了发生的几率)。表名使用的不是同一把锁。
现在同步代码块使用的锁改成this。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 /* 2 将买票系统的同步代码块改造成同步方法的形式。 3 */ 4 class Ticket implements Runnable 5 { 6 Object obj=new Object(); 7 boolean flag; 8 private int sum=100; 9 public void run() 10 { 11 if(flag==true) 12 { 13 while(true) 14 { 15 synchronized(this) 16 { 17 if(sum>0) 18 { 19 try 20 { 21 Thread.sleep(10); 22 } 23 catch (InterruptedException e) 24 { 25 } 26 System.out.println(Thread.currentThread().getName()+":object--"+sum--); 27 } 28 } 29 } 30 } 31 else 32 { 33 while(true) 34 show(); 35 } 36 } 37 public synchronized void show() 38 { 39 if(sum>0) 40 { 41 try 42 { 43 Thread.sleep(10); 44 } 45 catch (InterruptedException e) 46 { 47 } 48 System.out.println(Thread.currentThread().getName()+":function--"+sum--); 49 } 50 } 51 } 52 public class Demo 53 { 54 public static void main(String args[]) 55 { 56 Ticket t=new Ticket(); 57 t.flag=false; 58 new Thread(t).start(); 59 60 try//加入等待时间,让线程0启动 61 { 62 Thread.sleep(500); 63 } 64 catch (InterruptedException e) 65 { 66 } 67 t.flag=true; 68 new Thread(t).start(); 69 } 70 }
经过多次运行程序并比较结果,可以发现没有一次出现线程安全性问题,表名同步方法使用的锁就是本对象。
虽然使用同步方法更加简洁,但是应当注意,能使用同步代码块的就尽量使用同步代码块。
六、解决线程安全性问题的方法三:静态同步函数
使用方法:将通不方法改成静态的。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 /* 2 将买票系统的同步代码块改造成同步方法的形式。 3 */ 4 class Ticket implements Runnable 5 { 6 Object obj=new Object(); 7 boolean flag; 8 private static int sum=100; 9 public void run() 10 { 11 if(flag==true) 12 { 13 while(true) 14 { 15 synchronized(this) 16 { 17 if(sum>0) 18 { 19 try 20 { 21 Thread.sleep(10); 22 } 23 catch (InterruptedException e) 24 { 25 } 26 System.out.println(Thread.currentThread().getName()+":object--"+sum--); 27 } 28 } 29 } 30 } 31 else 32 { 33 while(true) 34 show(); 35 } 36 } 37 public static synchronized void show() 38 { 39 if(sum>0) 40 { 41 try 42 { 43 Thread.sleep(10); 44 } 45 catch (InterruptedException e) 46 { 47 } 48 System.out.println(Thread.currentThread().getName()+":function--"+sum--); 49 } 50 } 51 } 52 public class Demo 53 { 54 public static void main(String args[]) 55 { 56 Ticket t=new Ticket(); 57 t.flag=false; 58 new Thread(t).start(); 59 60 try//加入等待时间,让线程0启动 61 { 62 Thread.sleep(500); 63 } 64 catch (InterruptedException e) 65 { 66 67 } 68 t.flag=true; 69 new Thread(t).start(); 70 } 71 }
结果:
线程安全性问题又出现了,表名静态同步函数所使用的锁不是本类对象,其原因是显而易见的。
其实,静态同步方法是哦用的锁是字节码文件对象,获取字节码文件对象的方法有两种:
1.对象名.getClass();
2.类名.class;
现改进代码:只是将同步代码块使用的对象锁改成this.getClass或者Ticket.class
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 /* 2 将买票系统的同步代码块改造成静态同步方法的形式。 3 */ 4 class Ticket implements Runnable 5 { 6 Object obj=new Object(); 7 boolean flag; 8 private static int sum=100; 9 public void run() 10 { 11 if(flag==true) 12 { 13 while(true) 14 { 15 synchronized(this.getClass()) 16 { 17 if(sum>0) 18 { 19 try 20 { 21 Thread.sleep(10); 22 } 23 catch (InterruptedException e) 24 { 25 } 26 System.out.println(Thread.currentThread().getName()+":object--"+sum--); 27 } 28 } 29 } 30 } 31 else 32 { 33 while(true) 34 show(); 35 } 36 } 37 public static synchronized void show() 38 { 39 if(sum>0) 40 { 41 try 42 { 43 Thread.sleep(10); 44 } 45 catch (InterruptedException e) 46 { 47 } 48 System.out.println(Thread.currentThread().getName()+":function--"+sum--); 49 } 50 } 51 } 52 public class Demo 53 { 54 public static void main(String args[]) 55 { 56 Ticket t=new Ticket(); 57 t.flag=false; 58 new Thread(t).start(); 59 60 try//加入等待时间,让线程0启动 61 { 62 Thread.sleep(50); 63 } 64 catch (InterruptedException e) 65 { 66 67 } 68 t.flag=true; 69 new Thread(t).start(); 70 } 71 }
经过多次运行程序并验证,可以得到静态同步函数使用的同步锁是本类的字节码文件对象这一结论。
七、懒汉式单例模式在多线程中的安全性问题极其解决方案
详情查看:【JAVA单例模式详解】