Javaの多线程基础(1)

一、多线程编程

1.多线程的概念

一般来说,程序只能循序单独运行一个程序块,不能同时运行多个程序块。但Java提供了内置的多线程支持。
多线程是在单个进程中运行多个不同的线程,执行不同的任务,
它允许不同的程序块在同一个程序中几乎同时运行,可以提高处理效率、达到多任务的目的。

2.进程与线程

学习多线程需要区分两个概念:进程线程
进程:

  1. 每一个进程有独立的一块内存空间和一组系统资源。进程间,数据和状态是完全独立的。
  2. 创建和执行一个线程的系统开销相对较大
  3. 进程表示程序的一次执行过程,它是系统运行程序的基本单位

线程:

  1. 线程不能独立存在,它是进程的一部分
  2. 一条线程表示程序中单个按顺序的程序流控制
  3. 同一个进程的线程间共享内存空间和系统资源;线程的创建和切换需要的开销比进程小得多。因此线程也称轻负荷进程

二、多线程的实现方法

Java默认拥有一个主线程,它是执行main方法的线程。
Java提供了多种实现多线程的方式。其中比较常用且简单的是继承Thread类实现Runnable接口

1.Thread类

继承Thread类是最简单的多线程实现方法。其步骤如下:

  1. 新建一个类继承自Thread类;
  2. 在run方法中覆写想要同时运行的代码块;
  3. 在需要运行线程的地方创建一个自建类的实例,即可同时运行写入的代码块。
public class Demo {
	public static void main(String[] args) {
		new TestThread().start();
		int t = 10;
		while(t-- > 0) {
			Thread.sleep(50);			//为了清晰表示运行结果,使用sleep暂停线程,该方法需要显示异常处理,此处省略
			System.out.println('1');
		}
	}
}

class TestThread extends Thread{
	@Override
	public void run() {
		int t = 10;
		Thread.sleep(25);
		while(t-- > 0) {
			Thread.sleep(50);
			System.out.println('2');
		}
	}
}
//Output:12121212121212121212

2.Runnable接口

Java只允许单继承,如果一个类已经是子类,还想使用多线程技术,就不能再使用Thread类,而要实现Runnable接口。

  1. 新建一个实现Runnbale接口的类;
  2. 覆写run方法;
  3. 在需要运行线程的地方创建一个以Thread对象包装的自建类实例;
  4. 调用包装对象的start方法即可运行写入的代码块。
public class Demo {
	public static void main(String[] args) {
		Thread thread = new Thread(new TestThread());	//Thread类允许使用一个实现了runnable接口的实例构造Thread实例
		thread.start();
		int t = 10;
		while(t-- > 0) {
			Thread.sleep(50);
			System.out.println('1');
		}
	}
}

class TestThread implements Runnable{
	@Override
	public void run() {
		int t = 10;
		Thread.sleep(25);
		while(t-- > 0) {
			Thread.sleep(50);
			System.out.println('2');
		}
	}
}
//Output:12121212121212121212

3.两者的区别

Thread类实际上也是一个实现了Runnable接口的类。但是使用这两种方式实现多个线程在资源共享上有一些区别:

//使用Thread类创建两个线程
public class Demo {
	public static void main(String[] args) throws Exception {
		TestThread t1 = new TestThread();
		TestThread t2 = new TestThread();
		t1.start();
		t2.start();
	}
}

class TestThread extends Thread{
	private int v = 0;
	@Override
	public void run() {
		int t = 10;
		while(t-- > 0)
			System.out.print(v++);
	}
}
//Output:01234567801234567899	两个线程同时运行,拥有独立的数据v
//使用Runnable接口创建两个线程
public class Demo {
	public static void main(String[] args) throws Exception {
		TestRunnable tr = new TestRunnable();
		t1 = new Thread(tr);
		t2 = new Thread(tr);
		t1.start();
		Thread.sleep(250);
		t2.start();
	}
}

class TestRunnable implements Runnable{
	private int v = 0;
	@Override
	public void run() {
		int t = 10;
		while(t-- > 0){
			//Thread.sleep(500);
			System.out.print(v++);
		}
	}
}
//Output1:0012345687991010111112131414	两个线程可能同时访问到数据v,因此两个线程读取的数据相同
//Output2:012345678910111213141516171819  加入注释掉的两条语句,将两个线程的运行错开,最终可以输出19,也就是执行了20次v++

由此可以发现,使用同一个Runnable对象构造的两个线程,互相之间可以共享对象的数据
而继承Thread类的TestThread类不能共享成员数据,只能创建两个不同的实例,因此它们之间的实例成员也是独立的。
并且如果试图用同一个TestThread对象执行两次start,会引发异常,原因就是线程的状态

