多线程通信
多线程通信即多线程交互问题,举个例子:线程一网上下载音乐,线程二播放下载的音乐。
交互问题都可以归类为生产者和消费者问题:
生产者生产时消费者休眠,生产好后生产者唤醒消费者然后生产者休眠,消费者消费完之后再唤醒生产者然后消费者休眠,生产者还是工作。
在这里生产者和消费者各代表一个线程。
在Object类中有几个方法是用于多线程通信问题:
变量和类型 | 方法 | 描述 |
---|---|---|
void | notify() | 唤醒正在此对象监视器上等待的单个线程。 |
void | notifyAll() | 唤醒等待此对象监视器的所有线程。 |
void | wait() | 导致当前线程等待它被唤醒,通常是通知或中断 。 |
void | wait(long timeoutMillis) | 导致当前线程等待它被唤醒,通常是通知或中断 ,或者直到经过一定量的实时。 |
void | wait(long timeoutMillis, int nanos) | 导致当前线程等待它被唤醒,通常是通知或中断,或者直到经过一定量的实时。 |
举例子
下面我们举一个关于生产者与消费者的例子:
厨师为生产者,服务员为消费者;
public class demo {
public static void main(String[] args) {
Food f = new Food();
new Cook(f).start();
new Waiter(f).start();
}
//厨师
static class Cook extends Thread{
private Food f;
public Cook(Food f){
this.f = f;
}
@Override
public void run() {
for(int i=0;i<100;i++){
if(i%2==0){
f.setNameAndsetTaste("饭","无味");
}else{
f.setNameAndsetTaste("菜","有味");
}
}
}
}
//服务生
static class Waiter extends Thread{
private Food f;
public Waiter(Food f){
this.f = f;
}
@Override
public void run() {
for(int i=0;i<100;i++){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
f.get();
}
}
}
//食物
static class Food extends Thread{
private String name;
private String taste;
public void setNameAndsetTaste(String name,String taste) {
this.name = name;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
}
public void get(){
System.out.println("服务员端走的菜是:"+name+",味道是:"+taste);
}
}
}
这里输出的结果是会出现问题,如图输出结果的一部分:

