多线程

参考来源:多线程 - Java教程 - 廖雪峰的官方网站

多线程基础:

在计算机中,我们把一个任务称为一个进程,浏览器就是一个进程,视频播放器是另一个进程,类似的,音乐播放器和Word都是进程。

某些进程内部还需要同时执行多个子任务。例如,我们在使用Word时,Word可以让我们一边打字,一边进行拼写检查,同时还可以在后台进行打印,我们把子任务称为线程。

进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个线程。

多进程和多线程的方式比较。

多进程与多线程比差别在于:

  • 创建进程开销比线程大。
  • 进程之间通信比线程要慢,因为线程之间通信可能就是读写同一个变量
  • 多进程稳定性比多线程高,因为在多进程的情况下,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃会直接导致整个进程崩溃。

创建新线程

  1. 使用extend Thread 重写run方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class MyRunnable implements Runnable{
    @Override
    public void run() {
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }
    System.out.println("my_runnable");
    }
    }
  2. 使用继承于Runnable 重写run方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class MyRunnable implements Runnable{
    @Override
    public void run() {
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }
    System.out.println("my_runnable");
    }
    }

    或者使用Java8 引入的lambda语法

    1
    2
    3
    Thread runnableThread1 = new Thread(()->{
    System.out.println("runnableThread1");
    });

两者的区别:使用runnable可以利用Java多继承的特性。如果原本就有继承就无法继承thread类。推荐使用runnable。
Thread也是继承Runnable

线程的执行流程

1
2
3
4
5
6
7
8
9
10
11
12
System.out.println("main start...");
Thread t = new Thread() {
public void run() {
System.out.println("thread run...");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {}
System.out.println("thread end.");
}
};
t.start();
System.out.println("main end...");

上面这段代码的执行流程

首先main线程启动打印main start
接着创建Thread对象。
调用Thread.start()启用线程
Thread和Main分别继续执行。

main线程打印 System.out.println(“main end…”);
但是t线程还没结束。

等待t线程执行完毕后t线程就结束了,main()方法也结束了,主线程就结束了。

线程的状态

在java中一个线程对象只能调用一次.start(),当run()方法执行结束后进程就结束了。

java进程的状态有一下几种

  • New:新创建的线程,尚未执行;
  • Runnable:运行中的线程,正在执行run()方法的Java代码;
  • Blocked:运行中的线程,因为某些操作被阻塞而挂起;
  • Waiting:运行中的线程,因为某些操作在等待中;
  • Timed Waiting:运行中的线程,因为执行sleep()方法正在计时等待;
  • Terminated:线程已终止,因为run()方法执行完毕。

BLOCKED(排队时)
阻塞态:当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态。当该线程持有锁时,该线程将自动变成RUNNABLE状态。

WAITING(休眠)
休眠态:一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。

TIMED_WAITING (指定休眠时间)
指定时间休眠态:基本同WAITING状态,多了个超时参数,调用对应方法时线程将进入TIMED_WAITING状态,这一状态将一直保持到超时期满或者接收到唤醒通知,带有超时参数的常用方法有Thread.sleep、锁对象.wait() 。该状态不同于WAITING,它是可以在指定的时间自行返回的

当线程启动后,它可以在RunnableBlockedWaitingTimed Waiting这几个状态之间切换,直到最后变成Terminated状态,线程终止。

线程终止的原因有:

  • 线程正常终止:run()方法执行到return语句返回;
  • 线程意外终止:run()方法因为未捕获的异常导致线程终止;
  • 对某个线程的Thread实例调用stop()方法强制终止(强烈不推荐使用)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    public static void main(String[] args) {
Thread thread = new Thread(()->{
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("执行线程");
});
System.out.println("开始");
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("end");

}
//使用join方法会让main线程等待直到thread线程结束main线程才会继续往下走

中断线程

如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()方法,使得自身线程能立刻结束运行。

基础使用
interrupt()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyThread extends Thread{
private static AtomicInteger counter = new AtomicInteger(0);

@Override
public void run(){
int n=0;
while(!isInterrupted()){
n++;
System.out.flush(); // 确保立即输出
System.out.println(n);
}
}
}

自身的线程不断去访问是否被打断的方法isInterrupted()

1
2
3
4
5
6
7
8
9
public static void main(String[] args) throws InterruptedException {
Thread thread = new MyThread();
thread.start();
Thread.sleep(10);
thread.interrupt();
thread.join();
System.out.println("end");
}

自身线程使用thread.interrupt();就可有打断
注意:自身线程调用thread.interrupt();时不一定会立马打断,他还需要线程执行时isInterrupted()到这里才会结束线程。区别在于是否在阻塞状态。如果在非阻塞的运行状态RUNNABLE,则需要判断isInterrupted()。如果在阻塞状态如join、sleep、wait)会在被中断时抛出InterruptedException。

另一种线程中断方法

另一个常用的中断线程的方法是设置标志位。我们通常会用一个running标志位来标识线程是否应该继续运行,在外部线程中,通过把HelloThread.running置为false,就可以让线程结束:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyThread2 extends Thread{
public volatile boolean running= true;
@Override
public void run() {
int n=0;
while(running){
System.out.println((n++)+"正在运行...");
}
}
}



MyThread2 thread = new MyThread2();
thread.start();
Thread.sleep(100);
thread.running = false;

volatile关键字

​ 此处使用了volatile关键字来定义这个running。
为什么使用volatile: 因为在在Java虚拟机中,变量保存在主内存中。各个线程访问主内存会备份一份副本到自己这。所以当各个线程对变量修改时是修改自己副本里的东西,然后再推送给主内存。如果不加volatile关键字那么这个推送的时间是不确定的。

因此,volatile关键字的目的是告诉虚拟机:

  • 每次访问变量时,总是获取主内存的最新值;
  • 每次修改变量后,立刻回写到主内存。

volatile关键字解决的是可见性问题:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值。

如果我们去掉volatile关键字,运行上述程序,发现效果和带volatile差不多,这是因为在x86的架构下,JVM回写主内存的速度非常快,但是,换成ARM的架构,就会有显著的延迟。

守护线程

Java 启动是由jvm启动main线程,main线程里再启动其他线程。当所有线程都结束后jvm退出进程结束。但是如果说有一个线程一直不结束怎么办?

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class LoopThread extends Thread{
@Override
public void run() {
while (true){

System.out.println(LocalTime.now());
try {
Thread.sleep(1000);
}catch (InterruptedException e){
System.out.println("error");
break;
}
}
System.out.println("over");
}
}

这个线程正常清况下一但启动就会一直循环。

那么我们需要关闭jvm怎么办?

把线程类型分成守护线程类型和非守护线程类型。当非守护线程类型全部执行完毕时不管守护线程有没有执行完毕jvm都会关闭。

1
2
3
4
5
6
7
8
9
10
11
12
    public static void main(String[] args) throws InterruptedException {
LoopThread thread = new LoopThread();
thread.setDaemon(true);
thread.start();
Thread.sleep(1000);
}



Connected to the target VM, address: '127.0.0.1:60757', transport: 'socket'
14:06:13.270548300
Disconnected from the target VM, address: '127.0.0.1:60757', transport: 'socket'

发现连”over”都没输出就结束了
因此,JVM退出时,不必关心守护线程是否已结束。