黑马程序员——Java基础---多线程

------Java培训、Android培训、iOS培训、.Net培训、期待与您交流! -------

一、多线程

1.多线程概述

 要研究多线程,我们首先要知道什么叫进程?说起进程大家应该比较熟悉,我们经常用任务管理器来管理我们计算机上运行的进程,如下图所示:
黑马程序员——Java基础---多线程_第1张图片
   
   可以看到,计算机中每个正在运行的程序都至少创建了一条进程,有的甚至创建了多个进程如Chrome。
直白点说,进程就是一个正在进行中的程序。
   每一个进程都有一个执行顺序,这个顺序是一个执行路径,或者叫做控制单元。
线程就是进程中独立的控制单元,线程控制着进程的执行,只要进程中有一个线程在执行,进程就不会停止。所以一个进程至少有一条线程存在。
   那么多线程,顾名思义,就是对一个进程中多条线程同时运行的状态的描述。
   举个比较大家熟知的例子,就是我们在执行一个java程序时,我们已经知道的除了执行main函数的那条线程外,还有一条负责垃圾回收的线程也在同时运行着。
   通过这个例子我们也能了解到多线程存在的意义,试想一下,如果线程只能一个接一个按顺序执行,当代码比较多,形成了许多的空引用对象垃圾,但是负责垃圾回收的线程没有及时执行时,就会造成内存不足的现象,但如果能多线程,保持垃圾回收这条线程同时在执行,就可以及时的管理内存环境,所以多线程能让程序具有同时运行的效果,提高程序执行效率。

2.多线程创建

   了解了多线程的好处,我们就要学会使用多线程,这里我们首先要学会创建多线程。
   多线程创建主要有两种方式:继承和实现。下面我们一一介绍:

1.继承Thread方式

     为什么要继承Thread类呢?具体要怎么做呢?
   所以将查API养成一个习惯非常重要,因为API文档已经给了我们答案。
   查找API文档,通过他的描述我们发现,原来java已经做了对线程这类事物的描述,所定义的类正是这个Thread,那么通过之前学过的知识,我们只要继承这个类就好了。(下图截自API文档)
黑马程序员——Java基础---多线程_第2张图片
   
 









   这里有两点要注意:
   1.为什么要重写run方法:因为Thread类中定义的run()方法功能就是存储要执行多线程的代码。你想将哪些代码实现多线程,就将哪些代码写到run方法中。
   2.为什么要写start()方法:调用这个线程的start方法的作用就是启动这个线程,如果你直接调用run方法,启动的还是一个线程,并不会实现多线程。
下面代码演示下:
/**
 * 需求:创建多线程和主函数交替执行。
 * 思路:继承Thread类,将要执行的代码存储进run方法
 * @author jinlong
 * */
package com.blog.part3.多线程;
//创建线程Test
class Test extends Thread
{    
	 private String name;
	//构造函数
	 Test(String name)
	 {
		 this.name=name;
	 }
	 
	 //复写run方法,将要执行多线程的代码写进去
	 public void run()
	 {
		 for(int x=0;x<60;x++)
			 //这里说一些Thread.currentThread().getName()方法是为了获取正在运行的进程的名字
			 //每个进程在运行时都有默认名字,当然也可以人为赋予名字
			System.out.println(this.name+".....正运行....."+x);
	 }
	
}
public class ThreadDemo {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
       //创键两个线程
		Test a=new Test("线程一");
		Test b=new Test("线程二");
		//启动这两个线程
		a.start();
		b.start();
		
		//设置下主线程执行的代码
		for(int x=0;x<200;x++)
			System.out.println("主线程运行ing");
	}

}
运行结果:
黑马程序员——Java基础---多线程_第3张图片
   我们可以看到,主函数和程序一、程序二明显的出现了交替打印的情况。我们可以知道,这三条线程是在同时执行的,只不过打印出来的时候存在先后。

2.实现Runnable接口

    同样的我们去查API文档:
黑马程序员——Java基础---多线程_第4张图片
   从这段话,我们可以看出,第二种创建线程的方法是为了弥补第一种方法的不足而开发出来的
   总结下:
     1.首先java是只支持单继承的,一个类已经继承了其他类那么就无法再继承Thread类了
     2.Thread类本身也实现了Runnable接口,原来run方法其实来自Runnable接口。
     3.再就是像API中所说的规范了,除非打算修改或增强类的基本行为,否则不该为该类创建子类。
下边举例具体创建过程:
/**
 * 需求:用实现Runnble方式创建多线程,解决多窗口卖票问题。
 * 思路:1.创建类实现Runnable接口,把代码写入run方法。
 *           2.创建实现接口的子类实例对象。
 *           3.将对象作为构造函数参数,创建四条不同线程并且启动。
 * @author jinlong
 * */