综上,Runnable接口相较于Thread类有几个明显的优势:
● 可以使用多个相同代码的线程处理同一个资源。
● 避免单线程特性带来的局限。
● 令代码与数据相互独立,增强程序的健壮性。

三、线程的状态与优先级

1.线程的状态

一个线程在其生命周期内,有五种可能的状态,它们的关系如下:
在这里插入图片描述
对一个Thread类实例调用getState方法能够获取线程的当前状态,可能得到的结果如下:
● NEW:尚未启动的线程处于的状态,对应图中的创建
● RUNNABLE:正在JVM中执行的线程处于的状态,对应就绪运行,两种状态由CPU调度切换;
● BLOCKED:受阻塞并等待某个监视器锁的线程处于的状态,属于阻塞的一种;
● WAITING:无限期地等待另一个线程来执行某一个特定操作的线程处于的状态,属于阻塞的一种;
● TIMED_WAITING:等待另一个线程来执行,取决于指定等待时间的线程处于的状态,属于阻塞的一种;
● TERMINATED:已经结束/退出的线程处于的状态,对应图中的阻止

任意一个确定的时刻,线程只能处于上面的一个状态。

线程在不同的状态,只能调用对应的状态转换方法,如图中所示;例如在程序运行前调用join、sleep等进入阻塞状态的方法;在运行状态下调用start方法;在线程结束后调用阻塞方法或start方法等,都会引发特定的illegalThreadStateException异常。

2.线程的优先级

每一个Java线程都有一个优先级,便于操作系统确定线程的调度顺序。
线程的优先级是一个整数,取值范围为1(Thread.MIN_PRIORITY) ~ 10(Thread.MAX_PRIORITY)。
默认情况下,线程会自动分配一个普通优先级 5 (NORM_PRIORITY)。
一般来说,具有较高优先级的线程对程序而言更为重要,并应先于较低优先级的线程分配CPU资源;但线程的优先级并不保证线程的执行顺序,并且资源的分配依赖于平台完成。

四、线程操作的方法

1.线程名称

Thread类的getName方法可以获取线程的名称,setName方法可以设置线程的名称;在创建实例时也可使用对应的构造函数指定线程的名称;若没有为线程指定名称,系统会为线程自动分配名称。
此外,setName和getName方法没有限制调用时的状态。可以在线程启动前设置名称,也可以启动后修改名称。

Thread test = new Thread(new TestRunnable());	
		System.out.println(test.getName());		//自动分配的名称:Thread-0
		test.setName("test");
		System.out.println(test.getName());		//修改后的名称:test

2.线程启动

Thread类的start方法,可以启动线程;isAlive方法可以判断线程是否已经启动,或是否尚未终止;

