多线程的那些事(3)JAVA多线程设计

主要介绍JAVA多线程的一些基本概念,包括如何实现多线程,多线程之间如何共享数据,还有典型的生产者消费者模式。

1 JAVA多线程

Java在语言级提供了对多线程程序设计的支持。实现多线程程序的两种方式:

(1)实现Runnable接口

【例1】

//通过实现Runnable接口实现线程的共享变量
public class MyThread implements Runnable 
{
	     int index=0;  //多个线程共享变量
	     //线程的入口函数
       public void run()
       {    synchronized (this){ //同步
	            System.out.println(Thread.currentThread().getName()); 
       	        System.out.println(index++); 
			} //end synchronized      
       }
}
//----------------------------------------------------------------------
public class MultiThread
{
    public static void main(String[] args)
    {
    	  MyThread mt=new MyThread();
    	  //开始执行线程:mt作为参数传入是相同的对象mt,所以访问的MyThread中的变量也是同一个变量(多线程访问了同一种资源)
    	  Thread thread1=new Thread(mt);//第一个线程
          Thread thread2=new Thread(mt);//第二个线程
		  thread1.start();
		  thread2.start();	   
    }
}
(2)从Thread类继承,必须重写run()方法
(一个Thread对象代表了进程中的一个线程)

//定义线程
public class MyThread extends Thread
{
	     //线程的入口函数
       public void run()
       {
       	        System.out.println(getName());       	        
       	        yield();    //暂停该线程,允许其他线程运行   
       }
}
//-----------------------------------------------------------------
public class MultiThread
{
    public static void main(String[] args)
    {
    	  MyThread thread1=new MyThread(); //第一个线程
    	  thread1.start();     	          
    	  
    	  MyThread thread2=new MyThread();//第二个线程
    	  thread2.start();

    	  System.out.println("main:"+Thread.currentThread().getName());   	   
    }
}
(3)两种实现方式比较

使用“实现Runnable接口”实现线程,通常不需修改线程类(Thread)中的方法(除了run方法)时,使用Runnable接口实现线程。有以下两个好处:
当类已经继承了一个类,java不允许多继承,此时只能用Runnable接口;
当多个线程需要访问同一种资源时,如:如果每个线程执行的代码相同,可以使用同一个Runnable对象,这个Runnable对象中有那个共享数据(像上述例子中的index变量,为2个线程共享)。

2 同步

在介绍什么是同步之前,我们首先要明白为什么要同步?

2.1 为什么要同步

    当多个线程对同一个数据进行操作时,可能会导致每个线程拿到数据的状态不一致,因为此时每个线程对这个共享数据的操作对其他线程是不可见的。数据同步就是指在同一时间,只能由一个线程来访问被同步的类变量,当前线程访问完这些变量后,其他线程才能继续访问。这里说的访问是指有写操作的访问,如果所有访问类变量的线程都是读操作,一般是不需要数据同步的。如下图[1],当B执行到与A相同的锁监视的同步块时,A在同步快之中或之前所做的每件事,对B都是可见的。如果没有同步,就没有这样的保证。


何时需要同步?当访问共享的、可变的数据要求时要使用同步。[1]


2.2 JAVA中如何实现同步

The code segments within a program that access the same object from separate, concurrent threads are called “critical sections”(临界区)。
每一个对象都有一个监视器,或者叫做锁。
同步方法利用的是this所代表的对象的锁。
每个class也有一个锁,是这个class所对应的Class类对象的锁。
JAVA同步的两种方式:同步块和同步方法。

【例2】

/*
火车票售票系统(线程同步的2种方法):四个线程,同时卖这100张票
*/
public class TicketsSystem
{
	  public static void main(String[] args)
	  {
	  	//创建四个线程,同时卖这100张票(共享同一个资源tickets)
	  	SellThread st=new SellThread();
	  	
	  	new Thread(st).start();
	  	new Thread(st).start();
	  	new Thread(st).start();
	  	new Thread(st).start();
	  }
}

