1.线程概述
1.1 线程相关概念
什么是进程?什么是线程?
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是操作系统进行资源分配与调度的基本单位。
可以把进程简单的理解为正在操作系统中运行的一个程序
线程(Thread)是进程的一个执行单元。一个线程就是进程中一个单一顺序的控制流,进程的一个执行分支。
进程是线程的容器,一个进程至少有一个线程。一个进程中也可以有多个线程。
在操作系统中是以进程为单位分配资源,如虚拟存储空间,文件描述符等。每个线程都有各自的线程栈、自己的寄存器环境,自己的线程本地存储。
线程与子线程
JVM启动时会创建一个主线程,该主线程负责执行main方法。主线程就是运行main方法的线程。
java中的线程不是孤立的,线程之间存在一些联系。如果在A线程中创建了B线程,称B线程为A线程的子线程,相应的A线程就是B线程的父线程。
1.2 串行、并发、并行
并发可以提高事物的处理效率,即一段时间内可以处理或完成更多的事情。
并行是一种更为严格,理想的并发。
从硬件角度来说:如果单核cpu,一个处理器一次只能执行一个线程的情况下,处理器可以使用时间片轮转技术,可以让CPU快速的在各个线程之间进行切换,对于用户来说,感觉是三个线程在同时执行。
如果是多核心CPU,可以为不同的线程分配不同的CPU内核。
1.3 线程的创建与启动
在JAVA中,创建一个线程就是创建一个Thread类(子类)的对象(实例)
Thread类有两个常用的构造方法:Thread()与Thread(Runnable).对应的创建线程的两种方式:
- 定义Thread类的子类
- 定义一个Runnable接口的实现类
这两种创建线程的方式没有什么本质区别。
public class TestThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("我在走路......");
}
}
public static void main(String[] args) {
new TestThread().start();
for (int i = 0; i < 100; i++) {
System.out.println("我在看手机......");
}
}
}
public class TestRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("我在走路......");
}
}
public static void main(String[] args) {
TestRunnable t2 = new TestRunnable();
new Thread(t2).start();
for (int i = 0; i < 1000; i++) {
System.out.println("我在看手机......");
}
}
}
1.4 线程的常用方法
1.4.1 currentThread()方法
Thread.currentThread()方法可以获得当前线程。
JAVA中的任何一段代码都是执行在某个线程当中的。执行当前代码的线程就是当前线程。
同一段代码可能被不同的代码执行,因此当前线程是相对的,Thread.currentThread()方法的返回值是在代码实际运行时候的线程对象。
public class SubThread extends Thread{
public SubThread(){
System.out.println("子线程的构造方法打印当前线程的名称:"+Thread.currentThread().getName());
}
@Override
public void run() {
System.out.println("子线程的run方法打印当前线程的名称:"+Thread.currentThread().getName());
}
}
public class Test01CurrentThread {
public static void main(String[] args) {
System.out.println("main方法打印当前线程的名称:"+Thread.currentThread().getName());
//创建子线程,调用构造方法,在main线程中调用构造方法,所以输出当前线程 main 线程
SubThread subThread = new SubThread();
//输出run方法的,使用的是 Thread-0线程
subThread.start();
}
}
1.4.2 setName()/getName()方法
thread.setName(线程名称),设置线程名称
thread.getName(),返回线程名称
通过设置线程名称,有助于程序调试,提高程序的可读性,建议为每个线程都设置一个能够体现线程功能的名称。
1.4.3 isAlive()方法
thread.isAlive()判断当前线程是否处于活动状态。
public class SubThread3 extends Thread{
@Override
public void run() {
System.out.println("run方法,isAlive = "+this.isAlive());
}
}
public class Test {
public static void main(String[] args){
SubThread3 t3 = new SubThread3();
System.out.println("begin=="+t3.isAlive());
t3.start();
//Thread.sleep(400);
//这一行不确定,如果线程运行结束,则是false , 如果线程还在继续 就是 true
System.out.println("end=="+t3.isAlive());
}
}
1.4.4 sleep()方法
Thread.sleep(millis);让当前线程休眠指定的毫秒数。
当前线程是指currentThread()返回的线程。
public class SimpleTimer {
public static void main(String[] args) {
int remaining = 10;
while (true){
System.out.println("Remaining = "+ remaining);
remaining--;
if(remaining<0){
break;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Done!");
}
}
1.4.5 getId()方法
返回线程的唯一编号
注意:某个编号的线程运行结束后,该编号可能被后续创建的线程使用。
重启的JVM后,同一个线程的编号可能不一样。
public class SubThread4 extends Thread{
@Override
public void run() {
System.out.println("thread name = "+Thread.currentThread().getName() +
", id == "+this.getId());
}
}
public class Test {
public static void main(String[] args) {
System.out.println("thread name = "+Thread.currentThread().getName() +
", id == "+Thread.currentThread().getId());
for (int i = 0; i < 5; i++) {
//线程终止后,编号可能会分配给其他后面创建的线程使用
new SubThread4().start();
}
}
}
1.4.6 yieId()方法
Thread.yieId()方法的作用是放弃当前的CPU资源。
1.4.7 setPriority()方法
thread.setPriority(num),设置线程优先级,默认值是5
- java线程的优先级取值范围是 1 ~10,如果超出这个范围会抛异常:IllegalArgumentException.
- 在操作系统中,优先级较高的线程获得CPU的资源越多。
线程优先级本质上只是给线程调度器一个提示信息,以便于调度器决定先调度哪些线程。注意不能保证优先级高的线程先运行。 - Java优先级设置不当或着滥用可能会导致某些线程永远无法得到运行,即产生了线程饥饿。
- 线程的优先级并不是设置的越高越好,一般情况下使用普通的优先级即可,即在开发时不必设置线程的优先级。
- 线程的优先级具有继承性,在A线程中创建了B线程,则B线程的优先级和A是一样的。
1.4.8 interrupt()方法
中断线程
注意调用interrupt()方法仅仅是在当前线程打一个停止标志,并不是真正的停止线程。
public class TestThreadInterrupt {
public static void main(String[] args) {
//定义一个新线程
Thread t = new Thread(()->{
System.out.println("1.进入新线程");
try {
//让线程休眠五秒钟、目的是先让Main线程工作
Thread.sleep(5000);
System.out.println("2、新线程已经完成了休眠") ;
} catch (InterruptedException e) {
System.out.println("3、新线程休眠被终止");
return;
}
System.out.println("4、新线程run()方法正常结束");
});
t.start(); //新线程启动
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println("主线程休眠被终止") ;
}
t.interrupt(); //此时主线程唤醒了,但是新线程还在休眠。主线程吧新线程 中断
}
}
1.4.9 setDaemon()方法
JAVA中的线程分为用户线程与守护线程
守护线程是为其他线程提供服务的线程,如垃圾回收器(GC)就是一个典型的守护线程
守护线程不能单独运行,当JVM中没有其他用户线程,只有守护线程时,守护线程会自动销毁,JVM会退出。
public class TestThreadDaemon {
public static void main(String[] args) {
Thread t = new Thread(()->{
while (true){
System.out.println(Thread.currentThread().getName()+"在执行");
}
},"守护线程");
t.setDaemon(true); //开启后台线程(守护线程),
t.start();
for (int i = 0; i < 10; i++) {
System.out.println("main-----");
}
}
}
1.5 线程的生命周期
线程的生命周期是线程对象的生老病死,即线程的状态。
线程的生命周期可以通过getState()方法获得,线程的状态是Thread.State枚举类型定义的。由以下几种:
- New,新建状态。创建了线程对象,在调用start()启动之前的状态;
- RUNNABLE,可运行状态,它是一个复合状态,包含:READY和RUNNING两个状态。READY状态该线程可以被线程调度器进行调度使它处于RUNNING状态,RUNNING状态表示该线程正在执行。Thread.yieId()方法可以把线程由RUNNING状态转换为READY状态。
- BLOCKED阻塞状态,线程发起阻塞的I/O操作,或者申请由其他线程占用的独占资源,线程会转换为BLOCKED阻塞状态。处于阻塞状态的线程不会占用CPU资源。当阻塞I/O操作执行完,或者线程获得了其申请的资源,线程可以转换为RUNNABLE
- WAITING等待状态。线程执行了object.wait(),thread.join()方法会把线程转换为WAITING等待状态,执行object.notify()方法,或者加入的线程执行完毕,当前线程会转换为RUNNABLE状态。
- TIMED_WAITING状态,与WAITING状态类似,都是等待状态。区别在于处于该状态的线程不会无限的等待,如果线程没有在指定的时间范围内完成期望的操作,该线程自动转换为RUNNABLE
- TERMINATED终止状态
1.6 多线程编程的优势与存在的风险
多线程编程具有以下优势:
- 提高系统的吞吐率(Throughout). 多线程编程可以使一个进程有多个并发(concurrent),即同时进行的操作。
- 提高相应性(Responsiveness)。Web服务器会采用一些专门的线程负责用户的请求处理,缩短了用户的等待时间。
- 充分利用多核(Multicore)处理器资源。通过多线程可以充分的利用CPU资源。
多线程编程存在的问题与风险
- 线程安全(Thread safe)问题,多线程共享数据时,如果没有采取正确的并发访问控制措施,就可能会产生数据一致性问题,如读取脏数据(过期的数据),如丢失数据更新。
- 线程活性(thread liveness)问题。由于程序自身的缺陷或者由资源稀缺性导致线程一直处于非RUNNABLE状态,这就是线程活性问题。常见的活性故障由以下几种:
- 死锁(Deadlock),类似鹬蚌相争
- 锁死(Lockout),类似于睡美人故事中王子挂了
- 活锁(Livelock),类似小猫咬自己的尾巴
- 饥饿(Starvation)。类似于健壮的雏鸟总是从母鸟嘴中抢到食物。
- 上下文切换(Context Switch)。处理器从执行一个线程切换到执行另外一个线程
- 可靠性可能会有一个线程导致JVM意外终止,其他的线程也无法执行。