首先在我分析多线程执行过程中的内存分配时,我想说明一下。当我们创建一个线程时,我们内存会问我们的线程分配一个栈,这个栈是线程私有的。换句话说,我们之前执行的main方法实际上是主线程的入口,当我们的jvm编译执行到这句话时,相当于告诉jvm可以开始主线程的执行了。那么我来讲解一下多线程的执行规则。
Demo d1 = new Demo("A");//创建之后线程就被创建了
Demo d2 = new Demo("B");
//运行start方法会做两件事,开启线程,运行run方法
d1.start();
d2.start();
针对这四行代码,Demo继承了Thread类并重写了它的run方法。首先我们会创建A,B线程的私有栈。当我们执行到d1.start()时,实际上我们就开启了A线程。但是是否会执行run()方法里面的代码块。我们不清楚,调度权交给cpu。同理d2.start()时,开启了B线程。实际上A,B线程以及主线程是在不同的栈内进行执行操作,以及分配相应的变量的空间的。
接下来我们来看一个例子。关于售票的问题。
假设一个窗口有20张票,交给四个窗口来出售,模拟这样一情景。
首先我们知道没一张票都是不同的,任意两个窗口不可以卖同一张票 。而且四个窗口的卖票工作是同时执行的。四个窗口就像四个线程同时执行一样,但是要保证出售的票不重复,怎么做呢。
先看下面的代码
package com.zzu.my.thread.test.thread;
class SellTicket extends Thread{
private int tickets = 20;//初始化有20张票
//买票这个动作被多个窗口执行也就是被多个线程执行,要将这些代码中定义到线程任务中
//run方法中
@Override
public void run(){
while(true){
if(tickets>0){
System.out.println("窗口: "+Thread.currentThread().getName()+"出售了第"+tickets--+"张票");
}else{
break;
}
}
}
}
/**
* 模拟售票窗口
* @author Administrator
*
*/
public class TicketDemo {
public static void main(String[] args) {
SellTicket t1 = new SellTicket();
SellTicket t2 = new SellTicket();
SellTicket t3 = new SellTicket();
SellTicket t4 = new SellTicket();
t1.start();
t2.start();
t3.start();
t4.start();
}
}
private int tickets = 20;//初始化有20张票
注意这个地方,如果我执行程序,会发现我实际上卖了80张票。原因是什么?很简单,我们的四个线程实际上在四个栈中工作,我们创建一个线程的同时,我们的ticket变量也创建了一次。实际上四个线程分别对他们的四个tickets变量在进行操作,也就是卖了80张票。
那么有人会说,我给ticket加上static如何,解决了票是多线程共有的问题。但是还会有一个同步的问题,我留作下次再说。
现在我说一下,如果我不加static,是否可以实现题目要求呢。当然可以,换句话说,我们实际上是对一个tickets进行操作的。我只要在程序中只创建一个tickets就足够了。下面来看一下多线程的第二种写法,Runnable。
Runnable是一个接口,我们需要实现它的run()方法
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
那么我们再来看Thread的构造方法,
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
我们只需要将实现了Runnable接口的对象传进去就可以了。这样做实际上我们只创建了一个tickets变量,这个变量是多线程所共有的。这样也符合我们面向对象中的封装思想,将资源和操作资源的方法(线程)分离。
代码如下
package com.zzu.my.thread.test.runnable;
/**
* 创建多线程的第二种方法<br>
* 定义一个类实现Runnable<br>
* 解耦: 将线程对象和具体的任务进行解耦合<br>
* @author Administrator
*
*/
/**
* 定义一个类实现Runnable接口<br>
* 覆盖Runnable接口中的run()方法,将线程要运行的任务代码存储到该方法中。<br>
* 通过Thread类创建对象,并将实现了Runnable接口的对象作为Thread类的构造函数进行传递<br>
* 调用Thread类的start方法开启线程<br>
* @author Administrator
*
*/
class Ticket implements Runnable{
private int tickets = 20;
public void run() {
while(true){
synchronized (Ticket.class) {
if(tickets>0){
try {
Thread.sleep(100);//让线程到这个地方停止一段时间
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("窗口: "+Thread.currentThread().getName()+"出售了第"+tickets--+"张票");
}else{
break;
}
}
}
}
}
//将资源和线程进行分离
public class TicketDemo {
public static void main(String[] args) {
Ticket ticket = new Ticket();
//创建Thread 对象
//创建4个对象执行的是一个对象内的run()方法
Thread t1 = new Thread(ticket);
Thread t2 = new Thread(ticket);
Thread t3 = new Thread(ticket);
Thread t4 = new Thread(ticket);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
当然,这两种写法都没有考虑到线程的同步问题。
换句话说,多线程的执行是随机的,一个线程在执行过程中可能会因为各种情况被cpu剥夺执行权,这个时候如果run()方法里面涉及到共享变量,就会出现参数的错误。比如我在执行到要执行tickets自减一的时候,cpu剥夺了执行权。交给其他线程,这个时候相当于tickets在另一个线程中要减一。这就相当于同一张票卖了两次。实际上我们应当保证run()方法内部代码的原子性,那么怎么实现呢。我会在我的下一篇博客中写出来。