public class SellThread implements Runnable
{
      int tickets=100; //有100张票,多线程共享变量,注意同步
      Object obj=new Object();
      public void run()
      {
            //当前时哪个线程卖了第几张票
            while(true)
            {
            	/*【同步块】:synchronized(对象){临界区代码}   //每一个对象都有一个监视器,或者叫做锁,用该对象来标识锁(加锁,解锁)
            	  线程A进入同步块前检查对象obj是否加锁:
            	  若加锁,等待;
            	  若已解锁,进入同步块,并将对象obj加锁,直到出来同步块时,将对象解锁
            	*/
            	/* synchronized(obj)
            	{
            	       if(tickets>0)
            	      {
            	   	        try
            	   	        {
            	   	        	  Thread.sleep(10);
            	   	        }
            	   	        catch(Exception e)
            	   	        {
            	   	             e.printStackTrace();
            	   	        }
            	            System.out.println(Thread.currentThread().getName()+
                                             "sell tickets:"+tickets);
                            tickets--;
            	      }  
            	}*/
            	sell();
            }//end while
      }//end run()
      
	  //【同步方法】:在方法前加关键字synchronized(给方法中的this对象加锁和解锁)
      public synchronized void sell()
      {
          if(tickets>0)
          {
            	   	        try
            	   	        {
            	   	        	  Thread.sleep(10);
            	   	        }
            	   	        catch(Exception e)
            	   	        {
            	   	             e.printStackTrace();
            	   	        }
							
            	            System.out.println(Thread.currentThread().getName()+ "sell tickets:"+tickets); //模拟买票
                            tickets--;
           }  
      } //end sell()
} //class SellThread

3 生产者消费者模型

实际上,准确说应该是“生产者-消费者-仓储”模型,离开了仓储,生产者消费者模型就显得没有说服力了,这里的仓库是多线程之间共享的数据。对于此模型,应该明确一下几点:

1)生产者仅仅在仓储未满时候生产,否则暂停生产。
2)消费者仅仅在仓储有产品时候才能消费,否则等待。
3)当消费者发现仓储没产品可消费时候会通知生产者生产。
4)生产者在生产出可消费产品时候,应该通知等待的消费者去消费。
例子来自[2]。
仓库类因为是多线程共享的数据,所以要设计为一个线程安全的类。
//----------------------------------------------------------------------
/** 
* 存放共享数据的容器,此处为仓库
* 设计为线程安全的类,供消费线程安全使用
*/ 
public class ShareData { 
        private static final int max_size = 100; //最大库存量 
        private int curnum;     //当前库存量 

        ShareData() { 
        } 

        ShareData(int curnum) { 
                this.curnum = curnum; 
        } 

        /** 
         * 生产指定数量的产品  
         * @param neednum 
         */ 
        public synchronized void produce(int neednum) { 
                //是否需要生产 
                while (neednum + curnum > max_size) { 
                        System.out.println("要生产的产品数量" + neednum + "超过剩余库存量" + (max_size - curnum) + ",暂时不能执行生产任务!"); 
                        try { 
                                wait(); //当前的生产线程等待 
                        } catch (InterruptedException e) { 
                                e.printStackTrace(); 
                        } 
                } 
                //满足生产条件,则进行生产,这里简单的更改当前库存量 
                curnum += neednum; 
                System.out.println("已经生产了" + neednum + "个产品,现仓储量为" + curnum); 

                notifyAll();   //唤醒在此对象监视器上等待的所有线程 
        } 

        /** 
         * 消费指定数量的产品 
         * @param neednum 
         */ 
        public synchronized void consume(int neednum) { 
                //是否可消费 
                while (curnum < neednum) { 
                        try { 
                                wait();  //当前的生产线程等待 
                        } catch (InterruptedException e) { 
                                e.printStackTrace(); 
                        } 
                } 
                //满足消费条件,则进行消费,这里简单的更改当前库存量 
                curnum -= neednum; 
                System.out.println("已经消费了" + neednum + "个产品,现仓储量为" + curnum); 

                notifyAll();  //唤醒在此对象监视器上等待的所有线程 
        } 
} 
//----------------------------------------------------------------------
生产者线程
//----------------------------------------------------------------------
/** 
* 生产者 
*/ 
public class Producer implements Runnable { 
        private int neednum;                //生产产品的数量 
        private ShareData shareData;            //仓库 

        Producer(int neednum, ShareData shareData) { 
                this.neednum = neednum; 
                this.shareData = shareData; 
        } 

        public void run() { 
                //生产指定数量的产品 
                shareData.produce(neednum); 
        } 
} //end class Producer
//----------------------------------------------------------------------
消费者线程
/** 
* 消费者 
*/ 
public class Consumer implements Runnable  { 
        private int neednum;                //生产产品的数量 
        private ShareData shareData;            //仓库 

        Consumer(int neednum, ShareData shareData) { 
                this.neednum = neednum; 
                this.shareData = shareData; 
        } 

        public void run() { 
                //消费指定数量的产品 
                shareData.consume(neednum); 
        } 
}//end class Consumer
整个工作的任务单
//----------------------------------------------------------------------
/** 
* Java线程:并发协作-生产者消费者模型 
*/ 
public class Job { 
        public static void main(String[] args) { 
                ShareData shareData = new ShareData(30);  //共享数据
				
                Thread c1 = new new Thread(new Consumer(50, shareData)); 
				Thread c2 = new new Thread(new Consumer(20, shareData)); 
				Thread p1 = new new Thread(new Producer(10, shareData)); 
				Thread p2 = new new Thread(new Producer(10, shareData));
				Thread p3 = new new Thread(new Producer(10, shareData));
				Thread p4 = new new Thread(new Producer(10, shareData));
				Thread p5 = new new Thread(new Producer(10, shareData));
				Thread p6 = new new Thread(new Producer(10, shareData));
				Thread p7 = new new Thread(new Producer(80, shareData));				

                c1.start(); 
                c2.start(); 
                c3.start(); 
                p1.start(); 
                p2.start(); 
                p3.start(); 
                p4.start(); 
                p5.start(); 
                p6.start(); 
                p7.start(); 
        } 
} //end class Job
//----------------------------------------------------------------------
说明:wait,notify,notifyAll
1)每一个对象除了有一个锁之外,还有一个等待队列(wait set),当一个对象刚创建的时候,它的等待队列是空的。
2)我们应该在当前线程锁住对象的锁后,去调用该对象的wait方法
(有两个含义:wait方法只能在同步块或同步方法中被调用;当调用wait方法时,该线程进入了该对象的等待队列中)。
3)当调用对象的notify方法时,将从该对象的等待队列中删除一个任意选择的线程,这个线程将再次成为就绪状态的线程。
4)当调用对象的notifyAll方法时,将从该对象的等待队列中删除所有等待的线程,这些线程将成为就绪状态的线程。
notifyAll() 方法,起到的是一个通知作用,不释放锁,也不获取锁。只是告诉该对象上等待的线程“可以竞争执行了,都醒来去执行吧”。
5)wait和notify主要用于producer-consumer(生产者-消费者)这种关系中,并且只能在同步方法或同步块中调用,并且是针对同一个对象的等待和通知。
更多JAVA线程的常用方法见博客“进程和线程

4 线程通信

JAVA中实现多线程间通信则主要采用"共享变量"和"管道流"这两种方法。

4.1 线程通信-共享变量

1、如果每个线程执行的代码相同,可以使用同一个Runnable对象,这个Runnable对象中有那个共享数据,例如,卖票系统就可以这么做。如【例2】
2、如果每个线程执行的代码不同,这时候需要用不同的Runnable对象,有如下两种方式来实现这些Runnable对象之间的数据共享:
(1)将这些Runnable对象作为某一个类中的内部类,共享数据作为这个外部类中的成员变量,每个线程对共享数据的操作方法也分配给外部类,以便实现对共享数据进行的各个操作的互斥和通信,作为内部类的各个Runnable对象调用外部类的这些方法。

public class WorkThread {
   int shareData=1; //共享数据
   
   public void newJob(){
      WorkThreadA workThreadA=new WorkThreadA();
	  Thread threadA=new Thread(workThreadA); //线程A
	  
	  WorkThreadB workThreadB=new WorkThreadB();
	  Thread threadB=new Thread(workThreadB); //线程B
	  
	  threadA.start();
	  threadB.start();
   }//end newJob()
   
//线程A  
class WorkThreadA implements Runnable{
   public void run(){
    ...
   }
}//end class WorkThreadA

//线程B 
class WorkThreadB implements Runnable{
   public void run(){
    ...
   }
}//end class WorkThreadB

}//end class WorkThread

