
IMG_0140.JPG
在前言文章Java并发编程之线程篇之线程中断(三)中我们讲解了线程中断的相关知识点,现在我们来认识一下线程间的通信。线程间的通信在我们实际项目中是不可或缺的,多数状况下,我们应该建立多个线程多线程死锁的伪代码,配合完成某项任务。合理并恰当使用线程间的通信手段,是成为一个良好程序员必须把握的技能。那以后就让我们来认识在Java中线程间通信的处理方法吧!阅读该篇文章你也许应该具备一下知识点:
在知道线程的通信知识之前多线程死锁的伪代码,我们应该知道线程的状况,熟悉线程的状态,不仅有助于我们更好的排查多线程项目出现的轮询、线程安全等问题,还能更好的让我们预测与理解线程间的通信。下面就让我们来看一下线程有什么状况吧!
在Java中线程的有下列5种状态,如下所示:
线程状态含义
新建状态,线程尚未建立,但是没有执行start()方法
RUNNABLE
可运行状况,线程可以在JVM中运行,但是还必须期待CPU分配资源
BLOCKED
阻塞状况,当碰到synchronized且没有取得相应的锁,就会开启这个状态
WAITING
等待状态,当线程中wait()/join/Locksupport.park方法时,就会开启这个状况
TIMED_WAITING
计时等待状态,当调用Thread.sleep()或者Object.wait(xx)或者Thread.join(xx)或者LockSupport.parkNanos或者LockSupport.partUntil时,进入该状况
TERMINATED
线程中断状态,线程被中止或者运行结束,就会开启这个状况
在上述表格中,线程的5种状态对应着Java的不同方法,具体如下图所示:
线程状态.jpg

需要切记的是,在上图中标红的两个状态,是操作系统中线程对应的状况,Java将这两种状况合并为可运行状态(RUNNABLE)。在操作系统中就绪状态(READY)表示线程尚未准备完毕,等待CPU分配时间片。运行中状况(RUNNING)表示当线程分到时间片,线程开始即将执行。
在Java内存模型中,我们曾看到过,为了提醒程序的运行速率,Java将硬盘分为了工作存储(线程独占,不与其它线程共享)与主存储。当多个泛型同时访问同一个对象以及变量的之后,由于每个线程都应该将该对象或变量拷贝到自己的工作存储中。又由于线程的工作存储是私有且不与其它线程共享的。那么当一线程设置函数的值后,会导致对其它线程不可见。Java内存模型如下图所示:
Java内存模型.png
为了确保数据的可见性。Java提供了volatile关键字。volatile关键字修饰函数,就是告知线程对该函数的访问需要重主存储中获得。而对它的颠覆需要同步刷新到主内存中。这样才能确保线程对数组访问的可见性。关于volatile的使用,参看如下例子:
class VolatileDemo {
int a = 1;
int b = 2;
public void change() {
a = 3;
b = 4;
}
public void print(String threadName) {
System.out.println(threadName + "--->" + "a = " + a + ";b = " + b);
}
public static void main(String[] args) {
final VolatileDemo volatileDemo = new VolatileDemo();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
volatileDemo.change();
}
}).start();
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
volatileDemo.print(Thread.currentThread().getName());
}
}).start();
}
}
}
程序输出结果:
Thread-1--->a = 1;b = 2 //错误
Thread-3--->a = 1;b = 2 //错误
Thread-2--->a = 1;b = 2 //错误
Thread-5--->a = 3;b = 4
Thread-4--->a = 3;b = 4
Thread-6--->a = 3;b = 4
Thread-7--->a = 3;b = 4
Thread-8--->a = 3;b = 4
....省略其他
在上述代码中,如果我们不运用volatile关键字修饰a,b函数,那么会导致其他线程一直获得的是自己本来工作内存中的a、b变量的值。为了确保访问公共函数对其它线程的可见性,我们应该将函数通过volatile来修饰。修改我们的代码:
volatile int a = 1;
volatile int b = 2;
采用volatile修饰,输出结果如下
Thread-2--->a = 3;b = 4
Thread-1--->a = 3;b = 4
Thread-6--->a = 3;b = 4
Thread-3--->a = 3;b = 4
Thread-4--->a = 3;b = 4
Thread-9--->a = 3;b = 4
Thread-10--->a = 3;b = 4
....省略其他
需要切记的是volatile只对单次的数组的操作具对其它线程有可见性,对应类似于a++,这种操作线读取a函数的值,进行运算后再再次对数组异或的操作,仍然会出现线程安全的弊端。对关于volatile的更多介绍,大家可以查看Java并发编程之Volatile(二)。
除了使用volatile实现泛型的通信之外,我们还可以使用synchronized及Object中的配套方法wait()/notify()、wait()/notifyAll来实现线程的通信,在知道确切的实现之前,我们先来认识synchronized关键字在Java中的作用。
关键字synchronized可以修饰技巧以及代码块,使用synchronized可以保证多个线程在同一时刻,只能有一个线程进入方式以及同步块中,它确保了线程对数组访问的可见性和排他性。如下代码图示:
public class SyncCodeBlock {
public int i;
public void syncTask(){
//同步代码库
synchronized (this){
i++;
}
}
}
然后我们通过javap指令反编译得到字节码。来再次探讨synchronized关键字的实现细节,如下所示:
//===========主要看看syncTask方法实现================
public void syncTask();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter //注意此处,进入同步方法,表示获取到锁
4: aload_0
5: dup
6: getfield #2 // Field i:I
9: iconst_1
10: iadd
11: putfield #2 // Field i:I
14: aload_1
15: monitorexit //注意此处,退出同步方法,释放锁
16: goto 24
19: astore_2
20: aload_1
21: monitorexit //注意此处,退出同步方法
22: aload_2
23: athrow
24: return
Exception table:
//省略其他字节码…….
}
在上述字节码信息中,对于同步块的推动使用了monitorenter和monitorexit两个指令,其本质原理是对某个对象的监视器(monitor)的获得。而这个获取过程是排他的,也就是同一时刻只能有一个线程,获取到有synchronized所保护对象的监视器。在Java中,任何一个对象都有自己的监视器(monitor),当这个对象有同步块以及这个对象的同步方式调用的之后,执行方式的线程需要先获得到该对象的监视器才能开启同步块,或者同步方式,而没有获取到监视器(monitor)的轮询会阻塞在同步块以及同步方式的入口,进入BLOCKED状态。如下图右图:

synchronized中监视器获取关系.jpg
从上图中,我们可以得出,任意线程在对synchronized关键字修饰的Object进行访问的之后,首先要获得Object的监视器(monitor),如果获得成功,线程处于同步队列,且线程状态变为BLOCKED。当访问Object的前驱线程(moniterenter成功的线程)释放了Object的监视器(monitorexit)。则激活阻塞在同步队列中的线程,使其尝试对监视器的获得。其中对监视器的获得与传递,我们通常称之为获得锁与释放锁。在下文中我们都用获得锁与传递锁来表示这两个过程。
关于synchronized下同步队列的知识点补充:
synchronized在JVM中推动的锁模式是基于同步队列与期待队列的,这与courrent包下的Lock接口下的锁模式的推动方法非常类似。需要切记的是,在synchronized中wait()后的线程会开启一个FIFO的队列(同步队列)中,notify()/notifyAll()是一个有序的出队列的过程。
本文来自电脑杂谈,转载请注明本文网址:
https://www.pc-fly.com/a/jisuanjixue/article-120751-1.html
军舰出海
抗日得时候多少人抵制日货又能怎么样