package com.blog.part3.多线程;
//创建Ticket类实现Runnable接口
class Ticket implements Runnable 
{
    //定义变量,表示票剩余量
	private int tick=500;
	@Override
	//重写run方法
	public void run() 
	{
		while(true)
		{
			if(tick>0)
			{
				//显示当前线程以及剩余票
				System.out.println(Thread.currentThread().getName()+"正在卖第"+tick--+"张票");
			}
		}
		
	}
	
}
public class ThreadDemo1
{
	public static void main(String[] args)   
	{
		
      //创建对象
		Ticket t=new Ticket();
		
		//有多个窗口在同时卖票,创建多个Thread对象表示
		//这里每个Thread对象就是一个Thread线程。
		Thread t1=new Thread(t);
		Thread t2=new Thread(t);
		Thread t3=new Thread(t);
		Thread t4=new Thread(t);
		//启动这四个线程
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}
 

     为什么要将Runnable接口的子类对象传递给Thread的构造函数?

    因为,自定义的run方法所属的对象是Runnable接口的子类对象。所以要让线程去指定对象的run方法,就必须明确该run方法所属对象。 

 通过上变代码,我们可以发现这四个进程在交替执行,注意如果没有交替出现,可能是初始票数量不够大。
 总结下第二种方法的步骤:

创建步骤:

        a、定义类实现Runnable的接口。

        b、覆盖Runnable接口中的run方法。目的也是为了将线程要运行的代码存放在该run方法中。

        c、通过Thread类创建线程对象。

        d、Runnable接口的子类对象作为实参传递给Thread类的构造方法。

        e、调用Thread类中start方法启动线程。start方法会自动调用Runnable接口子类的run方法。

              实现方式好处:避免了单继承的局限性。在定义线程时,建议使用实现方式。


二、同步

       上边我们讲了什么是多线程,以及怎么创建多线程,现在我们接着研究多线程的安全性问题。

1.安全问题

1.线程状态

              想要研究同步我们首先要知道线程的几种存在状态:

         被创建:等待启动,调用start启动。

         运行状态:具有执行资格和执行权。

         临时状态(阻塞):有执行资格,但是没有执行权。

         冻结状态:遇到sleep( )方法和wait( )方法时,失去执行资格和执行权,sleep方法时间到或者调用notify()方法唤醒时时,获得执行资格,变为                                   临时状态。

         消忙状态:stop( )方法,或者run方法结束。

        图解:

黑马程序员——Java基础---多线程_第5张图片



2.产生原因

       为什么线程会有安全问题呢? 比如上边我们举的四个窗口同时卖票的例子,有时会出现这种结果
黑马程序员——Java基础---多线程_第6张图片
      Thread-2已经把最后一张票卖掉了,但是后边其他三个线程又个卖了一张票出现了负数票。出现这种情况的原因,在代码这一部分:
			if(tick>0)
			{
				//显示当前线程以及剩余票
				System.out.println(Thread.currentThread().getName()+"正在卖第"+tick-- +"张票");
			}
          原理是当Thread-2卖到最后一张票,执行到System打印语句时,但没执行tick--时,此时tick值为1,   CPU切换到Thread-1、Thread-2、Thread-3之后,这些线程依然可以通过if判断,从而执行“tick--”的操作,因而出现了0、-1、-2的情况。
          总结下:
    1. 多个线程在操作共享的数据。
    2. 操作共享数据的线程有多条。当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算,就会导致线程安全问题的产生。

2.同步

        针对上边出现的问题,怎么解决呢?
        思路:对多条操作共享数据的语句,只能让一个线程都执行完。在执           行过程中,其他线程不可以参与执行。
        java中对于多线程的安全问题提供了专业的解决方式——synchronized(同步)。
        同步也有两种实现方式:同步代码块和同步函数。都是利用关键字synchronized来实现。
        
1.同步代码块       

        用法:

                  synchronized(对象)

                  {需要被同步的代码}

        同步可以解决安全问题的根本原因就在那个对象上。其中对象如同锁。持有锁的线程可以在同步中执行。没有持有锁的线程即使获取cpu的执行权,也进不去,因为没有获取锁。


示例:

/*	
给卖票程序示例加上同步代码块。
*/
class Ticket implements Runnable
{
	private int tick=100;
	Object obj = new Object();
	public void run()
	{
		while(true)
		{
			//给程序加同步,即锁
			synchronized(obj)
			{
				if(tick>0)
				{
					try
					{	
						//使用线程中的sleep方法,模拟线程出现的安全问题
						//因为sleep方法有异常声明,所以这里要对其进行处理
						Thread.sleep(10);
					}
					catch (Exception e)
					{
					}
					//显示线程名及余票数
					System.out.println(Thread.currentThread().getName()+"..tick="+tick--);
				}
			}	
		}
	}
}

