Java多线程

程序、进程和线程

一、程序

  • 程序是存储在磁盘上, 包含可执行机器指令和数据的静态实体。 即进程或者任务是处于活动状态的计算机程序。

二、进程

  • 进程是资源(CPU、内存等)分配的基本单位,它是程序执行时的一个实例,即运行中的程序。

  • 一个运行着的程序,可能有多个进程。进程在操作系统中执行特定的任务。

  • 程序运行时系统就会创建一个进程,并为它分配资源,然后把该进程放入进程就绪队列,进程调度器选中它的时候就会为它分配CPU时间,程序开始真正运行。

三、 线程

  • 线程就是程序的执行路线,即进程内部的控制序列,或者说是进程的子任务。
  • 线程,轻量级,不拥有自己独立的内存资源,共享进程的代码区、数据区、堆区(注意没有栈区)、环境变量和命令行参数、文件描述符、信号处理函数、当前目录、用户ID和组ID等资源。
  • 线程拥有自己独立的栈,因此也有自己独立的局部变量。
  • 一个进程可以同时拥有多个线程,即同时被系统调度的多条执行路线,但至少要有一个主线程。

线程实现

继承Thread类

线程是程序中执行的线程。Java虚拟机允许应用程序同时运行多个执行线程。

每个线程都有优先权。 具有较高优先级的线程优先于具有较低优先级的线程执行。 每个线程可能也可能不会被标记为守护进程。 当在某个线程中运行的代码创建一个新的Thread对象时,新线程的优先级最初设置为等于创建线程的优先级,并且当且仅当创建线程是守护进程时才是守护进程线程。

当Java虚拟机启动时,通常会有一个非守护进程线程(通常调用某个指定类的名为main的方法)。 Java虚拟机继续执行线程,直到发生以下任一情况:

  • 已调用类Runtimeexit方法,并且安全管理器已允许执行退出操作。
  • 通过调用run方法返回或抛出超出run方法传播的异常,所有非守护程序线程的线程都已死亡。

创建线程方式一:继承Thread类,重写run()方法,调用start()开启线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyThread extends Thread{
@Override
public void run() {
//子线程方法
for (int i = 0; i < 20; i++) {
System.out.println("我是子线程" + i);
}
}

public static void main(String[] args) {
//开启子线程
new MyThread().start();

for (int i = 0; i < 20; i++) {
System.out.println("我是主线程" + i);
}
}
}

执行结果

可以发现,主线程和子线程是”同时“进行的。

Thread类实现了Runnable接口,内部通过静态代理调用了run()方法

实现Runnable接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyThread implements Runnable{
@Override
public void run() {
//子线程方法
for (int i = 0; i < 20; i++) {
System.out.println("我是子线程" + i);
}
}

public static void main(String[] args) {
//创建线程对象,代理线程
new Thread(new MyThread()).start();
for (int i = 0; i < 20; i++) {
System.out.println("我是主线程" + i);
}
}
}

使用方法基本和继承Thread类相同,执行结果也相似。

实现Callable接口

  • 实现Callable接口,需要返回值类型
  • 重写call方法,需要抛出异常
  • 创建目标对象
  • 创建执行服务: ExecutorService ser = Executors.newFixedThreadPool(1);
  • 提交执行: Future<Boolean> result1 = ser. submit(t1);
  • 获取结果: boolean r1 = result1.get()
  • 关闭服务: ser. shutdownNow();

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyThread implements Callable<Boolean> {
@Override
public Boolean call() {
//子线程方法
System.out.println("执行了子线程");
return true;
}

public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread t1 = new MyThread();
//创建执行服务
ExecutorService ser = Executors.newFixedThreadPool(1);
//提交执行
Future<Boolean> result1 = ser.submit(t1);
//获取结果
boolean r1 = result1.get();
//关闭服务
ser. shutdownNow();
}
}

线程状态

线程五大状态

  • 创建状态
  • 就绪状态
  • 阻塞状态
  • 运行状态
  • 死亡状态

五个状态的转化


创建状态:

1
Thread t = new Thread()

线程对象一旦创建就进入到了新生状态。

就绪状态:

当调用start()方法,线程立即进入就绪状态,但不意味着立即调度执行。

运行状态:

进入运行状态,线程才真正执行线程体的代码块。

阻塞状态:

当调用sleep, wait 或同步锁定时,线程进入阻塞状态,就是代码不往下执行,阻塞事件解除后,重新进入就绪状态,等待cpu调度执行。

不一定每个线程都会进入阻塞状态

死亡状态:

线程中断或者结束,一旦进入死亡状态,就不能再次启动。

方法 说明
setPriority(int newPriority) 更改线程的优先级
static void sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠
void join() 等待该线程终止
static void yield() 暂停当前正在执行的线程对象,并执行其他线程
void interrupt() 中断线程,别用这个方式
boolean isAlive() 测试线程是否处于活动状态

线程停止

  • 开始线程可以使用start()方法。
  • 建议线程正常停止,利用次数,不建议死循环。
  • 建议使用标志位,设置一个标志位。
  • 不要使用stop或者destroy等过时或者JDK不建议使用的方法。

