您的位置:首页 > 编程语言 > Java开发

【Java基础】——多线程

2015-06-17 22:12 337 查看

一、多线程概述

1、多线程

进程:正在进行中的程序。程序(任务)的执行过程。每一个进程都有一个执行顺序。该顺序是一个执行路径或者称为一个控制单元。

线程:就是进程中一个负责程序执行的独立控制单元(执行路径),线程控制着进程的执行。一个线程中可以多执行路径。一个进程中至少要有一个线程。

多线程程序在较低的层次上扩展了多任务的概念:一个程序同时执行多个任务。通常,每一个任务称为一个线程(thread),它是线程控制的简称。可以同时运行一个以上线程的程序称为多线程程序。
多线程与多进程的本质区别:在于每个进程拥有自己的一整套变量,而线程则共享数据。共享变量使线程之间的通信比进程之间的通信更有效、更容易。

2、多线程的好处和弊端:

好处:解决了多部分同时运行的问题
弊端:线程太多会导致效率的降低

其实应用程序的执行都是CPU在做着快速的切换完成的,这个切换是随机的。

3、JVM中的多线程

JVM在启动的时候会有一个进程java.exe。该进程中至少有一个线程负责java程序的执行。而且这个线程运行的代码存在于main方法中。该线程称为主线程。

扩展:其实更细节说明JVM多线程,JVM启动不止一个线程,还有负责垃圾回收机制的线程。

二、创建线程的两种方法

1、继承Thread类

步骤:

定义继承Thread类
复写Thread类中的run()方法
调用线程中的start()方法,该方法一方面启动线程,一方面调用了run()方法。

为什么要覆盖run()方法呢?将自定的方法存储在run()方法中,让线程运行。

2、声明实现Runnable接口

步骤:

定义类实现Runnable接口
覆盖Runnable接口中的run()方法,将线程要运行的代码存放在该run方法中。

通过Thread类建立线程对象
将Runnable接口的子类对象作为实际参数传递给Thread的构造函数。为什么要将Runnable接口的子类对象传递给给Tread的构造函数呢?因为,自定义的run方法所属的对象是Runnable接口的子类对象。

调用Thread类的start()方法开启线程并调用Runnable接口子类的run()方法。

实现Runnable接口的好处:

将线程的任务从线程的子类中分离出来,进行了单独的封装,按照面向对象的思想将任务封装成对象。
避免了java单继承的局限性。

所以在定义线程时,建议使用实现方式。

两种方式的区别:

继承Thread:线程代码存放在Thread子类run()方法中。
实现Runnable:线程代码存在接口的子类的run()方法中。

Java创建线程的两种方式(代码演示):

/*
	java创建线程的两种方法:
	在java中,创建线程有两种方法,一种是继承Thread类,一种是实现Runnable接口。
	本例子对创建线程的两种方式进行演示。
*/
public class ThreadTest{
	public static void main(String[] args){
		/*ThreadDemo  t1 = new ThreadDemo();
		ThreadDemo  t2 = new ThreadDemo();
		t1.start();
		t2.start();*/
		MyThread t = new MyThread();
		Thread t1 = new Thread(t);
		Thread t2 = new Thread(t);
		t1.start();
		t2.start();
		
	}
}

//创建线程的第一种方式
/*class ThreadDemo extends Thread{
	//复写run()方法
	public void run(){
		for(int i = 0;i < 50;i++){
			System.out.println(Thread.currentThread().getName()+"..."+"run+++"+i);
		}
	}
}*/
//创建线程的第二种方式
class MyThread implements Runnable{
	public void run(){
		for(int i = 0;i < 50;i++){
			System.out.println(Thread.currentThread().getName()+"......"+"MyThread run()");
		}
	}
}


三、线程安全问题

1、线程安全问题产生的原因

①多个线程在操作共享的数据

②操作共享数据的线程有多条。

当一个线程在执行操作共享数据的多条代码过程中,其它线程参与运算,就会导致线程安全问题的产生。

如下列代码所示,线程安全问题的产生代码演示:

/*
	线程安全问题的出现。
	本代码是出现了线程安全问题的代码
*/
class TiketDemo{
	public static void main(String[] args){
		Tiket t = new Tiket();
		
		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();
	}
}

class Tiket implements Runnable{
	private int tiket = 10;
	
