4.3 线程间的通信
4.3.1 volatile和synchronized关键字
关键字volatile可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新到共享内存,它能保证所有线程对变量访问的可见性。
关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性。
示例代码:
public class Synchronized {
public static void main(String[] args){
// 对Synchronized Class 对象进行加锁
synchronized(Synchronized.class){
}
// 静态同步方法,对Synchronized Class对象进行加锁
m();
}
public static synchronized void m(){
}
}
在Synchronized.class同级目录执行javap -v Synchronized.class,部分相关输出如下所示:
public static void main(java.lang.String[]);
Code:
stack=2, locals=1,args_size=1
0:ldc #1 //class com/../Synchronized
2:dup
3:monitorenter //monitorenter:监视器进入,获得锁
4:monitorexit //monitorexit:监视器退出,释放锁
5:invokestatic #1 6 //Method m:()V
8:return
public static synchronized void m();
flags:ACC_PUBLIC,ACC_STATIC,ACC_SYNCHRONIZED
Code:
stack=0,locals=0,args_size=0
0:return
上面的class信息中,对于同步块实现使用了monitorenter和monitorexit指令,而同步方法则是依靠方法修饰符上的ACC_SYNCHRONIZED来完成的。
任意线程对Object(Object由synchronized保护)的访问,首先要获得Object的监视器。
4.3.2 等待/通知机制
等待/通知的相关方法
方法名称 | 描述 |
---|---|
notify() | 通知一个在对象上等待的线程,使其从wait()方法返回,而返回的前提是该线程获取到了对象的锁 |
notifyAll() | 通知所有等待在该对象上的线程 |
wait() | 调用该方法的线程进入WAITTING状态,只有等待另外线程的通知或被中断才会返回,需要注意,调用wait()方法后,会释放对象的锁 |
wait(long) | 超时等待一段时间(单位:毫秒)后,如果没有通知就返回 |
wait(long,int) | 对于超时时间更细粒度的控制,可以达到纳秒 |
public class WaitNotify {
static boolean flag = true;
static Object lock = new Object();
public static void main(String[] args) {
Thread waitThread = new Thread(new Wait(), "WaitThread");
waitThread.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread notifyThread = new Thread(new Notify(), "NotifyThread");
notifyThread.start();
}
static class Wait implements Runnable{
@Override
public void run() {
//加锁,拥有lock的Monitor
synchronized (lock){
//条件不满足时,继续wait,同时释放了lock的锁
while (flag){
try{
System.out.println(Thread.currentThread() + " flag is true. wait @ " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
lock.wait();
}catch (InterruptedException e){
}
}
//条件满足时,完成工作
System.out.println(Thread.currentThread() + " flag is false. running @ " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
}
}
}
static class Notify implements Runnable{
@Override
public void run() {
//加锁,拥有lock的Monitor
synchronized (lock){
// 获取lock的锁,然后进行通知,通知时不会释放lock的锁,
// 直到当前线程释放了lock后,WaitThread 才能从wait方法中返回
System.out.println(Thread.currentThread() + " hold lock.notify @ " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
lock.notifyAll();
flag = false;
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 再次加锁
synchronized (lock){
System.out.println(Thread.currentThread() + " hold lock again .sleep @ " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
输出结果如下:
Thread[WaitThread,5,main] flag is true. wait @ 09:49:29
Thread[NotifyThread,5,main] hold lock.notify @ 09:49:30
Thread[NotifyThread,5,main] hold lock again .sleep @ 09:49:35
Thread[WaitThread,5,main] flag is false. running @ 09:49:40
4.3.3 等待/通知的经典范式
等待方遵循的原则:
1. 获取对象的锁
2. 如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件
3. 条件满足则执行对应的逻辑
伪代码如下:
synchronized(对象){
while(条件不满足){
对象.wait();
}
对应的处理逻辑
}
通知方遵循的原则:
- 获得对象的锁
- 改变条件
通知所有等待在对象上的线程
synchronized(对象){ 改变条件 对象.notifyAll(); }
4.3.4 管道输入/输出
它主要用于线程之间的数据传输,而传输的媒介是内存。
public class Piped {
public static void main(String[] args) throws Exception {
PipedWriter out = new PipedWriter();
PipedReader in = new PipedReader();
// 将输出流和输入流进行连接,否则在使用时会抛出IOException
out.connect(in);
Thread printThread = new Thread(new Print(in),"PrintThread");
printThread.start();
int receive = 0;
try {
while ((receive = System.in.read()) != -1) {
out.write(receive);
}
} finally {
out.close();
}
}
static class Print implements Runnable {
private PipedReader in;
public Print(PipedReader in) {
this.in = in;
}
@Override
public void run() {
int receive = 0;
try {
while ((receive = in.read()) != -1) {
System.out.print((char) receive);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
4.3.5 Thread.join()的使用
如果一个线程A执行了B.join()语句,意味着:当前线程A等待线程B中终止后才从B.join()返回。
查看java.lang.Thread的源码,对应的join部分的代码如下:
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;
}
}
}
当线程终止时,会调用线程自身的notifyAll()方法,会通知所有等待在该线程对象上的所有线程。
4.3.6 ThreadLocal的使用
ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。
以一个统计耗时的Profiler类为例,一般的耗时工具类方法可能如下:
public class LogTimeUtils {
private long beginTime;
private long endTime;
public LogTimeUtils(){
beginTime = System.currentTimeMillis();
endTime = System.currentTimeMillis();
}
/**
* 统计总时间
*/
public Long countTotalTime(){
return System.currentTimeMillis() - beginTime;
}
/**
* 统计间隔时间
*/
public Long countTime(){
long time = System.currentTimeMillis() - endTime;
endTime = System.currentTimeMillis();
return time;
}
}
该工具类适用于同一个方法的统计耗时,如果涉及到跨方法的耗时统计,需要显性的传递该logTimeUtil,污染了代码结构。
而利用ThreadLocal可以很好的解决这个问题,好处就是即使统计的begin()方法和end()方法不在一个方法或者类中,都可以适用。
public class Profiler {
private static final ThreadLocal<Long> TIME_THREADLOCAL = new ThreadLocal<Long>(){
@Override
protected Long initialValue(){
return System.currentTimeMillis();
}
};
public static final void begin(){
TIME_THREADLOCAL.set(System.currentTimeMillis());
}
public static final long end(){
return System.currentTimeMillis() - TIME_THREADLOCAL.get();
}
public static void main(String[] args) throws Exception{
Profiler.begin();
TimeUnit.SECONDS.sleep(1);
System.out.println("Cost : " + Profiler.end() + " mills.");
}
}