关闭

黑马程序员--自学笔记--多线程

标签: 黑马程序员java技术多线程
225人阅读 评论(0) 收藏 举报
分类:

多线程

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

一.概述

1.名词解释
    ① 进程:指正在运行的程序,负责这个程序的内存空间分配,代表该程序在内存中的执行区域
    ② 线程:指进程中一个负责程序执行的控制单元(执行路径)
    ③ 多线程:指是在一个进程中多个执行路径同时执行
2.多线程的优点:
    ① 多线程技术使程序的响应速度更快 ,因为用户界面可以在进行其它工作的同时一直处于活动状态;
    ② 当当前没有进行处理的任务时可以将处理器时间让给其它任务;
    ③ 如果存在占用大量处理时间的任务可以定期将处理器时间让给其它任务;
    ④ 在程序运行过程中可以随时停止任务进程;
    ⑤ 利用多线程还可以可以分别设置各个任务的优先级以优化性能。
3.多线程的弊端:
    ① 利用多线程的过程中,等候使用共享资源时将会造成程序的运行速度变慢。这些共享资源主要是独占性的资源 ,如打印机等;
    ② 对线程进行管理要求额外的CPU开销。线程的使用会给系统带来上下文切换的额外负担。当这种负担超过一定程度时,多线程的特点主要表现在其缺点上,比如用独立的线程来更新数组内每个元素;
    ③ 使用多线程是有可能会出现线程的死锁(即较长时间的等待或资源竞争以及死锁等多线程现象);
    ④ 对公有变量的同时读或写。当多个线程需要对公有变量进行写操作时,后一个线程往往会修改掉前一个线程存放的数据,从而使前一个线程的参数被修改;另外 ,当公用变量的读写操作是非原子性时,在不同的机器上,中断时间的不确定性,会导致数据在一个线程内的操作产生错误,从而产生莫其妙的错误,而这种错误是程序员无法预知的。
4.线程的状态:
    ① 创建:新创建一个线程对象;
    ② 可运行:线程对象被创建后,其他线程调用了该对象的start()方法。该状态的线程将会位于可运行线程池中,变成可运行的状态,同时等待获取cpu的执行权;
    ③ 运行:处于就绪状态的线程获取了CPU执行权,执行run()方法中程序代码;
    ④ 阻塞: 阻塞状态是指线程因为某种原因放弃CPU使用权,暂时停止运行run()方法中程序代码。这种状态一直持续到线程进入就绪状态,才有机会转到运行状态;
    ⑤ 死亡:这种状态发生在线程执行完它的任务时,或者是被强行停止时。
通过一个图进一步明确各个状态之间的关系:

二.创建线程的方式

1.继承Thread类来创建线程对象
步骤:
    ① 定义一个类继承Thread类;
    ② 复写Thread类中的run方法;(目的:将自定义代码存储run方法中,让线程运行)
    ③ 调用线程的start方法。(该方法两个作用:启动线程,调用run方法)
代码演示:    (注意:注释中包含重要知识点和注意点)
<span style="font-family:KaiTi_GB2312;"><strong><strong><span style="font-family:KaiTi_GB2312;">public class ExtendsDemo extends Thread {
	
	private String name ;
	public ExtendsDemo(String name){
		
		//自定义线程的名字
		//super(name) ;
		this.name = name ;
	}
	
	public void run(){
		
		//可以通过Thread对象的getName()方法获取线程名称(Thread-编号(从0开始))
		for(int x=0;x<10;x++){
			System.out.println(name+"----"+x+" | "+getName());
			
			//通过Thread的currentThread()方法来获取当前运行线程
			System.out.println(name+"----"+x+" | "+Thread.currentThread().getName());
		}
	}
}</span></strong></strong></span>
当使用继承Thread类的方法创建线程,启动线程的代码如下:(注意:注释中包含重要知识点和注意点)
<span style="font-family:KaiTi_GB2312;"><strong><span style="font-family:KaiTi_GB2312;">import cn.itzixue.bean.ExtendsDemo;

public class ThreadDemo01 {

	/*
	 * JVM启动时开启了多条线程,其中有两个容易分析的线程
	 * 		1.执行main()方法的线程
	 * 			该线程的任务代码定义在main()方法中
	 * 		2.负责垃圾回收的线程
	 */
	
