黑马程序员——多线程

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

一、多线程概述

1、进程

     进程:操作系统结构的基础,是一个正在执行的程序。如QQ,word等等。

     多进程:在同一时刻运行多个程序的能力。如听歌的同时可以聊QQ

 2、线程

      线程:一个程序同时执行多个任务,通常,每一个任务称为一个线程,线程即一条执行路径,一个执行单元。

      多线程:是为了使得多个线程并行的工作以完成多项任务,以提高系统的效率。

3、CPU执行的原理

      在听歌的时候可以聊QQ,表面上,CPU在同时处理这些程序,但是实际上却不是这样,CPU一次只能处理一个程序,但是cpu的计算效率非常快,不断切换计算任务,耗时短,造成并行处理的假象。

4、进程与线程区别

       多进程与多线程本质区别在于每个进程都有自己的一整套变量,而线程则共享数据。也就是说,QQ有着自己的变量,而听歌也有,他们之间的变量互不影响,但是线程确是共享数据。

二、创建线程的两种方式

1、继承Thread   

        线程本来就是一类事物,根据面向对象的思想,因此就存在了Thread这个类,通过查找Api帮助文档发现,继承Thread类需要复写run()方法,根据面向对象思想,的确,每个线程的任务不一样,当然需要自己指定任务。

       步骤:

      (1)写一个类,继承Thread

      (2)创建一个线程,Thread t1 = new  Thread子类();

      (3)开启线程t1.start(),这样线程就开始执行任务。

示例代码如下:

package cn.itheima.blog5;

//继承Thread类
public class TicketThread extends Thread{

	/**
	 * 通过继承Thread类来实现卖票
	 */
	
	private int ticket = 100;
	
	//重写run方法,将任务封装起来
	public void run(){
		while(true){
			if(ticket > 0){
				System.out.println(ticket--);
			}else{
				break;
			}
		}
	}
	
	public static void main(String[] args) {
		//创建一个线程
		Thread t1 = new TicketThread();
		//开启一个线程
		t1.start();
	}

}

 

 

2、实现Runnable接口

   当某些事物本身有自身的父类,但是又需要通过线程去完成任务,而java又不支持多继承,那该如何是好呢?因此就必须提供一个接口,这个接口便是Runnable,通过实现Runnable接口也可以创建一个线程。

       步骤:

      (1)class 类名 implements Runnable

      (2)复写run方法

      (3)创该类对象,类名 对象名 = new 类名()

      (4)新建一个Thread对象,将该类对象作为Thread的构造函数参数传入

      (5)开启线程任务

示例代码如下:

package cn.itheima.blog5;

public class TicketRunnable implements Runnable{

	private int ticket = 100;
	/**
	 * 实现Runnable接口
	 */
	//复写run方法
	@Override
	public void run() {
		sale();
	}
	
	//卖票的方法
	public void sale(){
		while(true){
			if(ticket > 0){
				System.out.println(Thread.currentThread().getName() + "----" + ticket--);
			}else{
				break;
			}
		}
	}
	
	public static void main(String[] args) {
		TicketRunnable tr = new TicketRunnable();
		Thread t1 = new Thread(tr);
		t1.start();
	}
}

 

3、对比总结

       当有100张票分为四个窗口卖,那用哪种方式创建线程比较好呢?

       我们肯定得创建一个卖票类,任务就是卖票,而且多个窗口卖,明显需要用到多线程,同时还要注意一点,实现的是票数共享。我们先尝试用继承Thread类来尝试一番,发现无论开启多少个线程,都无法模拟实现多个窗口共卖这一种票,而是自个为战,自己买自己的100张票。我们可以这样分析,新建每一个票类对象的时候都附上初始值100,当然会出现各自卖各自的100票,因此我们只能用实现Runnable接口这一方式来实现该功能。

      创建线程的两种方式的对比,实现Runnable相比继承Thread类具有如下优势

    (1)避免因单继承带来的局限性;

    (2)适合多个相同程序代码去处理同一资源;
    (3)增强了程序开发的健壮性,代码能够被多个线程共享,数据域代码是独立的。

三、多线程的状态

  1、创建状态

       在通过构造方法创建一个线程对象后,新的线程便处于创建状态,即Thread t = new Thread();便处于创建状态。

  2、就绪状态

     当线程调用start()后,便进入了就绪状态,说明线程具有cpu的执行资格,此时线程需要排队,等待cpu执行。

  3、运行状态

     当就绪状态的线程被调用并获得处理资源时,便进入了运行状态,说明线程具有cpu的执行权。

  4、阻塞状态

      在可执行状态下,若调用了sleep()、suspend()、wait()等方法,线程进入阻塞状态,阻塞后,线程不能进行排队,只有当阻塞的原因解除后线程才能转入就绪状态。

  5、死亡状态

     线程调用stop()或者run()方法运行结束,线程便进入死亡状态,此时线程已经不具有继续执行的能力。

