【黑马程序员】第五章:多线程

------- android培训java培训、期待与您交流! ----------


   在日常生活中,很多事情都是同事进行的。例如人可以同时进行呼吸、血液循环、思考问题等活动。在使用计算机的过长中,应用程序也可以同时运行,用户可以使用计算机一边听歌,一边玩游戏。在应用程序中,不同的程序块也是可以同时运行的,这种多个程序块同时运行的现象被称作并发执行。

   多线程就是指一个应用程序中有多条并发执行的线索,每条线索被称作一个线程,他们会交替执行,彼此间可以进行通信。

一、线程概述


1、进程

   在学习线程之前,需要了解一下什么是进程?在一个操作系统中,每个独立执行的程序都可以称为一个进程。目前大部分计算机山安装的都是多任务操作系统,即能够同时执行多个应用程序,表面上看是支持并发执行的,但是实际上这些进程并不是同时运行的。在计算机中,所有的应用程序都是有 CPU 执行的,对于一个 CPU 来讲,在某个时间点只能运行一个程序,也就是说只能执行一个进程。操作系统会为米格进程分配一段有限的 CPU 使用时间,CPU 在这段时间执行某个进程,然后在下一段时间切换到另一个进程去执行。

2、线程

   每个运行的程序都是一个进程,在一个进程中还可以有多个执行单元同时运行,这些执行单元可以看做程序执行的一条条线索,被称作线程。在 Java 程序启动时,就会产生一个进程,该进程会默认创建一个线程,在这个线程上回运行 main() 方法中的代码,这个称为主线程。

   代码都是按照顺序依次往下执行,没有出现两端代码交替运行的效果,这样的程序称作单线程程序,如果希望程序实现多段嗲吗交替运行的效果,则需要创建多个线程,即多线程程序。

二、线程的创建


   Java 提供了两种多线程实现方式,一种是继承 java.lang 包下的 Thread 类,覆盖 Thread 类的 run() 方法,在 run() 方法中实现运行在线程上的代码;另一种是实现 java.lang.Runnable 接口,同样是在 run() 方法中实现运行在线程上的代码。

1、继承 Thread 类创建多线程

   在 JDK 中提供了一个线程类 Thread,通过继承 Thread 类,并重写 Thread 类中的 run() 方法便可以实现多线程。在 Thread 类中提供了一个 start() 方法用于启动新线程,线程启动后,系统会自动调用 run() 方法。

   创建的步骤如下:

  • 定义类继承 Thread;
  • 覆盖 Thread 类中的 run() 方法,目的是将自定义代码存储在 run() 方法中;
  • 创建定义类的实例对象,相当于创建一个线程;
  • 用该对象调用线程的 start() 方法,该方法的作用是启动线程,调用 run() 方法。

   覆盖 run() 方法的原因:

  • Thread 类用于描述线程,该类定义了一个功能,用于存储线程要执行的代码,该存储功能就是 run() 方法。

public class Example{
	public void main(String[] args){
		MyThread myThread = new MyThread();
		myThread.start();
		while(true){
			System.out.println("main()方法在运行");
		}
	}
}
class MyThread extends Thread{
	public void run(){
		while(true){
			System.out.println("MyThread类的run()方法在运行");
		}
	}
}

2、实现 Runnable 接口创建多线程

   通过继承Thread 类实现了多继承,但是这种方式有一定的局限性,因为 Java 中只支持但继承,一个类一旦继承了某个父类就无法再继承 Thread 类。纬二路客服这种弊端,Thread 类提供了另外一个构造方法Tread(Tunnable target) ,其中 Runnable 是一个接口,它只有一个 run() 方法。当通过 Thread(Runnable target) 构造方法创建线程对象时,只需要为该方法传递一个实现了 Runnable 接口的实力对象,这样创建的线程将调用实现了 Runnable 接口中的 run() 方法作为运行嗲吗,而不需要调用 Thread 类中的 run() 方法。

   创建步骤:

  • 定义类实现 Runnable 接口;
  • 覆盖 Runnable 接口中的 run() 方法,目的是为了将线程要运行的代码存储在 run() 方法中;
  • 通过 Thread 类创建线程对象;
  • 将 Runnable 接口的子类对象作为实参传递给 Thread 类的构造函数;
  • 调用 Thread 类的 start() 方法启动线程,start() 方法会自动调用 Runnable 接口子类的 run() 方法。

class ThreadDemo2{
    public static void main(String[] args){
        MyThread myThread = new MyThread();
        Thread thread = new Thread(myThread);
        thread.start();
        while(true){
            System.out.println("main()方法在运行");
        }
    }
}
class MyThread implements Runnable{
    public void run(){
        while(true){
            System.out.println("MyThread类的run()方法在运行");
        }
    }
}


3、两种实现多线程方式的对比分析

   既然直接继承 Thread 类和实现 Runnable 接口都能实现多继承,那么这两种实现多线程的方式在实际应用中又有什么区别呢?

   实现 Runnable 接口相对于继承 Thread 类来说,有如下显著的好处:

  • 适合多个相同程序代码的线程去处理一个资源的情况,把线程同程序嗲吗、数据有效的分离,很好地体现了面向对象的设计思想
  • 可以避免由于 Java 单继承带来的局限性。在开发中继承遇到一种情况,就是使用一个已经继承了某一个类的子类创建线程,由于一个类不能同时有两个父类,所以不能用继承 Thread 类的方式,那么只能采用 Runnable 接口的方式。

   事实上,大部分的应用程序都会采用第二种方式来创建多线程,即实现 Runnable 接口。

4、线程的状态及生命周期

   在 Java 中任何对象都有生命周期,线程也不例外,它也有自己的生命周期。当 Thread 对象创建完成时,线程的生命周期变开始了。当 run() 方法中代码正常执行完毕或者线程抛出一个为捕获的异常(Exception)或错误(Error)时,线程的生命周期变回结束。

   线程整个生命周期可以分为五个阶段,分别是:

  • 新建状态:等待启动,调用 start() 方法;
  • 运行状态:具备执行资格和执行权;
  • 阻塞状态:有执行资格,但是没有执行权;
  • 冻结状态:遇到 sleep() 或 wait() 方法时,失去执行资格和执行权,sleep() 方法时间到或调用 notify() 方法时,获得执行资格,变为运行状态;
  • 消亡状态:stop() 方法,或 run() 方法结束。


三、线程的调度


   程序中的多个线程是并发执行的,某个线程弱项被执行必须要得到 CPU 的使用权,Java 虚拟机会按照特定的机制为程序中的每个线程分配 CPU 的使用权,这种机制被称作线程的调度。

1、线程的优先级

   在应用程序中,要对线程进行调度,最直接的方法就是设置线程的优先级。优先级越高的线程获得 CPU 执行的机会越大,而优先级越低的线程获得 CPU 执行的机会越小。线程的优先级用 1~10 之间的整数来表示,数字越高优先级越大。除了可以直接使用数字表示线程的优先级,还可以使用 Thread 类中提供的三个静态常量表示线程的优先级。


   程序在运行期间,处于就绪状态的每个线程都有自己的优先级,,然而线程优先级不是固定不变的,可以通过 Thread 类的 setPriority(int newPriority) 方法对其进行设置,该方法中的参数 newPriority 接收的是 1~10 之间的整数或者 Thread 类的三个静态常量。

class MaxPriority implements Runnable{
	public void run(){
		for (int i=0; i<10; i++){
			System.out.println(Thread.currentThread().getName()+"正在输出"+i);
		}
	}
}

class MinPriority implements Runnable{
	public void run(){
		for (int i=0; i<10; i++){
			System.out.println(Thread.currentThread().getName()+"正在输出"+i);
		}
	}
}

