黑马程序员——Java基础知识——多线程

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

一、线程

       在计算机中,每一个程序的运行,都是通过线程来实现的。我们可以把一个正在执行的程序称为进程,每一个进程的执行都有一个执行顺序。该顺序是一个执行路径,或者叫控制单元。线程就是进程中的一个独立的控制单元,线程在控制着进程的执行。只要进程中有一个线程在执行,进程就不会结束。一个进程中至少有一个线程。

       在多个程序运行时,CPU会随机地在多个进程中快速切换,哪个线程抢到了CPU的执行权,哪个线程就运行。运行Java程序时,虚拟机启动会有一个java.exe的进程,该进程中至少有一个线程负责java程序的执行。而且这个线程运行的代码存在于main方法中,称之为主线程。虚拟机启动除了执行主线程外,还有负责垃圾回收机制的线程。这种在在一个进程中有多个线程执行的方式,就是多线程。

       多线程的出现能让程序产生同时运行的效果,提高程序的运行效率。例如:在虚拟机启动后,进程中执行主线程时,一般会根据程序代码,在堆内存中产生很多对象,而对象调用完后,就成了垃圾,如果不及时清理,垃圾过多容易造成内存不足,影响程序的运行。所以如果只有主线程运行,程序的效率可能会很低;而如果有一个负责垃圾回收机制的线程运行时,就会对堆内存中的垃圾进行清理,保证了内存的稳定,就保证了程序的运行效率。 

二、创建线程

       有两种创建线程的方式:继承Thread类和实现Runnable接口。

       (1)继承Thread类 

             Thread类是Java提供的对线程这类事物描述的类,通过继承Thread类,复写其run方法来创建线程。步骤如下:

            1.定义一个类继承Thread

           2.覆盖Thread中的run方法。将自定义的代码放在run方法中,让线程运行时,执行这些代码。

           3.创建这个类的对象。相当于创建一个线程。然后用该对象调用线程的start方法。该方法的作用是:启动线程,调用run方法。注意如果直接用对象调用run方法,相当于没有启动创建的线程,还是只有主线程在执行。

           覆盖run方法的原因:Thread类用于描述线程。该类就定义了一个功能,用于存储线程要执行的代码。该存储功能就是run方法。也就是说,Thread类中的run方法,用于存储线程要运行的代码。

            下面通过一段程序,演示如何用继承方法创建线程,如下:

/**
需求:创建两个线程,和主线程交替运行。
*/

class MyThread extends Thread
{
	//覆盖父类的run方法,存入运行代码
	public void run(){
            for(int x=0;x<1000;x++)  
		System.out.println(Thread.currentThread().getName()+"在运行");
	}
}

class ThreadDemo
{
	public static void main(String[] args) 
	{
		//创建线程
		MyThread mt1=new MyThread();
                MyThread mt2=new MyThread(); 
               //启动线程
		mt1.start();
		mt2.start();
		for(int x=0;x<1000;x++)
		   System.out.println("Hello World!");
	}
}
       主线程和创建的线程会交替执行,因为CPU是随机地选择要执行的线程。这种方法可以创建线程,但如果定义的类已经是其他类的子类了,就无法再继承Thread类了,所以Java提供了另一中创建线程的方式,通过实现Runnable接口的方式。
       (2)实现Runnable接口

            实现Runnable接口,覆盖run方法。这种创建线程的方式避免了单继承的局限性,在定义创建线程时,一般都使用这种方式。具体步骤如下:

            1.定义一个类实现Runnable的接口。

           2.覆盖Runnable接口中的run方法。将线程要运行的代码存放在该run方法中。

            3.通过Thread类创建线程对象,并将Runnable接口的子类对象作为实际参数传递给Thread类的构造方法。这样做是因为自定义的run方法所属的对象是Runnable接口的子类对象。所以要让线程去指定对象的run方法,就必须明确该run方法所属对象。

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

           下面是一个简单的售票程序,应用的就是实现这种创建线程的方式,如下

/**
需求:简单的卖票程序。
多个窗口同时买票。
*/