	public static void main(String[] args) {
		
		/*
		 * 创建线程的目的是为了开启一条执行路径,去运行指定的代码和其他代码实现同时运行。
		 * 		(运行的指定代码就是这个执行路径的任务,而jvm创建的主线程的任务都定义在了主函数中。)
		 * 
		 * 自定义线程的任务通过Thread类中的run()方法来体现。
		 * 		(即run()方法是封装自定义线程运行任务的函数,run()方法中定义就是线程要运行的任务代码。)
		 * 自定义线程只需要继承Thread类,同时复写run()方法,并将运行的代码定义在run()方法中。
		 */
		
		ExtendsDemo demo1 = new ExtendsDemo("Thread_1") ;		//创建线程时已完成线程名称的定义
		ExtendsDemo demo2 = new ExtendsDemo("Thread_2") ;
		
		//调用run()方法不能使线程启动,启动线程需要调用Thread子类对象的start()方法
		//	(start()方法开启线程,并调用run()方法)
		demo1.run();		//如果调用的是run()方法,则是主线程在运行
		demo2.start();	
	}
}</span></strong></span>

2.实现Runnable接口来创建线程对象(Runnable接口的出现是将线程任务进行封装)
步骤:
    ① 定义一个类实现Runnable接口;
    ② 覆盖接口中的run()方法,并将线程的任务代码封装到run()方法中;
    ③ 通过Thread类创建线程对象,并将自定义的Runnable接口的子类对象作为Thread类的构造函数的参数进行传递(即:在线程对象创        建时就必须明确所要运行的任务。原因:线程的任务全部都封装在Runnable接口子类对象的run()方法中);
    ④ 调用线程对象的start()方法开启线程。

由于代码实现过程与继承Thread类创建线程对象相似,这里只演示线程对象的启动: (注意:注释中包含重要知识点和注意点)

<span style="font-family:KaiTi_GB2312;"><strong><strong><span style="font-family:KaiTi_GB2312;">public class RunnableDemmo01 {

	public static void main(String[] args) {
		
		ImplementsDemo impl1 = new ImplementsDemo() ;
		ImplementsDemo impl2 = new ImplementsDemo() ;
		
		//通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递
		Thread t1 = new Thread(impl1) ;
		Thread t2 = new Thread(impl2) ;
		
		t1.start() ;
		t2.start() ;
	}
	
}</span></strong></strong></span>

实现Runnable接口的优点:
    ① 将线程的任务从线程的子类中分离出来,进行了单独的封装(按照面向对象的思想将任务的封装成对象)
    ② 避免了java单继承的局限性
3.run()和start()的区别
    run()方法里面封装的是线程执行的代码,如果直接调用,是普通方法调用,由主线程执行。start()方法做了两件事情:一是启动了线程,二是调用了run()方法。要把自定义的类对象作为Thread类对象的构造参数传递的原因:因为自定义的类它没有start()方法,只有Thread类有。而如果单独创建Thread类的对象使用。那么,最终使用的run方法默认情况下跟自定义的类对象无关。为了让Thread类调用run()是自定义类里面的,所以,我们把它作为参数传递。

三.线程的安全问题

      线程安全问题出现的原因:当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与了进来执行,导致了共享数据的错误。总结归纳为以下两点:
    ① 线程在操作共享的数据
    ② 共享数据的线程代码有多条
    线程安全问题的解决思路:将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候,其他线程不可以参与运算。必须要把这些代码都执行完毕后,其他线程才可以参与运算。在Java中通过两种方式来解决线程安全问题:一种是同步函数;另一种是同步代码块。
1.同步代码块
    同步的前提:必须有多个线程并使用同一个锁通过同步代码块来防止线程安全问题的发生,synchronized(obj)中需要传入一个任意的对象参数作为锁将无法得到同步锁,进而将无法继续执行代码块中的内容)
代码演示: (注意:注释中包含重要知识点和注意点)
<span style="font-family:KaiTi_GB2312;"><strong>//使用了同步可以防止线程安全问题出现,但是却降低了运行的效率
synchronized(obj){
	
	if(num>0){
		
		//子类不能抛出父类没有的异常。由于是实现了Runnable接口,所以只能处理不能抛出
		try{
			Thread.sleep(10) ;
		}catch(InterruptedException e){}
		
		System.out.println(Thread.currentThread().getName()+"----"+num--);
	}
}</strong></span>
    其中,synchronized(obj)中的obj是Object类的对象。说白了,synchronized(obj)括号中需要传入的是一个对象,这个对象就是同步锁。