可以看到饭变成了有味,菜变成了无味。
原因是:setNameAndsetTaste中name和taste的设置中间隔了100毫秒,也就是在name设置好之后,服务员就过来把name和上次设置的,这次还未来得及从新设置的taste通过调用get()取走。
如何做:
- 使用线程安全解决?
此方法是行不通的,Cook和Waiter虽然操作的同一个堆内存,通过锁一个线程进去另外一个进不来,但是线程安全是非公平锁,有可能Cook出来后又立马抢到了锁又进去了一次,这回导致前面一次Cook产生的数据Waiter无法及时拿到。
-
只能使用消费者和生产者分别休眠进行解决。实现过程如下所示:
中间使用notifyAll和wait方法实现对线程的启动和休眠
public class demo {
public static void main(String[] args) {
Food f = new Food();
new Cook(f).start();
new Waiter(f).start();
}
//厨师
static class Cook extends Thread{
private Food f;
public Cook(Food f){
this.f = f;
}
@Override
public void run() {
for(int i=0;i<100;i++){
if(i%2==0){
f.setNameAndsetTaste("饭","无味");
}else{
f.setNameAndsetTaste("菜","有味");
}
}
}
}
//服务生
static class Waiter extends Thread{
private Food f;
public Waiter(Food f){
this.f = f;
}
@Override
public void run() {
for(int i=0;i<100;i++){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
f.get();
}
}
}
//食物
static class Food extends Thread{
private String name;
private String taste;
//true执行setNameAndsetTaste();false执行get()
boolean flag = true;
public void setNameAndsetTaste(String name,String taste) {
if(flag){
this.name = name;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
flag = false;
this.notifyAll(); //唤醒当前this下的所有线程
try {
this.wait(); //使当前使用this的线程休眠
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void get(){
if(!flag){
System.out.println("服务员端走的菜是:"+name+",味道是:"+taste);
flag = true;
this.notifyAll(); //唤醒使用这个对象的所有线程
try {
this.wait(); //使当前使用this的线程休眠
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
线程的六种状态
我们需要关注线程的六种状态来更好理解线程执行的流程
-
线程状态。线程可以处于以下状态之一:
-
NEW
尚未启动的线程处于此状态。
-
RUNNABLE
在Java虚拟机中执行的线程处于此状态。 -
BLOCKED
被阻塞等待监视器锁定的线程处于此状态。 -
WAITING
无限期等待另一个线程执行特定操作的线程处于此状态。 指被休眠未指定休眠时间,直到等到一个线程去唤醒它。 -
TIMED_WAITING
正在等待另一个线程执行最多指定等待时间的操作的线程处于此状态。 -
TERMINATED
已退出的线程处于此状态
-
带返回值的线程Callable
前面学到了线程的两种创建方式:继承Thread和实现Runnable。
现在介绍一个特色创建线程的方式:Callable。
Callable像是主线程指派了一个任务给这个线程,当任务完成后会返回一个结果给主线程。
Callable创建的线程可以跟主线程一起执行,也可以让主线程等它执行完返回结果后主线程再执行。
Callable接口的所有方法
变量和类型 | 方法 | 描述 |
---|---|---|
protected void | done() | 当此任务转换到状态 isDone (无论是正常还是通过取消),调用受保护的方法。 |
V | get() | 如果需要等待计算完成,然后检索其结果。 |
V | get(long timeout, TimeUnit unit) | 如果需要,最多等待计算完成的给定时间,然后检索其结果(如果可用)。 |
protected boolean | runAndReset() | 执行计算而不设置其结果,然后将此未来重置为初始状态,如果计算遇到异常或被取消则无法执行此操作。 |
protected void | set(V v) | 将此future的结果设置为给定值,除非已设置或已取消此未来。 |
protected void | setException(Throwable t) | 导致此未来报告带有给定throwable的ExecutionException 作为其原因,除非此未来已设置或已取消。 |
String | toString() | 返回此FutureTask的字符串表示形式。 |
几个方法说明:
get( ):
-
获取线程执行的结果。
-
如果线程A没有调用方法获取线程B的方法,那么就不会暂停等线程B的运行结果。
-
括号里能传入时间,如果超过这个时间还没结果则放弃索要结果。
isDone( ):
- 用来判断线程任务是否执行完毕
cancel( ):
- 停止线程运行。
- 需传入boolean参数:true表示我要取消。
- 返回boolean类型参数,返回true表示取消成功。返回false则表面程序已运行完毕无法进行取消。
Callable使用方法
Callable是一个接口,使用时需要实现
使用步骤
-
编写类实现Callable接口,并实现call方法:
class XXX implement Callable{
@Override
public call( ) throws Exception {
return T ;
}
}
-
创建FutureTask对象,并传入第一步编写的Callable类对象
FutureTask future = new FutureTask<>(callable);
-
通过Thread,启动线程:
new Thread(future).start( ) ;
不调用get方法
public static void main(String[] args) {
Callable<Integer> c = new MyCallable();
FutureTask<Integer> task = new FutureTask<>(c);
new Thread(task).start();
for(int i=0; i<10; i++){
System.out.println("B"+i);
}
}
static class MyCallable implements Callable<Integer>{
@Override
public Integer call() throws Exception {
for(int i=0; i<10; i++){
System.out.println("A"+i);
}
return 100;
}
}
打印结果如下,两个线程交替执行:
A0 B0 A1 B1 A2 B2 A3 B3 A4 B4 A5 B5 A6 B6 A7 B7 A8 B8 A9 B9
调用get方法
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> c = new MyCallable();
FutureTask<Integer> task = new FutureTask<>(c);
new Thread(task).start();
task.get(); //调用get
for(int i=0; i<10; i++){
System.out.println("B"+i);
}
}
static class MyCallable implements Callable<Integer>{
@Override
public Integer call() throws Exception {
for(int i=0; i<10; i++){
System.out.println("A"+i);
}
return 100;
}
}
打印结果如下,可以看出有差别:
A0 A1 A2 A3 A4 A5 A6 A7 A8 A9 B0 B1 B2 B3 B4 B5 B6 B7 B8 B9