使用标志位的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class MyThread implements Runnable{

private boolean flag = true;

@Override
public void run() {
//子线程方法
int i = 0;
while (flag) {
System.out.println("我是子线程" + i++);
}
}

public void stop() {
this.flag = false;
}

public static void main(String[] args) {
MyThread thread = new MyThread();
new Thread(thread).start();
for (int i = 0; i < 1000; i++) {
System.out.println("主线程" + i);
if (i == 800) {
thread.stop();
System.out.println("线程停止了");
}
}
}
}

结果如图

线程休眠

  • sleep (时间)指定当前线程阻塞的毫秒数
  • sleep存在异常InterruptedException
  • sleep时间达到后线程进入就绪状态
  • sleep可以模拟网络延时,倒计时
  • 每一个对象都有一-个锁, sleep不会释放锁

线程休眠的例子:

1
2
3
4
5
6
7
8
9
public class MyThread{
public static void main(String[] args) throws InterruptedException {
while (true) {
Date date = new Date(System.currentTimeMillis());
System.out.println(new SimpleDateFormat("HH:mm:ss").format(date));
Thread.sleep(1000);
}
}
}

每一秒输出当前时间,使用sleep(1000)来使得每次执行完休眠一秒。

线程礼让

  • 礼让线程,让当前正在执行的线程暂停,但不阻塞
  • 将线程从运行状态转为就绪状态
  • 让cpu重新调度,礼让不-定成功!看CPU心情

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyThread{
public static void main(String[] args) {
Runnable runnable = () -> {
System.out.println(Thread.currentThread().getName() + "线程开始");
Thread.yield(); //线程礼让
System.out.println(Thread.currentThread().getName() + "线程结束");
};

new Thread(runnable, "a").start();
new Thread(runnable, "b").start();
new Thread(runnable, "c").start();
}
}

执行结果

可以发现每次的结果都不相同

线程联合

可以想象成一个VIP线程,强制插队,查到当前线程,直到自己执行完毕。非常霸道

会使得线程阻塞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyThread{
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
for (int i = 0; i < 10; i++) {
System.out.println("VIP线程来了" + i);
}
};
Thread thread = new Thread(runnable);
thread.start();

for (int i = 0; i < 50; i++) {
if (i == 30) thread.join();
System.out.println("主线程" + i);
}
}
}

结果如图

线程状态观测

  • 线程状态。线程可以处于以下状态之一:

    • NEW
      尚未启动的线程处于此状态。
    • RUNNABLE
      在Java虚拟机中执行的线程处于此状态。
    • BLOCKED
      被阻塞等待监视器锁定的线程处于此状态。
    • WAITING
      无限期等待另一个线程执行特定操作的线程处于此状态。
    • TIMED_WAITING
      正在等待另一个线程执行最多指定等待时间的操作的线程处于此状态。
    • TERMINATED
      已退出的线程处于此状态。

    线程在给定时间点只能处于一种状态。 这些状态是虚拟机状态,不反映任何操作系统线程状态。

通过以下代码演示线程状态的监听

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyThread{
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(500);
} catch (InterruptedException ignored) {}
}

});
System.out.println(thread.getState());
thread.start();
while (thread.getState() != Thread.State.TERMINATED) {
Thread.sleep(500);
System.out.println(thread.getState());
}
}
}

运行结果

线程的优先级

Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程, 线程调度
器按照优先级决定应该调度哪个线程来执行。

观察Thread类源代码,可以看到优先级在jdk中的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 /**
* The minimum priority that a thread can have.
*/
public static final int MIN_PRIORITY = 1;

/**
* The default priority that is assigned to a thread.
*/
public static final int NORM_PRIORITY = 5;

/**
* The maximum priority that a thread can have.
*/
public static final int MAX_PRIORITY = 10;
  • 线程的优先级用数字表示,范围从1~10.
    • Thread.MIN_ PRIORITY= 1;
    • Thread.MAX_ PRIORITY= 10;
    • Thread.NORM_ PRIORITY = 5;

我们可以通过

1
System.out.println(Thread.currentThread().getPriority());

来输出当前线程的优先级,可以得到默认优先级是5

可以使用以下方式改变或获取优先级

1
2
getPriority() //得到优先级
setPriority(int xxx) 设置优先级

如果设置的优先级超出限制,则会抛出一个IllegalArgumentException异常:

1
2
3
if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
throw new IllegalArgumentException();
}

优先级低只是意味着获得调度的概率低,并不是优先级低就不会被调用了,这都是看CPU的调度。

最好在start前设置优先级

守护(daemon)线程

  • 线程分为用户线程守护线程
  • 虚拟机必须确保用户线程执行完毕
  • 虚拟机不用等待守护线程执行完毕
  • 如,后台记录操作日志,监控内存垃圾回收等待..

所谓守护线程,是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。

用户线程守护线程两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。 因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。

演示,先定义两个线程,一个守护线程,一个普通线程

1
2
3
4
5
6
7
8
9
10
11
Thread daemon = new Thread(()->{
while (true) {
System.out.println("守护线程...");
}
});
Thread normal = new Thread(()->{
for (int i = 0; i < 10; i++) {
System.out.println("子线程...");
}
System.out.println("子线程结束");
});

然后设置守护线程

1
daemon.setDaemon(true);

启动两个线程后,可以看到,守护线程并没有结束就停止了程序。