         上图代码显示安全问题已被解决,原因在于Object对象相当于是一把锁,只有抢到锁的线程,才能进入同步代码块向下执行。因此,当tick=1时,CPU切换到某个线程后,如上图的Thread-3线程,其他线程将无法通过同步代码块继而进行if判断语句,只有等到Thread-3线程执行完“tick--”操作(此后tick的值为0),并且跳出同步代码块后,才能抢到锁。其他线程即使抢到锁,然而,此时tick值已为0,也就无法通过if语句判断,从而无法再执行“tick--”的操作了,也就不会出现0、-1、-2等情况了。        

2.同步函数  

        格式:

                在函数上加上synchronized修饰符即可。

        那么同步函数用的是哪一个锁呢?

        函数需要被对象调用。那么函数都有一个所属对象引用。就是this。所以同步函数使用的锁是this

示例:

class Ticket implements Runnable
{
	private int tick=100;
	Object obj = new Object();
	public void run()
	{
		while(true)
		{
			show();
		}
	}
  //直接在函数上用synchronized修饰即可实现同步
public synchronized void show()
{
		if(tick>0)
	    {
		try
		{	
			//使用线程中的sleep方法,模拟线程出现的安全问题
			//因为sleep方法有异常声明,所以这里要对其进行处理
			Thread.sleep(10);
		}
		catch (Exception e)
		{
		}
		//显示线程名及余票数
		System.out.println(Thread.currentThread().getName()+"..tick="+tick--);
	}
}	
}

       上图代码,Synchronized锁住了show函数,控制了不同线程对象对show函数调用,同样解决了安全问题。

3.同步的利弊

       1、同步的前提

        a,必须要有两个或者两个以上的线程。

        b,必须是多个线程使用同一个锁。

        2、同步的利弊

        好处:解决了多线程的安全问题。

        弊端:多个线程需要判断锁,较为消耗资源。

        3、如何寻找多线程中的安全问题

        a,明确哪些代码是多线程运行代码。

        b,明确共享数据。

        c,明确多线程运行代码中哪些语句是操作共享数据的。


4.静态同步

   上边我们一直再说,同步的关键是锁住对象,那么当遇到静态函数时怎么办呢?  是谁在使用函数的锁呢?

       通过验证,发现不在是this。因为静态方法中也不可以定义this。静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象。如:

        类名.class    该对象的类型是Class或者 this.getClass

       这就是静态函数所使用的锁。而静态的同步方法,使用的锁是该方法所在类的字节码文件对象。类名.class

       

/*
加同步的单例设计模式————懒汉式
*/
class Single
{
	private static Single s = null;
	private Single(){}
        //函数被静态修饰了
	public static void getInstance()
	{
		if(s==null)
		{
                       //使用锁的对象其实是Single.class
			synchronized(Single.class)
			{
				if(s==null)
					s = new Single();
			}
		}
		return s;
	}
}

3.同步下的单例模式

    恶汉式:不存在安全问题。因为不存在多个线程共同操作数据的问题。
class Single{
       private static final Single s = new Single();
       private Single(){}
       public static Single getInstance(){
             return s ;
      }
}
    懒汉式:
class Single{
      private static Single s = null;
       private Single(){} 
       public static Single getInstance(){
             if(s ==null){
                   synchronized(Single.class){
                         if(s == null)
                               s = new Single();
                  }
            }
            return s ;
      }
}
    懒汉式存在安全问题,可以使用同步函数解决。
    但若直接使用同步函数,则效率较低,因为每次都需要判断。

黑马程序员——Java基础---多线程_第7张图片
      但若采取如下方式,即可提升效率。

黑马程序员——Java基础---多线程_第8张图片 

102.png (5.23 KB, 下载次数: 0)

下载附件  保存到相册

2015-6-6 16:25 上传