2.同步函数
    所谓的同步函数实际上就是在函数声明上加上关键字synchronized。不过需要注意的是:同步函数的锁是所在类的固定的this。而静态同步函数中没有this对象,静态的同步函数使用的锁是:该函数所属字节码文件对象,也可以使用getClass方法获取,也可以使用当前(类名.class)表示。
代码演示:
<span style="font-family:KaiTi_GB2312;"><strong><span style="font-size: 14px;">	public synchronized void show() {
		
		if(num>0){
			
			//子类不能抛出父类没有的异常。由于是实现了Runnable接口,所以只能处理不能抛出
			try{
				Thread.sleep(10) ;
			}catch(InterruptedException e){}
			
			System.out.println(Thread.currentThread().getName()+"--同步函数--"+num--);
		}
		flag = true ;
	</span><span style="font-size:14px;">}</span></strong></span>

    同步函数与同步代码块的区别是:同步函数的锁是所在类的固定的this,而同步代码块的锁是任意对象。由此可见同步代码块比同步函数使用起来更加灵活,所以开发中多用同步代码块。
3.单例设计模式中的线程安全问题
    单例设计模式有两种实现方式:一种是懒汉式,一种是饿汉式。由于饿汉式共享的代码只有一条,所以不会出现线程安全问题。但是懒汉式的单例设计模式因为存在有共享的数据,所以是存在线程安全问题的。
通过以下代码熟悉如何处理单例设计模式中的线程安全问题:(注意:注释中包含重要知识点和注意点)
<span style="font-family:KaiTi_GB2312;"><strong>//懒汉式----(懒汉式有共享的数据,所以会产生线程安全问题,可以通过同步进行解决)
public class Single {
	
	private static Single s = null ;
	private Single(){}
	
	//使用同步函数解决线程安全问题,步骤简化但是效率变低了
/*	public static synchronized Single getInstance(){
		
		if(s==null)
			s = new Single() ;
		return s ;
	}*/

//使用同步代码块解决线程安全问题
public static Single getInstance(){
	
	while(true){
		
		//多加一句if语句判断可以提高效率,因为减少所的判断
		if(s==null)
			//使用同步代码块是为了解决线程安全问题
			synchronized(Single.class){
				
				if(s==null){
					
					s = new Single() ;
				}
			}
		return s ;
	}
}</strong></span>

四.死锁

    死锁指的是多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放,这种情况特别容易发生在两个锁发生嵌套的时候,两个线程分别持有自己的锁不放,而去等待对方的锁。由于线程被无限地阻塞,而导致的程序不能正常终止。  
    死锁产生的原因:
        ① 系统资源不足
        ② 进程运行推进的顺序不合适
        ③ 资源分配不当
              ……
注意:在处理多线程问题时,一定要注意不要出现死锁的情况!!!!

五.线程间的通信

   当多个线程操作同一资源,但是每个线程操作的动作不一样的时候就需要进行线程间的通讯。而再进行线程间通讯的时候,我们就需要利用到线程的等待唤醒机制。
1.等待/唤醒机制所使用的方法
    ① wait() ;    // 让线程处于冻结状态,被调用wait()方法的线程将会被存储到线程池中
    ② notify() ;    // 唤醒线程池中的一个线程(任何一个都有可能)
    ③ notifyAll() ;    // 唤醒线程池中的所有线程
2.通过一个生产者消费者的案例代码,进一步认识线程间的通信过程:  (注意:注释中包含重要知识点和注意点)
① 定义生产者和消费者类(生产者消费者是两个不同的线程,需要实现Runnable接口)
<span style="font-family:KaiTi_GB2312;"><strong>package cn.itzixue.producerandconsumer;

//定义生产者类:

public class Consumer implements Runnable {

	Resource r = null ;
	Consumer(Resource r){
		
		this.r = r ;
	}
	
	public void run() {
		
		//此处的循环必须加,不然此线程只能运行一次r.out() ;
		while(true){
			
			r.out() ;
		}
	}
	
	//定义一个消费者类:
	