四、多线程的安全问题及其解决办法

1、安全威胁

       我们来写一段代码,来实现上面卖票的需求,为了方便观察结果,每个线程在卖票前都休眠一段时间。

代码如下:

package cn.itheima.blog5;

public class TicketSafeDemo implements Runnable{

	/**
	 * 卖票线程安全问题演示
	 */
	private int tickNum = 100;
	
	//封装任务
	@Override
	public void run() {
		sale();
	}
	
	//卖票操作
	public void sale() {
		while(true){
			if(tickNum > 0){
				try {
					//将线程休眠20ms
					Thread.sleep(20);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName() + "卖掉了" + tickNum-- + "号票");
			}else{
				break;
			}
		}
	}
	
	public static void main(String[] args) {
		//创建任务对象
		TicketSafeDemo ts = new TicketSafeDemo();
		//新建三个线程
		Thread t0 = new Thread(ts);
		Thread t1 = new Thread(ts);
		Thread t2 = new Thread(ts);
		//开启三个线程
		t0.start();
		t1.start();
		t2.start();

	}

}

运行结果:
         

        一个令人不解的问题出现了,既然是卖100张票,为什么还卖到了0号,甚至是-1号票呢,这样票不是多了吗?在这,其实就出现了多线程操作共享数据的时候产生的安全威胁。那又是为什么出现安全威胁呢?

    我们来读一读代码,设票最后还剩下一张,0线程进去,判断,成功,进入循环,一进去,休眠;然后1线程进去,判断,又去上面的票数没减,判断依然成功;线程2如线程1一般,当线程都苏醒的时候,直接往下走,不进行判断,这样就出现了0,甚至-1号票的情况。

    安全威胁出现的原因:多条语句操作共享数据

 2、解决方案--同步

        既然出现了问题,我们就需要解决。我们有一个想法,就是加锁。如火车上上厕所,一进门,就关上门上锁,别人无法进来,只能等进去的人出来,别人才能进去。移植到java中,我们是不是也能加上一个锁,当有线程访问数据的时候,其他线程无法进入操作数据,这样问题就解决了。

    方案一:同步代码块

    引入对象锁的概念,以及关键字synchronized,用法为synchronzied(对象){需要被同步的代码};

package cn.itheima.blog5;

public class TicketSynchronzied implements Runnable {

	/**
	 * 卖票线程安全问题解决方案之同步代码块
	 */
	private int tickNum = 100;
	Object obj = new Object();

	// 封装任务
	@Override
	public void run() {
		sale();
	}

	// 卖票操作
	public void sale() {

		while (true) {
			synchronized (obj) {
				if (tickNum > 0) {
					try {
						// 将线程休眠20ms
						Thread.sleep(20);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName() + "卖掉了"
							+ tickNum-- + "号票");
				}else{
					break;
				}
			}
		}
	}

	public static void main(String[] args) {
		// 创建任务对象
		TicketSynchronzied ts = new TicketSynchronzied();
		// 新建三个线程
		Thread t0 = new Thread(ts);
		Thread t1 = new Thread(ts);
		Thread t2 = new Thread(ts);
		// 开启三个线程
		t0.start();
		t1.start();
		t2.start();
	}

}

    方案二:同步函数

    在函数声明中添加synchronized,非静态函数的对象锁的对象为this,而静态函数的对象锁的对象为this.class。       

package cn.itheima.blog5;

public class TicketSynFunction implements Runnable{

	/**
	 * 卖票线程安全问题演示
	 */
	private int tickNum = 100;
	
	//封装任务
	@Override
	public void run() {
		sale();
	}
	
	//卖票操作
	public synchronized void sale() {
		while(true){
			if(tickNum > 0){
				try {
					//将线程休眠20ms
					Thread.sleep(20);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName() + "卖掉了" + tickNum-- + "号票");
			}else{
				break;
			}
		}
	}
	
	public static void main(String[] args) {
		//创建任务对象
		TicketSynFunction ts = new TicketSynFunction();
		//新建三个线程
		Thread t0 = new Thread(ts);
		Thread t1 = new Thread(ts);
		Thread t2 = new Thread(ts);
		//开启三个线程
		t0.start();
		t1.start();
		t2.start();

	}

}


 

            同步的好处与弊端

           好处:解决了安全威胁

           弊端:需要判断锁,浪费资源,使程序效率变慢

        同步的前提

        当安全威胁存在,我们常用的方法为加锁,但是,如果加上锁,威胁依然没有消失,那么我们就必须去考虑同步的前提;也就是多个线程是否用同一个锁。

 

 3、同步引发的问题--死锁       

        死锁:是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。通常是因为锁的嵌套而产生的。死锁程序代码示例如下:

package cn.itheima.blog5;

public class ThreadDeadLock implements Runnable {

	/**
	 * 死锁示例
	 */
	private boolean flag;

	private ThreadDeadLock(boolean flag) {
		super();
		this.flag = flag;
	}

	@Override
	public void run() {
		if (flag) {
			while (true) {
				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) {
				synchronized (MyLock.lockb) {
					System.out.println(Thread.currentThread().getName()
							+ "---else---lockb---");
					synchronized (MyLock.locka) {
						System.out.println(Thread.currentThread().getName()
								+ "---else---locka---");
					}
				}
			}
		}
	}

	public static void main(String[] args) {
		ThreadDeadLock deadLock1 = new ThreadDeadLock(false);
		ThreadDeadLock deadLock2 = new ThreadDeadLock(true);
		Thread t1 = new Thread(deadLock1);
		Thread t2 = new Thread(deadLock2);
		t1.start();
		t2.start();
	}

}

class MyLock {
	public static final Object locka = new Object();
	public static final Object lockb = new Object();
}

       运行结果为:

       
五、线程之间的通信

     多线程的通信:多个线程操作同一资源,但是任务却不同,如生产者与消费者,生产者的任务是生产,而消费者的任务是消费,虽然他们操作的都是商品这一资源。

     需求:生产一个商品,就等消费者消费,消费完这一商品后,才能继续生产。

     出现这一需求我们就要加入等待唤醒机制,用生产者与消费者的例子来说,等待唤醒机制就是生产者生产完一个商品后,就叫消费者来消费;等消费者消费完以后,就叫生产者来生产。下面写一段代码来实现该需求: 

package cn.itheima.blog5;

/*
 * 线程通信之等待唤醒机制(以生产者消费者为例)
 * 有资源,为水
 * 一个线程生产水,另一个线程取走水
 * 生产一瓶水,就消费一瓶水;若没有水,则等待生产
 */
//共享资源为水
class Water {
	private String brand;
	private String description;
	private boolean flag = false;

