高并发学习之04线程间通信

1.线程间通信

线程开始运行,拥有自己的栈空间,就如同一个脚本一样,按照既定的代码一步一步地执
行,直到终止。但是,每个运行中的线程,如果仅仅是孤立地运行,那么没有一点儿价值,或者说价值很少,如果多个线程能够相互配合完成工作,这将会带来巨大的价值。在JMM内存模型中我们说道JMM本身是为了解决并发编程中三大问题:原子性、可见性、有序性。而这三大问题本质上就是多线程间通信问题。

1.1 线程间协同

要想实现多个线程的协同:如线程执行的先后顺序、获取某个线程执行结果等等。涉及到线程之间相互通信,大致分为四类:

  • 文件共享
  • 网络共享
  • 共享变量
  • jdk提供的线程协调API:suspend/resume 、wait/notify、park/unpark

文件、网络共享这里就不说了!共享变量会通过关键字(volatile)细说。这里讨论下jdk提供的API。

1.2 suspend/resume

大家对于CD机肯定不会陌生,如果把它播放音乐比作一个线程的运作,那么对音乐播放
做出的暂停、恢复和停止操作对应在线程Thread的API就是suspend()、resume()和stop()。

 /**
 * 包子店
 */
 public static Object shop = null;
 public void suspendResumeTest() throws Exception {
        // 启动线程
        Thread consumerThread = new Thread(() -> {
            if (shop == null) { // 如果没包子,则进入等待
                System.out.println("1、进入等待");
                Thread.currentThread().suspend();
            }
            System.out.println("2、买到包子,回家");
        });
        consumerThread.start();
        // 3秒之后,生产一个包子
        Thread.sleep(3000L);
        shop = new Object();
        consumerThread.resume();
   }    

输出结果:
1、进入等待
2、买到包子,回家
3、通知消费者
这段代码很简单 启动一个消费者线程,如果店铺有包子就购买,没有包子则挂起等待,主线程休眠三秒后开始生产包子后唤起消费线程。这段代码开着没有问题,但是它有个隐患,消费者线程在挂起的时候是没有释放资源的,容易引起死锁!举个例子:

 public void suspendResumeDeadLockTest() throws Exception {
        // 启动线程
        Thread consumerThread = new Thread(() -> {
            if (shop == null) { // 如果没包子,则进入等待
                System.out.println("1、进入等待");
                // 当前线程拿到锁,然后挂起
                synchronized (this) {
                    Thread.currentThread().suspend();
                }
            }
            System.out.println("2、买到包子,回家");
        });
        consumerThread.start();
        // 3秒之后,生产一个包子
        Thread.sleep(3000L);
        shop = new Object();
        // 争取到锁以后,再恢复consumerThread
        synchronized (this) {
            consumerThread.resume();
        }
        System.out.println("3、通知消费者");
    }

这段代码是不会有结果的,在消费者进入等待过程中,没有释放锁,主线程永远拿不到锁!正因为suspend()、resume()和stop()方法带来的副作用,这些方法才被标注为不建议使用的过期方法,而暂停和恢复操作可以用后面提到的等待/通知机制来替代。

1.3 wait/notify

等待/通知的相关方法
等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B
调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait()和notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。

public static Object shop = null;
public void waitNotifyTest() throws Exception {
        // 启动线程
        new Thread(() -> {
            if (shop == null) { // 如果没包子,则进入等待
                synchronized (this) {
                    try {
                        System.out.println("1、进入等待");
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            System.out.println("2、买到包子,回家");
        }).start();
        // 3秒之后,生产一个包子
        Thread.sleep(3000L);
        shop = new Object();
        synchronized (this) {
            this.notifyAll();
            System.out.println("3、通知消费者");
        }
    }

调用结果:
1、进入等待
3、通知消费者
2、买到包子,回家
消费者获取到锁之后判断没有包子,就挂起等待,同时释放锁,主线程生产获取资源后生产包子,通知消费者消费。通过上面代码可以看出来消费者线程wait时释放了锁资源。
当然 wait和notify也有需要注意的地方。

 /**
     * 会导致程序永久等待的wait/notify
     */
    public void waitNotifyDeadLockTest() throws Exception {
        // 启动线程
        new Thread(() -> {
            if (shop == null) { // 如果没包子,则进入等待
                try {
                    Thread.sleep(5000L);
                } catch (InterruptedException e1) {
                    e1.printStackTrace();
                }
                synchronized (this) {
                    try {
                        System.out.println("1、进入等待");
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            System.out.println("2、买到包子,回家");
        }).start();
        // 3秒之后,生产一个包子
        Thread.sleep(3000L);
        shop = new Object();
        synchronized (this) {
            this.notifyAll();
            System.out.println("3、通知消费者");
        }
    }

消费者休眠5秒,主线程生产者休眠3秒,当主线程休眠结束通知消费者线程的时候,是唤不起消费者的。
通过上面两个例子,可以看出来wait、notify或者notifyAll,需要注意如下几点:

  • 使用wait()、notify()和notifyAll()时需要先对调用对象加锁。
  • 调用wait()方法后,线程状态由RUNNING变为WAITING,并将当前线程放置到对象的等待队列。
  • notify()或notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()或notifAll()的线程释放锁之后,等待线程才有机会从wait()返回。
  • notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll()方法则是将等待队列中所有的线程全部移到同步队列,被移动的线程状态由WAITING变为BLOCKED。
  • 从wait()方法返回的前提是获得了调用对象的锁。

从上述细节中可以看到,等待/通知机制依托于同步机制,其目的就是确保等待线程从
wait()方法返回时能够感知到通知线程对变量做出的修改。wait、notify运行过程
WaitThread首先获取了对象的锁,然后调用对象的wait()方法,从而放弃了锁并进入了对象的等待队列WaitQueue中,进入等待状态。由于WaitThread释放了对象的锁,NotifyThread随后获取了对象的锁,并调用对象的notify()方法,将WaitThread从WaitQueue移到SynchronizedQueue中,此时WaitThread的状态变为阻塞状态。NotifyThread释放了锁之后,WaitThread再次获取到锁并从wait()方法返回继续执行。

1.4 wait/notify的经典范式

通过1.3 wait、notify的事例我们可以提炼出等待/通知的经典范式,该范式分为两部分,分
别针对等待方(消费者)和通知方(生产者)。
等待方遵循如下原则。
1)获取对象的锁。
2)如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件。
3)条件满足则执行对应的逻辑。

synchronized(对象) {
	while(条件不满足) {
		对象.wait();
	}
	对应的处理逻辑
}

通知方遵循如下原则。
1)获得对象的锁。
2)改变条件。
3)通知所有等待在对象上的线程。

