多线程基础(一)
多线程
多线程基础:
在计算机中,我们把一个任务称为一个进程,浏览器就是一个进程,视频播放器是另一个进程,类似的,音乐播放器和Word都是进程。
某些进程内部还需要同时执行多个子任务。例如,我们在使用Word时,Word可以让我们一边打字,一边进行拼写检查,同时还可以在后台进行打印,我们把子任务称为线程。
进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个线程。
多进程和多线程的方式比较。
多进程与多线程比差别在于:
- 创建进程开销比线程大。
- 进程之间通信比线程要慢,因为线程之间通信可能就是读写同一个变量
- 多进程稳定性比多线程高,因为在多进程的情况下,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃会直接导致整个进程崩溃。
创建新线程
使用extend Thread 重写run方法
1
2
3
4
5
6
7
8
9
10
11public class MyRunnable implements Runnable{
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("my_runnable");
}
}使用继承于Runnable 重写run方法
1
2
3
4
5
6
7
8
9
10
11public class MyRunnable implements Runnable{
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("my_runnable");
}
}或者使用Java8 引入的lambda语法
1
2
3Thread runnableThread1 = new Thread(()->{
System.out.println("runnableThread1");
});
两者的区别:使用runnable可以利用Java多继承的特性。如果原本就有继承就无法继承thread类。推荐使用runnable。
Thread也是继承Runnable
线程的执行流程
1 | System.out.println("main start..."); |
上面这段代码的执行流程
首先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,它是可以在指定的时间自行返回的
当线程启动后,它可以在Runnable
、Blocked
、Waiting
和Timed Waiting
这几个状态之间切换,直到最后变成Terminated
状态,线程终止。
线程终止的原因有:
- 线程正常终止:
run()
方法执行到return
语句返回; - 线程意外终止:
run()
方法因为未捕获的异常导致线程终止; - 对某个线程的
Thread
实例调用stop()
方法强制终止(强烈不推荐使用)
1 | public static void main(String[] args) { |
中断线程
如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()
方法,使得自身线程能立刻结束运行。
基础使用
interrupt()
1 | public class MyThread extends Thread{ |
自身的线程不断去访问是否被打断的方法isInterrupted()
1 | public static void main(String[] args) throws InterruptedException { |
自身线程使用thread.interrupt();就可有打断
注意:自身线程调用thread.interrupt();时不一定会立马打断,他还需要线程执行时isInterrupted()到这里才会结束线程。区别在于是否在阻塞状态。如果在非阻塞的运行状态RUNNABLE,则需要判断isInterrupted()。如果在阻塞状态如join、sleep、wait)会在被中断时抛出InterruptedException。
另一种线程中断方法
另一个常用的中断线程的方法是设置标志位。我们通常会用一个running
标志位来标识线程是否应该继续运行,在外部线程中,通过把HelloThread.running
置为false
,就可以让线程结束:
1 | public class MyThread2 extends Thread{ |
volatile关键字
此处使用了volatile关键字来定义这个running。
为什么使用volatile: 因为在在Java虚拟机中,变量保存在主内存中。各个线程访问主内存会备份一份副本到自己这。所以当各个线程对变量修改时是修改自己副本里的东西,然后再推送给主内存。如果不加volatile关键字那么这个推送的时间是不确定的。
因此,volatile
关键字的目的是告诉虚拟机:
- 每次访问变量时,总是获取主内存的最新值;
- 每次修改变量后,立刻回写到主内存。
volatile
关键字解决的是可见性问题:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值。
如果我们去掉volatile
关键字,运行上述程序,发现效果和带volatile
差不多,这是因为在x86的架构下,JVM回写主内存的速度非常快,但是,换成ARM的架构,就会有显著的延迟。
守护线程
Java 启动是由jvm启动main线程,main线程里再启动其他线程。当所有线程都结束后jvm退出进程结束。但是如果说有一个线程一直不结束怎么办?
例如
1 | public class LoopThread extends Thread{ |
这个线程正常清况下一但启动就会一直循环。
那么我们需要关闭jvm怎么办?
把线程类型分成守护线程类型和非守护线程类型。当非守护线程类型全部执行完毕时不管守护线程有没有执行完毕jvm都会关闭。
1 | public static void main(String[] args) throws InterruptedException { |
发现连”over”都没输出就结束了
因此,JVM退出时,不必关心守护线程是否已结束。