class Ticket implements Runnable
{
	private int ticket = 100;
        //复写run方法
        public void run()
	{
		while(true)
		{
			if(ticket>0)
			{
			System.out.println(Thread.currentThread().getName()+"....sale : "+ ticket--);
			}
		}
	}
}
class  TicketDemo
{
	public static void main(String[] args) 
	{

		Ticket t = new Ticket();
                //创建4个线程,表示4个同时售票的窗口
		Thread t1 = new Thread(t);
		Thread t2 = new Thread(t);
		Thread t3 = new Thread(t);
		Thread t4 = new Thread(t);
		//启动线程,4个窗口开始同时售票。
                t1.start();
		t2.start();
		t3.start();
		t4.start();	

	}
}
       这种创建线程的方式,是把线程要运行的代码放在Runnable接口的子类的run方法中;而继承Thread类是把要运行的代码放在Thread的子类的run方法中。在以后的操作中一般用到的都是实现Runnable接口的方式。
 三、线程的运行状态

          首先看一下表示线程运行状态的图例:

黑马程序员——Java基础知识——多线程_第1张图片


        从上图可以看出,线程从创建、运行、到最后结束整个过程中的各种状态,具体有:

       被创建:等待启动,调用start启动。如果线程已经启动,处在运行时,再次调用start方法,没有意义,会提示线程状态异常。

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

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

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

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

四、线程安全

        当多条语句在操作多个线程的共享数据时,当一个线程对多条语句只执行了一部分,还没有执行完时,另一个线程可能就会参与进来执行,这样会导致共享数据的错误。线程的安全问题一旦出现对程序的影响很大。所以Java中提供了解决线程安全问题的方法,叫做同步(synchronized),就是对多条操作共享数据的语句,只能让一个线程都执行完。在执行过程中,其他线程不可以参与执行,这样就解决了线程的安全问题。同步中分为两种解决方法,一种是同步代码块,另一种是同步函数

       (1)同步代码块

          格式:synchronized(对象){需要被同步的代码}

          就以上面售票的程序为例,利用同步代码块,确保线程安全。如下:

        

class Ticket implements Runnable
{
	private  int ticket = 100;
	//定义用于使用同步的对象。
        Object obj = new Object();
	public void run()
	{
		while(true)
		{
			//给程序实现同步
                   synchronized(obj)
			{
				if(ticket>0)
				{
				  System.out.println(Thread.currentThread().getName()+"....sale : "+ ticket--);
				}
			}
		}
	}
}
class  TicketDemo2
{
	public static void main(String[] args) 
	{

		Thread t2 = new Thread(t);
		Thread t3 = new Thread(t);
		Thread t4 = new Thread(t);
		t1.start();
		t2.start();
		t3.start();
		t4.start();
     }
}
           在同步代码块中,对象就如同一把锁。持有锁的线程才可以在同步中执行。没有持有锁的线程即使获得CPU的执行权,也进不去,因为没有获取锁。

       (2)同步函数

             格式:就是在函数上加上synchronized即可。因为非静态函数需要被对象调用,所以非静态函数中都有一个所属对象引用,即this。同步函数使用的锁就this

              下面还以售票的程序,来用同步函数的格式,实现同步。如下:

class Ticket implements Runnable
{
	private  int ticket = 100;
	public void run()
	{
		while(true)
		     show();	
	}		
	  //通过同步函数,实现同步。	
	  public synchronized void show()
	 {
            if(ticket>0)
		System.out.println(Thread.currentThread().getName()+"....sale : "+ ticket--);
	  }
}
class  TicketDemo2
{
	public static void main(String[] args) 
	{
                Ticket t = new Ticket();
                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();
       }
}
        无论使用上面两种方法的哪一种,都必须保证存在两个或两个以上的线程,并且这些线程都是在使用同一个锁,否则实现不了不同,线程还是会有问题。在使用同步时要明确哪些是多线程的运行代码,哪些是多线程的共享数据以及运行代码中哪些是用来操作共享数据,明确了这些关键点之后,定义的同步会保证线程的安全
