线程学习
案例描述:你和你女朋友公用一张银行卡,你向银行卡中存钱、你女朋友取钱
初级应用:使用Runnable实现线程
public class Test {
public static void main(String[] args) {
//1创建银行卡
BankCard card = new BankCard();//0
//2创建存钱和取钱功能
AddMoney add = new AddMoney(card);
SubMoney sub = new SubMoney(card);
//3创建线程对象
Thread shaobao = new Thread(add, "少泊");
Thread xiaohai = new Thread(sub, "小海");
//4启动
shaobao.start();
xiaohai.start();
}
}
public class BankCard {
private double money;
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
}
public class AddMoney implements Runnable {
private BankCard card;
public AddMoney(BankCard card) {
this.card = card;
}
@Override
public void run() {
//存
for (int i = 0; i < 10; i++) {
card.setMoney(card.getMoney() + 1000);
System.out.println(Thread.currentThread().getName() + "存了1000元,余额是:" + card.getMoney());
}
}
}
public class SubMoney implements Runnable {
private BankCard card;
public SubMoney(BankCard card) {
this.card = card;
}
@Override
public void run() {
//取
for (int i = 0; i < 10; i++) {
if (card.getMoney() >= 1000) {
card.setMoney(card.getMoney() - 1000);
System.out.println(Thread.currentThread().getName() + "取了1000,余额是:" + card.getMoney());
} else {
System.out.println("余额不足,及时存钱");
i--;
}
}
}
}
高级应用
该程序可以进行简化,具体实现如下:
public static void main(String[] args) {
BankCard card = new BankCard();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
card.setMoney(card.getMoney() + 1000);
System.out.println(Thread.currentThread().getName() + "存了1000,余额:" + card.getMoney());
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if (card.getMoney() >= 1000) {
card.setMoney(card.getMoney() - 1000);
System.out.println(Thread.currentThread().getName() + "取了1000,余额:" + card.getMoney());
} else {
System.out.println("余额不足");
i--;
}
}
}
}).start();
}
public class BankCard {
private double money;
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
}
以上代码虽然实现了案例要求,但是会存在 一方还没有存钱,而另一方在不知情的情况下多次去进行取钱的情况,如图:
为了解决上述问题,我们可以引进线程同步的概念。
同步代码块的使用(同步:Synchronized:有等待):
语法:
synchronized(锁) {
//需要访问临界资源的代码段
}
说明:
a.程序走到代码段中,就用锁来锁住了临界资源,这个时候,其他线程不能执行代码段中的代码,只能在锁外边等待
b.执行完代码段中的这段代码,会自动解锁。然后剩下的其他线程开始争抢cpu时间片
c.一定要保证不同的线程看到的是同一把锁,否则同步代码块没有意义
public class AddMoney implements Runnable {
private BankCard card;
public AddMoney(BankCard card) {
this.card = card;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
synchronized (card) {
card.setMoney(card.getMoney() + 1000);
System.out.println(Thread.currentThread().getName() + "存了1000,余额是:" + card.getMoney());
}
}
}
}
public class SubMoney implements Runnable{
private BankCard card;
public SubMoney(BankCard card) {
this.card = card;
}
@Override
public void run() {
for(int i=0;i<10;i++){
synchronized (card) {
if (card.getMoney() >= 1000) {
card.setMoney(card.getMoney() - 1000);
System.out.println(Thread.currentThread().getName() + "取了1000,余额是:" + card.getMoney());
} else {
System.out.println("余额不足");
i--;
}
}
}
}
}
public class BankCard {
private double money;
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
}
public static void main(String[] args) {
BankCard card=new BankCard();
AddMoney add=new AddMoney(card);
SubMoney sub=new SubMoney(card);
new Thread(add,"少泊").start();
new Thread(sub,"发海").start();
}
实现线程同步也可以使用接口Lock来实现,而且更加有效:
从jdk1.5之后加入新的接口 Lock,ReentrantLock(可重入锁)是Lock接口的实现类。
通过显式定义同步锁对象来实现同步,同步锁提供了比synchronized代码块更广泛的锁定操
注意:最好将 unlock的操作放到finally块中
通过使用ReentrantLock这个类来进行锁的操作,它实现了Lock接口,使用ReentrantLock可以显式地加锁、释放锁
public class AddMoney implements Runnable {
private BankCard card;
private Lock lock;
public AddMoney(BankCard card, Lock lock) {
this.card = card;
this.lock=lock;
}
@Override
public void run() {
for(int i=0;i<10;i++) {
lock.lock();
try {
card.setMoney(card.getMoney() + 1000);
System.out.println(Thread.currentThread().getName() + "存了1000,余额是:" + card.getMoney());
} finally {
lock.unlock();
}
}
}
}
public class SubMoney implements Runnable{
private BankCard card;
private Lock lock;
public SubMoney(BankCard card,Lock lock) {
this.card = card;
this.lock=lock;
}
@Override
public void run() {
for(int i=0;i<10;i++){
lock.lock();
try {
if (card.getMoney() >= 1000) {
card.setMoney(card.getMoney() - 1000);
System.out.println(Thread.currentThread().getName() + "取了1000,余额是:" + card.getMoney());
} else {
System.out.println("余额不足");
i--;
}
} finally {
lock.unlock();
}
}
}
}
public class BankCard {
private double money;
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
}
public static void main(String[] args) {
BankCard card=new BankCard();
Lock lock=new ReentrantLock();
AddMoney add=new AddMoney(card,lock);
SubMoney sub=new SubMoney(card,lock);
new Thread(add,"少泊").start();
new Thread(sub,"发海").start();
}
线程同步虽然有效的解决了在没存钱时多次取钱的问题,但可以通过运行结果发现,在加入线程锁之后,只能先等一个人做完全部存钱行为后才可以进行取钱。那如何实现有顺序的执行两个不同的操作线程呢?
这时候我们就可以开始学习线程之间的通信,此时我们更改案例需求,将其变为:
你和你朋友公用一张银行卡,你向卡中存钱,你朋友取钱,保证你存一笔,然后取一笔,再存一笔,再取一笔,顺序执行存取操作
实现功能:使用线程通信。
在jdk1.5之前有三个方法实现线程通信:
wait(): 等待,线程执行这个方法进入等待队列(和锁有关,一个锁对应一个等待队列), 需要被唤醒
notify(): 通知唤醒,从等待队列中随机唤醒一个线程
notifyAll():全部唤醒,把等待队列中所有的线程都唤醒
public class AddMoney implements Runnable{
private BankCard card;
public AddMoney(BankCard card) {
this.card = card;
}
@Override
public void run() {
for(int i=0;i<10;i++){
card.save();
}
}
}
public class SubMoney implements Runnable {
private BankCard card;
public SubMoney(BankCard card) {
this.card = card;
}
@Override
public void run() {
for(int i=0;i<10;i++){
card.take();
}
}
}
public class BankCard {
private double money;
private boolean flag;//true 有钱,可以取 false没钱 ,可以存
//存钱
public synchronized void save(){ //this
if(flag){//有钱
try {
this.wait();//等待,释放cpu,释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
money=money+1000;
System.out.println(Thread.currentThread().getName()+"存了1000,余额是:"+money);
flag=true;//修改标记
this.notify();//唤醒取钱线程
}
//取钱
public synchronized void take(){//this
if(!flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
money=money-1000;
System.out.println(Thread.currentThread().getName()+"取了1000,余额是:"+money);
flag=false;//修改标记为false
this.notify();//通知存钱线程
}
}
public static void main(String[] args) {
//1创建card
BankCard card=new BankCard();
//2创建存钱和取钱
AddMoney add=new AddMoney(card);
SubMoney sub=new SubMoney(card);
//3创建线程对象
Thread shaobo=new Thread(add, "少泊");
Thread fengjie=new Thread(sub,"凤姐");
shaobo.start();
fengjie.start();
}
运行结果如下:
显然此时我们已经完成了案例的要求,但是此时存钱和取钱的线程各只有一个,那如果我们再添加更多的存取线程后,上述代码还能很好的执行么?我们试着向代码中存取线程各自增加一个新的线程
public static void main(String[] args) {
//1创建card
BankCard card=new BankCard();
//2创建存钱和取钱
AddMoney add=new AddMoney(card);
SubMoney sub=new SubMoney(card);
//3创建线程对象
Thread shaobo=new Thread(add, "少泊");
Thread fengjie=new Thread(sub,"凤姐");
Thread fahai=new Thread(add, "发海");
Thread bingbing=new Thread(sub, "冰冰");
shaobo.start();
fengjie.start();
fahai.start();
bingbing.start();
}
此时的运行结果会出现一个新的状况:
可以发现,再增加新的线程后,出现了余额为负数的情况,而这种情况明显是错误的,这时该怎么解决呢?
首先,我们先思考这种情况是怎么产生的?
我们用以下步骤来描述问题的发生:
1.存钱线程1 抢到CPU,执行代码,存钱成功,修改标记为true
2.存钱线程2 抢到CPU,执行代码,等待并释放CPU和锁
3.存钱线程1 抢到CPu,执行代码,等待并释放CPU和锁
4.取钱线程1 抢到CPU,执行代码,取钱成功,修改标记为false,唤醒存钱线程 1
5.存钱线程1 抢到CPU,执行代码,存钱成功,修改标记为true,唤醒存钱线程2
6.存钱线程2 抢到CPU,执行代码,存钱成功,修改标记为true,唤醒
我们可以看到上述步骤中,在步骤5和步骤6中,都发生了存钱成功,并且修改标记为true的现象。说明问题就在此处发生,而发生的原因是:在线程进行判断后,不满足条件的线程进入休眠,而当它被唤醒时,判断条件已经完成,线程将会顺序执行代码,即判断标记失效了,我们该怎么处理该问题呢?
解决方法:将条件语句改为while循环
public class BankCard {
private double money;
private boolean flag;//true 有钱,可以取 false没钱 ,可以存
//存钱
public synchronized void save(){ //this
while (flag){//有钱
try {
this.wait();//等待,释放cpu,释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
money=money+1000;
System.out.println(Thread.currentThread().getName()+"存了1000,余额是:"+money);
flag=true;//修改标记
this.notify();//唤醒取钱线程
}
//取钱
public synchronized void take(){//this
while (!flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
money=money-1000;
System.out.println(Thread.currentThread().getName()+"取了1000,余额是:"+money);
flag=false;//修改标记为false
this.notify();//通知存钱线程
}
}
在解决这个问题后,我们再次执行代码,会发现执行正常了,但是多次执行后,会产生一个新的问题:死锁
此时我们需要重新分析代码的执行过程:
1.存钱线程1 抢到CPU,执行代码,存钱成功,修改标记为true,唤醒
2.存钱线程2 抢到CPU,执行代码,等待并释放CPU和锁
3.存钱线程1 抢到CPU,执行代码,等待并释放CPU和锁
4.取钱线程1 抢到CPU,执行代码,取钱成功,修改标记为false,唤醒存钱线程1
5.取钱线程2 抢到CPU,执行代码,等待并释放CPU和锁
6.取钱线程1 抢到CPU,执行代码,等待并释放CPU和锁
7.存钱线程1 抢到CPU,执行代码,存钱成功,修改标记为true,唤醒存钱线程2
8.存钱线程2 抢到CPU,执行代码,等待并释放CPU和锁
9.存钱线程1 抢到CPU,执行代码,等待并释放CPU和锁
从上述执行步骤可以看到,在执行过程中,四个线程都进入了休眠状态,从而造成死锁现象。
这时,我们就需要使用到notifyAll()方法
notify():当生产者/消费者向缓冲区放入/取出一个产品时,向其他等待的线程发出可执行的通知,同时放弃锁,使自己处于等待状态
·是Object的方法
·调用方式:对象.notify();
·表示唤醒 对象 所标记外边在等待的一个线程
notifyAll():全部唤醒
·是Object的方法
·调用方式:对象.notifyAll()
·表示唤醒 对象 所标记外边等待的所有线程
代码修改为:
public class BankCard {
private double money;
private boolean flag;//true 有钱,可以取 false没钱 ,可以存
//存钱
public synchronized void save(){ //this
while (flag){//有钱
try {
this.wait();//等待,释放cpu,释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
money=money+1000;
System.out.println(Thread.currentThread().getName()+"存了1000,余额是:"+money);
flag=true;//修改标记
this.notifyAll();//唤醒取钱线程
}
//取钱
public synchronized void take(){//this
while (!flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
money=money-1000;
System.out.println(Thread.currentThread().getName()+"取了1000,余额是:"+money);
flag=false;//修改标记为false
this.notifyAll();//通知存钱线程
}
}
money=money+1000;
System.out.println(Thread.currentThread().getName()+"存了1000,余额是:"+money);
flag=true;//修改标记
this.notifyAll();//唤醒取钱线程
}
//取钱
public synchronized void take(){//this
while (!flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
money=money-1000;
System.out.println(Thread.currentThread().getName()+"取了1000,余额是:"+money);
flag=false;//修改标记为false
this.notifyAll();//通知存钱线程
}
}
这时,一个较为完善的代码就修改成功了