      双重否定的方法:
     任何一个线程在执行到第一个if判断语句时,如果Single对象已经创建,则直接获取即可,而不用判断是否能够获取锁,相对于上面使用同 步函数的方法就提升了效率。如果当前线程发现Single对象尚未创建,则再判断是否能够获取锁。
    1. 如果能够获取锁,那么就通过第二个if判断语句判断是否需要创建Single对象。因为可能当此线程获取到锁之前,已经有一个线程创建完   Single对象,并且放弃了锁。此时它便没有必要再去创建,可以直接跳出同步代码块,放弃锁,获取Single对象即可。如果有必要,则再创建。
    2. 如果不能获取到锁,则等待,直至能够获取到锁为止,再按步骤一执行。
           当然这种办法也并不是很完美, 参考连接:http://www.infoq.com/cn/articles/double-checked-locking-with-delay-initialization/

4.死锁

      当同步中嵌套同步时,就会出现死锁。
示例1:
/**
 * 需求:设计演示死锁
 * 思路:利用标记更改判断条件,设计嵌套形成死锁
 * @author jinlong
 * */

package com.blog.part3.多线程;

class Ticket implements Runnable
{
    private static int num = 100;
    Object obj = new Object();
    //设计标记flag控制状态
    boolean flag = true;

    public void run()
    {
          if(flag )
          {
                while(true )
                {
                      //函数加锁
                      synchronized(obj )
                      {
                           show();
                     }
               }
         } else
                while(true )
                     show();
   }
     //函数上加锁
    public synchronized void show()
    {
          synchronized(obj )
          {
                if(num > 0)
                {
                	//异常处理
                      try{
                    	  //休眠200毫秒
                           Thread. sleep(200);
                     } catch(InterruptedException e){
                           e.printStackTrace();
                     }
                     System.out.println(Thread.currentThread().getName() + "...function..." + num--);
               }
         }
   }
}

class DeadLockDemo
{
    public static void main(String[] args)
    {
    	//实例化。
         Ticket t = new Ticket();
         //创键两个线程。
         Thread t1 = new Thread(t);
         Thread t2 = new Thread(t);
         
         t1.start();
          try{
               Thread. sleep(200);
         } catch(InterruptedException e){
               e.printStackTrace();
         }
          //改变标记状态
         t. flag = false ;
         t2.start();
   }
} 

运行结果:
黑马程序员——Java基础---多线程_第9张图片
       由上图可以看到程序已经被锁死,无法向下执行。  
分析形成原因:上边代码,会出现一种情况,线程1启动时,flag为true,那么线程1通过了if语句判断,执行到obj对象锁   ,将要执行show函数。与此同时,线程2也启动了,这时flag改为了false,那么线程2通过else语句执行了show函数,但是show函数本身也有同步锁,当线程2也执行到show函数的获取obj锁这一步时,程序卡住了。

示例2:
/**
 * 需求:演示死锁
 * 思路:设计两个对象,每一个线程都要经过两个对象锁,但是两个对象锁的执行顺序不同。
 *           预期中目标是:线程1加载了a锁正要用b锁,于此同时线程2加载了b锁正要用a锁。
 *           这样a,b两个对象锁都有人使用同时又都需要被使用。形成死锁
 * @author jinlong
 * */

package com.blog.part3.多线程;
//定义Test类
class Test implements Runnable{
	//私有函数控制Test状态
    private boolean flag ;
    //根据传入值初始化flag
   Test( boolean flag){
          this.flag = flag;
   }

    public void run()
    {
    	 //根据flag值,将两条线程分开
          if(flag ){
                while(true )
                	//这里先执行locka对象锁
                      synchronized(MyLock.locka){
                           System.out.println(Thread.currentThread().getName() + "...if locka...");
                            synchronized(MyLock.lockb){
                                 System.out.println(Thread.currentThread().getName() + "...if lockb...");
                           }
                     }
         } else{
                while(true )
                	//这里先执行lockb对象锁
                      synchronized(MyLock.lockb){
                           System.out.println(Thread.currentThread().getName() + "...else lockb...");
                            synchronized(MyLock.locka){
                                 System.out.println(Thread.currentThread().getName() + "...else locka...");
                     }
               }
         }
   }
}

class MyLock{
	//设计两个对象locka和lockb
    public static final Object locka = new Object();
    public static final Object lockb = new Object();
}

class DeadLockDemo1{
    public static void main(String[] args){
    	//根据传入参数,赋予ab两个线程不同路线。
         Test a = new Test(true );
         Test b = new Test(false );

         Thread t1 = new Thread(a);
         Thread t2 = new Thread(b);

         t1.start();
         t2.start();
   }
}
运行结果:
黑马程序员——Java基础---多线程_第10张图片

可见达到了预期结果,此时Thread-1需要lockb锁,线程Thread-0需要locka锁,但是他们互相占用了对方需要的锁,出现死锁。

三、线程间通信

1.基本概念 

   多个线程在处理统一资源,但是任务却不同,这时候就需要线程间通信。

