蒟蒻变大佬必学之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;
			}
				
		}
	}
}

运行一下,结果如下:

Runnable接口内定义变量,实现共享

WTF?

你这不仅仅是卖错票了,还卖出负数来了,难道程序的数学老师是体育老师代替的?

其实想要明白程序的老师到底是谁(弱弱说一句,不就是蒟蒻程序员自己嘛?),需要理解这个地方,我们需要看线程的数据更底层的实现原理

每个线程在工作时,会从RAM中读取一个工作缓存,该线程需要数据的时候,会从内存当中读取一个数据出来,而线程对数据的操作正是作用于这个工作缓存上,这就导致其他的线程没有看到这个工作缓存,最后刷新到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的详细使用暂时不在本篇的讨论范围内,可以看一下这篇校招题的synchronized考察

总结:

使用static和Runnable接口内部定义变量,均不能很好的保证变量的准确性,正确使用volatile和synchronized关键字,对变量和关键区代码修饰,保证程序运行的正确性 ,实现业务需求。

posted @ 2020-03-20 13:22  零壹博弈  阅读(61)  评论(0)    收藏  举报