	public synchronized void set(String brand, String description) {
		//如果有资源则等待取出
			if (flag) {
				try {
					wait();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			//无资源则生产水	
			this.brand = brand;
			this.description = description;
			//生产完水后,将标志位设定为有水
			flag = true;
			//唤醒等待的线程
			notify();
	}
	
	public synchronized void get(){
			//若没水,则等待生产;若有,则取走
			if(!flag){
				try {
					wait();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			System.out.println(brand + "..." + description);
			//将标志置为没水
			flag = false;
			//唤醒等待线程
			notify();
	}

}

class Producer implements Runnable {

	private Water r;

	public Producer(Water r) {
		this.r = r;
	}

	@Override
	public void run() {
		//交替生产两种水
		int x = 0;
		while(true){
			if(x == 0){
				r.set("润田", "好喝");
			}else{
				r.set("wahaha", "woxihuan");
			}
			x = (x + 1) % 2;
		}
	}

}

class Customer implements Runnable {

	private Water r;

	public Customer(Water r) {
		this.r = r;
	}

	@Override
	public void run() {
		while(true){
			r.get();
		}
	}

}

public class WaitAndNotify {
	public static void main(String[] args) {
		//新建资源
		Water r = new Water();
		//新建任务
		Producer pro = new Producer(r);
		Customer cust = new Customer(r);
		//新建执行路径
		Thread t1 = new Thread(pro);
		Thread t2 = new Thread(cust);
		//开启执行
		t1.start();
		t2.start();
	}

}


        上述代码只是解决了单个消费者,单生产者的问题,但是,若生产者与消费者均是多个,问题又会出现一个产品被消费几次,或者一个产品被生产几次的情况。代码如下:

package cn.itheima.blog5;

class Product_1{
	private String name;
	private int count = 0;
	private boolean flag = false;
	
	public synchronized void set(String name){
		//有产品,等待
		if(flag){
			try {
				wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		this.name = name + count++;
		flag = true;
		System.out.println(Thread.currentThread().getName() + "...生产了..." + this.name);
		notify();
	}
	
	public synchronized void get(){
		//没产品,取走
		if(!flag){
			try {
				wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		System.out.println(Thread.currentThread().getName() + "消费了" + name);
		flag = false;
		notify();
	}

}

class Input_1 implements Runnable{

	private Product_1 p;
	public Input_1(Product_1 p){
		this.p = p;
	}
	
	@Override
	public void run() {
		while(true){
				p.set("商品");
		}
	}
	
}

class Output_1 implements Runnable{
	
	private Product_1 p;
	public Output_1(Product_1 p){
		this.p = p;
	}

	@Override
	public void run() {
		while(true)
			p.get();
	}
	
}

public class MoreProCust_1 {

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		//产品
		Product_1 p = new Product_1();
		//两个线程操作同一任务--生产
		Input_1 input = new Input_1(p);
		Thread t1 = new Thread(input);
		Thread t2 = new Thread(input);
		//两个线程操作同一任务--消费
		Output_1 out = new Output_1(p);
		Thread t3 = new Thread(out);
		Thread t4 = new Thread(out);
		//开启任务
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}

}


       运行结果:

            

产生此种现象的因素主要有两点:

       1、于在判断标志位的时,线程若进入临时阻塞状态,后面唤醒后不会进行判断;

       2、唤醒线程的不确定性

   解决方案一:

          用while循环与notifyAll()改良,具体代码如下:

package cn.itheima.blog5;

class Product_1{
	private String name;
	private int count = 0;
	private boolean flag = false;
	
	public synchronized void set(String name){
		//有产品,等待
		while(flag){
			try {
				wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		this.name = name + count++;
		flag = true;
		System.out.println(Thread.currentThread().getName() + "...生产了..." + this.name);
		notifyAll();
	}
	
	public synchronized void get(){
		//没产品,取走
		while(!flag){
			try {
				wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		System.out.println(Thread.currentThread().getName() + "消费了" + name);
		flag = false;
		notifyAll();
	}

}

class Input_1 implements Runnable{

	private Product_1 p;
	public Input_1(Product_1 p){
		this.p = p;
	}
	
	@Override
	public void run() {
		while(true){
				p.set("商品");
		}
	}
	
}

class Output_1 implements Runnable{
	
	private Product_1 p;
	public Output_1(Product_1 p){
		this.p = p;
	}

	@Override
	public void run() {
		while(true)
			p.get();
	}
	
}

public class MoreProCust_1 {

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		//产品
		Product_1 p = new Product_1();
		//两个线程操作同一任务--生产
		Input_1 input = new Input_1(p);
		Thread t1 = new Thread(input);
		Thread t2 = new Thread(input);
		//两个线程操作同一任务--消费
		Output_1 out = new Output_1(p);
		Thread t3 = new Thread(out);
		Thread t4 = new Thread(out);
		//开启任务
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}

}

 

     解决方案二: jdk1.5新特性

      1.5以前的对象锁是一种隐式操作。根据面向对象思想,锁也是对象,因此就将锁进行封装,同时再此基础上增加监视器,突破以前一个锁只有一个监视器的局限性,将Object 监视器方法(waitnotifynotifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用。现用jdk1.5的新特性来解决该问题,代码如下:

package cn.itheima.blog5;

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

/**
 * 多生产者,多消费者的JDK1.5解决方案
 */

class Product_2 {
	private String name;
	private int count = 0;
	private boolean flag = false;
	//得到锁对象
	private Lock lock = new ReentrantLock();
	//分别创建生产者与消费者对应于该锁的监视器
	Condition input_con = lock.newCondition();
	Condition output_con = lock.newCondition();

	public void set(String name) {
		//加锁
		lock.lock();
		try {
			while (flag) {
				try {
					//生产者监视器控制的线程进入临时阻塞状态
					input_con.await();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			this.name = name + count++;
			flag = true;
			System.out.println(Thread.currentThread().getName() + "...生产了..."
					+ this.name);
			//唤醒消费者监视器控制的线程
			output_con.signal();
		} finally {
			//为防止锁无法释放,因此放在finally块中,释放锁
			lock.unlock();
		}
	}

	public void get() {
		//加锁
		lock.lock();
		try{
			while (!flag) {
				try {
					//消费者监视器控制的线程进入临时阻塞状态
					output_con.await();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			System.out.println(Thread.currentThread().getName() + "消费了" + name);
			flag = false;
			//唤醒生产者监视器控制的线程
			input_con.signal();
		}finally{
			//释放锁
			lock.unlock();
		}
	}
}

class Input_2 implements Runnable{

	private Product_2 p;
	public Input_2(Product_2 p){
		this.p = p;
	}
	
	@Override
	public void run() {
		while(true){
				p.set("商品");
		}
	}
}

class Output_2 implements Runnable{
	
	private Product_2 p;
	public Output_2(Product_2 p){
		this.p = p;
	}

	@Override
	public void run() {
		while(true)
			p.get();
	}
}

public class MoreProCust_2 {

	public static void main(String[] args) {
		Product_2 p = new Product_2();
		Input_2 input = new Input_2(p);
		Thread t1 = new Thread(input);
		Thread t2 = new Thread(input);
		
		Output_2 out = new Output_2(p);
		Thread t3 = new Thread(out);
		Thread t4 = new Thread(out);
		
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}

}


        刚开始对多线程技术的学习,的确有丈二和尚,摸不着头脑的感觉,但是毕老师有一句话可以解决很多问题,那就是线程其实就是一条执行路径,通过这句话我们就可以分析许多线程中出现的问题,然后一步步解决。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值