class TestRunnable implements Runnable{
	@Override
	public void run() {
		for(int i = 0;i < 10;i++) {
			try {
				Thread.sleep(100);		//每次停留100ms,1s后线程结束
			}
			catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
}

public class Demo {
	public static void main(String[] args) throws Exception {
		Thread t = new Thread(new TestRunnable());
		System.out.println(t.isAlive());	//线程启动前:false
		t.start();
		System.out.println(t.isAlive());	//线程刚启动:true
		Thread.sleep(500);
		System.out.println(t.isAlive());	//线程启动后0.5s:true
		System.out.println(t.getState());	//线程状态:TIMED_WAITING
		Thread.sleep(600);
		System.out.println(t.isAlive());	//线程启动后1.1s:false
	}
}

在这里例子中可以看出,处于阻塞状态的线程,也被认为是活线程(isAlive返回true)。也就是说isAlive方法只有在线程开始前线程结束后,才返回false。

3.后台线程

Java程序中,线程分为前台线程后台线程。程序结束的标志是所有前台线程结束。也就是说,即使还有后台线程正在运行,在前台线程结束时,整个进程就结束了。
默认情况下,新建的线程都是前台线程。若需要将线程设置为后台线程,则需要在线程运行前调用setDaemon方法。

class TestRunnable implements Runnable{
	private int v = 0;
	@Override
	public void run() {
		while(true) {	//无限循环
			try {
				Thread.sleep(100);
				System.out.print(v++);
			}
			catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
}

public class Demo {
	public static void main(String[] args) throws Exception {
		Thread t = new Thread(new TestRunnable());
		t.setDaemon(true);	//若没有该语句,程序将无限打印自然数
		t.start();
		Thread.sleep(1000);
	}
}
//Output:012345678	由于线程运行start语句的时间消耗,不能打印到9

4.线程插入

Thread类的join方法可以将指定线程插入到当前线程的前面,以合并线程,达到线程的顺序执行
调用join方法的当前线程(执行程序块的线程,而不是调用join的实例线程)将进入WAITING状态

class TestRunnable implements Runnable{
	@Override
	public void run() {
		for(int i = 0;i < 10;i++) {
			try {
				Thread.sleep(100);
				System.out.print(i);
			}
			catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
}

public class Demo {
	public static void main(String[] args) throws Exception {
		Thread t = new Thread(new TestRunnable());
		t.start();
		t.join();
		for(int i = 0;i < 10;i++) {
			Thread.sleep(100);
			System.out.print(i);
		}
	}
}
//Output:01234567890123456789	在t线程执行完后,再执行main线程

join类有多个重载,可以指定两个线程合并的时间,在时间到后,两个线程由再次分离,同时运行。
调用指定时间的join方法的线程将进入TIMED_WAITING状态

class TestRunnable implements Runnable{
	@Override
	public void run() {
		for(int i = 0;i < 10;i++) {
			try {
				Thread.sleep(100);
				System.out.print(i);
			}
			catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
}

public class Demo {
	public static void main(String[] args) throws Exception {
		Thread t = new Thread(new TestRunnable());
		t.start();
		t.join(500);
		for(int i = 0;i < 10;i++) {
			Thread.sleep(100);
			System.out.print(i);
		}
	}
}
//Output:01234051627384956789	在500ms后,t线程和main线程同时执行

5.线程休眠

Thread类的sleep静态方法可以让当前执行的线程休眠一段时间。
调用了sleep方法的线程将进入TIMED_WAITING状态
该方法会可能会抛出一个检查性异常InteruptedException,需要显示处理。

package learning_test;

class TestRunnable implements Runnable{
	@Override
	public void run() {
		try {
			Thread.sleep(10000);
		}
		catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

public class Demo {
	public static void main(String[] args) throws Exception {
		Thread t = new Thread(new TestRunnable());
		t.start();
		System.out.println(t.getState());	//RUNNABLE	线程start方法调用和run方法调用需要时间,因此立即检查状态可能得到RUNNABlE
		Thread.sleep(100);
		System.out.println(t.getState());	//TIMED_WAITTING 留给t线程进入run方法的时间,此时t线程由于sleep休眠,线程将进入TIMED_WAITING状态
	}
}

6.线程中断

Java中线程中断有三种方式:
● 对于非无限执行的run方法,在run方法结束后,线程自动中断;
对于无限执行的run方法:
● 在while循环中使用标识符,当需要线程结束时,修改标识符。

class TestRunnable implements Runnable{
	public boolean exit = false;
	@Override
	public void run() {
		while(!exit){
			<Statement>
		}
	}
}

● 使用stop方法。该方法由于线程不安全,已经被弃用;
● 使用interrupt方法。该方法不同于stop方法那样立即停止run方法的执行,它仅仅是给线程发送停止标记,通知线程终止;但收到通知后,run方法并不会强制终止。

class TestRunnable implements Runnable {
	@Override
	public void run() {
		for (int i = 0; i < 10; i++) {
			System.out.print(i);
		}
	}
}

public class Demo {
	public static void main(String[] args) throws Exception {
		Thread t = new Thread(new TestRunnable());
		t.start();
		t.interrupt();
	}
}
//Output:0123456789

可以看到,在主线程中给t线程发送interrupt信息后,t线程并没有强制结束。这虽然避免了stop方法带来的安全问题,也需要我们单独处理程序的结束。

class TestRunnable implements Runnable {
	@Override
	public void run() {
		int v = 0;
		while(true) {
			if(Thread.currentThread().isInterrupted())
				break;
			System.out.println(v++);
		}
	}
}

public class Demo {
	public static void main(String[] args) throws Exception {
		Thread t = new Thread(new TestRunnable());
		t.start();
		Thread.sleep(1);	//给t线程执行的时间
		t.interrupt();		//发送停止信息
	}
}
//Output:本次输出0 ~ 180,具体数字每次运行不定

除了使用break停止线程,interrupt方法还会引发sleep方法的异常。利用异常处理机制可以中断线程。

package learning_test;

class TestRunnable implements Runnable {
	@Override
	public void run() {
		int v = 0;
		try {
			while (true) {
				Thread.sleep(100);
				System.out.print(v++);
			}
		} catch (InterruptedException e) {
			System.out.print('\t');
			System.out.println(e.getMessage());
		}
	}
}

public class Demo {
	public static void main(String[] args) throws Exception {
		Thread t = new Thread(new TestRunnable());
		t.start();
		Thread.sleep(1000);
		t.interrupt();
	}
}
//Output:012345678	sleep interrupted
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章