蒟蒻变大佬必学之Java多线程间的信息共享
简单回顾一下线程类的使用流程:
–通过继承Thread或实现Runnable
–通过start方法,调用run方法,run方法工作
–线程run结束后,线程退出
线程运行时对变量的访问可视作粗粒度和细粒度两种
• 粗粒度:
各个子线程通过调用start()方法进入运行状态后,一直run(),结果就是子线程与子线程之间、和main线程之间缺乏交流,达不到业务要求。
考虑一种卖票的场景,4个线程模拟卖票的场景,票的总数为100,然而当所有线程执行完毕后,却是卖出了4*100张票,显然与实际不符,具体代码如下:
public class TicketSelling
{
public static void main(String [] args)
{
new Tickets().start();
new Tickets().start();
new Tickets().start();
new Tickets().start();
}
}
class Tickets extends Thread
{
private int tickets=100; //每个线程卖100张,没有共享
//private static int tickets=100; //static变量是共享的,所有的线程共享
public void run()
{
while(true)
{
if(tickets>0)
{
System.out.println(Thread.currentThread().getName() +
" is selling ticket " + tickets);
tickets = tickets - 1;
}
else
{
break;
}
}
}
}
• 细粒度:
为了解决上述问题,我们需要线程之间有信息交流通讯 ,由于JDK原生库暂不支持发送消息(类似MPI并行库直接发送消息),很容易想到的就是,通过共享变量达到信息共享。
通过共享变量在多个线程中共享消息两种方法:
–static变量
–同一个Runnable类的成员变量

类的一个重要特性是所有类共享static修饰的变量,这个可以作为简单的信息共享方法
好的,那么加入我们来把tickets变量修饰为static,是不是就解决问题了呢?
//private int tickets=100; //每个线程卖100张,没有共享
private static int tickets=100; //static变量是共享的,所有的线程共享
跑一下,运行结果如下

很容易看出错误,4个线程同时在卖第1张票(也就是图中的tickets100),这样结果肯定是出问题的,static不是所有实例共享吗?难道我学了个假的Java?
人要学会变通,此路不通,那我们换一个方法试它一试?
前面提到有static修饰变量,实现实例间的共享,还有一个就是Runnable接口中定义一个变量实现共享,代码如下:
public class TicketSelling
{
public static void main(String [] args)
{
Tickets t=new Tickets();
//只被创建依次,就是t。而new Thread(t) 并没有创建Tickets 对象,而是把t包装成线程对象,然后启动
//也就是说,下面的几行启动代码,用的是同一个Tickets的对象t,从而实现变量的共享
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}
}
class Tickets implements Runnable
{
private int tickets=100;
public void run()
{
while(true)
{
if(tickets>0)
{
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
tickets--;
System.out.println(Thread.currentThread().getName() +" is selling ticket " + tickets);
}
else
{
break;
}
}
}
}
运行一下,结果如下:

WTF?
你这不仅仅是卖错票了,还卖出负数来了,难道程序的数学老师是体育老师代替的?
其实想要明白程序的老师到底是谁(弱弱说一句,不就是蒟蒻程序员自己嘛?),需要理解这个地方,我们需要看线程的数据更底层的实现原理。
每个线程在工作时,会从RAM中读取一个工作缓存,该线程需要数据的时候,会从内存当中读取一个数据出来,而线程对数据的操作正是作用于这个工作缓存上,这就导致其他的线程没有看到这个工作缓存,最后刷新到RAM中,就会出现异常,可以看一下这张示意图:

这样我们就可以理解为什么说:
i++这个操作,并非原子性操作
计算机内部执行该操作,步骤如下:
–读取主存i(正本)到工作缓存(副本)中
–每个CPU执行(副本)i+1操作
–CPU将结果写入到缓存(副本)中
–数据从工作缓存(副本)刷到主存(正本)中
说了这么多,难道真的没有办法了吗?
其实办法是有的,我们可以采用volatile 关键字修饰变量 ,保证不同线程对共享变量操作时的可见性
public class ThreadDemo
{
public static void main(String args[]) throws Exception
{
TestThread t = new TestThread();
t.start();
Thread.sleep(2000);
t.flag = false;
System.out.println("main thread is exiting");
}
}
class TestThread extends Thread
{
//boolean flag = true; //子线程不会停止
volatile boolean flag = true; //用volatile修饰的变量可以及时在各线程里面通知
public void run()
{
int i=0;
while(flag)
{
i++;
}
System.out.println("test thread3 is exiting");
}
}
这里说明一下,单纯采用volatile关键字修饰对于上一个卖票的例子是不可行的,还是会多卖票,因为volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。具体的解释可以看一下这篇博文,写得很好,有兴趣的同学,可以加深一下理解:
volatile相关整理
我们还需要引入另外一个重要的机制——关键步骤加锁限制
–互斥:某一个线程运行一个代码段(关键区),其他线程不能同时 运行这个代码段
–同步:多个线程的运行,必须按照某一种规定的先后顺序来运行
–互斥是同步的一种特例
互斥的关键字是synchronized –synchronized代码块/函数,只能一个线程进入。
public class ThreadDemo {
public static void main(String[] args) {
TestThread t = new TestThread();
new Thread(t, "Thread-0").start();
new Thread(t, "Thread-1").start();
new Thread(t, "Thread-2").start();
new Thread(t, "Thread-3").start();
}
}
class TestThread implements Runnable {
private volatile int tickets = 100; // 由多个线程共享
String str = new String("");
public void run() {
while (true) {
sale();
if (tickets <= 0) {
break; //票数小于0时,退出循环
}
}
}
public synchronized void sale() {
// 同步函数,对卖票这个关键步骤进行加锁限制
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + " is saling ticket " + tickets--);
}
}
}
运行结果如下图所示,总算是成功了:

正所谓有得必有失,synchronized使用简便 ,但会加大性能负担
(同一时间对关键区的代码只能有限个线程被访问,就像高速公路上因为修路,限制了路宽,导致车流量在该处受阻,效率自然就下降了)
对于synchronized的详细使用暂时不在本篇的讨论范围内,可以看一下这篇校招题的synchronized考察
总结:
使用static和Runnable接口内部定义变量,均不能很好的保证变量的准确性,正确使用volatile和synchronized关键字,对变量和关键区代码修饰,保证程序运行的正确性 ,实现业务需求。

浙公网安备 33010602011771号