【Java】多线程编程

1进程和线程

进程:一个进程就是一个执行中的程序。每一个进程都有自己独立的一块内存空间,一组系统资源。

线程:线程就是进程中的一个负责程序执行的控制单元(执行路径)。同类的多个线程是共享一块内存空间和一组系统资源。所以系统在各个线程之间切换时,开销要比进程小得多,正因如此,线程被称为轻量级进程。一个进程中可以包含多个线程。

Java程序至少会有一个线程,这就是主线程,程序启动后由JVM创建主线程,程序结束时由JVM停止主线程。主线程负责管理子线程,即子线程的启动,挂起,停止等操作。

获取主线程示例代码如下:

package duoxiancheng;
public class HelloThread {
	public static void main(String[] args) {
		//获取主线程
		Thread mainThread=Thread.currentThread();
		System.out.println("主线程名:"+mainThread.getName());
	}
}

Thread.currentThread()可以获得当前线程对象,getName()是Thread类的实例方法,可以获得线程的名字。

2创建子线程

Java中创建一个子线程涉及java.lang.Thread类java.lang.Runnable接口

Thread是线程类,创建一个Thread对象就会产生一个新的线程。

实现Runnable接口对象是线程执行对象,需要实现run()方法。子线程处理代码放到run()方法中,run()方法称为线程体。

有两种方式可以创建子线程:

①实现Runnable接口,实现run()方法。

②继承Thread类,重写run()方法。

2.1实现Runnable接口

创建线程Thread对象时,可以将线程执行对象传递给它,这需要用到Thread类的两个构造方法:

Thread(Runnable target):target是线程执行对象,实现Runnable接口。线程名字是由JVM分配的。

Thread(Runnable target, String name) :target是线程执行对象,实现Runnable接口。name是线程名字。

下面看一个具体示例。实现Runnable接口的线程执行对象Runner代码如下:

package duoxiancheng;
//线程执行对象
public class Runner implements Runnable {
	//编写执行线程代码
	@Override
	public void run() {
		for(int i=1;i<=10;i++)
		{
			//打印次数和线程的名字
			System.out.printf("第%d次执行 - %s\n",i,Thread.currentThread().getName());
			//随机生成休眠时间
			try {
				long sleepTime=(long)(1000*Math.random());
				Thread.sleep(sleepTime);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		//线程执行结束
		System.out.println("执行完成!"+Thread.currentThread().getName());
	}
}

代码Thread.sleep(sleepTime)是休眠当前线程,sleep是静态方法。它有两个版本:

static void sleep(long millis):使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。

static void sleep(long millis, int nanos):导致正在执行的线程以指定的毫秒数加上指定的纳秒数来暂停(临时停止执行)。

测试程序HelloThread代码如下:

package duoxiancheng;
public class HelloThread {
	public static void main(String[] args) {
		//创建线程t1,参数是一个线程执行对象Runner
		Thread t1=new Thread(new Runner());
		//开始线程t1
		t1.start();
		
		//创建线程t2,参数是一个线程执行对象Runner
		Thread t2=new Thread(new Runner(),"MyThread");
		//开始线程t2
		t2.start();
	}
}

线程创建完成还需要调用start()方法才能执行,start()方法一旦调用,线程进入可以执行状态,可以执行状态下的线程等待CPU调度执行,CPU调度后线程进入执行状态,运行run()方法。

运行结果:

2.2继承Thread线程类

事实上,Thread类也实现了Runnable接口,所以Thread类也可以作为线程执行对象,这需要继承Thread类覆盖run()方法。

采用继承Thread类重新实现2.1节示例。自定义线程类MyThread代码如下:

package duoxiancheng;
//线程执行对象
public class MyThread extends Thread {
	public MyThread()
	{
		super();
	}
	public MyThread(String name)
	{
		super(name);
	}
	//编写执行线程代码
	@Override
	public void run() {
		// TODO Auto-generated method stub
		for(int i=1;i<=10;i++)
		{
			//打印次数和线程的名字
			System.out.printf("第%d次执行 - %s\n",i,Thread.currentThread().getName());
			//随机生成休眠时间
			try {
				long sleepTime=(long)(1000*Math.random());
				Thread.sleep(sleepTime);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		//线程执行结束
		System.out.println("执行完成!"+Thread.currentThread().getName());
	}
}

测试程序HelloThread代码如下:

package duoxiancheng;
public class HelloThread {
	public static void main(String[] args) {
		//创建线程t1
		Thread t1=new MyThread();
		//开始线程t1
		t1.start();
		
		//创建线程t2
		Thread t2=new MyThread("MyThread");
		//开始线程t2
		t2.start();
	}
}

2.3使用匿名内部类和Lambda表达式实现线程体

如果线程体使用的地方不是很多,可以不用单独定义一个类。可以使用匿名内部类或Lambda表达式实现Runnable接口。

函数式接口,匿名内部类可以使用lamda表达式。

重新实现2.1节示例.代码如下:

package duoxiancheng;
public class HelloThread {
	public static void main(String[] args) {
		//创建线程t1,参数是实现Runnable接口的匿名内部类
		Thread t1=new Thread(new Runnable() {		//①
			//编写执行线程代码
			@Override
			public void run() {
				for(int i=1;i<=10;i++)
				{
					//打印次数和线程的名字
					System.out.printf("第%d次执行 - %s\n",i,Thread.currentThread().getName());
					//随机生成休眠时间
					try {
						long sleepTime=(long)(1000*Math.random());
						Thread.sleep(sleepTime);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				//线程执行结束
				System.out.println("执行完成!"+Thread.currentThread().getName());
			}
		});
		//开始线程t1
		t1.start();
		
		//创建线程t2,参数是实现Runnable接口的Lambda表达式
		Thread t2=new Thread(()->{		//②
			for(int i=1;i<=10;i++)
			{
				//打印次数和线程的名字
				System.out.printf("第%d次执行 - %s\n",i,Thread.currentThread().getName());
				//随机生成休眠时间
				try {
					long sleepTime=(long)(1000*Math.random());
					Thread.sleep(sleepTime);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			//线程执行结束
			System.out.println("执行完成!"+Thread.currentThread().getName());
		},"MyThread");
		//开始线程t2
		t2.start();
	}
}

上述代码第①行采用匿名内部类实现Runnable接口,覆盖run()方法。这里使用的是Thread(Runnable target)构造方法。代码第②行采用Lambda表达式实现Runnable接口,覆盖run()方法。这里使用的是Thread(Runnable target, String name)构造方法,Lambda表达式是它的第一个参数。

3线程状态

线程从创建、运行到结束总是处于下面五个状态之一:新建状态就绪状态运行状态阻塞状态死亡状态

1.新建状态

新建状态(new)是用new操作符创建一个线程时。此时程序还没有开始运行线程中的代码,它仅仅是一个空的线程对象。

2.就绪状态

当主线程调用新线程的start()方法后,它就进入就绪状态(Runnable)。此时的线程尚未真正开始执行run()方法,它必须等待CPU的调度。

3.运行状态

CPU调度就绪状态的线程,线程进入运行状态(running),处于运行状态的线程独占CPU,执行run()方法。

4.阻塞状态

因为某种原因进入运行状态的线程会进入不可运行状态,即阻塞状态(blocked),处于阻塞状态的线程JVM系统不能执行,即使CPU空闲,也不能执行该线程。线程运行过程中,可能由于各种原因进入阻塞状态:

①当前线程调用sleep()方法,进入睡眠状态。

②当前线程调用wait()方法。除非线程收到nofify()或者notifyAll()消息,否则不会变成就绪态。

③被其他线程调用了join()方法,等待其他线程结束。

④发出I/O请求,等待I/O操作完成期间。

处于阻塞状态可以重新回到就绪状态,如休眠结束,其他线程加入,I/O操作完成,调用notify()或notifyAll()唤醒wait线程。

5.死亡状态

线程退出run()方法后,就会进入死亡状态(dead)。线程进入死亡状态有可能是正常执行完成run()方法后进入,也有可能是由于发生异常而进入的。

4线程管理

4.1线程优先级

Java提供了10种优先级,分别用1~10整数表示,最高优先级是10,用常量MAX_PRIORITY表示;最低优先级是1,用常量MIN_PRIORITY表示;默认优先级是5,用常量NORM_PRIORITY表示。

Thread类提供了setPriority(int newPriority)方法用以设置线程优先级,通过getPriority()方法可以获得线程优先级。

设置线程优先级示例代码如下:

package duoxiancheng;
public class HelloThread2 {
	public static void main(String[] args) {
		//创建线程t1,参数是一个线程执行对象Runner
		Thread t1=new Thread(new Runner());
		t1.setPriority(Thread.MAX_PRIORITY);
		//开始线程t1
		t1.start();
		
		//创建线程t2,参数是一个线程执行对象Runner
		Thread t2=new Thread(new Runner());
		t2.setPriority(Thread.MIN_PRIORITY);
		//开始线程t2
		t2.start();
	}
}

多次运行上面的示例会发现,t1线程经常先运行,但是偶尔t2线程也会先运行。这些现象说明,影响线程获得CPU时间的因素,除了线程优先级外,还与操作系统有关。

4.2等待线程结束

在介绍线程状态时提到过join()方法,当前线程调用t1线程的join()方法,则阻塞当前线程,等待t1线程结束,如果t1线程结束或等待超时,则当前线程回到就绪状态。

Thread类提供了多个版本的join(),其定义如下:

void join() :等待这个线程结束。

void join(long millis) :等待这个线程结束的时长最多 millis毫秒。

void join(long millis, int nanos) :等待该线程结束的时间最长为 millis毫秒加上 nanos纳秒。

使用join()方法的场景是,一个线程依赖于另外一个线程的运行结果,所以调用另一个线程的join()方法等它运行完成。

4.3线程让步

线程类Thread还提供一个静态方法yield(),调用yield()方法能够使当前线程给其他线程让步。它类似于sleep()方法,能够使运行状态的线程放弃CPU使用权,暂停片刻,然后重新回到就绪状态。sleep()方法不同的是,sleep()方法是线程进行休眠,能够给其他线程运行的机会,无论线程优先级高低都有机会运行。而yield()方法只给相同优先级或更高优先级线程机会。yield()方法在实际开发中很少使用,大都使用sleep()方法,sleep()方法可以控制时间,而yield()方法不能。

4.4线程停止

线程体中的run()方法结束,线程进入死亡状态,线程就停止了。但是有些业务比较复杂,例如想开发一个下载程序,每隔一段执行一次下载任务,下载任务一般会由子线程执行,休眠一段时间再执行。这个下载子线程中会有一个死循环,为了能够停止子线程,设置一个结束变量。

示例代码如下:

package duoxiancheng;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class HelloThread3 {
	public static String command="";		//①
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		//创建线程t1,参数是一个线程执行对象Runner
		Thread t1=new Thread(()->{
			//一直循环,直到满足条件再停止线程
			while(!command.equalsIgnoreCase("exit"))		//②
			{
				//线程开始工作
				//TODO
				System.out.println("下载中......");
				try {
					//线程休眠
					Thread.sleep(10000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			//线程执行结束
			System.out.println("执行完成!");
		});
		//开始线程t1
		t1.start();
		
		
		try(InputStreamReader ir=new InputStreamReader(System.in);		//③
				BufferedReader in=new BufferedReader(ir)){
			//从键盘接收了一个字符串的输入
			command=in.readLine();		//④
		} catch (IOException e) {
			e.printStackTrace();
		}
		
	}
}

上述代码第①行是设置一个结束变量。代码第②行是在子线程的线程体中判断用户输入的是否为exit字符串,如果不是则进行循环,否则结束循环,结束循环就结束了run()方法,线程就停止了。

代码第③行中的System.in是一个很特殊的输入流,能够从控制台(键盘)读取字符。代码第④行是通过流System.in读取键盘输入的字符串。

运行结果:

注意:控制线程的停止有人会想到使用Thread提供的stop()方法,这个方法已经不推荐使用,因为这个方法有时会引发严重的系统故障,类似还有suspend()resume()挂起方法。Java现在推荐的做法就是采用本例的结束变量方式。

5线程安全

5.1临界资源问题

多个线程间共享的数据称为共享资源临界资源,由于是CPU负责线程的调度,程序员无法精确控制多线程的交替顺序。这种情况下,多线程对临界资源的访问有时会导致数据的不一致性。

5.2多线程同步

为了防止多线程对临界资源的访问有时会导致数据的不一致性,Java提供了“互斥”机制,可以为这些资源对象加上一把“互斥锁”,在任一时刻只能由一个线程访问,即使该线程出现阻塞,该对象的被锁定状态也不会解除,其他线程仍不能访问该对象,这就是多线程同步。线程同步是保证线程安全的重要手段,但是线程同步客观上会导致性能下降。

可以使用synchronized关键字通过两种方式实现线程同步:一种是synchronized方法,使用synchronized关键字修饰方法,对方法进行同步;另一种是synchronized语句,将synchronized关键字放在对象前面限制一段代码的执行。

1.synchronized方法

synchronized关键字修饰方法实现线程同步,方法所在的对象被锁定。

以售票系统为例。TicketDB文件代码如下:

package duoxiancheng;
//机票数据库
public class TicketDB {
	//机票的数量
	private int ticketCount=5;
	
	//获得当前机票数量
	public synchronized int getTicketCount()
	{
		return ticketCount;
	}
	
	//销售机票
	public synchronized void sellTicket()
	{
		try {
			//线程休眠,阻塞当前线程,模拟等待用户付款
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.printf("第%d号票,已经售出\n",ticketCount);
		ticketCount--;
	}
}

调用代码如下:

package duoxiancheng;

public class HelloWorld {
	public static void main(String[] args) {
		TicketDB db=new TicketDB();
		
		//创建线程t1
		Thread t1=new Thread(()->{
			while(true)
			{
				int currTicketCount=db.getTicketCount();
				//查询是否有票
				if(currTicketCount>0)
				{
					db.sellTicket();
				}
				else
				{
					//无票退出
					break;
				}
			}
		});
		//开始线程t1
		t1.start();
		
		//创建线程t2
		Thread t2=new Thread(()->{
			while(true)
			{
				int currTicketCount=db.getTicketCount();
				//查询是否有票
				if(currTicketCount>0)
				{
					db.sellTicket();
				}
				else
				{
					//无票退出
					break;
				}
			}
		});
		//开始线程t2
		t2.start();
	}
}

2.synchronized语句

synchronized语句方式主要用于第三方类,不方便修改它的代码情况。其TicketDB.java类方法可以不用加synchronized关键字,但调用代码HelloWorld.java需做修改:

package duoxiancheng;
public class HelloWorld {
	public static void main(String[] args) {
		TicketDB db=new TicketDB();
		
		//创建线程t1
		Thread t1=new Thread(()->{
			while(true)
			{
				synchronized (db) {
					int currTicketCount=db.getTicketCount();
					//查询是否有票
					if(currTicketCount>0)
					{
						db.sellTicket();
					}
					else
					{
						//无票退出
						break;
					}
				}
			}
		});
		//开始线程t1
		t1.start();
		
		//创建线程t2
		Thread t2=new Thread(()->{
			while(true)
			{
				synchronized (db) {
					int currTicketCount=db.getTicketCount();
					//查询是否有票
					if(currTicketCount>0)
					{
						db.sellTicket();
					}
					else
					{
						//无票退出
						break;
					}
				}
			}
		});
		//开始线程t2
		t2.start();
	}
}

6线程间通信

如果两个线程之间有依赖关系,线程之间必须进行通信,互相协调才能完成工作。

例如有一个经典的堆栈问题,一个线程生成了一些数据,将数据压栈;另一个线程消费了这些数据,将数据出栈。这两个线程互相依赖,当堆栈为空,消费线程无法取出数据时,应该通知生成线程添加数据;当堆栈已满,生产线程无法添加数据时,应该通知消费线程取出数据。

为了实现线程间通信,需要使用Object类中声明的5个方法:

void wait():等待当前线程释放对象锁,然后当前线程处于对象等待队列中阻塞状态,如下图所示,等待其他线程唤醒。

void wait(long timeout):同wait()方法,等待timeout毫秒时间。

void wait(long timeout,int nanos):同wait()方法,等待timeout毫秒加nanos纳秒时间。

void notify():当前线程唤醒此对象等待队列中的一个线程,如下图所示,该线程将进入就绪状态。

void notifyAll():当前线程唤醒此对象等待队列中的所有线程,如下图所示,这些线程将进入就绪状态。

下面是消费和生产示例中堆栈类代码:

package duoxiancheng;
//堆栈类
public class Stack {
	//堆栈指针初始值为0
	private int pointer=0;
	//堆栈有5个字符的空间
	private char[] data=new char[5];
	
	//压栈方法,加上互斥锁
	public synchronized void push(char c)
	{
		//堆栈已满,不能压栈
		while(pointer==data.length)
		{
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		//通知其他线程把数据出栈
		this.notify();
		//数据压栈
		data[pointer]=c;
		//指针向上移动
		pointer++;
	}
	
	//出栈方法,加上互斥锁
	public synchronized char pop()
	{
		//堆栈无数据,不能出栈
		while(pointer==0)
		{
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		//通知其他线程压栈
		this.notify();
		//指针向下移动
		pointer--;
		//数据出栈
		return data[pointer];
	}
}

调用代码如下:

package duoxiancheng;
public class HelloWorld {
	public static void main(String[] args) {
		Stack stack=new Stack();
		
		//下面的消费者和生产者所操作的是同一个堆栈对象stack
		//生产者线程
		Thread producer=new Thread(()->{
			char c;
			for(int i=0;i<10;i++)
			{
				//随机产生10个字符
				c=(char)(Math.random()*26+'A');
				//把字符压栈
				stack.push(c);
				//打印字符
				System.out.println("生产:"+c);
				try {
					//每产生一个字符线程就睡眠
					Thread.sleep((int)(Math.random()*1000));
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		});
		
		//消费者线程
		Thread consumer=new Thread(()->{
			char c;
			for(int i=0;i<10;i++)
			{
				//从堆栈中读取字符
				c=stack.pop();
				//打印字符
				System.out.println("消费:"+c);
				try {
					//每读取一个字符线程就睡眠
					Thread.sleep((int)(Math.random()*1000));
					} catch (InterruptedException e) {
						e.printStackTrace();
						}
				}
				});
		producer.start();	//启动生产者线程
		consumer.start();	//启动消费者线程
	}
}

猜你喜欢

转载自blog.csdn.net/shimadear/article/details/87905187