synchronized(对象) {
	改变条件
	对象.notifyAll();
}
1.6 park/unpark 的使用

无论是wait/notify 或者是已经过时的suspend/resume ,在使用上都有过多的限制和需要注意的地方。有没有一个用的更爽的方式,答案当然是有的啊,在J.U.C的包中LockSupport提供了一种更为简洁的方式:park/unpark。

public static Object shop = null;
public void parkUnparkTest() throws Exception {
        // 启动线程
        Thread consumerThread = new Thread(() -> {
            if (shop == null) { // 如果没包子,则进入等待
                System.out.println("1、进入等待");
                LockSupport.park();
            }
            System.out.println("2、买到包子,回家");
        });
        consumerThread.start();
        // 3秒之后,生产一个包子
        Thread.sleep(3000L);
        shop = new Object();
        LockSupport.unpark(consumerThread);
        System.out.println("3、通知消费者");
    }

park 和unpark不要求调用顺序,多次调用unpark之后在调用park,线程会直接运行。但不会叠加,也就是说连续多次调用park方法,第一次会拿到“许可”直接运行,后续调用会进入等待。
LockSupport提供的阻塞和唤醒方法

1.7 Thread.join()的使用

如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才
从thread.join()返回。线程Thread除了提供join()方法之外,还提供了join(long millis)join(long millis,int nanos)两个具备超时特性的方法。这两个超时方法表示,如果线程thread在给定的超时时间里没有终止,那么将会从该超时方法中返回。
看下面事例代码:
在下面所示的例子中,创建了10个线程,编号0~9,每个线程调用前一个线程的join()方法,也就是线程0结束了,线程1才能从join()方法中返回,而线程0需要等待main线程结束。

public class Join {
    public static void main(String[] args) throws Exception {
        Thread previous = Thread.currentThread();
        for (int i = 0; i < 10; i++) {
			// 每个线程拥有前一个线程的引用,需要等待前一个线程终止,才能从等待中返回
            Thread thread = new Thread(new Domino(previous), String.valueOf(i));
            thread.start();
            previous = thread;
        }
        TimeUnit.SECONDS.sleep(5);
        System.out.println(Thread.currentThread().getName() + " terminate.");
    }

    static class Domino implements Runnable {
        private Thread thread;

        public Domino(Thread thread) {
            this.thread = thread;
        }

        public void run() {
            try {
                thread.join();
            } catch (InterruptedException e) {
            }
            System.out.println(Thread.currentThread().getName() + " terminate.");
        }
    }
}

输出如下:

main terminate.
0 terminate.
1 terminate.
2 terminate.
3 terminate.
4 terminate.
5 terminate.
6 terminate.
7 terminate.
8 terminate.
9 terminate.

从上述输出可以看到,每个线程终止的前提是前驱线程的终止,每个线程等待前驱线程终止后,才从join()方法返回,这里涉及了等待/通知机制(等待前驱线程结束,接收前驱线程结束通知)。

1.8 ThreadLocal的使用

ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。
可以通过set(T)方法来设置一个值,在当前线程下再通过get()方法获取到原先设置的值。
说白了,JVM维护了一个Map<Thread,T>,每个线程就是一个map里面的key,如果想使用value,必须通过线程这个key来获取。

public class Demo7 {
	/** threadLocal变量,每个线程都有一个副本,互不干扰 */
	public static ThreadLocal<String> value = new ThreadLocal<>();

	/**
	 * threadlocal测试
	 * 
	 * @throws Exception
	 */
	public void threadLocalTest() throws Exception {

		// threadlocal线程封闭示例
		value.set("这是主线程设置的123"); // 主线程设置值
		String v = value.get();
		System.out.println("线程1执行之前,主线程取到的值:" + v);

		new Thread(new Runnable() {
			@Override
			public void run() {
				String v = value.get();
				System.out.println("线程1取到的值:" + v);
				// 设置 threadLocal
				value.set("这是线程1设置的456");

				v = value.get();
				System.out.println("重新设置之后,线程1取到的值:" + v);
				System.out.println("线程1执行结束");
			}
		}).start();

		Thread.sleep(5000L); // 等待所有线程执行结束

		v = value.get();
		System.out.println("线程1执行之后,主线程取到的值:" + v);

	}

	public static void main(String[] args) throws Exception {
		new Demo7().threadLocalTest();
	}
}

执行结果如下:

线程1执行之前,主线程取到的值:这是主线程设置的123
线程1取到的值:null
重新设置之后,线程1取到的值:这是线程1设置的456
线程1执行结束
线程1执行之后,主线程取到的值:这是主线程设置的123
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值