java中线程的一些相关概念,第2篇,看我这篇文章前,先看上一篇
先了解一点线程相关的概念:主线程、线程优先级、线程组、精灵线程(守护线程)、线程状态、线程同步、死锁、线程间的通信(使用 wait()notify()notifyAll()进行线程间通信)
线程状态
图片和案例来自于网络的电子书,我觉得那本电子书写的挺好,挺通俗易懂的!感谢那本电子书的原作者!
案例引用自电子书中的案例
线程同步和死锁
package com.mysynchronized;
public class ZhuBaobao {
//存钱罐的余额
private int money;
public ZhuBaobao(int money) {
this.money = money;
}
/**
* 往存钱罐放钱的方法
* @param inMoney
*/
public void add(int inMoney) {
int oldMoney = money;
//取出旧的金额后让当前线程睡眠会以使别的线程操作它
try {
Thread.sleep(40);
} catch (InterruptedException e) {
e.printStackTrace();
}
money = oldMoney + inMoney;
System.out.println("宝哥哥存入" + inMoney +
"元, 现在余额" + money +"元");
}
/**
* 从存钱罐里取钱的方法
* @param outMoney
*/
public void get(int outMoney) {
int oldMoney = money;
//取出旧的金额后让当前线程睡眠会以使别的线程操作它
try {
Thread.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
money = oldMoney - outMoney;
System.out.println("林妹妹取走" + outMoney +
"元, 现在余额" + money +"元");
}
public String toString() {
return "我是存钱罐猪宝宝,我体内现在的余额是" + money + "元";
}
}
宝哥哥类: 为了使两个线程操作同一对象,宝哥哥类内部声明猪宝宝对象,通过构造方
法传参传入,并且在线程体上调用存款方法,循环存入十次钱。
package com.mysynchronized;
public class Baogege implements Runnable {
private ZhuBaobao zhubaobao;
public Baogege(ZhuBaobao zhubaobao) {
this.zhubaobao = zhubaobao;
}
public void run() {
add();
}
public void add() {
for (int i = 0; i < 10; i++) {
System.out.println("宝哥哥第" + i + "次存钱");
zhubaobao.add(100);
}
}
}
林妹妹类: 林妹妹类与宝哥哥基本相同,只不过这里的存款方法变成了取款。
package com.mysynchronized;
public class Linmeimei implements Runnable {
private ZhuBaobao zhubaobao;
public Linmeimei(ZhuBaobao zhubaobao) {
this.zhubaobao = zhubaobao;
}
public void run() {
get();
}
public void get() {
for (int i = 0; i < 10; i++) {
System.out.println("林妹妹第" + i + "次取钱");
zhubaobao.get(50);
}
}
}
运行类: 初始化猪宝宝的金额为 1000 元,通过构造器传参把猪宝宝传入宝哥哥和林妹
妹两个操作线程,调用线程的 start()方法,让两个线程对象同时并发访问这一资源,
主线程睡眠三秒钟以使两个线程运行完毕,打印猪宝宝存款余额。初始 1000 加宝哥哥
循环存入 1000 减林妹妹循环取走 500,期望结果为 1500 元。
package com.mysynchronized;
public class Run {
public static void main(String[] args) {
Thread
mainThread = Thread.currentThread();
//初始猪宝宝的余额是1000元
ZhuBaobao zhubaobao = new ZhuBaobao(1000);
//保证宝哥哥和林妹妹两个线程操作的是同一个对象
//利用构造器传参把猪宝宝传入
Baogege baogege = new Baogege(zhubaobao);
Linmeimei linmeimei = new Linmeimei(zhubaobao);
Thread t1 = new Thread(baogege);
Thread t2 = new Thread(linmeimei);
t1.start();
t2.start();
//主线程睡眠3秒以便其他两个线程先执行完
try {
mainThread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印账户余额
System.out.println(zhubaobao);
}
}
程序部分运行结果:
当然在您计算机上运行结果与这里可能略有差异,但数值大多不是您期望的 1500。这
就是多个线程访问同一资源的线程安全问题,那情况可能是这样的:
1. 林妹妹查看余额 1200 元
2. 宝哥哥查看余额 1200 元
3. 林妹妹更新余额为 1200-50 =1150 元
4. 宝哥哥更新余额为 1200+100=1300 元
那么这样数据就会出现异常,这里 java 利用同步来解决。其中同步又分为同步方法和同步
块。我们先看同步方法,同步方法的实现很简单,只要在方法的声明前面加上
synchronized 关键字就可以。它的原理是在任何时刻一个对象中只有一个同步方法可以执
行。只有对象当前执行的同步方法结束后,同一个对象的另一个同步方法才可能开始。这里
的思想是,保证每个线程在调用对象同步方法时以独占的方法操作该对象。
我们首先解决上面问题再分析:
package com.mysynchronized;
public class ZhuBaobao {
//存钱罐的余额
private int money;
public ZhuBaobao(int money) {
this.money = money;
}
//此方法加synchronized关键字,变为同步方法
public synchronized void add(int inMoney) {
int oldMoney = money;
//取出旧的金额后让当前线程睡眠会以使别的线程操作它
try {
Thread.sleep(40);
} catch (InterruptedException e) {
e.printStackTrace();
}
money = oldMoney + inMoney;
System.out.println("宝哥哥存入" + inMoney +
"元, 现在余额" + money +"元");
}
//此方法加synchronized关键字,变为同步方法
public synchronized void get(int outMoney) {
int oldMoney = money;
//取出旧的金额后让当前线程睡眠会以使别的线程操作它
try {
Thread.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
money = oldMoney - outMoney;
System.out.println("林妹妹取走" + outMoney +
"元, 现在余额" + money +"元");
}
public String toString() {
return "我是存钱罐猪宝宝,我体内现在的余额是" + money + "元";
}
}
程序的运行结果这里就不再给出,最后的余额刚好是我们期望的 1500 元。这里同步利
用了一种与每个对象相关的内部锁,这种锁在同步方法开始执行时由进程设置一种标志,称
为锁定。对象的每个同步方法都检查是否另一个方法设置了该锁,如果设置了,此方法就不
执行,直到该锁定由一解锁动作复位为止。这样,同一时刻只能由一个同步方法执行,因为
这个方法已经设置了锁,阻止了其他同步方法的启动。
下面我们讲解同步块,这里可以用线程同步的粒度问题来说明,线程同步的粒度越小越
好,即线程同步的代码块越小越好。比如上例中存款和取款两个方法中都有让当前线程睡眠
的代码,它的最初出现是为了让两个线程有时间同时修改一个资源数据,现在使用同步方法
后问题已经解决,代码可以删除,但这里我们假设这种代码还是有意义的,比如系统对当前
操作的用户做安全验证等操作,而这样的验证代码是不需要同步的,这时我们可以使用同步
块解决。
package com.mysynchronized;
public class ZhuBaobao {
//存钱罐的余额
private int money;
public ZhuBaobao(int money) {
this.money = money;
}
public void add(int inMoney) {
//安全验证等业务代码占用时间
try {
Thread.sleep(40);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(this) {
money += inMoney;
System.out.println("宝哥哥存入" + inMoney +
"元, 现在余额" + money +"元");
}
}
public void get(int outMoney) {
//安全验证等业务代码占用时间
try {
Thread.sleep(40);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(this) {
money -= outMoney;
System.out.println("林妹妹取走" + outMoney +
"元, 现在余额" + money +"元");
}
}
public String toString() {
return "我是存钱罐猪宝宝,我体内现在的余额是" + money + "元";
}
}
上面的代码不仅能保证数据的安全,还能提高我们程序的运行效率,这只是同步块的一
个作用。因为同步块后面需要传递同步的对象参数,所以它可以指定更多的对象享受同步的
优势,而不像同步方法那样只有包含相关代码的对象受益。
讲完同步,我们看看我们前面的学习中有哪些关于同步的知识。在我们在讲解集合框架
时旺旺老师就一直强调 Vector 与 Arraylist 的区别是一个是线程安全的一个是线程非安全
的,现在你打开源代码就会发现其实 Vector 所有对于内部存储对象的操作方法是同步的,
而 Arraylist 是非同步的,所以我们又把同步的方法称为线程安全的方法。同理 Hashtable
与 HashMap 也是前者是线程安全后者为非安全,StringBuffer 与 StringBuilder 道理更一
样,前者方法大都是同步的。同样您也应该知道了他们的英勇场合,对于单线程程序,您根
本没必要使用线程安全的类,那样反而会影响我们的程序运行效率。
讲解:死锁
任何事物都具有它的两面性,同步也不例外,当我们惬意的编写同步代码时,殊不知这
样的代码有可能引发一个非常可怕的后果,那就是:死锁。
关于死锁,我们看一个案例。这里假如开饭了,林妹妹宝哥哥两个线程都要吃饭,吃饭
需要操作饭碗和勺子两个资源,为了使资源数据保持安全,我们把这两个对象的操作方法都
设为同步的,也就是说操作它的线程是以独占的方式打开的。那假如宝哥哥线程先占有了饭
碗资源,而此时林妹妹占有了勺子资源,他们都不能完成吃饭的操作,而一直等待对方释放
资源,这样就发生了死锁。
简单的所死锁发生的原因是第一个线程等待第二个线程释放资源,而第二个线程有等待
第一个线程释放资源。导致死锁的根源是不恰当的使用了同步方法,并且程序的运行轨迹没
经过严密的考虑。
发生死锁一般要满足四个条件:
1. 互斥条件:一个资源每次只能被一个线程使用。
2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不变。
3. 不剥夺条件:线程已获得的资源,在未使用完成前,不能强行剥夺。
4. 循环等待条件:若干线程之间形成一种头尾相连的循环等待资源关系。
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之
一不满足,就不会发生死锁。
我们还要知道,java 本身既不能预防死锁,也不能发现死锁,但理解的死锁的原因,尤
其是产生死锁的四个必要条件,那我们在系统设计,线程调度方面就可以谨慎对待来避免死
锁的发生。
线程间通信
讲解:使用 wait()notify()notifyAll()进行线程间通信
到这我们还是回到讲解同步时使用的案例,当然数据安全的问题我们利用同步已经解
决。现在我们修改下程序,宝哥哥每次还是存 100 元,但林妹妹让每次取 400 元,这样只需
要取 5 次就可以花完初始的 1000 元加林妹妹存入的 1000 元。
林妹妹类: 我们看修改后的林妹妹类,变成取款 5 次,每次 400 元。
package com.mysynchronized;
public class Linmeimei implements Runnable {
private ZhuBaobao zhubaobao;
public Linmeimei(ZhuBaobao zhubaobao) {
this.zhubaobao = zhubaobao;
}
public void run() {
get();
}
public void get() {
for (int i = 0; i < 5; i++) {
System.out.println("林妹妹第" + i + "次取钱");
zhubaobao.get(400);
}
}
}
我们看到运行结果虽然还是期望的 0,但中间却出现了负值,这是因为林妹妹取款的金
额过大,速度过快,远大于存款的宝哥哥,那一个解决方法林妹妹取款时检查金额,如果小
于 400,就让它等待,直到大于等于 400 才让运行,那这里我们利用线程间通信的知识解
决。
它的原理是让当前锁定某对象的线程在线程体没执行完的情况下解锁,让其它的线程有
机会执行刚才被锁定的同步方法,当然执行完后要通知以前的线程继续执行。
线程间通信主要使用 Object 类提供的三个方法:wait(),notify()与 notifyAll()来实
现。
wait():等待方法,可以让当前线程放弃监视器并进入睡眠状态,直到其它线程进入同
一监视器并调用 notify()或 notifyAll()方法。
notify():唤醒方法。可唤醒同一对象监视器中调用了 wait()方法的第一个线程。
notifyAll():唤醒所有线程的方法。唤醒同一对象监视器中调用了 wait()方法的所有线
程,具有最高优先级的线程首先被唤醒。
那上面林妹妹取款的问题我们就可以解决,每次取钱时先判断余额是否充足,如果不
够,取款操作的线程林妹妹就进入等待状态,放弃存钱罐猪宝宝的控制权,存款线程宝哥哥
存完款之后调用 notifyAll()方法通知当前挂起的线程,林妹妹继续判断,不满足继续等
待,直到某一时刻满足条件才进行取款操作,代码如下所示:
package com.threadMessage1;
public class ZhuBaobao {
//存钱罐的余额
private int money;
public ZhuBaobao(int money) {
this.money = money;
}
/**
* 往存钱罐放钱的方法
* @param inMoney
*/
public void add(int inMoney) {
//安全验证等业务代码占用时间
try {
Thread.sleep(40);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(this) {
money += inMoney;
System.out.println("宝哥哥存入" + inMoney + "元," +
"现在余额" + money +"元");
//唤醒前期挂起的所有线程
notifyAll();
}
}
/**
* 从存钱罐里取钱的方法
*/
public synchronized void get(int outMoney) {
//如果余额不足等待
while (outMoney > money) {
try {
System.out.println("余额" + money + "林妹妹等待");
//让当前线程处于等待状态
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
money -= outMoney;
System.out.println("林妹妹取走" + outMoney + "元," +
" 现在余额" + money +"元");
}
public String toString() {
return "我是存钱罐猪宝宝,我体内现在的余额是" + money + "元";
}
}
这里程序的运行结果就不再给出,我们发现,只有当存款余额大于林妹妹的取款金额时
才会发生取款操作,负责取款操作会放弃当前对象控制权进入等待状态。
下面我们再看一个有意思的线程间通信例子。
案例原型:模拟三人买票。售票员最初只有一张五元钱。排队的三人中,A 有一张 20
元人民币,B 有一张 10 元人民币,C 有一张五元人民币。如果买票人的钱不是零钱,而售票
员有没有零钱找,那么此人必须等待,并允许后面的人买票,以便售票员获得零钱。
分析:A,B,C 三人买票就像三个线程,每个线程必须满足要么售票员有足够的找零或
提供售票员的是五元零钱才能执行。A,B,C 三人同时只能有一人向售票员买票(利用同步
解决),不满足条件时需要等待。
买票人类: 要保证三个买票人请求的是同一个售票员对象,所以在买票人内部定义了一个售票员对象,定义表示当前买票人拥有的金额变量 money。
package com.wangwang.threadMessage2;
public class TicketSeller {
//表示持有的三种货币的数目
int five = 1, ten = 0, twenty = 0;
public synchronized void sellTicket(int money) {
if (money == 5) {//如果是五元钱
five ++;//面值五元货币数量加以
System.out.println(Thread.currentThread().getName() + "给我五元钱,售票一张,无找零");
System.out.println("five = " + five + " ten= " + ten + " twenty = " + twenty );
} else if (money == 10) {
while (five < 1) {//如果五元货币数量少于一
try {
System.out.println(Thread.currentThread().getName() + "请您一旁等待");
wait();//当前线程处于等待状态
System.out.println(Thread.currentThread().getName() + "结束等待");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
five --;//五元货币数量减一
ten ++;//十元货币数量加一
System.out.println(Thread.currentThread().getName() + "给我十元钱,售票一张,找零五元");
System.out.println("five = " + five + " ten= " + ten + " twenty = " + twenty );
} else if (money == 20) {
while (five < 1 || ten < 1) {//只要五元十元货币一个少一,无法找零
try {
System.out.println(Thread.currentThread().getName() + "请您一旁等待");
wait();
System.out.println(Thread.currentThread().getName() + "结束等待");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
five --;
ten --;
twenty ++;
System.out.println(Thread.currentThread().getName() + "给我二十元钱,售票一张,找零十五元");
System.out.println("five = " + five + " ten= " + ten + " twenty = " + twenty );
}
notifyAll();//通知当前挂起的线程进入就需状态
}
}
package com.wangwang.threadMessage2;
public class Cinema implements Runnable {
//表示当前持有的货币金额
int money;
//售票员类,要求所有的买票线程请求的同一个售票员对象
TicketSeller ticketSeller;
public Cinema(TicketSeller ticketSeller, int money) {
this.ticketSeller = ticketSeller;
this.money = money;
}
public void run() {
ticketSeller.sellTicket(money);
}
}
package com.wangwang.threadMessage2;
public class Run {
public static void main(String[] args) {
//创建售票员对象
TicketSeller ticketSeller = new TicketSeller();
//创建三个买票人对象,保证他们向一个售票员买票
//并且传入他们各自持有的金额
Cinema fiveCinema = new Cinema(ticketSeller, 5);
Cinema tenCinema = new Cinema(ticketSeller, 10);
Cinema twentyCinema = new Cinema(ticketSeller, 20);
//给三个买票人线程设置三个名字
Thread fiveThread = new Thread(fiveCinema, "FiveCinema");
Thread tenThread = new Thread(tenCinema, "TenCinema");
Thread twentyThread = new Thread(twentyCinema, "TwentyCinema");
twentyThread.start();
tenThread.start();
fiveThread.start();
}
}
您还可以把三个买票线程的启动顺序修改下,看程序的运行结果。到这里,您掌握线程间通
信了吗?