会当凌绝顶,一览众山小
| @Author:TTODS
使用多线程可以让我们的程序更加充分的利用CPU,提高程序的效率,但是同时也带来了一些问题。多线程在使用线程公共资源的时候往往会遇到问题。
想象一下这样一个情景:一个商店中有三件商品,但是四个顾客同时来购买商品,他们都能买到商品吗?
我们用代码模拟一下:
public class Test{
public static void main(String[] args) {
//创建一个商店
Store store = new Store();
//创建四个共用一个商店的顾客线程
CustomerThread ct1 = new CustomerThread("顾客1",store);
CustomerThread ct2 = new CustomerThread("顾客2",store);
CustomerThread ct3 = new CustomerThread("顾客3",store);
CustomerThread ct4 = new CustomerThread("顾客4",store);
//启动四个线程
ct1.start();
ct2.start();
ct3.start();
ct4.start();
}
}
//商店类
class Store {
//剩余商品数量
static int goodsNumble = 3;
public Store(){
}
//出售一件商品
void sell() {
//先判断商品是否售完
if(goodsNumble<=0)
System.out.println("商店:出售失败,商品卖完了");
else {
System.out.println("此时还有"+goodsNumble+"件商品");
goodsNumble--;
System.out.println("售出一件商品,商品数量减1");
}
}
}
class CustomerThread extends Thread{
private Store store;
public CustomerThread(String name,Store store) {
// TODO 自动生成的构造函数存根
setName(name);
this.store = store;
}
@Override
public void run() {
buy();
}
//购买商品,调用store的sell方法
public void buy() {
store.sell();
}
}
从上面的代码和运行结果我们可以看到,因为顾客几乎是同时到达商店的,所以他们到的时候商品的水量都是3,于是商店给每一位顾客都出售了一件商品。这显然不是我们想要的结果。然后我们给Store类的sell()方法加上synchronized关键字,局部修改如下:
synchronized void sell() {
//先判断商品是否售完
if(goodsNumble<=0)
System.out.println("商店:出售失败,商品卖完了");
else {
System.out.println("此时还有"+goodsNumble+"件商品");
goodsNumble--;
System.out.println("售出一件商品,商品数量减1");
}
}
可以看到在我们给sell
方法使用了synchronized
关键字后Stroe
会按照一定的顺序来为顾客服务,这就是synchronized
方法的效果.当当前线程使用synchronized
方法时,会给当前对象(store)上锁,上锁后其他线程在使用相同对象的方法时进入阻塞,需等待此线程中的该方法结束,才能继续运行。
为了介绍线程同步的三个方法,我们先来看一下这样一个场景:有一个房间,房间里居住着四个人,四个人共用着房间里的一部电话。每个人都可以在任何时候使用电话,但是如果他们在同一时间都来使用这部电话又会发生什么呢?
我们用代码模拟一下:
public class SynchronizedMethod {
public static void main(String[] args) {
Room r = new Room();
//创建并启动4个学生线程
StudentThread s1 =new StudentThread("s1",r);
StudentThread s2 =new StudentThread("s2",r);
StudentThread s3 =new StudentThread("s3",r);
StudentThread s4 =new StudentThread("s4",r);
s1.start();
s2.start();
s3.start();
s4.start();
}
}
class Room {
public Phone phone;
public Room() {
// TODO 自动生成的构造函数存根
phone = new Phone();
}
}
class Phone{
static final int FREE = 1,BUSY = 0;
//电话的状态
private int status=FREE;
public Phone() {
}
//判断电话是否空闲
public boolean isFree() {
return status==FREE;
}
//打电话,半秒后挂断,为了打印电话使用者的名字,我们传入一个参数
public void call(String user) {
if(!isFree()) {
System.out.println("有人正在使用电话哦");
return;
}
status = BUSY;
System.out.println(user+"打出了电话...");
try {
Thread.sleep(500);
hangUp(user);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
//挂电话
public void hangUp(String user) {
status = Phone.FREE;
System.out.println(user+"挂断了电话...");
}
}
class StudentThread extends Thread{
private Room room;
public StudentThread(String name,Room room) {
setName(name);
this.room = room;
}
@Override
public void run() {
this.room.phone.call(getName());
}
}
我们可以发现当线程s1使用了对象phone
的call
方法的时候,其他三个线程仍然使用了call方法。这就是线程不同步的效果。接下来我们分别用三种方法实现线程同步。
synchronized
方法Phone
类的call
声明为sychronized
方法(在call
方法前面加上关键字即可),修改部分代码如下: public synchronized void call(String user) {
if(!isFree()) {
System.out.println("有人正在使用电话哦");
return;
}
status = BUSY;
System.out.println(user+"打出了电话...");
try {
Thread.sleep(500);
hangUp(user);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
synchronized
块call
函数内添加synchronized
代码块,此方法与上一方法相比,优点在于你不需将整个方法都变成同步的,只需把关键代码块变为同步即可,修改部分代码如下:public void call(String user) {
synchronized (this) {
if(!isFree()) {
System.out.println("有人正在使用电话哦");
return;
}
status = BUSY;
System.out.println(user+"打出了电话...");
try {
Thread.sleep(500);
hangUp(user);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Phone{
private Lock lock= new ReentrantLock();
static final int FREE = 1,BUSY = 0;
//电话的状态
private int status=FREE;
public Phone() {
}
//判断电话是否空闲
public boolean isFree() {
return status==FREE;
}
//打电话,半秒后挂断,为了打印电话使用者的名字,我们传入一个参数
public void call(String user) {
//上锁
lock.lock();
if(!isFree()) {
System.out.println("有人正在使用电话哦");
return;
}
status = BUSY;
System.out.println(user+"打出了电话...");
try {
Thread.sleep(500);
hangUp(user);
} catch (InterruptedException e) {
e.printStackTrace();
}
//释放锁
lock.unlock();
}
//挂电话
public void hangUp(String user) {
status = Phone.FREE;
System.out.println(user+"挂断了电话...");
}
}
线程死锁,是指多个线程在使用公共资源时各占了资源的一部分,导致每个线程都等待其他线程释放资源才能完成工作,最终导致每个线程都无法完成工作,互相卡死。
想象一下这样的情景:上学时,你和你的同桌共用一个笔记本和一支钢笔,只是你的老师讲到了一个要点,于是你顺势抢到了钢笔,而你的同桌拿到了笔记本,谁也不肯让步,于是谁也无法完成笔记。
我们用代码模拟一下:
public class DeadLock {
public static void main(String[] args) {
Stationery stationery = new Stationery();
//p1 takeNotes时先获取并锁定本子,然后再试图获取笔
Person p1 = new Person("p1",stationery) {
@Override
public void takeNotes() {
synchronized (stationery.notebook) {
getNotebook();
synchronized (stationery.pen) {
getPen();
}
}
}
};
//p2 takeNotes时先获取并锁定笔,然后再试图获取本子
Person p2 = new Person("p2",stationery) {
@Override
public void takeNotes() {
synchronized (stationery.pen) {
getPen();
synchronized (stationery.notebook) {
getNotebook();
}
}
}
};
p1.start();
p2.start();
}
}
//文具类(共用资源)
class Stationery{
//笔
public static Pen pen = new Pen();
//笔记本
public static Notebook notebook= new Notebook();
}
class Pen{
}
class Notebook{
}
abstract class Person extends Thread{
Stationery stationery;
public Person(String name,Stationery s) {
// TODO 自动生成的构造函数存根
setName(name);
stationery = s;
}
//获取笔
Pen getPen() {
System.out.println(getName()+":我拿打笔了");
try {
Thread.sleep(1);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
return stationery.pen;
};
//获取本子
Notebook getNotebook() {
System.out.println(getName()+":我拿打笔记本了");
try {
Thread.sleep(1);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
return stationery.notebook;
};
//抽象方法
public abstract void takeNotes();
@Override
public void run() {
takeNotes();
};
}
Thread t = new Thread()
,线程对象被创建,此时还没有使用start()方法,启动线程t.start()
后进入就绪状态,但具体什么时候运行取决于CPU的调度Thread.sleep()
或者进行I/O操作等不在占用CPU,则由运行态进入阻塞态,待事件结束再进入就绪态t.stop()
线程结束,进入终止态(消亡状态)