    等待/唤醒机制涉及的方法:
      1. wait():让线程处于冻结状态,被wait的线程会被存储到线程池中。
      2. notify():唤醒线程池中的一个线程(任何一个都有可能)。
      3. notifyAll():唤醒线程池中的所有线程。


    P.S.
    1、这些方法都必须定义在同步中,因为这些方法是用于操作线程状态的方法。
    2、必须要明确到底操作的是哪个锁上的线程!
    3、wait和sleep区别?
         1)wait可以指定时间也可以不指定。sleep必须指定时间。
         2)在同步中时,对CPU的执行权和锁的处理不同。
         wait:释放执行权,释放锁。
         sleep:释放执行权,不释放锁。


    为什么操作线程的方法wait、notify、notifyAll定义在了object类中,因为这些方法是监视器的方法,监视器其实就是锁。
锁可以是任意的对象,任意的对象调用的方式一定在object类中

2.经典案例   

经典案例一:
/**
 * 需求:间隔打印mike和lili的信息,
 * 思路:设计两个线程,控制这两个线程有规律的交替执行,
 *           同时要保证每一条数据的完整性。不能出现名字性别不对应情况。
 *@author jinlong           
 * */
package com.blog.part3.多线程;
//定义Person类
class Person{
	//定义Person两个基本属性,name和sex
    private String name ;
    private String sex ;
    //定义标记
    private boolean flag = false;
    //set方法用来修改Person属性信息
    public synchronized void set(String name,String sex)
    {
          if(flag )
                try{
                	//冻结当前线程进入
                      this.wait();
               } catch(InterruptedException e){
                     e.printStackTrace();
               }
          this.name = name;
          this.sex = sex;
          flag = true ;
          //唤醒其他线程
          this.notify();
   }
   //out方法用来打印输出name和sex
   public synchronized void out()
   {
          if(!flag )
                try{
                      this.wait();
               } catch(InterruptedException e){
                     e.printStackTrace();
               }
          System. out.println(name + "..." + sex);
          flag = false ;
          this.notify();
  } 
}

//输入类,实现Runnable,保证对Person属性的修改能同步
class Input implements Runnable
{
   Person r;
   Input(Person r){
          this.r = r;
   }

   public void run(){
          int x = 0;
          while(true ){
                if(x == 0){
                      r.set( "mike","男" );
               } else{
                      r.set( "lili","女" );
               }
                //实现交替输入mike和lili
               x = (x + 1)%2;
         }
   }
}

//定义输出类,同样保证输出信息的对应
class Output implements Runnable{
   Person r;

   Output(Person r){
          this.r = r;
   }

    public void run(){
          while(true ){
                r.out();
         }
   }
}

class PersonDemo {
    public static void main(String[] args){
          //创建资源
         Person r = new Person();
          //创建任务
         Input in = new Input(r);
         Output out = new Output(r);
          //创建线程,执行路径
         Thread t1 = new Thread(in);
         Thread t2 = new Thread(out);
          //开启线程
         t1.start();
         t2.start();
   }
}
运行结果:
黑马程序员——Java基础---多线程_第11张图片
示例二:

/**
 * 需求:演示生产消费经典案例
 * 思路:首先要保证生产和消费的同步执行。防止产生死锁。
 * */
package com.blog.part3.多线程;
//定义Res类代表产品
class Res 
{	
	//定义私有属性name代表产品名字
	private String name;
	//变量count代表产品当前数量
	private int    count;

	private boolean flag=false;
   
	 //生产方法,即生产产品,函数同步
	public synchronized void shengChan(String name)
	{
		while(flag)
			try
			{
				wait();
			}
			catch (Exception e)
			{
				
			}
		//产品名为:传入参数+产品ID+当前数量
		this.name = name +"产品ID"+count++;
		//打印当前线程名,以及当前线程生产的产品名字
		System.out.println(Thread.currentThread().getName()+"......生产者++++"+this.name);
		flag = true;
		//唤醒其他线程
		notifyAll();
	}

	 //定义xiaofei方法,同样函数同步
	public synchronized void xiaoFei()
	{
				while(!flag)
			try
			{
				wait();
			}
			catch (Exception e)
			{
				
			}
		//打印当前线程作为消费者
		System.out.println(Thread.currentThread().getName()+"......消费者"+this.name);
		flag=false;
		//唤醒其他线程
		notifyAll();
	}

		
}
//定义消费者类,用来增加产品量
class Produce implements Runnable 
{
	//私有成员变量Res类型的r
	private Res r;
    //通过构造函数给r赋值
	Produce(Res r)
	{
		this.r=r;
	}
	public void run()
	{
		while(true)
		{
			//将生产者的生产动作写如run方法
			r.shengChan("++制造商品++");
		}
	}

}
//定义消费者
class Consumer implements Runnable 
{
	   //私有成员 r
		private Res r;
    //通过构造函数给r赋值
	Consumer(Res r)
	{
		this.r=r;
	}
	public void run()
	{
		while(true)
		{
			//消费者行为
			r.xiaoFei();
		}
	}

}