	public void run(){
		while(true){
			if(tiket > 0){
			try{
				Thread.sleep(1000);
			}
			catch(Exception e){
				
			}
			System.out.println(Thread.currentThread().getName()+"卖了"+tiket--);
			}
		}
	}
}//看如下输出结果。。居然买了 -1  -2 票。。。。出现了线程安全问题
/*
Thread-1卖了9
Thread-0卖了10
Thread-2卖了8
Thread-3卖了7
Thread-1卖了6
Thread-0卖了5
Thread-3卖了4
Thread-2卖了4
Thread-0卖了3
Thread-1卖了2
Thread-2卖了1
Thread-3卖了0
Thread-1卖了-1
Thread-0卖了-2
*/


2、线程安全问题的解决

①解决问题的思路:将多余操作共享数据的代码封装起来。

②java中,用同步代码块或者同步函数可以解决这个问题。

同步代码块(用法如下)

synchronized(对象)
{需要被同步的代码}
同步可以解决安全问题的根本原因就在那个对象上,对象就如同锁,持有锁的线程可以在同步中执行,没有持有锁的线程即使获取了cpu的执行权,也进不去,因为没有获取锁。

同步函数

同步函数的格式是在函数上加上synchronized修饰符即可。

③同步的前提:

必须要有两个或者以上的线程
必须是多个线程使用同一个锁

④同步的好处和弊端:

好处:解决了线程安全问题
弊端:相对降低效率,因为同步外的线程都会判断同步锁,较为消耗资源。

线程安全问题的解决(代码实例,沿用上面出现安全问题的例子):

/*
	线程安全问题的出现。
	本代码是出现了线程安全问题的代码
*/
class TiketDemo{
	public static void main(String[] args){
		Tiket t = new Tiket();
		
		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();
	}
}

class Tiket implements Runnable{
	private int tiket = 10;
	
	// Object obj = new Object();
	public void run(){
		while(true){
			sale();//调用同步函数。
			//解决方案一、加入同步代码块
			/*synchronized(obj){
				if(tiket > 0){
					try{
					Thread.sleep(1000);
					}
					catch(Exception e){
				
					}
					System.out.println(Thread.currentThread().getName()+"卖了"+tiket--);
				}
			}*/
			
			//解决方案二:同步函数
		}
	}
	//同步函数。
	public synchronized void sale(){
		if(tiket > 0){
			try{
				Thread.sleep(1000);
			}
			catch(Exception e){
				
			}
			System.out.println(Thread.currentThread().getName()+"卖了"+tiket--);
		}
	}
}


3、线程状态

①线程可以有如下六种状态

New(新生):新建状态,至今尚未启动
Runnable(可运行):运行状态,正在Java虚拟机中执行了的线程处于这种状态
Blocked(被阻塞):受到阻塞并等待某个监听器的锁的线程处于这个状态
Waiting(等待):冻结状态
Timed Waiting(计时等待):等待状态
Terminated(被终止):已退出

要确定一个线程的当前状态,可以调用getState方法。

②线程状态图



4、线程间通信——等待唤醒机制

概念:多个线程在操作同一个资源,但是操作的动作不同。

①等待唤醒机制

a、涉及的方法:

wait():让线程处于冻结状态,被wait的线程会被存储到线程池中。
notify():随机选择一个在该对象上调用wait方法的线程,解除其阻塞状态。
notifyAll():接触那些在该对象上调用wait方法的线程的阻塞状态。

wait() notify() notifyAll全都使用在同步中!因为要对持有监视器(锁)的线程进行操作。所以要使用在同步中,因为只有同步才有锁。

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

因为这些方法在操作同步中线程时,都必须要标识他们所操作线程持有的锁。只有同一个锁上的被等待线程可以被同一个锁上的notify唤醒,不可以对不同锁中的线程进行唤醒。也就是说,等待和唤醒必须是同一个锁。而锁可以是任意对象,所以可以被任意对象调用的方法定义在Object类中。

c、等待唤醒机制代码实例:

/*
	线程间的通信:其实就是多个线程在操作同一个资源,但操作的动作不同。
	如一个写入线程,一个读出线程。
	等待唤醒机制。
*/
//资源类
class Res
{
	String name;
	String sex;
	boolean flag = false;//定义一个标记,初始为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="huangxiang";
					r.sex="nan";
				}
				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();
	}
}


