一.  多线程概述

1. 进程和线程的概念及区别

进程: 进程是一个正在运行的程序。程序是一个没有生命的实体,只有处理器赋予程序生命时,它才能成为一个活动的实体,我们称其为进程。

线程: 一个程序内部的顺序控制流,也可以说是进程中的一个独立的控制单元。线程在控制着进程的执行。

①只要进程中有一个线程在执行,进程就不会结束。②一个进程中至少有一个线程。

2. 多线程概述及初步理解

    在java虚拟机启动的时候会有一个java.exe的执行程序,也就是一个进程。该进程中至少有一个线程负责java程序的执行。而且这个线程运行的代码存在于main方法

中。该线程称之为主线程。JVM启动除了执行一个主线程,还有负责垃圾回收机制的线程。这种在一个进程中有多个线程同时执行的方式,就叫做多线程。

3. 多线程存在的意义

多线程的出现能让程序产生同时运行效果。可以提高程序执行效率。

例如:在java.exe进程执行主线程时,如果程序代码特别多,在堆内存中产生了很多对象,而同时对象调用完后,就成了垃圾。如果垃圾过多就有可能是堆内存出现内存

不足的现象,只是如果只有一个线程工作的话,程序的执行将会很低效。而如果有另一个线程帮助处理的话,如垃圾回收机制线程来帮助回收垃圾的话,程序的运行将变得

更有效率。

 

二.  创建线程的方式

创建线程的方式主要有两种:1. 继承Java.lang.Thread类  2. 实现runnable接口,下面具体说明:

1. 继承Java.lang.Thread类

通过继承Thread类,然后复写其run方法的方式来创建线程

实现步骤:   a.定义类继承Thread → b.重写Thread类中的run方法  → c. 创建定义该类的实例对象,调用start方法开启线程

注:如果对象直接调用run方法,等同于在主线程中执行普通方法run(),自定义的线程并没有启动。

2. 实现runnable接口方式

使用继承方式有一个弊端,那就是如果该类本来就继承了其他父类,那么就无法通过Thread类来创建线程了。

这样就有了第二种创建线程的方式:实现Runnable接口,并重写其中run方法的方式。

实现步骤:   a.定义类实现Runnable接口 → b.重写runnable接口中的run方法  → c.通过Thread类创建线程对象,将Runnable接口的子类对象作为实参传递给Thread类的构造函数→ d 调用Thread类的

start方法开启线程

实现方式实现的好处: 避免了单继承的局限性。

两种方式开启线程的区别:

① 线程代码存放的位置不一样,继承Thread方式:存放在Thread子类中   而实现Runnable方式:存放在runnable接口的子类中

② 实现runnable方式开启线程能避免单继承的局限性

③ 实现runnable方式开启因为线程代码存放位置,能将一些资源独立出来,比如下面例子的票号,就不需要加static静态修饰

package create;

/*
 * 需求:简易的多窗口卖票程序
 * 总票数100张,从100票号开始卖,卖出一张票票号减1
 */
public class TicketDemo{
	
	static class Ticket implements Runnable{

		private  static int tick = 100; //设置起始票号
		@Override
		public void run() {
			while(true){
				if(tick>0){
					System.out.println(Thread.currentThread().getName()+"号线程售出:"+tick-- +"号票");  
				}
			}
		}
	}
	
	public static void main(String[] args) {
	//创建Runnable接口子类的实例对象  
        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();  
	}
}

 

三. 线程的几种状态及演变

① 新建状态(New)

当用new操作符创建一个线程时, 例如new Thread(r),线程还没有开始运行,此时线程处在新建状态。 当一个线程处于新生状态时,程序还没有开始运行线程中的代码

② 就绪状态(Runnable)

一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。start()

方法返回后,线程就处于就绪状态。

处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程。因为在单CPU的计算机系统中,不可能同时运行多个线程,一个时刻仅有

一个线程处于运行状态。因此此时可能有多个线程处于就绪状态。对多个处于就绪状态的线程是由Java运行时系统的线程调度程序(thread scheduler)来调度的。

③ 运行状态(Running)

当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法.

④ 阻塞状态(Blocked)

线程在执行过程中,可能由于各种原因进入阻塞状态:

1. 调用sleep()方法进入睡眠状态

2. 线程试图得到一个锁,而该锁正被其他线程持有

3. 线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者(不怎么会用)

......

注意:处于阻塞状态的线程可以通过notify()方法激活到就绪状态

⑤ 死亡状态(Dead)

通过stop(),interrupt()方法强制结束线程,或者执行完run()方法结束

为了确定线程在当前是否存活着(就是要么是可运行的,要么是被阻塞了),需要使用isAlive方法。如果是可运行或被阻塞,这个方法返回true; 如果线程仍旧是new状态且不是可运行的, 或者线程死亡

了,则返回false

 

状态转换图:

 

 

 四. 多线程安全问题

 多线程在提高效率的同时,如果操作不当,就可能带来安全隐患,具体表现如下:

1. 导致安全问题的出现的原因

 当多条语句在操作同一线程共享数据时,一个线程对多条语句只执行了一部分,还没用执行完,另一个线程参与进来执行。导致共享数据的错误

 简单来说,原因有两点:① 多个线程访问出现延迟   

                                ② 线程随机性

2. 解决办法——同步

同步就是当多个线程操作共享数据时,只能让一个线程都执行完,在执行过程中,当其他线程涉及该共享数据时,只能等待。

同步的关键字是---synchronized(同步)

3. 同步的两种方式:   ① 同步代码块    ② 同步函数

① 同步代码块

用法:

 synchronized(对象){

需要被同步的代码;

}

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

 ② 同步函数

public synchronized void show(){

需要同步的语句;

}

我们知道,同步需要有一个锁,也就是对象,那么同步函数用的是哪个锁呢?

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

4. 同步的前提

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

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

利用这两个性质,可以判断如果符合这两个条件,就不会出现安全问题!

注意:如果同步函数被静态修饰后,使用的锁该类对应的字节码对象。 如:类名.class 该对象的类型是Class

 

五. 死锁 

当同步中嵌套同步时,线程占有一个锁对象,进而请求另一个锁对象,循环下去--就有可能出现死锁现象

例子: 

package create;

/**
 * 死锁小例子
 * @author J
 */
public class DeadLock implements Runnable {
	// 静态对象是类的所有对象共享的
	private static Object o1 = new Object(), o2 = new Object();

	private boolean flag; // 标志,用于区分进入进程的不同同步区域

	DeadLock(boolean flag) {
		this.flag = flag;
	}

	@Override
	public void run() {
		if (flag) {
			synchronized (o1) {
				System.out.println(Thread.currentThread().getName()
						+ "------if_locka");
				synchronized (o2) {
					System.out.println(Thread.currentThread().getName()
							+ "------if_lockb");
				}
			}
		} else {
			synchronized (o2) {
				System.out.println(Thread.currentThread().getName()
						+ "------if_lockb");
				synchronized (o1) {
					System.out.println(Thread.currentThread().getName()
							+ "------if_locka");
				}
			}
		}
	}

	public static void main(String[] args) {
		// 创建2个进程,并启动
		new Thread(new DeadLock(true)).start();
		new Thread(new DeadLock(false)).start();
	}

}

 

六. 线程间通信

当多个线程并发执行时,在默认情况下CPU是随机切换线程的。如果我们希望他们有规律的执行, 就可以使用通信, 例如两个线程执行一次打印操作,一个线程输入数据完成后,另一个线程取数据打印

具体实现: 等待唤醒机制,即:① 如果希望线程等待,就调用wait()  ② 如果需要唤醒等待的线程,就调用notify方法. 

注意: wait(),notify(), notifyAll()这三个方法都需要使用在同步中,因为同步才拥有锁,而这些方法用于对持有同一个监视器(锁)的线程进行操作

换言之,在同一个锁上wait的对象只能被同一个锁上的notify唤醒

而锁是任意对象,所以这三个方法定义在object类上

 例子:  使用同步操作同一资源

package create;

/**
 * 使用同步使用同一资源,并且两个线程协调进行 即: 产生一个就使用一个
 * 
 * @author J
 * 
 */
public class ResouseDemo {

	/*
	 * 资源:包含 姓名,性别,以及一个标志位
	 */
	static class Resource {
		private String name;
		private String sex;
		private boolean flag;

		public synchronized void setInput(String name, String sex) {
			if (flag) {
				try {
					wait();
				} catch (Exception e) {
				}
			}
			this.name = name;
			this.sex = sex;
			flag = true;
			notify();// 唤醒等待
		}

		public synchronized void getOutput() {
			if (!flag) {
				try {
					wait();
				} catch (Exception e) {
				}// 如果木有资源,等待存入资源
			}
			System.out.println(name + sex);
			flag = false;
			notify(); // 将输入线程唤醒
		}
	}

	static class Input implements Runnable {
		private Resource r;

		public Input(Resource r) {
			this.r = r;
		}

		@Override
		public void run() {
			int x = 0;
			// 循环交替输入资源
			while (true) {
				if(x==0){
					r.setInput("韩雷雷",".....男");  
				}else{
					r.setInput("李东东",".....女");
				}
				x = (x+1)%2;
			}
		}
	}

	static class Output implements Runnable {
		private Resource r;

		public Output(Resource r) {
			this.r = r;
		}

		// 循环交替打印输出
		@Override
		public void run() {
			while (true) {
				r.getOutput();
			}
		}
	}

	public static void main(String[] args) {
		Resource r = new Resource(); // 定义一个资源
		new Thread(new Input(r)).start();// 开启存线程
		new Thread(new Output(r)).start();// 开启取线程
	}

}

几个小问题??

1. wait(),notify(),notifyAll(),用来操作线程为什么定义在了Object类中?

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

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

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

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

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

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

3. 为甚么要定义notifyAll?

因为在需要唤醒对方线程时。如果只用notify,容易出现只唤醒本方线程的情况。导致程序中的所以线程都等待。

注意: 当有超过两个线程进行通信时,修改如下:1.因为多个线程,所以需要用notifyAll()  2. if判断改为while判断,因为唤醒之后要重新判断标记

  2. JDK1.5以后线程通信解决方案(重要)

JDK1.5以后使用ReentrantLock类的lock()和unlock()方法代替进行同步synchronized进行同步

使用步骤: ① Lock lock=new ReentrantLock()得到lock对象,在需要同步的多个线程中用lock()和unlock()代替synchronized用于同步

  ② Condition condition=lock.newCondition()得到condition对象,用condition.await()和condition.signal()等待和唤醒线程

注意: 一个lock对象可以有多个condition对象,用与分别控制不同线程的等待和唤醒,这是JDK1.5以后新特性的明显好处

不同的线程使用不同的Condition, 这样就能区分唤醒的时候找哪个线程了

升级解决方案的示例:生产者与消费者问题

import java.util.concurrent.locks.*;

/*
 * 用JDK1.5以后新特性解决
 * 生产者与消费者问题,商品总个数为不超过10个
 */
public class JDK5XinTeXing {
	static class Resource {
		private String name;
		private int conut = 0;

		private Lock lock = new ReentrantLock(); // 接口具体实现类(多态)

		Condition condition_pro = lock.newCondition();
		Condition condition_con = lock.newCondition();

		public void setProducer(String name) throws InterruptedException {
			lock.lock(); // 上锁
			try {
				while (conut >= 10) { // 生产超过10个,唤醒消费者消费
					condition_con.signal();
					condition_pro.await();
				}
				this.name = name;
				conut++;
				System.out.println(Thread.currentThread().getName() + "生产1个"
						+ this.name + ",总个数:" + conut);// 打印生产
			} finally {
				lock.unlock();// 解锁,这个动作一定执行
			}
			Thread.sleep(1000);
		}

		public void getConsumer() throws InterruptedException {
			lock.lock();
			try {
				while (conut <= 0) { // 商品数目只有0个,唤醒生产者
					condition_pro.signal();
					condition_con.await();
				}
				conut--;
				System.out.println(Thread.currentThread().getName() + "消费1个"
						+ this.name + ",总个数:" + conut);// 打印消费
			} finally {
				lock.unlock();
			}
			Thread.sleep(1000);
		}
	}

	// 生产者线程
	static class Producer implements Runnable {
		private Resource res;

		Producer(Resource res) {
			this.res = res;
		}

		@Override
		public void run() {
			while (true) {
				try {
					res.setProducer("牛奶");
				} catch (InterruptedException e) {
				}
			}
		}
	}

	// 消费者线程
	static 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) {
				}
			}
		}
	}

	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 
	}
}

运行结果图: 

 

七. 停止线程

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

那么现在我们该如果停止线程呢?———只有一种办法,那就是让run方法结束。

1. 线程中run()方法中的运行代码一般都是循环结构,只要让循环结束 —> run()方法就结束 —> 线程就结束了

一般做法是设置一个flag标记,并在线程代码中给出修改标记的实现方法

如:

  class a implements Runnable{
	private boolean flag = true;
	@Override
	public void run() {
		while(flag){
			System.out.println(Thread.currentThread().getName()+"....run");  
		}
	}
	public void changeFlag(){
		flag = false;
	}
  

  2.  有一种特殊情况:即线程因为某些原因处于冻结状态run方法无法结束,那么线程就不会结束。

比如:同步的wait()方法,Thread.sleep()方法使线程挂起,这是就需要对冻结进行清除,强制让线程恢复到运行状态中来

要做到我们可以使用: Thread类提供该方法interrupt() .

注意使用点: interrput()使用后会一砖头拍下去到catch(InterrpuException e)(接受包扎),在catch代码块中要对标记进行改变

class Test implements Runnable {
	private boolean flag = true;
	public synchronized void run() {
		while (flag) {
			try {
				wait();
			} catch (InterruptedException e) {
				System.out.println("分线程进入异常---");
				flag=false; //打断之后要改变标记使线程停止
			}
			System.out.println("分线程进入主循环---");
		}
	}

	public static void main(String[] args) {
		Test t = new Test();
		Thread thread = new Thread(t);
		thread.start();
		thread.interrupt();
	}
}

 

八. 线程中的其他一些方法讲解

1. SetDaemon(boolean on) 将该线程标记为用户线程(false)或者守护线程(true) ,

守护雅典娜:雅典娜是主线程,如果其他线程都是守护线程,雅典娜运行完了,其他线程就结束了

① 当正在运行的线程都是守护线程时,Java虚拟机自动退出

② 该方法必须在启动线程前使用

2.   public final void join() throws InterrupttedException

说明:在A线程中遇到B.join()时

① A将释放CPU执行权利  ② 只有等到B线程执行完,A线程才开始继续执行 ③ Join可以用于在主线程中临时加入线程执行

3.  setPriority(int newPriority) 设置优先级,优先级高的容易得到CPU执行权,默认值为5

 

        MAX_PRIORITY 最高优先级10

 

        MIN_PRIORITY   最低优先级1

 

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

 

4.   public static void yield() ----  Thread.yield()

      可以暂停当前线程,让其他线程执行

 

 九. 什么时候用多线程?

当某些代码需要同时被执行时,就用单独的线程进行封装。