class  ProConDemo
{
	public static void main(String[] args) 
	{
		//创建Res的唯一对象r
		Res r= new Res();
		//创建生产者对象p和消费者对象c,并且他们初始化参数值都为r
		Produce p= new Produce(r);
		Consumer c= new Consumer(r);
		//给生产者定义两个线程,即有两个生产者
		Thread t1= new Thread (p);
		Thread t2= new Thread (p);
		//给消费者定义两个线程,即两个消费者。
		Thread t3= new Thread (c);
		Thread t4= new Thread (c);
        //启动线程。
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}
运行结果:
黑马程序员——Java基础---多线程_第12张图片

对比示例一,有两个变化:
   1.notify变成了notifyAll:因为示例二生产者和消费者各有两个线程,而notify只能唤醒一个线程,如果唤醒的是本方的线程那么会形成死锁,没有意义。
   2.if判断变成了while。因为if判断标记只有一次,会导致不该运行的语句运行了,而while则是只有满足条件的才会执行。


3.JDK1.5新特性 

1.Lock锁 

   同步代码块对于锁的操作是隐式的。
    JDK1.5以后将同步和锁封装成了对象,并将操作锁的隐式方式定义到了该对象中,将隐式动作变成了显示动作。

    Lock接口:出现替代了同步代码块或者同步函数,将同步的隐式操作变成显示锁操作。同时更为灵活,可以一个锁上加上多组监视器。
    lock():获取锁。
    unlock():释放锁,为了防止异常出现,导致锁无法被关闭,所以锁的关闭动作要放在finally中。

    Condition接口:出现替代了Object中的wait、notify、notifyAll方法。将这些监视器方法单独进行了封装,变成Condition监视器对象,可以     任意锁进行组合。
    Condition接口中的await方法对应于Object中的wait方法。
    Condition接口中的signal方法对应于Object中的notify方法。
    Condition接口中的signalAll方法对应于Object中的notifyAll方法。

示例:
/**
 * 需求:1.生产者生产商品,供消费者使用 有两个或者多个生产者,生产一次就等待消费一次 
              有两个或者多个消费者,等待生产者生产一次就消费掉 。
              2.使用Lock、Condition
   思路:定义资源类,存取方法通过Lock实现同步
             定义生产者和消费者,各自实现Runnable接口,实现对资源类的同步操作          
 * */

package com.blog.part3.多线程;
//使用Lock需要导入包
import java.util.concurrent.locks.*;  
//定义产品资源类
class Resource   
{     
	//定义产品属性变量name和count
    private String name;  
    private int count=1;  
    //标记
    private boolean flag = false;  
      
    //多态  
    private Lock lock=new ReentrantLock();  
  
    //创建两Condition对象,分别来控制等待或唤醒本方和对方线程  
    Condition condition_pro=lock.newCondition();  
    Condition condition_con=lock.newCondition();  
     //定义生产者使用的方法,产品增加
    //p1、p2共享此方法  
    public void setProducer(String name)throws InterruptedException  
    {  
        lock.lock();//加锁  
        try  
        {  
            while(flag)//重复判断标识,确认是否生产  
                condition_pro.await();//本方等待  
  
            this.name=name+"......"+count++;//生产  
            System.out.println(Thread.currentThread().getName()+"...生产..."+this.name);//打印生产  
            flag=true;//控制生产\消费标识  
            condition_con.signal();//唤醒对方  
        }  
        finally  
        {  
            lock.unlock();//解锁,这个动作一定执行  
        }  
          
    }  
  
