一、等待/通知机制
1、什么是等待通知机制
在单线程编程中,要执行的操作需要满足一定的条件才能执行,可以把这个操作放在 if 语句块中.
- 在多线程编程中,可能 A 线程的条件没有满足只是暂时的, 稍后其他的线程 B 可能会更新条件使得 A 线程的条件得到满足. 可以将 A 线程暂停,直到它的条件得到满足后再将 A 线程唤醒.它的伪代码:
atomics{
//原子操作
while( 条件不成立 ){
等待
}
//当前线程被唤醒条件满足后,继续执行下面的操作
2、等待/通知机制的实现
Object 类中的 wait()方法可以使执行当前代码的线程等待,暂停执行,直到接到通知或被中断为止
- 注意:
- wait()方法只能在同步代码块中由锁对象调用
- 调用 wait()方法,当前线程会释放锁
其伪代码如下:
//在调用 wait()方法前获得对象的内部锁
synchronized( 锁对象 ){
while( 条件不成立 ){
//通过锁对象调用 wait()方法暂停线程,会释放锁对象
锁对象.wait();
}
//线程的条件满足了继续向下执行
}
Object 类的 notify()可以唤醒线程,该方法也必须在同步代码块中由锁对象调用 . 没有使 用 锁 对 象 调 用 wait()/notify() 会 抛 出 IlegalMonitorStateExeption 异常. 如果有多个等待的线程,notify()方法 只能唤醒其中的一个. 在同步代码块中调用 notify()方法后,并不会立即释放锁对象,需要等当前同步代码块执行完后才会释放锁对象,一般将 notify()方法放在同步代码块的最后. 它的伪代码如下:
synchronized( 锁对象 ){
//执行修改保护条件 的代码
//唤醒其他线程
锁对象.notify();
}
//演示wait()等待和notify()唤醒等待这两个方法
//需要通过 notify()唤醒等待的线程
synchronized (lock) {
System.out.println("线程 1 开始等待: " + System.currentTimeMillis());
try {
lock.wait();//线程等待,会释放锁对象,当前线程转入 blocked 阻塞状态
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程 1 结束等待:" + System.currentTimeMillis());
}
}
});
//notify()方法也需要在同步代码块中,由锁对象调用
synchronized (lock){
System.out.println("线程 2 开始唤醒 : " + System.currentTimeMillis());
lock.notify();
//唤醒在 lock 锁对象上等待的某一个线程
System.out.println("线程 2 结束唤醒 : " + System.currentTimeMillis());
}
}
});
t1.start();
Thread.sleep(3000);//main 线程睡眠 3 秒,确保 t1 入睡
t2.start();
}}
3、interrupt()方法会中断 wait()
当线程处于 wait()等待状态时,
调用线程对象的 interrupt()方法会中断线程的等待状态, 会产生 InterruptedException 异常
4、notify()与 notifyAll()
notify()一次只能唤醒一个线程,如果有多个等待的线程,只能随机 唤醒其中的某一个; 想要唤醒所有等待线程,需要调用 notifyAll().
5、wait(long)的使用
wait(long)带有 long 类型参数的 wait()等待,如果在参数指定的时间内没有被唤醒,超时后会自动唤醒
6、通知过早
线程 wait()等待后,可以调用 notify()唤醒线程, 如果 notify()唤醒的过早,在等待之前就调用了 notify()可能会打乱程序正常的运行逻辑.
7、wait 等待条件发生了变化(重点)
wait()方法套在while循环中,线程下次醒过来后会继续进行循环,判断条件是否满足,满足就重新等待。
在使用 wait/nofity 模式时,注意 wait 条件发生了变化,也可能会造 成逻辑的混乱
wait()方法之所以要用while而不是if是因为 :
当多个线程并发访问同一个资源的时候, 若消费者同时被唤醒,但是只有一个资源可用, 那么if会导致资源被用完后直接去获取资源(发生越界异常等),而while则会让每个消费者获取之前再去判断一下资源是否可用.可用则获取,不可用则继续wait住.
二、通过管道流实现线程间的通信
三、Join()
join有什么用? 他是用来确定线程何时结束的 .看例子
main{
a.start();
a.join
b.start();
}
主线程中调用a.join(),在a线程结束前,主线程陷入阻塞状态,a线程结束后,主线程顺序执行a.join()后的代码,并不会因为a的死亡导致主线程的死亡
四、ThreadLocal的使用
除了控制资源的访问外, 还可以通过增加资源来保证线程安全. ThreadLocal 主要解决为每个线程绑定自己的值
通常情况下,我们创建的变量是可以被任何一个线程访问并修改的(在当前线程的本地内存的共享变量中)。如果想实现每一个线程都有自己的专属本地变量该如何解决呢? JDK 中提供的ThreadLocal
类正是为了解决这样的问题。 ThreadLocal
类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal
类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。
**如果你创建了一个ThreadLocal
变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal
变量名的由来。他们可以使用 get()
和 set()
方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全
/**
* 在多线程环境中,把字符串转换为日期对象,多个线程使用同一个 SimpleDateFormat 对象可能会产生线程安全问题,有异常
* 为每个线程指定自己的 SimpleDateFormat 对象, 使用 ThreadLocal
*/
ThreadLocal指定初始值
/**
* ThreadLocal 初始值, 定义 ThreadLocal 类的子类,在子类中重写 initialValue()方法指定初始值,
且第一次调用 get()方法不会返回 null
*/
class SubThreadLocal extends ThreadLocal<Date> {
// 重写 initialValue 方法,设置初始值
@Override
protected Date initialValue() {
return new Date(System.currentTimeMillis() - 1000 * 60 * 15);
}
}
五、额外补充:操作系统中进程间的通信方式
1、管道/匿名管道(Pipes) :用于具有亲缘关系的父子进程间或者兄弟进程之间的通信。双方通信时,需要建立起两个管道。一个读一个写
2、有名管道(Names Pipes) : 匿名管道由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道。有名管道严格遵循先进先出。可以实现本机任意两个进程通信。
3、信号(Signal) :通知某一进程某个事件已经发生;(有终止信号、定时器信号等)
4、消息队列(Message Queuing) ;
5、信号量(Semaphores) :信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信方式主要用于解决与同步相关的问题并避免竞争条件。
6、共享内存(Shared memory) :使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。可以说这是最有用的进程间通信方式。
7、套接字(Sockets) : 此方法主要用于在客户端和服务器之间通过网络进行通信。套接字是支持 TCP/IP 的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。