1.1 线程相关概念
- 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是操作系统进行资源分配与调度的基本单位。可以理解为正在操作系统中运行的一个程序。
- 线程(thread)是进程的一个执行单元。
- 一个线程就是进行中一个单一顺序的控制流,进程的一个执行分支;
- 进程是线程的容器,一个进程至少有一个线程,一个进程中也可以有多个线程;
- 在操作系统中是以进程为单位分配资源,如虚拟存储空间、文件描述符,每个线程都有各自的线程栈、自己的寄存器环境、自己的线程本地存储。
- 主线程与子线程
- JVM启动时会创建一个主线程,该主线程负责执行main方法,主线程就是运行main方法的线程;
- Java中的线程不孤立,线程之间存在一些联系,如果在A线程中创建了B线程,称B线程为A线程的子线程,相应的A线程就是B线程的父线程。
串行、并发与并行
- 串行(Sequential):所有任务逐个完成;
- 并发(Concurrent):交替执行,减少CPU空闲时间,可以提高处理事物的效率,即一段时间内可以处理或者完成更多的事情;
- 并行(Parallel):同时执行,是一种理想的并发。
- 从硬件角度来说,如果单核CPU,一个处理器一次只能执行一个线程的情况下,处理器可以使用时间片轮转技术,可以让CPU快速地在各个线程之间进行切换,对于用户来说,感觉是三个线程在同时执行,如果是多核心CPU,可以为不同的线程分配不同的CPU内核。
1.2 线程的创建与启动
在Java中,创建一个线程就是创建一个Thread类(子类)的对象(实例)。Thread类有两个常用的构造方法:Thread()与Thread(Runnable),对应创建线程的两种方式(这两种创建线程的方式没有本质的区别):定义Thread类的子类; 定义一个Runnable接口的实现类。
定义Thread类的子类 ```java /**
1) 定义类继承Thread */ public class MyThread extends Thread{
/**
- 2) 重写Thread父类中的run()
- run()方法体中的代码是子线程要执行的任务 */ @Override public void run() { System.out.println(“这是子线程打印的内容”); } }
public class Test { public static void main(String[] args) { System.out.println(“JVM启动main线程,main线程执行main方法”); // 3) 创建子线程对象 MyThread thread = new MyThread(); // 4) 启动线程 thread.start(); /* 调用线程的start()方法来启动线程,启动线程的实质是请求JVM运行 相应的线程,这个线程具体在什么时候运行由线程高度器(Scheduler)决定。 注意:1. start()方法调用结束并不意味着子线程开始运行;
2. 新开启的线程会执行run()方法;
3. 如果开启了多个线程,start()调用的顺序并不一定就是线程启动的顺序;
4. 多线程运行结果与代码执行或者调用顺序无关。
*/
System.out.println("main线程后面其他的代码……");
}
}
2. 定义一个Runnable接口的实现类。
```java
/**
* 当线程类已经有父类,就不能用继承Thread类的形式创建线程,可以使用实现Runnable接口的形式
* 1) 定义类实现Runnable接口
*/
public class MyRunnable implements Runnable{
/**
* 2) 重写Runnable接口中的抽象方法run(),run()方法就是子线程要执行的代码
*/
@Override
public void run() {
for (int i = 1; i <= 100; i++){
System.out.println("sub thread --> " + i);
}
}
}
public class Test {
public static void main(String[] args) {
// 3) 创建Runnable接口的实现类对象
MyRunnable runnable = new MyRunnable();
// 4) 创建线程对象,也可以传递匿名内部类对象
Thread thread = new Thread(runnable);
// 5) 开启线程
thread.start();
// 当前是main线程
for (int i = 1; i <= 100; i++){
System.out.println("main -> " + i);
}
}
}
1.3 线程的常用方法
1.3.1 currentThread()方法
Thread.currentThread()方法可以获得当前线程,Java中的任何一段代码都是执行在某个线程中的,执行当前代码的线程就是当前线程。同一段代码可能被不同的线程执行,因此当前线程是相对的,Thread.currentThread()方法的返回值是代码实际运行时候的线程对象。
/**
* 定义一个线程类
*/
public class SubThread1 extends Thread{
public SubThread1(){
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("mian方法中打印当前线程:" + Thread.currentThread().getName());
// 创建子线程,在main线程中调用构造方法,所以构造方法的当前线程是main线程
SubThread1 t1 = new SubThread1();
// 启动子线程,子线程调用run()方法,所以run()方法的当前线程是Thread-0子线程
t1.start();
// 在main线程中直接调用run()方法,此时run()方法的当前线程是main线程
t1.run();
}
}
1.3.2 setName()/getName()
thread.setName(线程名称):计线程名称 thread.getName():返回线程名称
1.3.3 isAlive()
thread.isAlive()判断当前线程是否处于活动状态,即已启动并且尚未终止。
public class SubThread extends Thread{
@Override
public void run() {
System.out.println("run方法,isAlive = " + this.isAlive());
}
}
public class Test {
public static void main(String[] args) {
SubThread t1 = new SubThread();
System.out.println("begin==" + t1.isAlive());
t1.start();
System.out.println("end==" + t1.isAlive());
}
}
1.3.4 sleep()
Thread.sleep(mills):让当前线程休眠指定的毫秒数。
1.3.5 getId()
thread.getId():可以获得线程的唯一标识。
注意:某个编号的线程运行结束后,该编号可能被后续创建的线程使用;重启JVM后,同一个线程的编号可能不一样。
1.3.6 yield()
Thread.yield():放弃当前的CPU资源。
public class SubThread extends Thread{
@Override
public void run() {
long begin = System.currentTimeMillis();
long sum = 0;
for (int i = 1; i <= 1e+6; i++){
sum += i;
Thread.yield(); //线程让步,放弃CPU资源,等待资源重新分配
}
long end = System.currentTimeMillis();
System.out.println("子线程用时:" + (end - begin));
}
}
public class Test {
public static void main(String[] args) {
// 子线程计算累加
SubThread t1 = new SubThread();
t1.start();
// main线程中计算累加
long begin = System.currentTimeMillis();
long sum = 0;
for (int i = 1; i <= 1e+6; i++){
sum += i;
}
long end = System.currentTimeMillis();
System.out.println("main线程用时:" + (end - begin));
}
}
运行结果:
main线程用时:4 子线程用时:764
1.3.7 setPrority()
thread.setPrority(num):设置线程的优先级,取值为1-10,超出范围会抛出IllegalArgumentException。
注意:1. 线程优先级本质只是给线程调度器一个提示信息,不能保证优先级高的线程先运行;
- 设置不当或者滥用可能导致某些线程永远无法得到运行,即产生线程饥饿;
- 优先级具有继承性,在某线程中创建的子线程的优先级与该线程一致。
1.3.8 interrupt()
interrupt():中断线程。
注意:调用这个方法仅仅是在当前线程打一个
停止标志
,并不是真正停止线程,需要配合isInterrupted()方法实现子线程的中断。
1.3.9 setDaemon()
Java中线程分为用户线程与守护线程。守护线程是为其他线程提供服务的线程,如垃圾回收器(GC)就是一个典型的守护线程。
注意:守护线程不能单独运行,当JVM中没有其他用户线程,只有守护线程时,守护线程会自动销毁,JVM会退出。
1.4 线程的生命周期
线程的生命周期可以通过getState()方法获得,线程的姿态是Thread.State枚举类型定义的,有以下几种:
- NEW:新建状态。创建了线程对象,在调用start()启动之前的状态;
- RUNNABLE: 可运行状态。它是一个复合状态,包含REDAY和RUNNING两个状态
- REDAY状态线程可以被线程调度器进行调度使它处于RUNNING状态;
- RUNNING状态表示该线程正在执行,Thread.yield()方法可以把线程由RUNNING状态转换为REDAY状态。
- BLOCKED:阻塞状态。线程发起阻塞的I/O操作,或者申请由其他线程占用的独占资源,线程会转换为BLOCKED阻塞状态,处于阻塞状态的线程不会占用CPU资源。当阻塞I/O操作执行完成,或者线程获得了其申请的资源,线程可以转换为RUNNING状态。
- WAITING:等待状态。线程执行了object.wait(),thread.join()方法会把线程转换为WAITING状态,执行object.notify()方法,或者加入的线程执行完毕,当前线程会转换为RUNNABLE状态。
- TIMED_WAITING:与WAITING状态类似,都是等待状态,区别在于处于该状态的线程不会无限的等待,如果线程没有在指定的时间范围内完成期望的操作,该线程自动转换为RUNNABLE。
-
1.5 多线程编程的优势与存储的风险
优势:
提高系统的呑吐率(Throughout)。多线程编程可以使一个进程有多个并发(concurrent),即同时进行的)的操作。
- 提高响应性(Responsiveness)。Web服务器会采用一些专门的线程负责用户的请求处理,缩短了用户的等待时间。
- 充分利用多核(Multicore)处理器资源。通过多线程可以充分的利用CPU资源。
多线程编程存在的问题与风险:
- 线程安全(Thread safe)问题:多线程共享数据时,如果没有采取正确的并发访问控制措施,就可能会产生数据一致性问题,如读脏数据,丢失数据更新。
- 线程活性(thread liveness)问题:由于程序自身的缺陷或者由资源稀缺性导致线程一直处于非RUNNABLE状态,这就是线程活性问题,常见的活性故障有以下几种:
- 死锁(Deadlock):线程相互锁定;
- 锁死(Lockout):解锁条件无法成立,类似睡美人故事王子死了;
- 活锁(Livelock):解锁条件一直无法达到,类似小猫咬自己尾巴;
- 饥饿(Starvation):无法获得资源而导致线程死亡,类似健壮的雏鸟从母鸟嘴中抢到食物。
- 上下文切换(Context Switch):处理器从执行一个线程切换到执行另外一个线程。
- 可靠性:可能会由一个线程导致JVM意外终止,其他的线程也无法执行。