    //c1、c2共享此方法  
    public void getConsumer()throws InterruptedException  
    {  
        lock.lock();  //
        try  
        {  
            while(!flag)//重复判断标识,确认是否可以消费  
                condition_con.await();  
  
            System.out.println(Thread.currentThread().getName()+".消费."+this.name);//打印消费  
            flag=false;//控制生产\消费标识  
            condition_pro.signal();  
        }  
        finally  
        {  
            lock.unlock();  
        }  
  
    }  
}  
  
//生产者线程  
class Producer implements Runnable   
{  
    private Resource res;  
    Producer(Resource res)  
    {  
        this.res=res;  
    }  
    //复写run方法  
    public void run()  
    {  
        while(true)  
        {  
            try  
            {  
                res.setProducer("商品");  
            }  
            catch (InterruptedException e)  
            {  
            }  
        }  
    }  
}  
  
//消费者线程  
class Consumer implements Runnable  
{  
    private Resource res;  
    Consumer(Resource res)  
    {  
        this.res=res;  
    }  
    //复写run  
    public void run()  
    {  
        while(true)  
        {  
            try  
            {  
                res.getConsumer();  
            }  
            catch (InterruptedException e)  
            {  
            }  
        }  
    }  
  
}  
  
class  ProConDemo1
{  
    public static void main(String[] args)   
    {  
        Resource res=new Resource();  
  
        new Thread(new Producer(res)).start();//第一个生产线程 p1  
        new Thread(new Consumer(res)).start();//第一个消费线程 c1  
  
        new Thread(new Producer(res)).start();//第二个生产线程 p2  
        new Thread(new Consumer(res)).start();//第二个消费线程 c2  
    }  
}
运行结果:
黑马程序员——Java基础---多线程_第13张图片

2.停止线程

 

          在JDK 1.5版本之前,有stop停止线程的方法,但升级之后,此方法已经过时。

如果停止线程,只有一种办法,那就是让run方法结束。

   1、开启多线程运行,运行代码通常是循环结构。只要控制住循环,就可以让run方法结束,也就是线程结束。

   如:run方法中有如下代码,设置一个flag标记。那么只要在主函数或者其他线程中,在该线程执行一段时间后,将标记flag赋值false,该run方法就会结束,线程也就停止了。

  public  void run()  
{  
    while(flag)  
    {     
        System.out.println(Thread.currentThread().getName()+"....run");  
    }  
}  
   2、 上面的 1 方法可以解决一般情况,但是有一种特殊情况:就是当线程处于冻结状态。就不会读取到标记。那么线程就不会结束。

            当没有指定的方式让冻结的线程恢复到运行状态时,这时需要对冻结进行清除。强制让线程恢复到运行状态中来。这样就可以操作标记让线程结束。Thread类提供该方法interrupt();

package com.blog.part3.多线程;

class StopThread implements Runnable  
{  
    private boolean flag =true;  
    public  void run()  
    {  
        while(flag)  
        {  
        	
            System.out.println(Thread.currentThread().getName()+"....run");  
       
        }  
    }  
    public void changeFlag()  
    {  
        flag = false;  
    }  
}  
  
class  StopThreadDemo  
{  
    public static void main(String[] args)   
    {  
        StopThread st = new StopThread();  
        Thread t1 = new Thread(st);  
        Thread t2 = new Thread(st);   
        t1.start();  
        t2.start();   
  
        int num = 0;  
        while(true)  
        {  
            if(num++ == 60)  
            {  
                t1.interrupt();//清除冻结状态  
                t2.interrupt();  
                st.changeFlag();//改变循环标记  
                break;  
            }  
            System.out.println(Thread.currentThread().getName()+"......."+num);  
        }  
        System.out.println("over");  
    }  
}  

四、线程类其他方法

1.setDaemon方法:

  概念/目的:后台线程主要是为其他线程(相对可以称之为前台线程)提供服务,或“守护线程”。如JVM中的垃圾回收线程。

  生命周期:后台线程的生命周期与前台线程生命周期有一定关联。主要体现在:当所有的前台线程都进入死亡状态时,后台线程会自动死亡(其实这   个也很好理解,因为后台线程存在的目的在于为前台线程服务的,既然所有的前台线程都死亡了,那它自己还留着有什么用)。

  设置后台线程:调用Thread对象的setDaemon(true)方法可以将指定的线程设置为后台线程。


/**
 * 需求:演示守护线程
 * 思路:1.设计一个永真死循环,不打扰情况下不断自增
 *        2.不使用线程守护时,循环会一直进行下去
 *        3.对这个死循环线程使用守护后,当主函数结束时,死循环也结束
 * @author jinlong
 * */
package com.blog.part3.多线程;
import java.io.IOException;
class DaemoDemo extends Thread 
{
   public void run() 
   {   
	   //永真循环线程
       for(int i=0;;i++){
           try {
        	   //间隔1000毫秒
               Thread.sleep(1000);
           } catch (InterruptedException ex) {   }
           System.out.println(i);
       }
   }

