【JAVA多线程安全问题解析】
一、问题的提出
以买票系统为例:
class Ticket implements Runnable
{
public int sum=10;
public void run()
{
while(true)
{
if(sum>0)
{
System.out.println(Thread.currentThread().getName()+":"+sum--);
}
}
}
}
public class Demo
{
public static void main(String args[])
{
Ticket t=new Ticket();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}
}
这个代码有问题。仔细分析可以知道,如果四个线程同时进入了run方法中,假设当时sum==1,则第一个线程可以进入if块中,但是如果CPU突然切换到了其他线程,那么第一个线程将会等待CPU执行权,但是并没有改变sum的值,此时sum仍然是1;同理,假设极端情况发生了,即第2、3个线程均进入了if块,而且均在改变sum值之前就并指运行,等待CPU执行权,那么第四个线程改变完sum的值称为0之后,其余三个线程会将sum的值变为-1,-2,-3(但是输出只能到-2),很明显的,问题发生了,虽然几率不大,但是一旦发生就是致命的问题。
使用Thread.sleep()方法可以暂停线程的执行,通过输出即可检验。
class Ticket implements Runnable
{
public int sum=10;
public void run()
{
while(true)
{
if(sum>0)
{
try
{
Thread.sleep(50);
}
catch(InterruptedException e){}
System.out.println(Thread.currentThread().getName()+":"+sum--);
} }
}
}
public class Demo
{
public static void main(String args[])
{
Ticket t=new Ticket();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}
}
9
运行结果:
注意,本例中还出现了另一个线程安全性问题:第二条和第三条同时卖出了9号票,这是因为sum--还没来得及自减CPU就切换到了其他线程。
注意使用sleep方法产生的异常只能捕获不能抛出。
二、线程安全性问题造成的原因是什么?
1.多个线程在操作共享的数据。四个线程操作共享的ticket数据
2.操作共享数据的线程代码有多条。
当一个线程在执行操作共享数据的多条代码过程中其他线程参与了运算就会导致线程安全问题的产生。
通过以上得分以方法,我们可以发现并解决大多数的线程安全问题。
在本例中,由于操作共享数据ticket的线程有多条,而且每个线程操作共享数据的代码有三条(除去try-catch):
if(sum>0)
{
try
{
Thread.sleep(50);
}
catch(InterruptedException e){}
System.out.println(Thread.currentThread().getName()+":"+sum--);
}
因此出现了线程安全性问题。
解决线程安全性问题的分析:
关键问题:两个语句被分开读了。
解决思路:将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候其他线程是不可以参与运算的,必须要当前线程把这些代码都执行完毕后其他线程才可以参与运算。
三、解决线程安全性问题的方法一:同步代码块
使用同步代码块的格式:
synchronized(对象 )
{
需要被同步的代码。
}
需要说明的是,这里的对象类型是任意的,但是要保证各个线程所使用的对象是统一个对象,可以将此对象定义为Ticket的成员,还可以是其它现有的对象,如this或者字节码文件对象等。
现在将代码改成如下格式:
class Ticket implements Runnable
{
Object obj=new Object();
private int sum=10;
public void run()
{
while(true)
{
try
{
Thread.sleep(50);
}
catch(InterruptedException e){}
synchronized(obj)
{
if(sum>0)
{
try
{
Thread.sleep(50);
}
catch(InterruptedException e){}
System.out.println(Thread.currentThread().getName()+":"+sum--);
}
}
}
}
}
public class Demo
{
public static void main(String args[])
{
Ticket t=new Ticket();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start(); }
}
除了改成同步代码块之外,还需要改动其它:在if与while之间加入sleep方法,这样做的目的是为了便于观察多个线程协调运作的情况,否则容易出现单个线程将任务完成的情况,出现这样的原因就是判断同步锁的消耗大于进行下一次循环的消耗,因此,将进行下一次循环所用的时间延长即可轻易解决掉这个问题。
运行结果:
我们发现已经没有线程安全性问题了(即使增加if中sleep的时间)。
四、分析同步的好处和弊端以及其他问题的解决方案。
先分析一下同步的原理:
同步代码块需要的对象相当于一把锁,这把锁称为同步锁。我们可以和火车上的卫生间相比较:当我们需要上卫生间的时候,会先看一下指示灯是否是绿灯,如果是绿灯则表示没有人(这相当于判断锁的过程),这时候我们可以打开门(拿到锁),干活(执行同步代码块代码)。如果这时候外面的人想进来,就会发现灯变红了(这是因为锁的控制权在蹲坑的人手里),所以他们进不来(拿不到锁),等到卫生间里的人出来了(释放锁),外面的人才能进去(拿到锁),这样就保证了卫生间里的人只有一个(保证执行同步代码块的线程只有一个)。
同步的好处:
解决了线程安全性问题。
同步的弊端:
进入同步代码块中的线程不会一直持有CPU执行权,CPU切换到其他线程,判断锁之后又进不去同步代码块,相当于做了无用功。
这样就相对降低了执行效率。
如果我们加了同步代码块之后仍然出现了线程安全性问题,原因是什么?
同步中必须有多个线程并使用同一把锁,这是同步的前提。如果出现了即使加上锁仍然出现了线程安全性问题,很有可能是多个线程用的不是同一把锁。
举例:
class Ticket implements Runnable
{ private int sum=10;
public void run()
{
Object obj=new Object();
while(true)
{
try
{
Thread.sleep(50);
}
catch(InterruptedException e){}
synchronized(obj)
{
if(sum>0)
{
try
{
Thread.sleep(50);
}
catch(InterruptedException e){}
System.out.println(Thread.currentThread().getName()+":"+sum--);
}
}
}
}
}
public class Demo
{
public static void main(String args[])
{
Ticket t=new Ticket();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start(); }
}
将同步锁的定义放在了run方法中,这样每个线程都有了自己的锁,线程安全性问题依旧:
五、解决线程安全性问题的方法二:同步函数
使用格式:
在同步方法的修饰符中添加synchronized关键字即可
先介绍一个小案例:
储户存钱问题:
需求:有两个储户,每个都到银行存钱,每次存100,共存三次。
class Bank
{
private int sum;
public void add(int num)
{
sum+=num;
System.out.println(Thread.currentThread().getName()+":Bank有钱"+sum);
}
}
class Cus implements Runnable
{
private Bank bank;
public Cus(){}
public Cus(Bank bank)
{
this.bank=bank;
}
public void run()
{
for(int i=1;i<=3;i++)
{
System.out.println(Thread.currentThread().getName()+":存入100");
this.bank.add(100);
}
}
}
public class Demo
{
public static void main(String args[])
{
Bank bank=new Bank();
new Thread(new Cus(bank)).start();
new Thread(new Cus(bank)).start();
}
}
我们运行代码很多次,没有发现线程异常,虽然没有发现异常但是并不代表以后不会发生。假设其中一个线程在sum+=num之后进入堵塞状态,那么肯定就会发生两个线程打印出的银行账目相同的情况。
现在我们模拟sum+=num之后线程堵塞的情况,我们可以通过使用sleep方法实现。
class Bank
{
private int sum;
public void add(int num)
{
sum+=num;
try
{
Thread.sleep(50);
}
catch (InterruptedException e)
{ }
System.out.println(Thread.currentThread().getName()+":Bank有钱"+sum);
}
}
class Cus implements Runnable
{
private Bank bank;
public Cus(){}
public Cus(Bank bank)
{
this.bank=bank;
}
public void run()
{
for(int i=1;i<=3;i++)
{
System.out.println(Thread.currentThread().getName()+":存入100");
this.bank.add(100);
}
}
}
public class Demo
{
public static void main(String args[])
{
Bank bank=new Bank();
new Thread(new Cus(bank)).start();
new Thread(new Cus(bank)).start();
}
}
运行结果:
很明显出现了线程安全性问题。
使用同步代码块可以解决这个问题,但是我们可以使用同步方法解决这个问题,因为add方法本身就是一个单独的封装体。
class Bank
{
private int sum;
public synchronized void add(int num)
{
sum+=num;
try
{
Thread.sleep(50);
}
catch (InterruptedException e)
{ }
System.out.println(Thread.currentThread().getName()+":Bank有钱"+sum);
}
}
class Cus implements Runnable
{
private Bank bank;
public Cus(){}
public Cus(Bank bank)
{
this.bank=bank;
}
public void run()
{
for(int i=1;i<=3;i++)
{
System.out.println(Thread.currentThread().getName()+":存入100");
this.bank.add(100);
}
}
}
public class Demo
{
public static void main(String args[])
{
Bank bank=new Bank();
new Thread(new Cus(bank)).start();
new Thread(new Cus(bank)).start();
}
}
现象:
我们可以发现存入了200但是银行只有100,问题解决了一半。
经过分析我们可以知道问题出在
System.out.println(Thread.currentThread().getName()+":存入100");
this.bank.add(100);
这两句代码没有同步,也就是说一个线程在执行到第一行代码的时候CPU切换到了其他线程。
我们可以将这两个代码同步起来解决这个问题。
/**
线程安全性问题使用同步方法解决。
*/
class Bank
{
private int sum;
public synchronized void add(int num)
{
sum+=num;
try
{
Thread.sleep(50);
}
catch (InterruptedException e)
{ }
System.out.println(Thread.currentThread().getName()+":Bank有钱"+sum);
}
}
class Cus implements Runnable
{
private Bank bank;
public Cus(){}
public Cus(Bank bank)
{
this.bank=bank;
}
public void run()
{
for(int i=1;i<=3;i++)
{
synchronized(bank)
{
System.out.println(Thread.currentThread().getName()+":存入100");
this.bank.add(100);
}
}
}
}
public class Demo
{
public static void main(String args[])
{
Bank bank=new Bank();
new Thread(new Cus(bank)).start();
new Thread(new Cus(bank)).start();
}
}
现象:
我们发现线程0一直执行,直到循环结束才轮到线程1,运行很多次仍然是这样,出现这样的原因就是判断锁需要的时间过大
我们可以使用sleep方法解决这个问题,以达到交替显示的效果。
class Bank
{
private int sum;
public synchronized void add(int num)
{
sum+=num;
try
{
Thread.sleep(50);
}
catch (InterruptedException e)
{ }
System.out.println(Thread.currentThread().getName()+":Bank有钱"+sum);
}
}
class Cus implements Runnable
{
private Bank bank;
public Cus(){}
public Cus(Bank bank)
{
this.bank=bank;
}
public void run()
{
for(int i=1;i<=3;i++)
{
synchronized(bank)
{
System.out.println(Thread.currentThread().getName()+":存入100");
this.bank.add(100);
}
try
{
Thread.sleep(50);
}
catch (InterruptedException e)
{ }
}
}
}
public class Demo
{
public static void main(String args[])
{
Bank bank=new Bank();
new Thread(new Cus(bank)).start();
new Thread(new Cus(bank)).start();
}
}
效果:
经过多次改进代码,我们达到了理想的效果,但是我们应当注意,锁的嵌套有可能会出现死锁,要慎用。
现在将买票系统的同步代码块改成同步函数:
/*
将买票系统的同步代码块改造成同步方法的形式。
*/
class Ticket implements Runnable
{
Object obj=new Object();
private int sum=10;
public void run()
{
while(true)
{
try
{
Thread.sleep(50);
}
catch(InterruptedException e){}
show();
}
}
public synchronized void show()
{
if(sum>0)
{
try
{
Thread.sleep(50);
}
catch(InterruptedException e){}
System.out.println(Thread.currentThread().getName()+":"+sum--);
}
}
}
public class Demo
{
public static void main(String args[])
{
Ticket t=new Ticket();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start(); }
}
结果:
我们可以看到并没有发生线程安全性问题,改造成功。
现在开始分析同步函数使用的锁是什么锁。
答案:this锁,即本对象。
验证:
为了便于比较和保持代码简洁,现在使用两个线程买票。
两个线程分别使用同步代码块和同步方法,如果没有出现线程安全性问题,则证明同步代码块和同步方法使用的是同一把锁。
同步代码块使用Object锁,同时票的数量改为100。
class Ticket implements Runnable
{
Object obj=new Object();
boolean flag;
private int sum=100;
public void run()
{
if(flag==true)
{
while(true)
{
synchronized(obj)
{
if(sum>0)
{
try
{
Thread.sleep(10);
}
catch (InterruptedException e)
{
}
System.out.println(Thread.currentThread().getName()+":object--"+sum--);
}
}
}
}
else
{
while(true)
show();
}
}
public synchronized void show()
{
if(sum>0)
{
try
{
Thread.sleep(10);
}
catch (InterruptedException e)
{
}
System.out.println(Thread.currentThread().getName()+":function--"+sum--);
}
}
}
public class Demo
{
public static void main(String args[])
{
Ticket t=new Ticket();
t.flag=false;
new Thread(t).start(); t.flag=true;
new Thread(t).start();
}
}
现象就是所有的线程都经由同步代码块而没有经过同步方法。
原因就是主方法一口气执行完了,就将flag的值改为了true,这时候两个线程都还没有启动,当启动的时候,发现flag的值为true所以都走了同步代码块。
我们应当找出一种方法,让线程0在启动之后并且进入死循环才让标志变量改变,我们只需要在改变标志变量之前等待一段时间即可,等待的目的就是让线程0启动。
/*
将买票系统的同步代码块改造成同步方法的形式。
*/
class Ticket implements Runnable
{
Object obj=new Object();
boolean flag;
private int sum=100;
public void run()
{
if(flag==true)
{
while(true)
{
synchronized(obj)
{
if(sum>0)
{
try
{
Thread.sleep(10);
}
catch (InterruptedException e)
{
}
System.out.println(Thread.currentThread().getName()+":object--"+sum--);
}
}
}
}
else
{
while(true)
show();
}
}
public synchronized void show()
{
if(sum>0)
{
try
{
Thread.sleep(10);
}
catch (InterruptedException e)
{
}
System.out.println(Thread.currentThread().getName()+":function--"+sum--);
}
}
}
public class Demo
{
public static void main(String args[])
{
Ticket t=new Ticket();
t.flag=false;
new Thread(t).start(); try//加入等待时间,让线程0启动
{
Thread.sleep(500);
}
catch (InterruptedException e)
{
}
t.flag=true;
new Thread(t).start();
}
}
执行结果:
我们发现了线程安全性问题的存在(通过设置延长时间大大增加了发生的几率)。表名使用的不是同一把锁。
现在同步代码块使用的锁改成this。
/*
将买票系统的同步代码块改造成同步方法的形式。
*/
class Ticket implements Runnable
{
Object obj=new Object();
boolean flag;
private int sum=100;
public void run()
{
if(flag==true)
{
while(true)
{
synchronized(this)
{
if(sum>0)
{
try
{
Thread.sleep(10);
}
catch (InterruptedException e)
{
}
System.out.println(Thread.currentThread().getName()+":object--"+sum--);
}
}
}
}
else
{
while(true)
show();
}
}
public synchronized void show()
{
if(sum>0)
{
try
{
Thread.sleep(10);
}
catch (InterruptedException e)
{
}
System.out.println(Thread.currentThread().getName()+":function--"+sum--);
}
}
}
public class Demo
{
public static void main(String args[])
{
Ticket t=new Ticket();
t.flag=false;
new Thread(t).start(); try//加入等待时间,让线程0启动
{
Thread.sleep(500);
}
catch (InterruptedException e)
{
}
t.flag=true;
new Thread(t).start();
}
}
经过多次运行程序并比较结果,可以发现没有一次出现线程安全性问题,表名同步方法使用的锁就是本对象。
虽然使用同步方法更加简洁,但是应当注意,能使用同步代码块的就尽量使用同步代码块。
六、解决线程安全性问题的方法三:静态同步函数
使用方法:将通不方法改成静态的。
/*
将买票系统的同步代码块改造成同步方法的形式。
*/
class Ticket implements Runnable
{
Object obj=new Object();
boolean flag;
private static int sum=100;
public void run()
{
if(flag==true)
{
while(true)
{
synchronized(this)
{
if(sum>0)
{
try
{
Thread.sleep(10);
}
catch (InterruptedException e)
{
}
System.out.println(Thread.currentThread().getName()+":object--"+sum--);
}
}
}
}
else
{
while(true)
show();
}
}
public static synchronized void show()
{
if(sum>0)
{
try
{
Thread.sleep(10);
}
catch (InterruptedException e)
{
}
System.out.println(Thread.currentThread().getName()+":function--"+sum--);
}
}
}
public class Demo
{
public static void main(String args[])
{
Ticket t=new Ticket();
t.flag=false;
new Thread(t).start(); try//加入等待时间,让线程0启动
{
Thread.sleep(500);
}
catch (InterruptedException e)
{ }
t.flag=true;
new Thread(t).start();
}
}
结果:
线程安全性问题又出现了,表名静态同步函数所使用的锁不是本类对象,其原因是显而易见的。
其实,静态同步方法是哦用的锁是字节码文件对象,获取字节码文件对象的方法有两种:
1.对象名.getClass();
2.类名.class;
现改进代码:只是将同步代码块使用的对象锁改成this.getClass或者Ticket.class
/*
将买票系统的同步代码块改造成静态同步方法的形式。
*/
class Ticket implements Runnable
{
Object obj=new Object();
boolean flag;
private static int sum=100;
public void run()
{
if(flag==true)
{
while(true)
{
synchronized(this.getClass())
{
if(sum>0)
{
try
{
Thread.sleep(10);
}
catch (InterruptedException e)
{
}
System.out.println(Thread.currentThread().getName()+":object--"+sum--);
}
}
}
}
else
{
while(true)
show();
}
}
public static synchronized void show()
{
if(sum>0)
{
try
{
Thread.sleep(10);
}
catch (InterruptedException e)
{
}
System.out.println(Thread.currentThread().getName()+":function--"+sum--);
}
}
}
public class Demo
{
public static void main(String args[])
{
Ticket t=new Ticket();
t.flag=false;
new Thread(t).start(); try//加入等待时间,让线程0启动
{
Thread.sleep(50);
}
catch (InterruptedException e)
{ }
t.flag=true;
new Thread(t).start();
}
}
经过多次运行程序并验证,可以得到静态同步函数使用的同步锁是本类的字节码文件对象这一结论。
七、懒汉式单例模式在多线程中的安全性问题极其解决方案
详情查看:【JAVA单例模式详解】
【JAVA多线程安全问题解析】的更多相关文章
- Java 多线程安全问题简单切入详细解析
线程安全 假如Java程序中有多个线程在同时运行,而这些线程可能会同时运行一部分的代码.如果说该Java程序每次运行的结果和单线程的运行结果是一样的,并且其他的变量值也都是和预期的结果是一样的,那么就 ...
- 最全面的Java多线程用法解析
1.创建线程 在Java中创建线程有两种方法:使用Thread类和使用Runnable接口.在使用Runnable接口时需要建立一个Thread实例.因此,无论是通过Thread类还是Runnable ...
- java基础知识回顾之java Thread类学习(四)--java多线程安全问题(锁)
上一节售票系统中我们发现,打印出了错票,0,-1,出现了多线程安全问题.我们分析为什么会发生多线程安全问题? 看下面线程的主要代码: @Override public void run() { // ...
- Java多线程-实例解析
Java多线程实例 3种实现方法Java中的多线程有三种实现方式:1.继承Thread类,重写run方法.Thread本质上也是一个实现了Runnable的实例,他代表一个线程的实例,并且启动线程的唯 ...
- java 多线程安全问题-同步代码块
/* 多线程的安全问题: while(true) { if(tick>0) { //线程0,1,2,3在余票为1时,都停滞在这里,之后分别获得CPU执行权,打印出0,-1,-2等错票 Syste ...
- Java——多线程安全问题
静态代码块中没有this /* * 线程安全问题产生的原因: * 1.多个线程操作共享的数据 * 2.操作共享数据的线程代码有多条 * * 当一个线程在执行操作共享数据的多条代码过程中,其他线程 ...
- java多线程安全问题-同步修饰符于函数
上一篇文章通过卖票使用同步代码块的方法解决安全问题本篇文章首先探讨如何找出这样的安全问题,并提出第二种方式(非静态函数synchronized修饰)解决安全问题 /* 需求: 银行有一个公共账号金库 ...
- Java多线程安全问题
body, table{font-family: 微软雅黑; font-size: 10pt} table{border-collapse: collapse; border: solid gray; ...
- java基础知识回顾之java Thread类学习(五)--java多线程安全问题(锁)同步的前提
这里举个例子讲解,同步synchronized在什么地方加,以及同步的前提: * 1.必须要有两个以上的线程,才需要同步. * 2.必须是多个线程使用同一个锁. * 3.必须保证同步中只能有一个线程在 ...
随机推荐
- 使用shell脚本自动化对硬盘进行分区
在Linux系统中,可以使用fdisk 对硬盘进行分区,但是要手动执行很多命令,这样使用有很麻烦,现在记一个用fdisk 的脚本自动执行. #make partition dd count= fdis ...
- ubuntu安装cacti错误
安装cacti时,明明mysql信息都配置正确了,权限也分配好了,可是仍然报错,如下: 这时可以试试到/etc/cacti目录下,修改debian.php中的mysql配置信息,问题应该就能解决了.
- python entrypoint
entrypoint, 实际是一张匹配表.匹配简短指令和具体的python函数的执行路径.有点快捷方式的概念. 不同的是,这种快捷方式不仅可以给命令行使用,还可以供其他代码简单调用,而无需关注太多细节 ...
- Linux内核 TCP/IP、Socket参数调优
Linux内核 TCP/IP.Socket参数调优 2014-06-06 Harrison.... 阅 9611 转 165 转藏到我的图书馆 微信分享: Doc1: /proc/sy ...
- 【实例】html5-canvas中实现背景图片的移动
本例来自于<HTML5 Canvas核心技术 图形.动画与游戏开发> 在线演示 (图有点多,请多刷新几次) 本例还有一点代码不理解,我用注释和问号标注出来了,有大神看到求解答,谢谢 本例子 ...
- malloc/free与new/delete的区别
相同点:都可用于申请动态内存和释放内存 不同点:(1)操作对象有所不同.malloc与free是C++/C 语言的标准库函数,new/delete 是C++的运算符.对于非内部数据类的对象而言,光用m ...
- Read N Characters Given Read4 I & II
The API: int read4(char *buf) reads 4 characters at a time from a file. The return value is the actu ...
- jQuery获取循环中的选中单选按钮radio的值
1.<input type="radio" name="testradio" value="jquery获取radio的值" /> ...
- Python2.7<-------->Python3.x
版本差异 from __future__ Python2.7 Python3.x 除法 / // Unicode u'' ...
- [Android] RelativeLayout, LinearLayout,FrameLayout
Android RelativeLayout 属性 // 相对于给定ID控件 android:layout_above 将该控件的底部置于给定ID的控件之上; android:layout_below ...