——- android培训、java培训、期待与您交流! ———-
一、多线程的安全问题
1 概述
在前面的例子中我把ticket–和打印语句分开的时候,
出现了一张票被卖4词的情况。
当我把这两条语句合并的时候,再把票额调高到30000时候,
多次测试,也会偶尔出现一张票被卖2次以上。
这就说明多线程共享数据存在安全问题。
……票额到30000,CPU利用率都飙升到97了!(⊙﹏⊙)b
下面我们通过查阅API,分析原因,解决问题。
通过查阅API,我们在Thread类中找到了sleep(long millis)
方法:
这个方法让线程睡眠一定的时间,参数为long类型,时间为毫米级.
因为这个方法是静态方法,所以可以直接用类名调用。
即:Thread.sleep(10);//让线程睡眠10毫米。
编译后,得到了如下提示:
ThreadDemo4.java:50: 错误: 未报告的异常错误InterruptedException; 必须对其进行捕
获或声明以便抛出
Thread.sleep(10);
^
1 个错误
再次查阅API,发现sleep方法是有这个异常InterruptedException。
这个异常我们不采取抛出的方式,而是采取处理掉。
即:try{}catch(){};
编译运行后的结果出现了票额为负数的。
Ticket 3 is saled by Thread-0
Ticket 2 is saled by Thread-0
Ticket 0 is saled by Thread-2
Ticket 1 is saled by Thread-3
Ticket -1 is saled by Thread-1
这直观的说明了多线程有安全问题!
2 出现安全问题的原因
出现安全问题的原因是什么呢???
因为run方法中有多条操作语句,特别是共享数据ticket被两条语句操作。
这就会产生严重的安全问题:当线程2执行到第一条判断ticket语句的时候,
线程1可能正在执行ticket–操作,这样就让共享数据ticket出现问题!
3 解决问题的方法
那么解决问题的办法是什么呢???
直观的想法是,让操作共享数据ticket语句的代码变成一条,
但是上一个例子中,已经证明在多次运行的时候,还是谁出现问题。
在Java,系统给出了专业的解决办法:同步代码块
synchronized(对象){共享数据被操作的那部分代码}。
在使用同步代码块的时候要注意:
一定是共享数据被操作的那部分代码块,而不是其他。
对象其实就可以类比为锁。
4 同步的前提:
1、一定是>=2个线程的情况下使用
2、一定是多个线程使用同一个锁
同步的时候一定是只有一个线程在运行同步代码块
5 同步的好处与局限
同步的好处是:保证了多线程的安全性!
同步的局限性是:每次线程都得先判断锁是开还是关,消耗掉了更多的资源。
System.exit(0);
//注意在使用java ThreadDemo> .\1.txt
命令的时候,
java就一直在调用CPU,会消耗非常多的CPU资源,CPU利用率到达97%,甚至让电脑死机。
但是因为是多线程,主线程执行到这一句的时候,程序就结束了。
这一句还是不能用。
6 代码1
这个是上面分析所用的代码
class Ticket implements Runnable
{
//私有化票额,因为接口是共享的,所以不需要static修饰,提高了效率
private int ticket =100 ;
//创建一个对象,供synchrnozied传递
Object obj = new Object();
//覆盖Runanble接口的run方法,把线程执行的代码存入
public void run()
{
//建立一个循环售票,当票售完即停止
while(true)
{
synchronized(obj)
{
if(ticket > 0)
{
//因为sleep方法抛出了中断异常,所以必须处理掉,这里采取不声明的方式
try{Thread.sleep(10);}catch(Exception e){}
//打印当前线程对象售出的票号,并立即减少1张票额
System.out.println("Ticket "+(ticket--)+" is saled by "+Thread.currentThread().getName());
}
}
}
}
}
class ThreadDemo4
{
public static void main(String[] args)
{
//创建一个Ticket对象,注意这里和第一种创建线程方法的不同。
//这里创建的是票的对象,第一种方法,创建的是售票线程的对象。
//在这里就共享了票额。
Ticket t =new Ticket();
//利用构造函数Thread(Runnable target)创建4个售票线程
//这里利用了多态的形式,建立了接口的引用
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
//调用start方法,启动线程并调用run方法,开始售票
t1.start();
t2.start();
t3.start();
t4.start();
}
}
7 代码2
下面再用一个例子来说明多线程的安全问题
目的:该程序是否有安全问题??
需求:
银行有一个金库。
有两个储户分别存300元,每次存100,存3次。
分析:
名词提炼法
银行,金库,储户
类:银行,储户
银行的属性:金库
银行的方法:存钱
明确实现方法的结果,打印金库金额。返回值为void
明确未知变量:存入的金额。double
储户的属性:无
储户的方法:无。
问题概述:
两个储户分别调用银行的存钱方法往银行存钱,
这就要求用多线程解决。
每次存100元,存3词。
这就规定了存钱方法所传递的参数money是double,
用循环解决。
//建立一个银行类
class Banke
{
//私有化金库
private double moneySum = 0;
//定义存钱方法
public void add(double money)
{
//用户把钱存到银行,银行金库存款增加
moneySum = moneySum + money;
//打印金库存款
System.out.println("金库有存款:"+moneySum+"元");
}
}
//定义储户类,用Runnable实现
class Customer implements Runnable
{
//建立银行对象
Banke b = new Banke();
//覆盖run方法,添加线程要执行的代码
public void run()
{
//用户存入3词,每次存100元
for(int i = 0;i < 3;i++)
{
//银行调用银行的存钱方法
b.add(100);
}
}
}
//定义主类
class BankDemo
{
public static void main(String[] args)
{
//建立储户的对象
Customer c = new Customer();
//创建储户线程1,
Thread t1 = new Thread(c);//这里是利用了多态,Thread(Runnable target)
//创建储户线程2,
Thread t2 = new Thread(c);//这里是利用了多态,Thread(Runnable target)
//启动储户线程1
t1.start();
//启动储户线程2
t2.start();
}
}
运行结果
金库有存款:200.0元
金库有存款:300.0元
金库有存款:400.0元
金库有存款:200.0元
金库有存款:500.0元
金库有存款:600.0元
出现问题的原因是:
线程执行完但是没有打印结果。
8 代码3
和上次一样,利用sleep()让线程睡眠
//建立一个银行类
class Banke
{
//私有化金库
private double moneySum = 0;
//定义存钱方法
public void add(double money)
{
//用户把钱存到银行,银行金库存款增加
moneySum = moneySum + money;
//利用sleep让线程在这里停一会儿,观察运行结果
try{Thread.sleep(10);}catch(Exception e){}
//打印金库存款
System.out.println("金库有存款:"+moneySum+"元");
}
}
//定义储户类,用Runnable实现
class Customer implements Runnable
{
//建立银行对象
Banke b = new Banke();
//覆盖run方法,添加线程要执行的代码
public void run()
{
//用户存入3词,每次存100元
for(int i = 0;i < 3;i++)
{
//银行调用银行的存钱方法
b.add(100);
}
}
}
//定义主类
class BankDemo
{
public static void main(String[] args)
{
//建立储户的对象
Customer c = new Customer();
//创建储户线程1,
Thread t1 = new Thread(c);//这里是利用了多态,Thread(Runnable target)
//创建储户线程2,
Thread t2 = new Thread(c);//这里是利用了多态,Thread(Runnable target)
//启动储户线程1
t1.start();
//启动储户线程2
t2.start();
}
}
运行结果
金库有存款:200.0元
金库有存款:200.0元
金库有存款:400.0元
金库有存款:400.0元
金库有存款:600.0元
金库有存款:600.0元
结果显示确实存在安全问题
二、多线程的同步代码块
1 理论分析
那么这个安全问题如何解决呢??
复习上节课的内容:
利用同步代码块来解决
那么如何同步呢??
步骤:
1、找到多线程运行的那部分代码
2、找到多线程运行的共享数据
3、多线程运行的代码中哪些代码是操作共享数据的
对应本题:
步骤:
1、多线程运行的那部分代码
public void run()
{
//用户存入3词,每次存100元
for(int i = 0;i < 3;i++)
{
//银行调用银行的存钱方法
b.add(100);
}
}
以及add方法的那部分代码
public void add(double money)
{
//用户把钱存到银行,银行金库存款增加
moneySum = moneySum + money;
//利用sleep让线程在这里停一会儿,观察运行结果
try{Thread.sleep(10);}catch(Exception e){}
//打印金库存款
System.out.println("金库有存款:"+moneySum+"元");
}
2、多线程共享的数据很明显就是金库存款moneySum
3、多线程中哪些代码是操作moneySum的呢???
//用户把钱存到银行,银行金库存款增加
moneySum = moneySum + money;
//利用sleep让线程在这里停一会儿,观察运行结果
try{Thread.sleep(10);}catch(Exception e){}
//打印金库存款
System.out.println("金库有存款:"+moneySum+"元");
所以,同步代码块要同步的就是这部分代码了,同步代码块为:
synchronized(对象)
{
//用户把钱存到银行,银行金库存款增加
moneySum = moneySum + money;
//利用sleep让线程在这里停一会儿,观察运行结果
try{Thread.sleep(10);}catch(Exception e){}
//打印金库存款
System.out.println("金库有存款:"+moneySum+"元");
}
2 代码
//同步代码块解决安全问题
//建立一个银行类
class Banke
{
//私有化金库
private double moneySum = 0;
//创建一个对象,供synchronized使用
Object obj = new Object();
//定义存钱方法
public void add(double money)
{
synchronized(obj)
{
//用户把钱存到银行,银行金库存款增加
moneySum = moneySum + money;
//利用sleep让线程在这里停一会儿,观察运行结果
try{Thread.sleep(10);}catch(Exception e){}
//打印金库存款
System.out.println("金库有存款:"+moneySum+"元");
}
}
}
//定义储户类,用Runnable实现
class Customer implements Runnable
{
//建立银行对象
Banke b = new Banke();
//覆盖run方法,添加线程要执行的代码
public void run()
{
//用户存入3词,每次存100元
for(int i = 0;i < 3;i++)
{
//银行调用银行的存钱方法
b.add(100);
}
}
}
//定义主类
class BankDemo
{
public static void main(String[] args)
{
//建立储户的对象
Customer c = new Customer();
//创建储户线程1,
Thread t1 = new Thread(c);//这里是利用了多态,Thread(Runnable target)
//创建储户线程2,
Thread t2 = new Thread(c);//这里是利用了多态,Thread(Runnable target)
//启动储户线程1
t1.start();
//启动储户线程2
t2.start();
}
}
运行结果为:
金库有存款:100.0元
金库有存款:200.0元
金库有存款:300.0元
金库有存款:400.0元
金库有存款:500.0元
金库有存款:600.0元
多次运行,结果依旧不变,说明多线程安全问题已经解决。
三、多线程的同步函数
1 分析
既然代码块可以有同步,那么函数能同步吗??
答案是肯定的。
被synchronized修饰的函数就称之为同步函数。
所以上例的另一种解决安全问题的方法是:
//定义存钱方法
public stnchronized void add(double money)
{
//用户把钱存到银行,银行金库存款增加
moneySum = moneySum + money;
//利用sleep让线程在这里停一会儿,观察运行结果
try{Thread.sleep(10);}catch(Exception e){}
//打印金库存款
System.out.println("金库有存款:"+moneySum+"元");
}
2 代码
//同步函数解决安全问题
//建立一个银行类
class Banke
{
//私有化金库
private double moneySum = 0;
//定义存钱方法
public synchronized void add(double money)
{
//用户把钱存到银行,银行金库存款增加
moneySum = moneySum + money;
//利用sleep让线程在这里停一会儿,观察运行结果
try{Thread.sleep(10);}catch(Exception e){}
//打印金库存款
System.out.println("金库有存款:"+moneySum+"元");
}
}
//定义储户类,用Runnable实现
class Customer implements Runnable
{
//建立银行对象
Banke b = new Banke();
//覆盖run方法,添加线程要执行的代码
public void run()
{
//用户存入3词,每次存100元
for(int i = 0;i < 3;i++)
{
//银行调用银行的存钱方法
b.add(100);
}
}
}
//定义主类
class BankDemo
{
public static void main(String[] args)
{
//建立储户的对象
Customer c = new Customer();
//创建储户线程1,
Thread t1 = new Thread(c);//这里是利用了多态,Thread(Runnable target)
//创建储户线程2,
Thread t2 = new Thread(c);//这里是利用了多态,Thread(Runnable target)
//启动储户线程1
t1.start();
//启动储户线程2
t2.start();
}
}
运行结果
金库有存款:100.0元
金库有存款:200.0元
金库有存款:300.0元
金库有存款:400.0元
金库有存款:500.0元
金库有存款:600.0元
结果显示,安全问题已经被解决
四、小结
1 同步代码块的写法
synchronized(对象)
{
共享数据被操作的代码
}
2 同步的前提
1、大于等于2个线程在运行
2、多个线程使用的是同一个锁
3 同步利弊
同步:只有一个线程运行
好处:多线程–安全
局限:判断锁—–消耗了更多的资源
4 如何同步,如何找问题
1、找多线程运行的代码
2、找多线程共享的数据
3、找1中的哪些代码操作了2