线程安全问题解决(案例)–java多线程
在日常工作当中,当我们用到多线程操作公共资源时免不了会遇到一些问题,比如程序执行时多个线程用时操作一个公共资源却发现结果和我们预想的不一致,那到底是为何呢?我们来做一个小例子。
案例1:
案例介绍:
当我们想要开启两个线程同时对公共资源数组saveWord添加数据时,发现程序执行完毕时结果每次都不一样。
package com.nxw.Thread;
import java.util.Arrays;
public class ThreadSafe {
private static int index=0;
public static void main(String[] args) throws InterruptedException {
final String[] saveWord = new String[2];
Runnable runnable1 = new Runnable() {
public void run() {
saveWord[index] = "hello ";
index++;
}
};
Runnable runnable2 = new Runnable() {
public void run() {
saveWord[index] = " world";
index++;
}
};
Thread thread1 = new Thread(runnable1,"子线程1");
Thread thread2 = new Thread(runnable2,"子线程2");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(Arrays.toString(saveWord));
}
}
以上代码多执行几次你就会发现,每次执行的结果都不一样。
执行结果:
- [hello , null]
- [hello , word]
- [word, null]
那么出现这样的结果肯定不是我们想要的,而我们想要的结果要么是[hello , word]要么是[word,hello],那么为什么会出现有null这样的问题呢?
想要知道问题的原因,我们首先的搞清楚线程的概念,线程在执行时是怎么分配的。
我们的线程在执行的时候是有时间限制的,并不是一直等待你执行完毕之后下一个线程才开始执行。而是有一个时间的限制。比如说例子1中run方法执行完毕需要花费5s的时间,而操作系统规定每个线程只给你3s的时间去执行,所以当thread1刚刚把word添加到数组中的时候,thread2就开始执行了,这个时候了thread2获取的index仍然是0,所以就把thread1添加到数组中的值给覆盖掉了。给数据带来的安全问题。
那么如何在应用程序当中来保证线程的安全性呢?
保证资源安全:
要想在应用程序当中来保证线程的安全性,那么就要用到java的同步机制。
方式:使用同步代码块
package com.nxw.Thread;
import java.util.Arrays;
public class ThreadSafe {
private static int index=0;
public static void main(String[] args) throws InterruptedException {
final String[] saveWord = new String[2];
Runnable runnable1 = new Runnable() {
public void run() {
synchronized(saveWord){
saveWord[index] = "hello ";
index++;
}
}
};
Runnable runnable2 = new Runnable() {
public void run() {
synchronized (saveWord){
saveWord[index] = " world";
index++;
}
}
};
Thread thread1 = new Thread(runnable1,"子线程1");
Thread thread2 = new Thread(runnable2,"子线程2");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(Arrays.toString(saveWord));
}
}
由于我们在代码中加了thread1.join(),thread2.join(),所以这两个线程的执行是优先于main方法的主线程的。
加上synchronized代码块后对共享资源saveWord上了锁,当有线程使用这个共享资源saveWord时其他线程等待,保证了共享资源的安全性。
案例2
案例介绍:
目前售票处共有100张票,为了快速的把票卖完,开了四个窗口来共卖这100张票。
package com.nxw.Thread.ticketSync;
public class Test {
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread thread1 = new Thread(ticket,"win1");
Thread thread2 = new Thread(ticket,"win2");
Thread thread3 = new Thread(ticket,"win3");
Thread thread4 = new Thread(ticket,"win4");
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
static class Ticket implements Runnable{
private int ticket = 100;
//创建锁
private Object lock = new Object();
public void run() {
while (true){
synchronized (lock){
if(ticket<=0){
break;
}
System.out.println("线程:"+Thread.currentThread().getName()+"买了第"+ticket+"张票");
ticket--;
}
}
}
}
}
为了保证票的安全,不会被其他窗口出现重复卖一张票的情况,我们在run方法里加了一把锁,谁拥有锁,谁就去卖这张票,保证了不会出现一票多卖的情况。
案例3
案例介绍:
有一张银行卡,一个线程往里存钱,一个线程从里取钱。
package com.nxw.Thread.money;
public class TestMoney {
public static void main(String[] args) {
//1、创建银行卡
final BankCard card = new BankCard();
//2、存钱
Runnable addMoney = new Runnable() {
public void run() {
for(int i=0;i<10;i++){
card.setMoney(card.getMoney()+10000);
System.out.println("线程:"+Thread.currentThread().getName()
+"存了"+10000+"元,目前卡余额为:"+card.getMoney());
}
}
};
//3、取钱
Runnable subMoney = new Runnable() {
public void run() {
for(int i=0;i<10;i++){
if(card.getMoney()<=0){
System.out.println("余额不足,请先存钱!");
i--;
}else{
card.setMoney(card.getMoney()-10000);
System.out.println("线程:"+Thread.currentThread().getName()
+"取了"+10000+"元,目前卡余额为:"+card.getMoney());
}
}
}
};
Thread add = new Thread(addMoney,"明明");
Thread sub = new Thread(subMoney,"丽丽");
add.start();
sub.start();
}
static class BankCard{
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
private double money;
}
class addMoney{
}
}
多运行几次代码你会发现结果有些问题
余额不足,请先存钱!
线程:明明存了10000元,目前卡余额为:10000.0
线程:明明存了10000元,目前卡余额为:10000.0
余额不足,请先存钱!
线程:明明存了10000元,目前卡余额为:20000.0
线程:明明存了10000元,目前卡余额为:30000.0
线程:明明存了10000元,目前卡余额为:40000.0
线程:明明存了10000元,目前卡余额为:40000.0
第二行结果和第三行结果都是存了一万元,第三行应该为20000,怎么还是余额还是10000?
第7行和第八行余额都是40000,显然结果是不正确的。
那为什么会出现这样的情况呢?我们来分析一下。
当存钱线程刚执行完card.setMoney(card.getMoney()+10000);还没有执行打印的时候分配给该线程的时间片到期了,这个时候取钱线程开始执行,刚执行完card.setMoney(card.getMoney()-10000);时间片也到期了,这个时候又轮到存钱线程执行,可是刚刚存的钱已经被取走了,所以就导致了输出的余额一样的情况。
解决方法: 保证存钱和取钱的两段代码都要同步(存的时候不能取,取得时候不能存)
package com.nxw.Thread.money;
public class TestMoney {
public static void main(String[] args) {
//1、创建银行卡
final BankCard card = new BankCard();
//2、存钱
Runnable addMoney = new Runnable() {
public void run() {
for(int i=0;i<10;i++){
synchronized (card){
card.setMoney(card.getMoney()+10000);
System.out.println("线程:"+Thread.currentThread().getName()
+"存了"+10000+"元,目前卡余额为:"+card.getMoney());
}
}
}
};
//3、取钱
Runnable subMoney = new Runnable() {
public void run() {
for(int i=0;i<10;i++){
synchronized (card){
if(card.getMoney()<=0){
System.out.println("余额不足,请先存钱!");
i--;
}else{
card.setMoney(card.getMoney()-10000);
System.out.println("线程:"+Thread.currentThread().getName()
+"取了"+10000+"元,目前卡余额为:"+card.getMoney());
}
}
}
}
};
Thread add = new Thread(addMoney,"明明");
Thread sub = new Thread(subMoney,"丽丽");
add.start();
sub.start();
}
static class BankCard{
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
private double money;
}
class addMoney{
}
}
再次执行代码,你会发现问题已经解决,不会出现余额结果相同的情况。