Java 线程通信机制
锁的一致性:使用wait()和notify()时,线程必须持有同一个对象的锁,否则无法实现通信,甚至可能导致死锁。异常处理:在调用wait()、sleep()等方法时,要妥善处理InterruptedException异常。锁的释放:在同步块或Lock的使用中,要确保锁能正确释放,避免死锁。例如,使用Lock时,应在finally块中释放锁。通知的及时性:notify()或signal()应在合适
在多线程编程中,线程之间并非孤立运行,常常需要相互协作、传递信息。Java 提供了多种线程通信机制,使得主线程与子线程、子线程与子线程之间能够高效地进行数据交互与同步。本文将深入探讨 Java 中的线程通信机制,结合丰富的代码示例,解析主线程与子线程、子线程与子线程之间的通信实现。
1. 线程通信的基本概念
线程通信是指多个线程之间为了协调工作,相互传递信息、同步执行顺序的过程。在 Java 中,主要通过wait()、notify()、notifyAll()(基于Object类)以及Lock结合Condition接口的方式来实现线程通信。这些机制使得线程能够在特定条件下等待或唤醒,从而实现灵活的协作。
2. 主线程与子线程的通信
2.1 主线程向子线程传递数据
主线程可以在创建子线程时,通过构造方法等方式将数据传递给子线程。
代码示例
package com.lj.demo23;
public class Test {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + ",主线程");
int a = 10;
// 创建子线程并通过构造方法传递数据
UserThread u = new UserThread(a);
u.start();
}
}
package com.lj.demo23;
public class UserThread extends Thread {
int a;
public UserThread(int a) {
this.a = a;
}
public void run() {
// 子线程接收并使用主线程传递的数据
System.out.println(Thread.currentThread().getName() + ",执行run方法,接收到的值:" + a);
}
}
主线程在创建UserThread实例时,通过构造方法将整数a的值传递给子线程。
子线程在run方法中接收并打印了主线程传递的值,实现了主线程到子线程的数据传递。
输出结果
main,主线程
Thread-0,执行run方法,接收到的值:10
3. 子线程向主线程传递数据
子线程可以通过提供公共方法,让主线程在合适的时机获取子线程处理后的数据。
3.1创建callable接口
子线程通过创建callable接口,返回值给主线程。后面具体再细致的讲。
代码示例
package com.lj.demo24;
public class Test {
public static void main(String[] args) {
UserThread ut = new UserThread();
ut.start();
try {
// 主线程休眠,给子线程时间计算
Thread.sleep(5 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 主线程获取子线程计算的结果
System.out.println("主线程获取子线程计算的和:" + ut.getSum());
}
}
package com.lj.demo24;
public class UserThread extends Thread {
private int sum = 0;
public void run() {
// 子线程计算 0 到 100 的和
for (int i = 0; i <= 100; i++) {
this.sum += i;
}
}
public int getSum() {
// 提供方法让主线程获取计算结果
return this.sum;
}
}
子线程UserThread在run方法中计算0到100的和,并将结果存储在sum变量中。
主线程通过调用子线程的getSum方法,获取子线程计算的结果,实现了子线程到主线程的数据传递。这种方式简单直接,但需要主线程明确知道子线程何时完成计算,否则可能获取到不完整或错误的结果。
输出结果
主线程获取子线程计算的和:5050
3.2 基于wait()和notify()的线程通信
wait()方法使当前线程等待,直到其他线程调用该对象的notify()或notifyAll()方法,wait()对象的范围不能大于notify()对象的范围,否则会发生死锁现象;
notify()方法唤醒在此对象监视器上等待的单个线程;
notifyAll()方法唤醒在此对象监视器上等待的所有线程。
使用notify()和notifyAll()这两个方法时,线程必须持有对象的锁。
3.2.1 object对象当锁
package com.lj.demo26;
public class Test {
public static void main(String[] args) {
Object obj = new Object();
UserThread ut = new UserThread(obj);
ut.start();
// 同步块,主线程获取 ut 关联的 obj 锁
synchronized (ut) {
System.out.println(Thread.currentThread().getName() + "before...");
try {
// 主线程持有 ut 的锁,在此等待
ut.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("主线程获取的值为:" + ut.getSum());
}
}
package com.lj.demo26;
public class UserThread extends Thread {
private int sum = 0;
Object obj;
public UserThread(Object obj) {
this.obj = obj;
}
public void run() {
System.out.println("子线程去计算");
// 同步块,子线程获取 obj 锁
synchronized (obj) {
for (int i = 0; i <= 100; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.sum += i;
System.out.println(this.sum);
}
// 子线程计算完成,通知等待的主线程
obj.notify();
}
}
public int getSum() {
return this.sum;
}
}
主线程和子线程通过同一个Object对象obj进行同步。主线程在获取obj锁后,调用ut.wait()进入等待状态,释放obj锁。
子线程获取obj锁后,进行计算,计算完成后调用obj.notify()唤醒主线程。主线程被唤醒后,继续执行,获取子线程计算的结果。
输出结果
mainbefore...
子线程去计算
0
1
3
6
10
15
21
28
36
45
55
66
......
3570
3655
3741
3828
3916
4005
4095
4186
4278
4371
4465
4560
4656
4753
4851
4950
5050
主线程获取的值为:5050
下面示例与上面的原理一致,只是锁对象不同,都体现了通过共享锁对象,利用wait()和 notify() 实现线程通信。
3.2.2 实例当锁
package com.lj.demo27;
public class Test {
public static void main(String[] args) {
Object obj = new Object();
UserThread ut = new UserThread();
ut.start();
// 同步块 obj子线程对象
// 主线程objut的锁
synchronized (obj) {
System.out.println(Thread.currentThread().getName()+"before...");
try {
// 主线程持有obj的锁,在此被阻塞
obj.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("主线程获取的值为:"+ut.getSum());
}
}
package com.lj.demo27;
public class UserThread extends Thread{
private int sum = 0;
public void run()
{
System.out.println("子线程去计算");
synchronized (this) {
for(int i=0;i<=100;i++)
{
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
this.sum +=i;
System.out.println(this.sum);
}
//算好时,通知主线程获取结果
//和子线程持有同一个对象的锁被wait掉的线程继续执行
this.notify();
}
}
public int getSum()
{
return this.sum;
}
}
子线程使用this(自身实例)作为锁,主线程也以子线程实例为锁进行同步,子线程计算完成后调用this,notify()通知主线程。
3.2.3 自定义对象当锁
package com.lj.demo28;
public class Test {
public static void main(String[] args) {
User u =new User();
Object obj = new Object();
UserThread ut = new UserThread(u);
ut.start();
// 同步块 ut子线程对象
// 主线程持有ut的锁
synchronized (u) {
System.out.println(Thread.currentThread().getName()+"before...");
try {
// 主线程持有ut的锁,在此被阻塞
u.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("主线程获取的值为:"+ut.getSum());
}
}
package com.lj.demo28;
public class User {
}
package com.lj.demo28;
public class UserThread extends Thread{
private int sum = 0;
User u ;
public UserThread( User u )
{
this.u = u;
}
public void run()
{
System.out.println("子线程去计算");
synchronized (u) {
for(int i=0;i<=100;i++)
{
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
this.sum +=i;
System.out.println(this.sum);
}
//算好时,通知主线程获取结果
//和子线程持有同一个对象的锁被wait掉的线程继续执行
// this.notify();
//如果是当前类UserThread的锁,可以默认不写this.notify();
u.notify();
}
}
public int getSum()
{
return this.sum;
}
}
使用自定义User类对象作为锁,子线程和主线程通过该对象同步,子线程计算完成后调用u.notify()通知主线程。
3.2.4 接口对象当锁
package com.lj.demo30;
public class Test {
public static void main(String[] args) {
UserThread ut = new UserThread();
ut.start();
// 同步块 ut子线程对象
// 主线程持有ut的锁
synchronized (ut) {
System.out.println(Thread.currentThread().getName() + "before...");
try {
// 主线程持有ut的锁,在此被阻塞
ut.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("主线程获取的值为:" + ut.getSum());
}
}
package com.lj.demo30;
public interface IUser {
}
package com.lj.demo30;
public class UserThread extends Thread implements IUser{
private int sum = 0;
public void run()
{
System.out.println("子线程去计算");
synchronized (IUser.class) {
for(int i=0;i<=100;i++)
{
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
this.sum +=i;
System.out.println(this.sum);
}
IUser.class.notify();
}
}
public int getSum()
{
return this.sum;
}
}
使用接口IUser的Class对象作为锁,子线程和主线程通过该Class对象同步,子线程计算完成后调用IUser.Class.notify()通知主线程。
3.2.5线程通信的死锁问题
package com.lj.demo29;
/**
* 错误的通信模式导致死锁
* 锁对象不匹配造成线程永久等待
*/
public class Test {
public static void main(String[] args) {
User lockUser = new User();
Object lockObj = new Object();
UserThread ut = new UserThread(lockObj);
ut.start();
/**
* 错误:主线程在lockUser上等待
* 但子线程在lockObj上通知
* 导致主线程永远无法被唤醒 → 死锁
*/
synchronized (lockUser) { // 使用User对象作为锁
try {
lockUser.wait(); // 在User对象上等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("这里永远不会执行到");
}
}
class UserThread extends Thread {
private int sum = 0;
private Object lockObj;
public UserThread(Object lockObj) {
this.lockObj = lockObj;
}
public void run() {
synchronized (lockObj) { // 使用Object对象作为锁
for(int i = 0; i <= 100; i++) {
this.sum += i;
}
lockObj.notify(); // 在Object对象上通知(错误!)
}
}
public int getSum() {
return this.sum;
}
}
3.2.5.1 避免死锁
-
锁对象必须一致:wait()和notify()必须在同一个对象上调用
-
对象层级关系:通知对象的范围不能小于等待对象的范围
-
明确的锁协议:定义清晰的锁获取和释放协议
3.3 基于Lock和Condition的线程通信
Lock接口提供了比synchronized更灵活的锁操作,Condition接口则与Lock配合,实现更精细的线程等待与通知。
代码示例
package com.lj.demo31;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Test {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
// 创建与 lock 关联的 Condition 对象
Condition cond = lock.newCondition();
UserThread u1 = new UserThread(lock, cond);
u1.start();
lock.lock();
try {
// 主线程等待,直到被唤醒
cond.await();
System.out.println("主线程获取子线程计算的和:" + u1.getSum());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
package com.lj.demo31;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
public class UserThread extends Thread {
private Lock lock;
private int sum;
private Condition cond;
public UserThread(Lock lock, Condition cond) {
this.lock = lock;
this.cond = cond;
}
public void run() {
lock.lock();
try {
for (int i = 0; i <= 100; i++) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.sum += i;
}
// 子线程计算完成,唤醒等待的主线程
cond.signal();
} finally {
lock.unlock();
}
}
public int getSum() {
return this.sum;
}
}
输出结果
主线程获取子线程计算的和:5050
主线程和子线程通过ReentrantLock进行同步,Condition对象cond与该锁关联。
主线程获取锁后,调用cond.await()进入等待状态,释放锁。子线程获取锁后进行计算,计算完成后调用cond.signal()唤醒主线程。主线程被唤醒后,获取子线程计算的结果。这种方式相比synchronized与wait()/notify(),提供了更灵活的线程控制,例如可以创建多个Condition对象,实现对不同线程的精准通知。
3.4 对比
特性 | synchronized/wait-notify | Lock/Condition |
---|---|---|
灵活性 | 基础,功能有限 | 高度灵活,功能丰富 |
超时控制 | 不支持 | 支持await(timeout) |
多个条件 | 不支持 | 支持多个Condition |
中断响应 | 有限支持 | 完全支持 |
性能 | JVM优化较好 | 在高竞争下更好 |
4. 子线程与子线程之间的通信
子线程之间的通信原理与主线程和子线程的通信类似,也是通过共享锁对象,利用wait()、notify() 或Lock、Condition来实现。例如,两个子线程可以共享一个Object对象作为锁,一个子线程在满足条件时等待,另一个子线程在完成特定操作后通知等待的子线程继续执行,这里不做示例。
5.总结
- 锁的一致性:使用wait()和notify()时,线程必须持有同一个对象的锁,否则无法实现通信,甚至可能导致死锁。
- 异常处理:在调用wait()、sleep()等方法时,要妥善处理InterruptedException异常。
- 锁的释放:在同步块或Lock的使用中,要确保锁能正确释放,避免死锁。例如,使用Lock时,应在finally块中释放锁。
- 通知的及时性:notify()或signal()应在合适的时机调用,确保等待的线程能及时被唤醒,避免线程长时间等待。
更多推荐
所有评论(0)