一、线程间通讯问题
当多个线程同时操作一个对象时,就有可能发生错误,下面我们就通过三个经典案例来具体说明多线程可能遇到的问题。
1.三个经典案例
1.1 案例一: 不安全的买票
// 不安全的买票
public class UnsafeBuyTicket {
public static void main(String[] args) {
BuyTicket buyTicket = new BuyTicket();
new Thread(buyTicket, "我").start();
new Thread(buyTicket, "小明").start();
new Thread(buyTicket, "黄牛党").start();
}
}
class BuyTicket implements Runnable {
private Integer ticketNum = 10;
private boolean flag = true;
@Override
public void run() {
while (flag) {
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 买票方法
public void buy() throws InterruptedException {
if(ticketNum <= 0) {
flag = false;
System.out.println(Thread.currentThread().getName() + "来买票,但是票卖完了!");
return;
}
// 模拟延迟
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + "买到了票,剩余" + --ticketNum + "张票");
}
}
上述代码,我们创建了实现了一个Runnable接口类,然后使用该类的实例化对象启动了三个线程,程序运行结果如下:
从上图中,我们不难发现出现了重复买票以及剩余票数为负的情形!
1.2 案例二:不安全的银行
public class UnsafeBank {
public static void main(String[] args) {
Account account = new Account(100, "账户基金");
Thread you = new Drawing(account, 50, "you");
Thread gf = new Drawing(account, 100, "gf");
you.start();
gf.start();
}
}
class Account {
private Integer money; // 余额
private String cardNo; // 卡号
public Account(Integer money, String cardNo) {
this.money = money;
this.cardNo = cardNo;
}
public Integer getMoney() {
return money;
}
public void setMoney(Integer money) {
this.money = money;
}
public String getCardNo() {
return cardNo;
}
public void setCardNo(String cardNo) {
this.cardNo = cardNo;
}
}
class Drawing extends Thread {
private Account account;
private Integer drawingMoney;
private Integer nowMoney = 0;
public Drawing(Account account, Integer drawingMoney, String name) {
super(name);
this.account = account;
this.drawingMoney = drawingMoney;
}
public void drawMoney() throws InterruptedException {
if(account.getMoney() < drawingMoney) {
System.out.println(this.getName() + "取钱," + account.getCardNo() + "余额不足!");
return;
}
// sleep可以放大问题的发生性
Thread.sleep(1000);
nowMoney = nowMoney + drawingMoney;
account.setMoney(account.getMoney() - drawingMoney);
// this.getName()等价于Thread.currentThread().getName()
System.out.println(this.getName() + "取了" + drawingMoney + "万元, 手头剩余" + nowMoney + "万元,账户剩余" + account.getMoney() + "万元!");
}
@Override
public void run() {
try {
drawMoney();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上述代码,我们启动两个线程,操作同一个账户对象,运行结果如下:
问题:账户中一共只有100万元,结果两人一共取出150万元,账户中仍有50万元!
1.3 案例三: 不安全的数组
public class UnsafeList {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(()-> {
list.add(Thread.currentThread().getName());
}).start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
}
}
问题:列表长度应该是10000,问什么会少了呢?
2.案例结果分析及解决办法
上述三个案例,都是多个线程操作同一对象。一个线程为执行完自己的逻,另一个线程就又拿到该对象去执行自己的逻辑,这样结果肯定会出现问题!那么我们该如何解决这个问题呢?这里就需要引入线程同步的知识了。
3.线程同步
概念:线程同步是一种等待机制,多个需要同时访问此对象的线程进入这个对象等待池并形成队列,等待前面一个线程使用完毕,下一个再使用!(简而言之,如果多个线程要操作同一对象,请一个个排好队)
实现方式:使用synchronized
关键字
带来的问题:
- 一个线程持有锁会导致其他需要此锁的线程挂起
- 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题
- 如果一个优先级高的进程等待一个优先级低的进程线程释放锁,会导致优先级倒置,引起性能问题
同步方法与同步块:
- 同步方法:synchronized关键字添加在方法前面,这个默认锁住的是包含这个方法的对象
- 同步块:synchronized (obj) {} 这里表示锁住obj这个对象
下面我们来修改上案例:
案例1:不安全的买票
public class UnsafeBuyTicket {
public static void main(String[] args) {
BuyTicket buyTicket = new BuyTicket();
new Thread(buyTicket, "我").start();
new Thread(buyTicket, "小明").start();
new Thread(buyTicket, "黄牛党").start();
}
}
class BuyTicket implements Runnable {
private Integer ticketNum = 10;
private boolean flag = true;
@Override
public void run() {
while (flag) {
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 买票方法
public synchronized void buy() throws InterruptedException {
if(ticketNum <= 0) {
flag = false;
System.out.println(Thread.currentThread().getName() + "来买票,但是票卖完了!");
return;
}
// 模拟延迟
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + "买到了票,剩余" + --ticketNum + "张票");
}
}
我们在买票方法上添加了synchronized关键字,相当于是直接锁住了buyTicket这个对象,运行结果如下:
结果正常!
案例二:不安全的银行
public class UnsafeBank {
public static void main(String[] args) {
Account account = new Account(100, "账户基金");
Thread you = new Drawing(account, 50, "you");
Thread gf = new Drawing(account, 100, "gf");
you.start();
gf.start();
}
}
class Account {
private Integer money; // 余额
private String cardNo; // 卡号
public Account(Integer money, String cardNo) {
this.money = money;
this.cardNo = cardNo;
}
public Integer getMoney() {
return money;
}
public void setMoney(Integer money) {
this.money = money;
}
public String getCardNo() {
return cardNo;
}
public void setCardNo(String cardNo) {
this.cardNo = cardNo;
}
}
class Drawing extends Thread {
private Account account;
private Integer drawingMoney;
private Integer nowMoney = 0;
public Drawing(Account account, Integer drawingMoney, String name) {
super(name);
this.account = account;
this.drawingMoney = drawingMoney;
}
public void drawMoney() throws InterruptedException {
synchronized (account) {
if(account.getMoney() < drawingMoney) {
System.out.println(this.getName() + "取钱," + account.getCardNo() + "余额不足!");
return;
}
// sleep可以放大问题的发生性
Thread.sleep(1000);
nowMoney = nowMoney + drawingMoney;
account.setMoney(account.getMoney() - drawingMoney);
// this.getName()等价于Thread.currentThread().getName()
System.out.println(this.getName() + "取了" + drawingMoney + "万元, 手头剩余" + nowMoney + "万元,账户剩余" + account.getMoney() + "万元!");
}
}
@Override
public void run() {
try {
drawMoney();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
我们在drawMoneny方法中添加了同步块,锁住了account对象,运行结果如下:
运行结果正常!
注意:这里不能直接在drawMoney()方法前直接添加
synchronized
关键字,在这里添加表示锁住的对象实Drawing,而我们需要锁住的对像不是Drawing而是account.
案例三:不安全的数组
public class UnsafeList {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(()-> {
synchronized (list) {
list.add(Thread.currentThread().getName());
}
}).start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
}
}
我们在run()
方法中添加了synchronized
关键字,运行结果如下:
结果正常!
4.死锁
概念:多个线程各自占有一些共享资源,并且相互等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源的情形!某一个同步块拥有两个以上的对象的锁时,就会发生这种情况!(多个线程互相抱着对方需要的资源,然后形成僵持)
public class TestDeadLock {
public static void main(String[] args) {
Thread g1 = new MakeUp(0, "H");
Thread g2 = new MakeUp(1, "B");
g1.start();
g2.start();
}
}
class Mirror {}
class Lipstick {}
class MakeUp extends Thread {
static Mirror mirror = new Mirror();
static Lipstick lipstick = new Lipstick();
Integer choice;
public MakeUp(Integer choice, String name) {
super(name);
this.choice = choice;
}
@Override
public void run() {
try {
makeUp();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void makeUp() throws InterruptedException{
if(this.choice.equals(0)) {
synchronized (mirror) {
System.out.println(this.getName() + "得到了镜子!");
Thread.sleep(1000);
synchronized (lipstick) {
System.out.println("1s后" + this.getName() + "得到了口红!");
}
}
} else {
synchronized (lipstick) {
System.out.println(this.getName() + "得到了口红!");
Thread.sleep(2000);
synchronized (mirror) {
System.out.println("2s后" + this.getName() + "得到了镜子!");
}
}
}
}
}
上述代码,H与B两个线程都各自占用了mirror与lipStick两个对象,互相等待对方释放自己的需要的对象,这就形成了死锁!
5.线程协作(生产者消费者模式)
5.1 管程法
- 生产者生产产品,将产品放入缓存区
- 消费者从缓存区拿到产品,消费
- 需要生产者、消费者、缓存区以及产品四个对象
// 测试:生产者消费者模式 --> 利用缓冲区解决:管程法
// 生产者 消费者 消费对象 缓冲区
public class TestPC {
public static void main(String[] args) {
SyncContainer syncContainer = new SyncContainer();
Productor productor = new Productor(syncContainer);
Consumer consumer = new Consumer(syncContainer);
new Thread(productor).start();
new Thread(consumer).start();
}
}
// 生产者
class Productor implements Runnable {
private SyncContainer syncContainer;
Productor(SyncContainer syncContainer) {
this.syncContainer = syncContainer;
}
@Override
public void run() {
for (int i = 0; i < 50; i++) {
Chicken chicken = new Chicken(i);
try {
syncContainer.push(chicken);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 消费者
class Consumer implements Runnable {
private SyncContainer syncContainer;
Consumer(SyncContainer syncContainer) {
this.syncContainer = syncContainer;
}
@Override
public void run() {
for (int i = 0; i < 50; i++) {
try {
syncContainer.pop();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 产品
class Chicken {
public int i;
Chicken(int i) {
this.i = i;
}
}
// 缓冲区
class SyncContainer {
// 产品大小
Chicken [] chickens = new Chicken[10];
// 计数标志
int count = 0;
// 生产者生产方法
public synchronized void push(Chicken chicken) throws InterruptedException {
if(count == chickens.length) {
// 停止生产
this.wait();
}
chickens[count] = chicken;
count ++;
System.out.println("生产者生产了第" + chicken.i + "只鸡");
// 唤醒消费者消费
this.notifyAll();
}
// 消费者消费方法
public synchronized Chicken pop() throws InterruptedException {
if(count == 0) {
this.wait();
}
// 因为生产着生产之后都会让count ++,所以消费者在使用时必须先执行count --
count --;
Chicken chicken = chickens[count];
System.out.println("消费者消费了第" + chicken.i + "只鸡!");
// 通知生产者生产
this.notifyAll();
return chicken;
}
}
5.2信号灯法
package com.xdw.gaoji;
// 测试:生产着消费者模式 信号灯法
public class TestPC2 {
public static void main(String[] args) {
TV tv = new TV();
Player player = new Player(tv);
Watcher watcher = new Watcher(tv);
new Thread(player).start();
new Thread(watcher).start();
}
}
class Player implements Runnable {
TV tv;
public Player(TV tv) {
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if(i%2 == 0) {
tv.player("新闻联播!");
} else {
tv.player("电视剧!");
}
}
}
}
class Watcher implements Runnable {
TV tv;
public Watcher(TV tv) {
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
tv.watch();
}
}
}
class TV {
String voice;
boolean flag = true; // flag为true 生产着生产 flag为false 消费者消费
public synchronized void player(String voice) {
if(!flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 通知消费者消费
this.notifyAll();
this.voice = voice;
System.out.println("生产者生产了" + this.voice + "!");
flag = !flag;
}
public synchronized void watch() {
if(flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 通知生产着生产
this.notifyAll();
System.out.println("消费者消费了" + this.voice + "!");
flag = !flag;
}
}
上述两种方法,使用了线程的两个未介绍的方法: wait()
与noyify()/notifyAll()
。
wait()
: 使线程进入等待状态,会释放锁!
notify()
: 唤醒等待队列中的第一个类型
notifyAll()
: 唤醒所有等待中的线程
二、线程池
思路: 提前创建好多个线程,放入线程池中,使用时直接取出,使用完毕再放回,避免了线程的频繁创建与销毁。
实现类:使用ExecutorService
与Executors
两个类!
public class TestPool {
public static void main(String[] args) {
// 创建一个线程池服务
// newFixedThreadPool参数名称为线程池大小
ExecutorService service = Executors.newFixedThreadPool(4);
// 运行线程
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
// 关闭服务
service.shutdown();
}
}
class MyThread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}