(2)将共享数据封装在另外一个对象中,然后将这个对象逐一传递给各个Runnable对象。

//------------------------------------------------------------
public class WorkThread {
   public void newJob(){
      ShareData shareData=new ShareData(); //多个线程使用同一个对象,所以对象中的成员数据也是共享数据
   
      WorkThreadA workThreadA=new WorkThreadA(shareData);
	  Thread threadA=new Thread(workThreadA); //线程A
	  
	  WorkThreadB workThreadB=new WorkThreadB(shareData);
	  Thread threadB=new Thread(workThreadB); //线程B
	  
	  threadA.start();
	  threadB.start();
   }//end newJob()

}//end class WorkThread

//------------------------------------------------------------
//共享数据封装:将共享数据封装在一个线程安全的类中
//外部消费线程使用该类说明:本类已经封装为一个线程安全的类,通过使用方法getShareData(),setShareData对共享数据进行操作,
//外部消费线程无需在同步
public class ShareData{
   private int shareData; 
	
   //外部消费线程,通过以下同步方法对sharData进行操作
   public synchronized int getShareData(){
      return shareData;
   }//end getShareData
   
   public synchronized int setShareData(int shareData){
      return this.shareData=shareData;
   }//end setShareData
}//end class ShareData

//------------------------------------------------------------
//线程A  
public class WorkThreadA implements Runnable{
   private ShareData shareData;
   
   public WorkThreadA(){}
   
   public WorkThreadA(ShareData shareData){ 
     this.shareData=shareData;
   }

   public void run(){
    ...
	WorkThread.getShareData(); //因为共享数据已经封装为一个线程安全的类,消费线程无需在同步了
    ...
	WorkThread.setShareData();
	...
   }
}//end class WorkThreadA

//------------------------------------------------------------
//线程B 
public class WorkThreadB implements Runnable{
   private ShareData shareData;
 
   public WorkThreadB(){}
   
   public WorkThreadB(ShareData shareData){
     this.shareData=shareData;
   }

   public void run(){
    ...
	WorkThread.getShareData();
    ...
	WorkThread.setShareData();
	...
   }
}//end class WorkThreadB

(3)总之,要同步互斥的几段代码最好是分别放在几个独立的方法中,这些方法再放在同一个类中,这样比较容易实现它们之间的同步互斥和通信。
(4)极端且简单的方式,即在任意一个类中定义一个static的变量,这将被所有线程共享。

4.2 线程通信-管道流

例子来自[3]
public class CommunicateWhitPiping {
	public static void main(String[] args) {
		/**
		 * 创建管道输出流
		 */
		PipedOutputStream pos = new PipedOutputStream();
		/**
		 * 创建管道输入流
		 */
		PipedInputStream pis = new PipedInputStream();
		try {
			/**
			 * 将管道输入流与输出流连接 此过程也可通过重载的构造函数来实现
			 */
			pos.connect(pis);
		} catch (IOException e) {
			e.printStackTrace();
		}
		/**
		 * 创建生产者线程
		 */
		Producer p = new Producer(pos);
		/**
		 * 创建消费者线程
		 */
		Consumer c = new Consumer(pis);
		/**
		 * 启动线程
		 */
		p.start();
		c.start();
	}
}

/**
 * 生产者线程(与一个管道输入流相关联)
 * 
 */
class Producer extends Thread {
	private PipedOutputStream pos;

	public Producer(PipedOutputStream pos) {
		this.pos = pos;
	}

	public void run() {
		int i = 8;
		try {
			pos.write(i);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

/**
 * 消费者线程(与一个管道输入流相关联)
 * 
 */
class Consumer extends Thread {
	private PipedInputStream pis;

	public Consumer(PipedInputStream pis) {
		this.pis = pis;
	}

	public void run() {
		try {
			System.out.println(pis.read());
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

参考文献

[1]《Java Concurrency in Practice》

[2]http://lavasoft.blog.51cto.com/62575/221932

[3] http://my.oschina.net/u/248570/blog/53226

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值