class {
	public static void main(String[] args){
		Thread minPriority = new Thread(new MinPriority(),"优先级低的线程");
		Thread maxPriority = new Thread(new MaxPriority(),"优先级高的线程");
		minPriority.setPriority(Thread.MIN_PRIORITY);
		maxPriority.setPriority(10);
		minPriority.start();
		maxPriority.start();
	}
}

   创建了两个线程 minPriority 和 maxPriority ,分别将线程的优先级设置为 1 和 10,从运行的结果可以看出,优先级较高的 maxPriority 线程会先运行,运行完毕后优先级较低的 minPriority 线程才开始运行。

   需要注意的是,虽然 Java 中提供了 10 个线程优先级,但是这些优先级需要操作系统的支持,不同的操作系统对优先级的支持是不一样的,不能很好的和 Java 中的线程优先级对应,因袭在设计多线程程序时,其功能的实现一定不能依赖线程的优先级,而只能把线程优先级作为一种提高效率的手段。

2、线程休眠

   既然优先级高的程序会先执行, 而优先级底的程序会后执行,如果希望人为的控制线程,使正在执行的线程暂停,将 CPU 让给别的线程,这时可以使用静态方法 sleep(long millis) ,该方法可以让当前正在执行的线程暂停一段时间,进入休眠状态。当前线程调用 sleep(long millis) 方法后,在指定时间(参数 millis)内是不会执行的,这样其他的线程就可以得到后自行的机会了。

class ThreadDemo3{
	public static void main(String[] args)throws Exception{
		new Thread(new SleepThread()).start();
		for (int i=0; i<=10; i++){
			if (i == 5)	{
				Thread.sleep(2000);
			}
			System.out.println("主线程正在输出:" + i);
			Thread.sleep(500);
		}
	}
}
class SleepThread implements Runnable{
	public void run(){
		for (int i=0; i<=10; i++){
			if (i==3){
				try{
					Thread.sleep(2000);
				}
				catch (InterruptedException e){
					e.printStackTrace();
				}
			}
			System.out.println("线程一正在输出:"+i);
			try{
				Thread.sleep(500);
			}
			catch (Exception e){
				e.printStackTrace();
			}
		}
	}
}

   示例中开启了两个线程,在这个线程中分别调用了 Thread 的 sleep(500) 方法,目的是让一个线程在打印一次后休眠 500ms ,从而使另一个线程获得执行的机会,这样就可以实现两个线程的交替执行。

   需要注意的是,sleep() 是静态方法,只能控制当前正在运行的线程休眠,而不能控制其他线程休眠。当休眠时间结束后,线程就返回到就绪状态,而不是立即开始执行。

3、线程让步

   线程让步可以通过 yield() 方法来实现,该方法和 sleep() 方法有点相似,都可以让当前正在运行的线程暂停,区别在于 yield () 方法不会阻塞该线程,它只是将线程转换陈就绪状态,让系统的调度器重新调度一次。当某个线程调用 yield() 方法之后,只有与当前线程优先级相同或者更高的线程才能获得执行的机会。

class YieldThread extends Thread{
	public YieldThread(String name){
		super();
	}
	public void run(){
		for (int i=0; i<5; i++){
			System.out.println(Thread.currentThread().getName()+"---"+i);
			if (i == 5){
				System.out.print("线程让步:");
				Thread.yield();
			}
		}
	}
}
class ThreadDemo4{
	public static void main(String[] args){
		Thread y1 = new YieldThread("线程A");
		Thread y2 = new YieldThread("线程A");
		y1.start();
		y2.start();
	}
}

4、线程插队

   现实生活中经常能碰见“插队”的情况,同样在 Thread 类中也提供了一个 join() 方法来实现这个功能。当在某个线程中调用其他线程的 join() 方法时,调用的线程将被阻塞,知道被 join() 方法加入的此案成执行完成后才会继续执行。

