前面我们通过两种不同的线程模式来实现了小球运动。一种是多个子线程,一种是单个子线程。今天我们再来做一个简单的抽奖程序加深对线程的理解。
一、程序,进程,线程所用到的物理资源和数据不同步问题
A.物理资源
程序储存在磁盘上。
进程储存在内存上。
线程 需要用到CPU、高速缓存和内存。
比如,我们的电脑上有一个QQ软件。它的数据会被储存在磁盘上。当你点击图标开始运行时,首先这些数据会经过处理被加载到内存,形成一个进程。操作系统开辟一个主线程依次执行main()中的代码。线程里面只有简单的指令,没有具体的数据。当线程需要调用数据时,它首先会去高速缓存中寻找。如果找到,就直接取出相应的数据到CPU中;如果没有找到,去内存中寻找,把相应数据加载到高速缓存,再重新去高速缓存取数据。这样多个线程之间就可能会出现数据不同步的问题。
B.数据不同步的问题
既然线程的数据是从内存中去取的,那么如果两个线程用到了同一个引用,并且某一个线程不断在改变这个引用。那么另一个线程能否持续接收到变化的引用值呢?事实证明是不行的。在我们这个程序中具体表现就是flag这个引用,主线程改变这个引用的值,而子线程想利用这个引用的不同值来控制是否进行数字的滚动,但是后来发现子线程根本没有收到变化的引用值。
我们来看一下线程在运行过程中数据的读写情况
从这个图中我们可以看到,线程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)+"");
}
}
}
}