	public class Producer implements Runnable{
		
		Resource r = null ;
		Producer(Resource r) {
			
			this.r = r ;
		}

		public void run(){
			
			//此处的循环必须加,不然此线程只能运行一次r.set("牛排") ;
			while(true){
				
				r.set("牛排") ;
			}
				
		}
	}
<span style="font-size:14px;">}
</span></strong></span>

② 定义资源类(资源类在这整个程序中属于共享的数据,所以应该解决线程安全问题,对代码进行同步处理)
<span style="font-family:KaiTi_GB2312;"><strong>package cn.itzixue.producerandconsumer;

public class Resource {

	private String name ;
	private int count = 1 ;
	boolean flag = false ;  //此标记是为了判断是否可以生产以及是否可以消费
	
	public synchronized void set(String name){
		
		/*
		 * 此处使用while循环判断标记,是为了让暂存在线程池中的线程在被唤醒时能够在判断一次标记,
		 * 		防止多生产多消费的发生。
		 * 如果是使用if判断,只能够判断一次,如果线程池中有暂存的线程,则当其被唤醒时,不会再经过判断而是直接向下执行代码,
		 * 		这将会导致多生产多消费的发生。
		 */
		while(flag)
			try {
				this.wait();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		
			this.name = name + count ;
			count++ ;
			System.out.println("生产者 :"+
					Thread.currentThread().getName()+"--生产--"+this.name) ;
			
			flag = true ;
			
			/*
			 * 此处使用notifyAll()是为了将处于线程池中的所有线程唤醒,防止死锁现象的发生。
			 * 
			 * 如果使用notify(),则因为前面代码中判断标识使用的是while循环,假如当两个消费者线程处于线程池中等待,
			 * 		并唤醒了生产这种的其中一个线程,这一生产者的线程在生产完之后,生产标志变成不需要生产,
			 * 同时唤醒的随机线程是另一个生产者的线程,则被唤醒的线程在经过标志判断后再次等待,存于线程池中,
			 * 		此时所有线程都在线程池中等待,导致了死锁发生。			
			 */
			notifyAll() ;
	}
	
	public synchronized void out(){
		
		while(!flag)
			try {
				this.wait();    //wait()、notify()、notifyAll()是Object监视器的方法,这些方法都需要在同步中使用。
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		
		System.out.println("消费者 :"+
					Thread.currentThread().getName()+"--消费--"+this.name);
		
		flag = false ;
		notifyAll() ;
	}
}<span style="font-size:14px;">
</span></strong></span>

3.使用wait()方法和使用sleep()方法的区别 
    ① wait()方法可以指定时间也可以不指定,sleep()方法必须指定时间
    ② 在同步中,wait()方法和sleep()方法对CPU的执行权和锁的处理不同:
        ☆ wait() :释放执行权,释放锁
        ☆ sleep() :释放执行权,不释放锁

六.JDK1.5新特性


   在JDK1.5以后将同步和锁封装成了对象,并将原本操作锁的隐式方式定义到了该对象中,将隐式的操作动作变成了显式的操作动作。其体现在两个接口上:一个是Lock接口;另一个是Condition接口。
1.Lock接口
    Lock接口的出现替代了同步代码块或者是同步函数,并将同步锁的隐式操作变成显示的操作锁。同时使得代码更为灵活,并且可以一个锁上加上多组监视器。
    ① 创建并初始化锁对象:(需使用其实现子类)
        Lock lock = new ReentrantLock() ;    // 使用Lock接口的实现类ReentrantLock对实例化锁对象
    ② 获取锁:(使用lock()方法)
        lock.lock() ;    // 使用Lock对象的lock()方法获取锁
    ③ 释放锁:(使用unlock()方法)
        lock.unlock() ;     
        // 使用Lock对象的unlock()方法释放锁。为了防止异常出现,导致锁无法被关闭,所以锁的关闭动作应该放在finally中
2.Condition接口
    Condition接口的出现替代了Object中的wait()、notify()、notifyAll()方法。并将这些监视器方法单独得进行了封装,变成Condition监视器对象,并且可以使用任意锁进行组合。
    ① 创建并初始化监视器对象:(需通过锁对象来获取)
        Condition producer_con = lock.newCondition() ;    
        // 使用锁对象的newCondition()方法获取对应锁的监视器对象
    ② Condition接口中的await()方法对应于Object中的wait()方法(线程等待)
    ③ Condition接口中的signal()方法对应于Object中的notify()方法(唤醒其中一个)
    ④ Condition接口中的signalAll()方法对应于Object中的notifyAll()方法(唤醒全部线程)
3.利用JDK1.5新特性的方法来完善原来关于生产者消费者的问题的代码
    由于生产者消费者类的定义没有变化,只改变了资源类的实现同步的方法,所以此处只演示JDK1.5新特性在资源类中的使用:    (注意:注释中包含中要注意点和知识点)
<span style="font-family:KaiTi_GB2312;"><strong>package cn.itzixue.producerandconsumerNEW;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Resource {

	private String name ;
	private int count = 1 ;
	boolean flag = false ; //此标记是为了判断是否可以生产以及是否可以消费 
	
	/*
	 * jdk1.5以后将同步和锁封装成了对象,并将操作锁的隐式方式定义到了该对象中,将隐式操作锁变成了显示动作
	 */
	Lock lock = new ReentrantLock() ;
	
	/*
	 * synchronized中的锁只能拥有一组监视器方法,而在jdk1.5之后,Condition接口替代了Object中的监视器对象
	 * 		因为将监视器封装成了对象,所以可以获取到多组监视器对象
	 */
	//通过已有的锁获取两组监视器,一组监视生产者,一组监视消费者
	Condition producer_con = lock.newCondition() ;
	Condition consumer_con = lock.newCondition() ;
	
	public void set(String name){
		
		lock.lock() ;    //获取锁
		
		try{
			while(flag)
				try {
					//this.wait() ;
					producer_con.await() ;  //当生产者判断出不需要生产时,调用生产者的监视器方法,让其暂存到线程池中等待
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			
				this.name = name + count ;
				count++ ;
				System.out.println("生产者 :"+
						Thread.currentThread().getName()+"--生产--"+this.name) ;
				
				flag = true ;
				
				//notifyAll() ;
				consumer_con.signal() ;  //唤醒线程池中的线程时,只需要调用消费者的监视器对象,唤醒消费者的线程,可有效避免死锁产生
		}
		finally{
			lock.unlock() ;    //释放锁(通常定义在finally中)
		}
	}
	
	public synchronized void out(){
		
		lock.lock() ;
		
		try{
			while(!flag)
				try {
					//this.wait() ;
					consumer_con.await() ;  //当消费者判断出不能够消费时,调用消费者的监视器方法,唤醒生产者的线程,让其暂存到线程池中等待
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			
			System.out.println("消费者 :"+
						Thread.currentThread().getName()+"--消费--"+this.name);
			
			flag = false ;
			//notifyAll() ;
			producer_con.signal() ;   //唤醒线程池中的线程时,只需要调用生产者的监视器对象,可有效避免死锁产生
		}
		finally{
			lock.unlock() ;
		}
	}
}
</strong></span>

七.停止线程


    在JDK1.5之前使用stop()方法来停止线程,但是由于该方法具有严重缺陷,所以已经过时。JDK1.5以后是通过在run()方法内对线程进行控制,通过run()方法的结束来实现结束线程。
    当线程处于冻结状态时,就不会读取到标记,就将会导致线程无法停止。这是需要对线程的冻结状态进行清除。强制让线程恢复到运行状态中来。这样就可以操作标记让线程得以结束。interrupt()方法就是用于让处于冻结等状态的线程回归正常运行状态的方法。

八.线程的其他方法
1.join()
    当A线程执行到了b线程.join()方法时,A线程就会被迫等待,等到B线程都执行完,A线程才会继续执行。(此时B和其他线程交替运行。)join()方法可以用来临时加入线程执行。
2.setPriority()
    通过setPriority()方法可以用来设置线程的优先级,其中:
        ① MAX_PRIORITY :最高优先级10
        ② MIN_PRIORITY :最低优先级1
        ③ NORM_PRIORITY :分配给线程的默认优先级
3.yield()
    使用该方法可以暂停当前正在执行的线程,让其他线程得以执行。







0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:9615次
    • 积分:348
    • 等级:
    • 排名:千里之外
    • 原创:24篇
    • 转载:0篇
    • 译文:0篇
    • 评论:14条
    文章存档