多线程共享数据,其实要分为两种情况:
1.多线程执行相同的代码处理数据,最经典的问题就是卖票;
2.多线程执行不同的代码处理数据,最经典的问题就是银行存取钱。
卖票问题探究:
最初的代码是:
public class Test1 {
public static void main(String[] args) {
Ticket target = new Ticket();
Thread threadA = new Thread(target, "A");
Thread threadB = new Thread(target, "B");
threadA.start();
threadB.start();
}
}
class Ticket implements Runnable {
private int leftTicket = 500;
@Override
public void run() {
while (leftTicket > 0) {
leftTicket = leftTicket - 1;
System.out.println(Thread.currentThread().getName() + "处理后还剩"
+ leftTicket + "张票");
}
}
}
以上代码创建了两个线程,因为创建这两个线程时传的是同一个Runnable对象,所以如果这个Runnable对象有成员变量的话,这两个线程就可都操作这个Runnable对象的成员变量。
执行上面代码多次,发现基本上每次打印的结果都不相同,这就是线程不安全。所谓线程安全就是多线程每次执行的结果都是固定的、可控的。为解决线程安全问题,就要用到synchronized或者Lock。
如果用传统的synchronized的话,有两种方式:
1.synchronized同步代码块:synchronized(this){...}或者synchronized(Object obj){...}。同步代码块放在run方法体中,相对于下面的synchronized同步方法,推荐优先使用synchronized同步代码块。
2.synchronized同步方法:用synchronized修饰普通方法,可以有返回值,也可以没有返回值,在run方法中调用此同步方法。
用synchronized同步代码块解决卖票问题:
public class Test1 {
public static void main(String[] args) {
Ticket target = new Ticket();
Thread threadA = new Thread(target, "A");
Thread threadB = new Thread(target, "B");
threadA.start();
threadB.start();
}
}
class Ticket implements Runnable {
private int leftTicket = 500;
@Override
public void run() {
while (leftTicket > 0) {
synchronized (this) {
leftTicket = leftTicket - 1;
System.out.println(Thread.currentThread().getName() + "处理后还剩"
+ leftTicket + "张票");
}
}
}
}
这样,每次执行的结果都一样,且leftTicket 的值是依次减小的。但是发现会多卖一张票,如果再有C线程一起卖的话,会多卖两张。。
问题就出在判断leftTicket大于0之后,在获取监视器锁之前,这期间leftTicket可能会被其他线程改变,导致leftTicket不一定大于0了,所以,应该在获得监视器锁之后,再判断leftTicket是否大于0。
优化如下:
public class Account {
private static int total = 20;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new SellTicketThread("线程1"));
Thread t2 = new Thread(new SellTicketThread("线程2"));
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("over");
}
static class SellTicketThread implements Runnable {
private final String name;
public SellTicketThread(String name) {
= name;
}
@Override
public void run() {
while (true) {
synchronized (Account.class) {
if (total >= 1) {
System.out.println(name + "准备卖第" + total + "张票");
total = total - 1;
} else {
return;
}
}
}
}
}
}
银行存取款问题探究:
存取款问题,因为存款与取款是对同一账户的成员变量进行操作,但是执行方法体不一样,所以需要创建两种线程。首先创建一个账户类,此类有一个余额成员变量。实例化这两种线程时要传同一个账户类对象,这样这两种线程操作的就是同一个账户对象也就是同一个账户的余额成员变量了。
如果在这两种线程的run方法中用synchronized同步代码块的话,则需要有2种synchronized同步代码块。因为同步代码块中必然会操作到余额变量,但是没有在线程类中声明此余额变量,只能通过账户的get方法得到余额,进而进行操作:
/**
* 账户类,包含余额成员变量
*
*/
class Account {
private double balance;
private boolean flag = false;
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public Account(double balance) {
super();
this.balance = balance;
}
}
class DrawThread extends Thread {
private Account account;
private double drawAmount;
public DrawThread(String name, Account account, double drawAmount) {
super(name);
this.account = account;
this.drawAmount = drawAmount;
}
@Override
public void run() {
while (true) {
synchronized (account) {
try {
if (!account.isFlag()) {
account.wait();
} else {
account.setBalance(account.getBalance() - drawAmount);
System.out.println(Thread.currentThread().getName() + "取现" + drawAmount + ",账户余额为:"
+ account.getBalance());
account.setFlag(false);
account.notifyAll();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
// 存款
class DepositThread extends Thread {
private Account account;
private double depositAmount;
public DepositThread(String name, Account account, double depositAmount) {
super(name);
this.account = account;
this.depositAmount = depositAmount;
}
@Override
public void run() {
while (true) {
synchronized (account) {
try {
if (account.isFlag()) {
account.wait();
} else {
account.setBalance(account.getBalance() + depositAmount);
System.out.println(Thread.currentThread().getName() + "存款" + depositAmount + ",账户余额为:"
+ account.getBalance());
account.setFlag(true);
account.notifyAll();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
public class ThreadTest {
public static void main(String[] args) {
Account acct = new Account(0);
for (int i = 1; i <= 2; i++) {
new DrawThread("取款者" + i, acct, 500).start();
new DepositThread("存款者" + i, acct, 500).start();
}
}
}
如果使用synchronized同步方法的话,需要有两种synchronized同步方法。在run方法中调用synchronized同步方法,为了避免上面的共享变量的声明的麻烦,可以把synchronized同步方法声明在账户类中,这样在线程类中就不用声明该共享变量了:
/**
* 账户类,包含余额成员变量
*
*/
class Account {
private double balance;
private boolean flag = false;
public Account(double balance) {
super();
this.balance = balance;
}
// 取款
public synchronized void draw(double drawAmount) {
try {
if (!flag) {
this.wait();
} else {
balance -= drawAmount;
System.out.println(Thread.currentThread().getName() + "取现" + drawAmount + ",账户余额为:" + balance);
flag = false;
this.notifyAll();
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 存款
public synchronized void deposit(double depositAmount) {
try {
if (flag) {
this.wait();
} else {
balance += depositAmount;
System.out.println(Thread.currentThread().getName() + "存款" + depositAmount + ",账户余额为:" + balance);
flag = true;
this.notifyAll();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
class DrawThread extends Thread {
private Account account;
private double drawAmount;
public DrawThread(String name, Account account, double drawAmount) {
super(name);
this.account = account;
this.drawAmount = drawAmount;
}
@Override
public void run() {
while (true) {
account.draw(drawAmount);
}
}
}
// 存款
class DepositThread extends Thread {
private Account account;
private double depositAmount;
public DepositThread(String name, Account account, double depositAmount) {
super(name);
this.account = account;
this.depositAmount = depositAmount;
}
@Override
public void run() {
while (true) {
account.deposit(depositAmount);
}
}
}
public class ThreadTest {
public static void main(String[] args) {
Account acct = new Account(0);
for (int i = 1; i <= 2; i++) {
new DrawThread("取款者" + i, acct, 500).start();
new DepositThread("存款者" + i, acct, 500).start();
}
}
}
每次准备面试时,复习完基础理论知识后,拿这俩题练手,再合适不过了。