d、在JDK1.5以后,出现了新的lock接口,将同步synchronized替换成显示的Lock操作。将Object中wait,notify,notifyAll,替换成了Condition对象。该Condition对象可以通过Lock锁进行获取,并支持多个相关的Condition对象。

该接口中的方法摘要如下:

lock():获取锁
lockInterruptibly():如果当前线程未被中断,则获取锁
newCondition():返回绑定到此 Lock 实例的新Condition
实例。
tryLock():仅在调用时锁为空闲状态才获取该锁。
tryLock(long time,TimeUnit unit):如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁
unlock():释放锁。

下面用生产者消费者问题来演示升级后的线程间通信问题:

import java.util.concurrent.locks.*;

class ProducerConsumerDemo2 
{
	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();
	}
}

/*
JDK1.5 中提供了多线程升级解决方案。
将同步Synchronized替换成现实Lock操作。
将Object中的wait,notify notifyAll,替换了Condition对象。
该对象可以Lock锁 进行获取。
该示例中,实现了本方只唤醒对方操作。

Lock:替代了Synchronized
	lock 
	unlock
	newCondition()

Condition:替代了Object wait notify notifyAll
	await();
	signal();
	signalAll();
*/
class Resource
{
	private String name;
	private int count = 1;
	private boolean flag = false;//设置标记
			//  t1    t2
	private Lock lock = new ReentrantLock();
	//创建两个Condition对象,分别控制等待或唤醒本方和对方线程。
	private Condition condition_pro = lock.newCondition();
	private Condition condition_con = lock.newCondition();

	public  void set(String name)throws InterruptedException
	{
		//锁。
		lock.lock();
		try
		{
			while(flag)
				condition_pro.await();//t1,t2
			this.name = name+"--"+count++;

			System.out.println(Thread.currentThread().getName()+"...生产者.."+this.name);
			flag = true;
			condition_con.signal();
		}
		finally
		{
			lock.unlock();//释放锁的动作一定要执行。
		}
	}

	//  t3   t4  
	public  void out()throws InterruptedException
	{
		lock.lock();
		try
		{
			while(!flag)
				condition_con.await();
			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)
		{
			try
			{
				res.set("+商品+");
			}
			catch (InterruptedException e)
			{
			}	
		}
	}
}
//消费者线程
class Consumer implements Runnable
{
	private Resource res;

	Consumer(Resource res)
	{
		this.res = res;
	}
	public void run()
	{
		while(true)
		{
			try
			{
				res.out();
			}
			catch (InterruptedException e)
			{
			}
		}
	}
}


5、停止线程

线程的停止有以下两个原因:

因为run方法正常退出而自然死亡。
因为一个没有捕获的异常终止了run方法而意外死亡。

特别是,可以调用线程中的stop方法杀死一个线程,该方法抛出ThreadDeath错误对象,由此杀死线程,但是,stop方法已经过时,不要在自己的代码中调用它。

那么,现在我们如何来停止线程呢?就是让run方法结束。

方案一:多线程运行通常是循环结构,只要控制了循环,就可以让run方法结束。如下列代码所示,我们在首先设置一个标记flag,在该线程执行一段时间之后,将标志设置为false,则该run方法结束,自然线程也就结束了。

public  void run()
{
	while(flag)
	{	
		System.out.println(Thread.currentThread().getName()+"....run");
	}
}


方案二:在方案一中的解决方案可以解决一般情况下线程的停止,但是有一种特殊情况,即当线程处于冻结状态时,方法读取不到该标记,那么线程也就不会结束,此时,我们就需要使用Thread类中的innerrupt()方法对冻结状态进行清除,让线程恢复到运行状态。如下列代码所示:

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);	
        t1.start();
        t2.start();	

		int num = 0;
		while(true)
		{
			if(num++ == 60)
			{
				t1.interrupt();//清除冻结状态
				t2.interrupt();
				st.changeFlag();//改变循环标记
				break;
			}
			System.out.println(Thread.currentThread().getName()+"......."+num);
		}
		System.out.println("over");
	}
}


6、守护线程(Daemon Thread)

守护线程和普通线程在写法上没什么区别,我们可以通过调用t.setDaemon(true)将线程转为守护线程。守护线程唯一的用途是为其它线程提供服务,当只剩下守护线程时,虚拟机就退出了,如果只剩下守护线程,那么就没有必要继续运行程序了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: