抽奖小程序——多线程

前面我们通过两种不同的线程模式来实现了小球运动。一种是多个子线程,一种是单个子线程。今天我们再来做一个简单的抽奖程序加深对线程的理解。

一、程序,进程,线程所用到的物理资源和数据不同步问题

A.物理资源

程序储存在磁盘上。

进程储存在内存上。

线程 需要用到CPU、高速缓存和内存。

比如,我们的电脑上有一个QQ软件。它的数据会被储存在磁盘上。当你点击图标开始运行时,首先这些数据会经过处理被加载到内存,形成一个进程。操作系统开辟一个主线程依次执行main()中的代码。线程里面只有简单的指令,没有具体的数据。当线程需要调用数据时,它首先会去高速缓存中寻找。如果找到,就直接取出相应的数据到CPU中;如果没有找到,去内存中寻找,把相应数据加载到高速缓存,再重新去高速缓存取数据。这样多个线程之间就可能会出现数据不同步的问题。

B.数据不同步的问题

既然线程的数据是从内存中去取的,那么如果两个线程用到了同一个引用,并且某一个线程不断在改变这个引用。那么另一个线程能否持续接收到变化的引用值呢?事实证明是不行的。在我们这个程序中具体表现就是flag这个引用,主线程改变这个引用的值,而子线程想利用这个引用的不同值来控制是否进行数字的滚动,但是后来发现子线程根本没有收到变化的引用值。

我们来看一下线程在运行过程中数据的读写情况

抽奖小程序——多线程_第1张图片

从这个图中我们可以看到,线程1在改变flag的值时,它仅仅是改变了高速缓存中的flag,并没有改变主存中的flag值,因此线程2在从主存中取出flag的时候取到的flag都是一个初始化的值,一直都没有改变。导致这种问题出现的原因是因为系统采用了一种叫做回写的技术,也就是在整个线程结束之后再把改变的引用flag值写回主存中。为何会这样子呢?因为操作系统认为某个引用如果在一个线程中被改变,那么这个改变只会用于该线程。因此我们完全没有必要每次都把这个改变的值写回内存,毕竟涉及内存的操作都很耗时间。举个简单的例子,如果计算机要计算1+2+3的值,那么计算过程依次为(1)a=0+1=1;(2)a=1+2=3;(3)a=3+3=6。在这个过程中高速缓存中a的值依次被改变为0->1->3->6。而内存中a的值只改变了一次,0->6。

解决方法:为变量定义volatile关键字。这个关键字的作用就是同步数据,也就是如果某一个线程在执行过程中改变了某个被加了volatile关键字的引用,那么它必须立即把改变的值写回到主存中。这样子,其他线程就能实时收到引用的改变值。

二、进程和线程

进程是拥有资源的基本单位, 线程是CPU调度的基本单位。资源包括代码和数据两个部分,CPU调度处理的是指令。那么我们可以通俗地认为线程就是进程中指令的集合。

三、具体代码

import java.awt.Dimension;
import java.awt.FlowLayout;
import java.util.ArrayList;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;

//抽奖程序面板
public class LotteryFrame extends JFrame {
	public JLabel labelnumber1,labelnumber2;
	
	public static void main(String[] args) {
		LotteryFrame lotteryframe=new LotteryFrame();
		lotteryframe.initUI();
	}
	
	public void initUI() {
		this.setTitle("抽奖小程序");
		this.setSize(800, 600);
		this.setLocationRelativeTo(null);
		this.setDefaultCloseOperation(3);
		this.setResizable(false);
		this.setLayout(new FlowLayout(FlowLayout.CENTER,0,0));
		
		Dimension dim1=new Dimension(60,30);//“抽奖号码”的标签大小
		Dimension dim2=new Dimension(8,10);//滚动号码的标签大小
		Dimension dim3=new Dimension(100,30);//按钮的大小
		
		//添加“中奖号码”标签
		JLabel labeltitle=new JLabel("抽奖号码: ");
		labeltitle.setPreferredSize(dim1);
		this.add(labeltitle);
		
		//添加“滚动号码”标签
		labelnumber1=new JLabel("0");
		labelnumber1.setPreferredSize(dim2);
		this.add(labelnumber1);
		
		//添加“滚动号码”标签
		labelnumber2=new JLabel("0");
		labelnumber2.setPreferredSize(dim2);
		this.add(labelnumber2);
		
		//添加“开始抽签”的按钮
		JButton button = new JButton("开始抽奖");
		button.setPreferredSize(dim3);
		this.add(button);
		
		//添加监听对象
		ButtonListener bl=new ButtonListener(labelnumber1,labelnumber2,button);
		button.addActionListener(bl);
		
		this.setVisible(true);
	}
}
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Random;
import javax.swing.JButton;
import javax.swing.JLabel;

//设置按钮监听机制
public class ButtonListener extends Thread implements ActionListener{
	public JLabel labelnumber1,labelnumber2;
	//这里必须要加volatile关键字,作用就是每当这个应用被改变时,都会被写到内存中
	//这个关键字用来解决线程之间数据不同步的
	public volatile boolean flag=false;//判断是否滚动数字
	public JButton button;
	public Random random=new Random();
	
	public ButtonListener(JLabel labelnumber1,JLabel labelnumber2,JButton button) {
		this.labelnumber1=labelnumber1;
		this.labelnumber2=labelnumber2;
		this.button=button;
		//启动线程
		start();
	}
	
	public void actionPerformed(ActionEvent e) {
		//判断点击时按钮的内容
		if(e.getActionCommand().equals("开始抽奖")) {
			flag=true;
			//改变按钮的内容
			button.setText("结束抽奖");
		}else {
			flag=false;
			//改变按钮的内容
			button.setText("开始抽奖");
		}
	}

	public void run() {
		//让线程一直启动着
		while(true) {
			//线程虽然一直启动,但是只有当flag=true时,才会进行数字的滚动
			if(flag) {
				labelnumber1.setText(random.nextInt(10)+"");//+""可以自动转化为字符串
				labelnumber2.setText(random.nextInt(10)+"");
			}
		}
	}
}

你可能感兴趣的:(JAVA)