   public static void main(String [] args)
   {
        DaemoDemo test = new DaemoDemo();
        test.setDaemon(true);    //调试时可以设置为false,那么这个程序是个死循环,没有退出条件。设置为true,即可主线程结束,test线程也结束。
        test.start();
        System.out.println("isDaemon = " + test.isDaemon());
       try {
           System.in.read();   // 接受输入,使程序在此停顿,一旦接收到用户输入,main线程结束,守护线程自动结束
       } catch (IOException ex) {}
   }
}


2.join方法

   当A线程执行到了b线程的.join()方法时,A线程就会等待,等B线程都执行完,A线程才会执行。(此时B和其他线程交替运行。)join可以用来临时加入线程执行。
/**
 *需求:演示join的使用
 *思路:创建一个线程,进行固定次数循环,主线程设计同样的固定次数循环
 *          比较使用join和不使用join的变换
 * */
package com.blog.part3.多线程;

public class JoinDemo extends Thread
{ 
	public void run()
	{
		//for循环50次
		for(int i=0;i<50;i++)
		{
			try
			{
				 Thread.sleep(1000);
			}
			catch(InterruptedException ex){}
			System.out.println("Test"+i);
		}
	}  
	 public static void main(String [] args)
	   {
		 JoinDemo test=new JoinDemo();
		 test.start();
        //加入join方法后,main线程会在test执行完之后才执行,不会抢走他的执行权
		 try {
			test.join();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

		 for(int i=0;i<50;i++)
		 {
			 System.out.println("main"+i);
		 }
	   }
}



3.setPriority方法    

        Java中,线程的优先级是介于Thread.MIN_PRIORITY到Thread.MAX_PRIORITY这两个常量之间的某个整数数值(介于1到10之间)。当利用某一线程又创建了一个新线程对象时,这个新线程将拥有与创建它的线程一样的优先级。例如,主线程的优先级默认情况下是5,那么利用主线程创建的新线程的优先级默认情况下也是5。

        这里要注意一点:具有较高线程优先级的对象,只是具有较多的执行机会,而非优先执行。

             

             MAX_PRIORITY 最高优先级10

             MIN_PRIORITY   最低优先级1

             NORM_PRIORITY 分配给线程的默认优先级5

		Mix t1 = new Mix("Thread 1");  		
		Mix t2 = new Mix("Thread 2");
		Mix t3 = new Mix ("Thread 3");
		
		t1.setPriority(Thread.NORM_PRIORITY-3); //设置优先级为2		
		t2.setPriority(Thread.NORM_PRIORITY+3); //设置优先级为8
		t3.setPriority(Thread.MAX_PRIORITY);//设置优先级为10


4. toString方法

        黑马程序员——Java基础---多线程_第14张图片
5.yield方法
黑马程序员——Java基础---多线程_第15张图片
   yield()方法还与线程优先级有关,当某个线程调用yiled()方法从运行状态转换到就绪状态后,CPU从就绪状态线程队列中只会选择与该线程优先级相同或优先级更高的线程去执行。
  
示例:
/**
 * 需求:演示yield方法的使用
 * 思路:1.设计两个优先级不同的线程myThread1和myThread2
 *           2.主函数设计一个循环打印输出,当变量值满足条件时,开启两个线程,同时主函数调用yield方法。
 *           3.多次运行,看是否有控制作用
 * @author jinlong
 * */
package com.blog.part3.多线程;
public class YiledDemo 
{

    public static void main(String[] args) 
    {
    	//创建两个线程
        Thread myThread1 = new MyThread1();
        Thread myThread2 = new MyThread2();
        //设置两个线程不同的优先级
        myThread1.setPriority(Thread.MAX_PRIORITY);
        myThread2.setPriority(Thread.MIN_PRIORITY);
        //主函数线程中进行的for循环
        for (int i = 0; i < 100; i++) 
        {
        	//主线程进行打印
            System.out.println("main thread i = " + i);
            //当i=20时,进行判断
            if (i == 20) 
            {
            	//启动两个线程
                myThread1.start();
                myThread2.start();
                //让主函数线程让步
                Thread.yield();
            }
        }
    }

}

class MyThread1 extends Thread
{

    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("myThread 1 --  i = " + i);
        }
    }
}

class MyThread2 extends Thread
{

    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("myThread 2 --  i = " + i);
        }
    }
}
运行结果:
黑马程序员——Java基础---多线程_第16张图片
       
        这里需要注意,有时候会出现main thread 不是在i=20时开始打印myThread 1线程, 是因为 当调用线程的yield()方法时,线程从运行状态转换为就绪状态,但接下来CPU调度就绪状态中的哪个线程具有一定的随机性,因此,可能会出现main线程调用了yield()方法后,接下来CPU仍然调度了main线程的情况。


你可能感兴趣的:(黑马程序员——Java基础---多线程)