在多线程编程中,线程之间并非孤立运行,常常需要相互协作、传递信息。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 避免死锁
  1. 锁对象必须一致wait()和notify()必须在同一个对象上调用

  2. 对象层级关系:通知对象的范围不能小于等待对象的范围

  3. 明确的锁协议:定义清晰的锁获取和释放协议

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.总结

  1. 锁的一致性:使用wait()和notify()时,线程必须持有同一个对象的锁,否则无法实现通信,甚至可能导致死锁。
  2. 异常处理:在调用wait()、sleep()等方法时,要妥善处理InterruptedException异常。
  3. 锁的释放:在同步块或Lock的使用中,要确保锁能正确释放,避免死锁。例如,使用Lock时,应在finally块中释放锁。
  4. 通知的及时性:notify()或signal()应在合适的时机调用,确保等待的线程能及时被唤醒,避免线程长时间等待。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