本文共 12475 字,大约阅读时间需要 41 分钟。
当需要多个线程之间相互协作的时候,就需要用到Java线程的通信方法。
Java中,锁的概念都是基于对象的,所以又称为对象锁。
package chapter7;public class ObjectLock { // 类属性只有一个[不管创建多少个实例],保证同一个锁 private static Object lock = new Object(); // 可尝试去掉static怎么保证线程安全 static class ThreadA implements Runnable{ @Override public void run() { synchronized (lock){ // 查看锁的内存地址 System.out.println(lock); for (int i = 0; i < 100; i ++){ System.out.println("Thread A " + i); } } } } static class ThreadB implements Runnable{ @Override public void run() { synchronized (lock){ // 查看锁的内存地址 System.out.println(lock); for (int i = 0; i < 100; i ++){ System.out.println("Thread B " + i); } } } } public static void main(String[] args) throws InterruptedException{ new Thread(new ThreadA()).start(); Thread.sleep(10); // 让threadA先执行 new Thread(new ThreadB()).start(); }}
在ThreadA 和ThreadB内需要同步的代码块里,都是用synchronized关键字加上一个对象锁lock;
基于锁的方式,线程需要不断地尝试获得锁,如果失败,会一直尝试。
Java有一个内建的等待机制(等待/通知机制)来允许线程在等待信号的时候变为非运行状态。java.lang.Object类定义了三个方法,wait()、notify()和notifyAll()来实现这个机制。
// Object中Java本地方法// 随机叫醒一个正在等待的线程public final native void notify();// 会叫醒所有正在等待的线程public final native void notifyAll();// 调用该方法的线程进入WAITING状态,释放对象的锁,一般用在同步方法或者同步代码块中public final native void wait(long timeout) throws InterruptedException;具体解析移步:
package chapter7;public class WaitAndNotify { // 保证多个线程使用同一个对象锁,如果多个线程使用的是不同的锁,那么 // 它们之间是不能使用等待/通知机制的。 private static Object lock = new Object(); // 对象锁 static class ThreadA implements Runnable{ @Override public void run() { synchronized (lock){ System.out.println(lock); for (int i = 0; i < 5; i ++){ try{ System.out.println("ThreadA: " + i); lock.notify(); // 唤醒其它等待这个对象锁的线程 lock.wait(); // 释放资源 } catch (InterruptedException e){ e.printStackTrace(); } } // 当退出for循环时,唤醒了另外一个线程,该线程等待 // 这将导致两个线程都会陷入等待 // 没有它,主线程无法结束 lock.notify(); } } } static class ThreadB implements Runnable{ @Override public void run() { synchronized (lock){ System.out.println(lock); for (int i = 0; i < 5; i ++){ try{ System.out.println("ThreadB: " + i); lock.notify(); // 唤醒其它等待这个对象锁的线程 lock.wait(); // 让出资源 } catch (InterruptedException e){ e.printStackTrace(); } } lock.notify(); } } } public static void main(String[] args) throws InterruptedException{ new Thread(new ThreadA()).start(); Thread.sleep(10); new Thread(new ThreadB()).start(); }}
notify()和notifyAll()方法不会保存调用它们的方法,通知信号过后便丢弃了。在某些情况下,这可能使等待线程永远在等待,不再醒来,因为线程错过了唤醒信号。为了避免这个情况,用一个变量来保存是否被通知过。在notify前,设置自己已经被通知过了。在wait后,设置自己没有被通知过,需要等待通知。
public class MyWaitNotify2{ MonitorObject myMonitorObject = new MonitorObject(); boolean wasSignalled = false; public void doWait(){ synchronized(myMonitorObject){ // 在wait与wait之间没有收到notify,那么只能调用wait if (!wasSignalled){ try( myMonitorObject.wait();) catch(InterruptedException e){ ...} } wasSignalled = false; } } public void doNotify(){ synchronized(myMonitorObject){ wasSignalled = true; myMonitorObject.notify(); } }}
volatile关键字可以保证指令不被重排序和内存的可见性。
public class MySignal { private static volatile int signal = 0; static class ThreadA implements Runnable{ @Override public void run() { while(signal < 5){ if (signal % 2 == 0){ System.out.println("ThreadA: " + signal); synchronized (this){ signal ++ ; // signal并不是原子操作 } } } } } static class ThreadB implements Runnable{ @Override public void run() { while(signal < 5){ if (signal % 2 == 1){ System.out.println("ThreadB: " + signal); synchronized (this){ signal ++ ; } } } } } public static void main(String[] args) throws InterruptedException{ new Thread(new ThreadA()).start(); Thread.sleep(100); new Thread(new ThreadB()).start(); }}
JDK提供了一个类似于“信号量”功能的类Semaphore。接下来介绍一种基于volatile关键字的自己实现的信号量通信。
volitile关键字能够保存内存的可见性,如果用volitile关键字声明了一个变量,在一个线程里面改变了这个变量的值,那其他线程立马可见更改后的值。
示例:我想让线程A输出0,然后线程B输出1,在然后A输出2...以此类推
package chapter7;/** * 由于signal++与signal = signal + 1并不是原子操作,所以需要加锁。 * 〈信号量〉 * * @author 我们 * @create 2021/1/26 * @since 1.0.0 */public class Signal { private static volatile int signal = 0; private static final Object lock = new Object(); static class ThreadA implements Runnable{ @Override public void run() { while(signal < 5){ if (signal % 2 == 0){ System.out.println("lock :" + lock + ", " + Thread.currentThread().getName() + " : " + signal); synchronized (lock){ signal ++; } } } } } static class ThreadB implements Runnable{ @Override public void run() { while(signal < 5){ if (signal % 2 == 1){ System.out.println("lock :" + lock + ", " + Thread.currentThread().getName() + " : " + signal); synchronized (lock){ signal = signal + 1; } } } } } public static void main(String[] args) throws InterruptedException { new Thread(new ThreadA()).start(); Thread.sleep(12); new Thread(new ThreadB()).start(); }}
管道是基于“管道流”的通信方式。JDK提供了PipedWriter、PipedReader、pipedOutputStream、PipedInputStream。其中,前面两个是基于字符的,后面两个是基于字节流的。
import java.io.IOException;import java.io.PipedReader;import java.io.PipedWriter;/** * 代码执行流程: * 1.线程ReaderThread开始执行 * 2.线程ReaderThread使用管道reader.read()进入阻塞 * 3.线程WriterThread开始执行 * 4.线程WriterThread用writer.write("test")往管道写入字符串 * 5.线程ReaderThread接受到管道输出的字符串并打印 * 6.线程ReaderThread执行完毕 */public class Pipe { static class ReaderThread implements Runnable{ private PipedReader reader; public ReaderThread(PipedReader reader){ this.reader = reader; } @Override public void run() { System.out.println("this is reader"); int receive = 0; try{ while((receive = reader.read()) != -1){ System.out.print((char) receive); } } catch (IOException e){ e.printStackTrace(); } } } static class WriterThread implements Runnable{ private PipedWriter writer; public WriterThread(PipedWriter writer){ this.writer = writer; } @Override public void run() { System.out.println("this is writer"); int receive = 0; try{ writer.write("test"); } catch (IOException e){ e.printStackTrace(); } finally { try{ writer.close(); } catch (IOException e){ e.printStackTrace(); } } } } public static void main(String[] args) throws IOException, InterruptedException{ PipedWriter writer = new PipedWriter(); PipedReader reader = new PipedReader(); writer.connect(reader); new Thread(new ReaderThread(reader)).start(); Thread.sleep(10); new Thread(new WriterThread(writer)).start(); }}/* output~: this is reader this is writer test*/
管道通信的应用场景:
使用管道多半与I/O流相关。当我们一个线程需要另一个线程先发送一个信息(比如字符串)或者文件时,就需要使用管道通信。
join方法是Thread类的一个实例方法。它的作用是让当前线程陷入“等待”状态,等join的这个线程执行完毕后,再继续执行当前线程。
有时候,主线程创建并启动了子线程,如果子线程中需要大量的耗时运算,主要线程往往将早于子线程结束之前结束。
如果主线程想等待子线程执行完毕之后,获得子线程中的处理完的某个数据,就要用到join方法。
public class Join { static class ThreadA implements Runnable{ @Override public void run() { try{ System.out.println("我是子线程,我先睡一秒"); Thread.sleep(1000); System.out.println("我是子线程,我睡了完了一秒"); } catch (InterruptedException e){ e.printStackTrace(); } } } public static void main(String[] args) throws InterruptedException{ Thread thread = new Thread(new ThreadA()); thread.start(); thread.join(); System.out.println("如果不加join方法,我会先被打出来,加了就不一样了"); }}/*output~:不加thread.join(): 如果不加join方法,我会先被打出来,加了就不一样了 我是子线程,我先睡一秒 我是子线程,我睡了完了一秒加thread.join(): 我是子线程,我先睡一秒 我是子线程,我睡了完了一秒 如果不加join方法,我会先被打出来,加了就不一样了 */
join方法有两个重载方法,一个是join(long),一个是join(long, int)。实际上,通过源码发现,join()方法及其重载方法底层都是利用了wait(long)这个方法。对于join(long,int),通过查看源码(JDK 1.8)发现,底层并没有精确到纳秒,而是对第二个参数做了简答的判断和处理。
public final synchronized void join(long millis) throws InterruptedException { long base = System.currentTimeMillis(); long now = 0; if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); } if (millis == 0) { while (isAlive()) { wait(0); } } else { while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } wait(delay); now = System.currentTimeMillis() - base; } } }
sleep方法是Thread类的一个静态方法。它的作用是让当前线程随眠一段时间,它有这样两个方法:
同样,查看源码(JDK 1.8)发现,第二个方法貌似只对第二个参数做了简单处理,没有精确到纳秒。实际上还是调用的第一个方法。
sleep方法是不会释放当前的锁的,而wait方法会。
wait与sleep方法的区别:
ThreadLocal是一个本地线程副本变量类。内部是一个弱引用的Map来维护。这里不详细介绍它的原理,而是只是介绍它的使用,以后有独立章节来介绍ThreadLocal类的原理。
ThreadLocal为线程本地变量或线程本地存储。严格来说,ThreadLocal类并不属于多线程间的通信,而是让每个线程有自己独立的变量,线程之间互不影响,它为每个线程都创建一个副本,每个线程都可以访问自己的内部副本变量。
package chapter7;public class ThreadLocalDemo { static class ThreadA implements Runnable{ private ThreadLocalthreadLocal; public ThreadA(ThreadLocal threadLocal) { this.threadLocal = threadLocal; } @Override public void run() { threadLocal.set("A"); try{ Thread.sleep(1000); } catch (InterruptedException e){ e.printStackTrace(); } System.out.println("ThreadA out: " + threadLocal + "->content:" + threadLocal.get()); } } static class ThreadB implements Runnable{ private ThreadLocal threadLocal; public ThreadB(ThreadLocal threadLocal) { this.threadLocal = threadLocal; } @Override public void run() { threadLocal.set("B"); try{ Thread.sleep(1000); } catch (InterruptedException e){ e.printStackTrace(); } System.out.println("ThreadB out: " + threadLocal + "->content:" + threadLocal.get()); } } public static void main(String[] args) { ThreadLocal threadLocal = new ThreadLocal<>(); new Thread(new ThreadA(threadLocal)).start(); new Thread(new ThreadB(threadLocal)).start(); }}/*output~:ThreadA out: java.lang.ThreadLocal@7e92ee0->content:AThreadB out: java.lang.ThreadLocal@7e92ee0->content:B */
ThreadLocal可以将某个静态变量(user ID或者ID)与线程状态关联,则可以使用ThreadLocal。常见的ThreadLocal使用场景为用来解决数据库连接、Session管理。这些都涉及多个复杂对象的初始化和关闭。如果在每个线程中声明一些私有变量来操作,那么这个线程就变得那么轻量了。
InheritableThreadLocal类与ThreadLocal类稍有不同,Inheritable是可继承的意思,它的子线程可以存取这个副本值。
转载地址:http://luczz.baihongyu.com/