一、多线程编程
1.多线程的概念
一般来说,程序只能循序单独运行一个程序块,不能同时运行多个程序块。但Java提供了内置的多线程支持。
多线程是在单个进程中运行多个不同的线程,执行不同的任务,
它允许不同的程序块在同一个程序中几乎同时运行,可以提高处理效率、达到多任务的目的。
2.进程与线程
学习多线程需要区分两个概念:进程与线程。
进程:
- 每一个进程有独立的一块内存空间和一组系统资源。进程间,数据和状态是完全独立的。
- 创建和执行一个线程的系统开销相对较大。
- 进程表示程序的一次执行过程,它是系统运行程序的基本单位。
线程:
- 线程不能独立存在,它是进程的一部分。
- 一条线程表示程序中单个按顺序的程序流控制。
- 同一个进程的线程间共享内存空间和系统资源;线程的创建和切换需要的开销比进程小得多。因此线程也称轻负荷进程。
二、多线程的实现方法
Java默认拥有一个主线程,它是执行main方法的线程。
Java提供了多种实现多线程的方式。其中比较常用且简单的是继承Thread类和实现Runnable接口。
1.Thread类
继承Thread类是最简单的多线程实现方法。其步骤如下:
- 新建一个类继承自Thread类;
- 在run方法中覆写想要同时运行的代码块;
- 在需要运行线程的地方创建一个自建类的实例,即可同时运行写入的代码块。
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接口。
- 新建一个实现Runnbale接口的类;
- 覆写run方法;
- 在需要运行线程的地方创建一个以Thread对象包装的自建类实例;
- 调用包装对象的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