class EmergencyThread implements Runnable{
	public void run(){
		for (int i=0; i<6; i++){
			System.out.println(Thread.currentThread().getName()+"---"+i);
			try{
				Thread.sleep(500);
			}catch (IterruptException e){
				e.printStackTrace();
			}
		}
	}
}
class ThreadDemo4{
	public static void main(String[] args)throws Exception{
		Thread t = new Thread(new EmergencyThread(),"线程一");
		t.start();
		for (int i=0; i<6; i++){
			System.out.println(Thread.currentThread().getName()+"---"+i);
			if (i==2){
				t.join();
			}
			Thread.sleep(500);
		}
	}
}

   main 线程开启了一个线程 t,两个线程的循环体中都调用了 Thread 的 sleep(500) 方法,以实现两个线程的交替执行。当 main 线程中的循环变量为 2 时,调用 t 线程的 join() 方法,这是 t 线程就会插队执行。

5、线程中断

   Java 中曾提供过过抢占式中断,例如 stop() 方法,但是问题多多。而且出于 Java 代码的健壮性,降低编程门槛,减少不清楚底层机制的程序员破坏系统的概率,如今 Java 的线程调度不再提供抢断式中断,而采用协作式中断。

   协作式中断的原理很简单,就是轮询某个表示中断的标记,将 Thread 里的中断标记设置为 true。对于处于 sleep 或 join 等操作的线程来说,如果被调用 interrupt() 方法后,抛出 InterruptException 异常,那么线程中的中断标记会由 true 变为 false,因为线程为了处理异常已经重新处于就绪状态。

public class InterruptThread extends Thread{  
	int count=0;  
         
	public void run(){  
		System.out.println(Thread.currentThread().getName()+"将要运行...");  
		while(!this.isInterrupted()){  
			System.out.println(Thread.currentThread().getName()+"运行中"+count++);  
			try{ 
				Thread.sleep(500);  
			}catch(InterruptedException e){  
				System.out.println(Thread.currentThread().getName()+"从阻塞中退出...");  
				System.out.println("this.isInterrupted()="+this.isInterrupted()); 
			}  
		}  
			System.out.println(Thread.currentThread().getName()+"已经终止!");  
	}  
}  

四、线程同步


   多线程的并发执行可以提高程序的效率,但是当多个线程去访问同一个资源时,也会引发一些安全问题。例如当药统计一个班的学生人数时,如果有同学进进出出则很难统计。为了解决这样的问题,需要实现多线程的同步,即限制某个资源在同一时刻只能被一个线程访问。

1、线程安全

   在售票案例中,极有可能发生意外情况,如一张票被多次打印,或者但因出票数为 0 或者 负数,这些问题都是由多线程操作共享资源所导致的线程安全问题。

public class SynchronizedDemo1{
	public static void main(String[] args){
		SaleThread s = new SaleThread();
		new Thread(s,"线程一").start();
		new Thread(s,"线程二").start();
		new Thread(s,"线程三").start();
		new Thread(s,"线程四").start();
	}
}
class SaleThread implements Runnable{
	private int tickets =10;
	public void run(){
		while (tickets>0){
			try{
				Thread.sleep(10);
			}catch (InterruptedException e){
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName()+"---卖出票"+tickets--);
		}
	}
}

   运行结果如图:


  最后几行打印出的票为 0 和负数,这种情况是不应该出现的,之所以出现了负数的票号是因为多线程在售票时出现了安全问题。在售票程序的 while 循环中添加了 sleep() 方法,这样就模拟了售票过程中线程的延迟。由于线程延迟,当皮哦啊好减为 1 时,假设线程一此时售出 1 号票,对拍哦化进行判断后进入 while 循环,在售票之前通过 sleep() 方法让线程进入休眠,这时线程二进行售票,由于此时飘花仍为 1,因此线程二也会进入循环,同理线程三也会进入 while 循环,休眠结束后三个线程都会进行售票,这样相当于票号减了三次,结果中出现了 0、-1这样的情况。

2、同步代码块

   了解到线程安全问题其实就是由多个线程同时处理共享资源导致的,要想结果上述例子中的线程安全问题,那么必须得保证用于处理共享资源代码在任何时刻只能有一个线程访问。

   为了实现这种限制,Java 中提供了同步机制。当多个线程使用同一个共享资源时,可以处理共享资源的代码放置在一个代码块中,使用 synchronized 关键字来修饰,被称作同步代码块,其语法格式如下:

synchronized(lock){
	//操作贡献资源代码
}

   上述代码中,lock 是一个锁对象,它是同步代码块的关键。当线程执行同步代码块时,首先检查锁对象的标志位,默认情况下标识位为 1,此时线程会执行同步代码块,同时将锁对象的标识位置为 0。当一个新的线程执行到这段同步代码块时,由于锁对象的标志位为 0,新线程会发生阻塞,等待当前线程执行完同步代码块后,锁对象的标识位被置为 1,新线程才能进入同步代码块执行其中的代码。

public class SynchronizedDemo2{
	public static void main(String[] args){
		SaleThread s = new SaleThread();
		new Thread(s,"线程一").start();
		new Thread(s,"线程二").start();
		new Thread(s,"线程三").start();
		new Thread(s,"线程四").start();
	}
}
class SaleThread implements Runnable{
	private int tickets =10;
	Object lock = new Object();
	public void run(){
		while (true){
			synchronized(lock){
				try{
					Thread.sleep(10);
				}catch (InterruptedException e){
					e.printStackTrace();
				}
				if (tickets>0){
					System.out.println(Thread.currentThread().getName()+"---卖出票"+tickets--);
				}else{
					break;
				}
			}
		}
	}
}

   运行结果如下:


   将有关 tickets 变量的操作全部放到同步代码块中。售出的票不再出现 0 和负数的情况,这是因为售票的嗲吗实现了同步,之前出现的线程安全问题得以解决。

   需要注意的是,同步代码块中的所对象可以是任意类型的对象,但多个线程共享的锁对象必须是唯一的。“任意”指的是共享所对象的类型,所以锁对象的创建代码不能放到 run() 方法中,否则每个线程运行到 run() 方法都会创建一个新的对象,这样每个线程都会有一个不同的锁,每个锁都有自己的标志位,线程之间变不能产生同步的效果。

3、同步方法

   了解到同步代码块可以有效解决线程的安全问题,当把共享资源的操作放在 synchronized 定义的区域内时,便为这些操作加了同步锁。在方法前面同样可以使用 synchronized 关键字修饰,被修饰的方法为同步方法,它能实现和同步代码块同样的功能,具体格式如下:

synchronized 返回值类型 方法名([参数1,……]){}

   被 synchronized 修饰的方法在某一时刻只允许一个线程访问,访问该方法的其他线程都会发生阻塞,知道当前线程访问完毕后,其他线程才有机会执行。

class Ticket implements Runnable{
	private int ticket = 10;
	public void run(){
		while (true){
			slaeTicket();
			if(ticket<=0)
				break;
		}
	}
	private synchronized void saleTicket(){
		if (ticket>0){
			try{
				Thread.sleep(10);
			}catch (InterruptedException e){
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName()+"---卖出的票"+ticket--);
		}
	}
}

class SynchornizedDemo3{
	public static void main(String[] args){
		Ticket t = new Ticket();
		new Thread(t,"线程一").start();
		new Thread(t,"线程二").start();
		new Thread(t,"线程三").start();
		new Thread(t,"线程四").start();
	} 
}

   疑问:同步代码块的锁是自己定义的任意类型的对象,那么同步方法是否也存在锁?如果有,它的锁是什么呢?

   答案是肯定的,同步方法也有锁,它的锁就是当前调用该方法的对象,也就是 this 指向的对象。这样做的好处是,同步方法被所有线程所共享,方法所在的对象相当于所有想成来说是唯一的,从而保证了锁的唯一性。当一个线程执行该方法时,其他的线程就不能进入该方法中,直到这个线程执行完该方法为止,从而达到了线程同步的效果。

   有时候需要同步的方法是静态方法,静态方法不需要创建对象就可以直接用 “ 类名 . 方法名() ” 的方式调用。这时候我们就会有一个疑问,如果不创建对象,静态同步方法的锁就不是 this,那么静态同步方法的锁是什么?Java 中静态方法的锁是该方法所在类的 class 对象,该对象可以直接用 “ 类名 . class ” 的方式获取。

   同步代码块和同步方法解决多线程问题有好处也有弊端。同步解决了多个线程同时访问共享数据的线程安全问题,只要加上同一个锁,在同一时间内只能有一条线程执行。但是线程在执行同步代码时每次都会判断锁的状态,非常消耗资源,效率较低。

4、死锁

   两个线程在运行时都在等待对方的锁,这样便造成了程序的停止,这种现象就成为死锁。

class Ticket implements Runnable{
	private  int tick = 1000;
	Object obj = new Object();
	boolean flag = true;
	public  void run(){
		if(flag){
			while(true){
				synchronized(obj){
					show();
				}
			}
		}
		else
			while(true)
				show();
	}
	public synchronized void show(){
		synchronized(obj){
			if(tick>0){
				try{Thread.sleep(10);}catch(Exception e){}
				System.out.println(Thread.currentThread().getName()+"....code : "+ tick--);
			}
		}
	}
}

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(10);}catch(Exception e){}
		t.flag = false;
		t2.start();
	}
}

五、线程间通信


   现代社会崇尚合作精神,分工合作在日常生活和工作中悟出不在。举个简单的例子,比如一条生产线的上下两个工序,他们必须以规定的速率完成各自的工作,才能保证产品在流水线中顺利的流转。如果下工序国漫,会造成产品在两道工序之间的积压;如果上工序过慢,会造成下工序长时间无事可做。在多线程的程序中,上下工序可以看做两个线程,这两个线程之间需要协同完成工作,就需要线程之间完成通讯。

/*
线程间通讯:
其实就是多个线程在操作同一个资源,
但是操作的动作不同。
*/

class Res{
	String name;
	String sex;
	boolean flag = false;
}

class Input implements Runnable{
	private Res r ;
	Input(Res r){
		this.r = r;
	}
	public void run(){
		int x = 0;
		while(true){
			synchronized(r){
				if(r.flag)
					try{r.wait();}catch(Exception e){}
				if(x==0){
					r.name="mike";
					r.sex="man";
				}
				else{
					r.name="丽丽";
					r.sex = "女女女女女";
				}
				x = (x+1)%2;
				r.flag = true;
				r.notify();
			}
		}
	}
}

class Output implements Runnable{
	private Res r;
	Output(Res r){
		this.r = r;
	}
	public void run(){
		while(true){
			synchronized(r){
				if(!r.flag)
					try{r.wait();}catch(Exception e){}
				System.out.println(r.name+"...."+r.sex);
				r.flag = false;
				r.notify();
			}
		}
	}
}

class  InputOutputDemo{
	public static void main(String[] args) {
		Res r = new Res();
		Input in = new Input(r);
		Output out = new Output(r);

		Thread t1 = new Thread(in);
		Thread t2 = new Thread(out);
		t1.start();
		t2.start();
	}
}

   1、在 Object 类中提供了 wait()、notify()、notifyAll() 方法用于解决线程间的通信问题,这几个方法的说明如下:

  • wait():使当前线程放弃同步锁并进入等待,直到其他线程进入此同步锁,并调用 notify() 方法,或 notifyAll() 方法唤醒该线程为止;
  • notify():唤醒此同步锁上等待的第一个调用 wait() 方法的线程;
  • notifyAll():唤醒此同步锁上调用 wait() 方法的所有线程。

   2、 为什么这些方法要定义在 Object 类中?

  • 这些方法存在于同步中;
  • 使用这些方法要标识所属的同步锁,同一个锁上 wait() 的线程,只允许被同一个锁的 notify() 唤醒;
  • 锁可以是任意对象,所以任意对象调用的方法必须定义在 Object 类中。

   3、wait() 与 sleep() 的区别?

  • wait() 释放 CPU 执行权,并且释放锁;
  • sleep() 释放 CPU 执行权,不释放锁。

   4、为什么要定义 notifyAll() 方法?

  • 在需要唤醒对方线程时,如果只是用 notify() 方法,容易出现只唤醒本方线程的情况,导致程序中的所有线程都在等待。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值