同步解决了多线程的安全问题,但多个线程需要判断锁,较为消耗资源。

       注意,因为静态函数中没有被对象调用,所以它内部没有对象引用,这时对一个静态函数同步,它的锁肯定不是this,而是它所属的类对应的字节码文件对象。格式为类名.class。因为静态函数进内存时,内存中一定有它所在的类的字节码文件对象。所以在静态函数同步时,就以字节码文件对象作为锁。

        在我们之前学到的单例设计模式中,懒汉式需要方法调用时,才会创建对象。但如果是在多线程中调用此方法时,就容易出现安全问题,保证不了对象在内存中的唯一性,所以这时也要使用到同步。如下:

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;
	}
}
五、线程间通信

       就是多个线程在操作同一个资源,但是操作的动作不同。为了实现一个流程,不同的操作动作需要交替,这时需要用到wait、notify、notifyAll等方法。如下:

class Resource
{
  private String name;
  private String sex;
  //定义判断标识
  boolean flag;
  //定义同步函数,表示输入
  public synchronized void input(String name,String sex)
  {   //如果已有资源,等待输入
	  if(!flag)
		  try{wait();}catch(Exception e){}
	  this.name=name;
	  this.sex=sex;
	  flag=false;
	  //唤醒等待线程
	  notify();
  }
  //表示输出
  public synchronized void output()
  {
	 //等待输出
	 if(flag)
		 try{wait();}catch(Exception e){}
	 System.out.println(Thread.currentThread().getName()+name+"........."+sex);	
	 flag=true;
	 //唤醒等待线程
	 notify();
  }

}
//定义输入线程
class InputDemo implements Runnable
{
	private Resource res;
	InputDemo(Resource res)
	{
		this.res=res;
	}
	public void run(){
		int x=0;
		while(true){
		if(x==0)
		     res.input("小明","男");
		else
			 res.input("haha","man");
		x=(x+1)%2;
		
		}
	}
}
//定义输出线程
class OutputDemo implements Runnable
{
	private Resource res;

	OutputDemo(Resource res)
	{
		this.res=res;
	}
	public void run(){
		while(true)
		  res.output();
	}
}
class Test
{
	public static void main(String[]args)
	{
		Resource res=new Resource();
		//创建并启动线程
		new Thread(new InputDemo(res)).start();
		new Thread(new OutputDemo(res)).start();
	}
}
执行结果为在控制台上交替打印"线程名"“小明”...........“男”和"线程名"“hehe”..........“man”。
       两个需要知道的知识点: (1)wait(),notify(),notifyAll(),用来操作线程为什么定义在了Object类中?

                                                             1.这些方法存在与同步中。

                                                             2.使用这些方法时必须要标识所属的同步的锁。同一个锁上wait的线程,只可以被同一个锁上的notify唤醒。

                                                             3. 锁可以是任意对象,所以任意对象调用的方法一定定义Object类中。

                                                    (2)wait(),sleep()有什么区别?

                                                               wait():释放cpu执行权,释放锁。

                                                               sleep():释放cpu执行权,不释放锁。

        在实际开发中,有时存在每个不同的操作动作都有多个线程在执行。如下:

/**
需求:多个生产者生产商品,每生产一件商品消费者就购买该商品。
*/
class ProducerConsumerDemo 
{
	public static void main(String[] args) 
	{
		Resource r = new Resource();
        
		Producer pro = new Producer(r);
		Consumer con = new Consumer(r);
         //创建线程
		Thread t1 = new Thread(pro);
		Thread t2 = new Thread(pro);
		Thread t3 = new Thread(con);
		Thread t4 = new Thread(con);
        //启动线程
		t1.start();
		t2.start();
		t3.start();
		t4.start();
       }
}
class Resource
{
	private String name;
	private int count = 1;
	private boolean flag = false;
	public synchronized void set(String name)
	{   
		//循环判断被唤醒线程的标识
		while(flag)
			try{wait();}catch(Exception e){}
		this.name = name+"--"+count++;
     System.out.println(Thread.currentThread().getName()+"...生产者.."+this.name);
		flag = true;
		//唤醒所有等待的线程
		notifyAll();
	} 
	public synchronized void out()
	{
		//循环判断被唤醒线程的标识
		while(!flag)
			try{wait();}catch(Exception e){}
   System.out.println(Thread.currentThread().getName()+"...消费者........."+this.name);
		flag = false;
		//唤醒所有等待的线程
		notifyAll();
	}
}
//定义线程 表示生产
class Producer implements Runnable
{
	private Resource res;

	Producer(Resource res)
	{
		this.res = res;
	}
	public void run()
	{
		while(true)
		{
			res.set("+商品+");
		}
	}
}
//定义线程 表示消费
class Consumer implements Runnable
{
	private Resource res;

	Consumer(Resource res)
	{
		this.res = res;
	}
	public void run()
	{
		while(true)
		{
			res.out();
		}
	}
}
     对于多个生产者和消费者,为什么要定义while判断标示:因为被唤醒的等待线程可能有多个,让被唤醒的线程再一次判断标识。
     定义notifyAll是因为需要唤醒对方线程,只定义notify,容易只唤醒本方线程,导致程序中的线程都停掉。

     JDK1.5中提供了多线程升级解决方案。将同步synchronized替换成显示的Lock操作,将Object中的wait、notify、notifyAll,替换成了Condition对象。该对象可以通过Lock锁获取,并支持多个相关的Condition对象。在这种方法中,实现了本方只唤醒对方的操作。如下:

/**
需求:多个生产者生产商品,每生产一件商品消费者就购买该商品。
*/
class ReflectTest1
{
	public static void main(String[] args) 
	{
		Resource r = new Resource();
                Producer pro = new Producer(r);
		Consumer con = new Consumer(r);
                //创建线程
		Thread t1 = new Thread(pro);
		Thread t2 = new Thread(pro);
		Thread t3 = new Thread(con);
		Thread t4 = new Thread(con);
               //启动线程
		t1.start();
		t2.start();
		t3.start();
		t4.start();
      }
}
class Resource
{
	private String name;
	private int count = 1;
	private boolean flag = false;
	//定义Lock对象
	Lock lock=new ReentrantLock();
	//创建Conditon对象,用来唤醒对方线程。
	Condition condition_con=lock.newCondition();
	Condition condition_pro=lock.newCondition();
	public void set(String name)
	{   //实现锁
		lock.lock();
	    try{
		while(flag)
			try{condition_pro.await();}catch(Exception e){}
		this.name = name+"--"+count++;
           System.out.println(Thread.currentThread().getName()+"...生产者.."+this.name);
		flag = true;
		//唤醒对方线程
		condition_con.signal();
	        }
        //释放锁的动作一定要执行
	    finally{
			lock.unlock();
		}
	} 
	public synchronized void out()
	{
		//实现锁
		lock.lock();
		try{
		while(!flag)
			try{condition_con.await();}catch(Exception e){}
    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;
	}
	public void run()
	{
		while(true)
		{
			res.set("+商品+");
		}
	}
}
//定义线程 表示消费
class Consumer implements Runnable
{
	private Resource res;

	Consumer(Resource res)
	{
		this.res = res;
	}
	public void run()
	{
		while(true)
		{
			res.out();
		}
	}
}
注意,释放锁的动作一定要执行,所以放在finally语句中。

六、停止线程

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

      开启线程运行,运行代码通常是循环结构,只要控制住循环,就可以让run方法结束,线程就结束了,这时一般需要通过定义标识,通过标识的变化来实现。如下:

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);
	        int num = 0;
		while(true)
		{
			if(num++ == 60)
			{       //调用改变标识的办法,结束线程。
				st.changeFlag();
				break;
			}
			System.out.println(Thread.currentThread().getName()+"......."+num);
		}
	}
}

  但是当线程处于冻结状态时,就不会读到标识,线程就不会结束。这时需要对冻结进行清除,强制让线程恢复到运行状态上来,然后再通过操作标识,让线程结束,完成清除动作,需要Thread类中的interrupt方法。如下:

class StopThread implements Runnable
{
	private boolean flag =true;
	public  void run()
	{
		while(flag)
		{       //线程冻结
			try{Thread.sleep(30)}catch(Exception e){}
			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();	
			}
			System.out.println(Thread.currentThread().getName()+"......."+num);
		}
		System.out.println("over");
	}
}


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

你可能感兴趣的:(Java学习)