一、线程概述

  • 一个软件中至少有一个应用程序,应用程序的一次运行就是一个进程,一个进程中至少有一个线程

    • 软件:应用程序和相关资源文件等构成一个软件系统
    • 程序:编程语言编写的一组指令的集合
    • 进程:是操作系统调度和分配资源的最小单位
    • 线程:进程中的一个执行单元,是CPU调度的最小单位
  • 单核CPU只能并发,多核CPU可以并行+并发

    • 并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行
    • 并发(concurrency):指两个或多个事件在同一个时间段内发生(同一个时刻只能有一条指令执行,但多个进程的指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果)
  • 不同的进程之间不共享内存,不同的线程共享同一个进程的内存

  • 进程之间切换的复杂度要远远高于线程调度

    • 线程调度:当系统只有一个CPU时,以某种顺序执行多个线程
      • 分时调度:所有线程轮流使用CPU的使用权,平均分配每个线程占用 CPU 的时间
      • 抢占式调度:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,Java使用的为抢占式调度
  • 多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU使用率更高

二、实现多线程

  • Java使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。Thread类实际上也是实现了Runnable接口的类

  • 当运行Java程序时,已经有一个main线程,实现多线程有两种方式,最终都是通过Thread对象的API来控制线程的

    • 方法一:继承Thread
      1. 定义Thread类的子类,并重写该类的run()方法,该方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体
      2. 创建Thread子类的实例,即创建线程对象
      3. 调用线程对象的start()方法来启动该线程
    • 方法二:实现Runnable接口
      1. 实现Runnable接口,重写run()方法
      2. 创建Thread类的实例,代理启动执行run()方法
  • 使用匿名内部类对象来实现线程的创建和启动
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
new Thread("新的线程!"){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+":正在执行!"+i);
}
}
}.start();
new Thread(new Runnable(){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+":" + i);
}
}
}).start();

三、Thread类

1、构造方法

  • Thread():分配一个新的线程对象
  • Thread(String name):分配一个指定名字的新的线程对象
  • Thread(Runnable target):分配一个带有指定目标的新的线程对象
  • Thread(Runnable target,String name):分配一个带有指定目标的新的线程对象并指定名字

2、常用方法

  • 静态方法

    • sleep(long millis):暂时停止执行(使当前正在执行的线程以指定的毫秒数暂停)
    • yield():暂停一下当前线程(重新调度一次系统的线程调度器)
  • 实例方法

    • run():此线程要执行的任务
    • getName():获取当前线程名称
    • currentThread():返回对当前正在执行的线程对象的引用
    • isAlive():测试线程是否处于活动状态
    • getPriority():返回线程优先级
    • setPriority(int newPriority):改变线程的优先级
      • 每个线程都有一定的优先级,优先级高的线程将获得较多的执行机会
      • 每个线程的默认优先级:与创建它的父线程具有相同的优先级
      • setPriority方法需要一个整数,并且范围在[1,10]之间,推荐三个优先级常量:
        • MAX_PRIORITY(10):最高优先级
        • MIN _PRIORITY (1):最低优先级
        • NORM_PRIORITY (5):普通优先级(默认情况下main线程具有普通优先级)
    • start():开始执行此线程(Java虚拟机调用此线程的run方法)
    • join():等待该线程终止
    • join(long millis):等待该线程终止的时间最长为millis毫秒
    • join(long millis, int nanos):等待该线程终止的时间最长为millis毫秒+nanos纳秒
    • stop():强迫线程停止执行,该方法具有固有的不安全性,已经标记为@Deprecated,不建议再使用(如果需要通过其他方式来停止线程,可以使用布尔变量的值来控制)

3、守护线程(了解)

  • 守护线程:在后台运行的,为其他线程提供服务(例:JVM的垃圾回收线程)
  • 守护线程的特点:如果所有非守护线程都死亡,那么守护线程自动死亡
  • setDaemon(true):将指定线程设置为守护线程(必须在线程启动之前设置,否则会报IllegalThreadStateException异常)
  • isDaemon():判断线程是否是守护线程

四、线程安全

1、线程安全问题

  • 线程之前资源共享情况如下
不能共享 可以共享
局部变量(方法栈) 静态变量(方法区)
不同对象的实例变量(堆) 同一对象的实例变量(堆)
  • 如果多个线程中对同一资源都有读和写的操作,就容易出现线程安全问题

2、解决线程安全问题

  • 为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制
  • 同步机制(同步监视器)的原理:相当于给某段代码加“锁”,任何线程想要执行这段代码,都要先获得“锁”

Java对象在堆中的数据分为对象头、实例变量、空白的填充。对象头中包含

  • Mark Word:记录了和当前对象有关的GC、锁标记等信息

  • 指向类的指针:每一个对象需要记录它是由哪个类创建出来的

  • 数组长度(只有数组对象才有)

  • 同步方法:synchronized关键字直接修饰方法,表示同一时刻只有一个线程能进入这个方法

    • 静态方法锁对象:默认当前类的类类对象
    • 非静态方法锁对象:默认当前类的实例对象
  • 同步代码块:synchronized(锁对象)关键字修饰某个代码块,表示只对这个区块的资源实行互斥访问

    • 锁对象:可以是任意类型,但是必须保证竞争【同一个共享资源】的多个线程必须使用同一个【同步锁对象】,常用Object的非多态实例对象、String的实例对象
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
package com.atguigu.safe;

public class SaleTicketDemo3 {
public static void main(String[] args) {
TicketSaleThread t1 = new TicketSaleThread();
TicketSaleThread t2 = new TicketSaleThread();
TicketSaleThread t3 = new TicketSaleThread();

t1.start();
t2.start();
t3.start();
}
}

class TicketSaleThread extends Thread{
private static int total = 100;
public void run(){//直接锁这里,肯定不行,会导致,只有一个窗口卖票
while(total>0) {
saleOneTicket();
}
}

public synchronized static void saleOneTicket(){//锁对象是TicketSaleThread类的Class对象,而一个类的Class对象在内存中肯定只有一个
if(total > 0) {//不加条件,相当于条件判断没有进入锁管控,线程安全问题就没有解决
System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余:" + --total);
}
}
}
package com.atguigu.safe;

public class SaleTicketDemo4 {
public static void main(String[] args) {
TicketSaleRunnable tr = new TicketSaleRunnable();
Thread t1 = new Thread(tr, "窗口一");
Thread t2 = new Thread(tr, "窗口二");
Thread t3 = new Thread(tr, "窗口三");

t1.start();
t2.start();
t3.start();
}
}

class TicketSaleRunnable implements Runnable {
private int total = 1000;

public void run() {//直接锁这里,肯定不行,会导致,只有一个窗口卖票
while (total > 0) {
saleOneTicket();
}
}
public synchronized void saleOneTicket(){//锁对象是this,这里就是TicketSaleRunnable对象,因为上面3个线程使用同一个TicketSaleRunnable对象,所以可以
if(total > 0) {//不加条件,相当于条件判断没有进入锁管控,线程安全问题就没有解决
System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余:" + --total);
}
}
}
package com.atguigu.safe;

public class SaleTicketDemo5 {
public static void main(String[] args) {
//2、创建资源对象
Ticket ticket = new Ticket();

//3、启动多个线程操作资源类的对象
Thread t1 = new Thread("窗口一") {
public void run() {//不能给run()直接假设,因为t1,t2,t3的三个run方法分别属于三个Thread类对象,
// run方法是非静态方法,那么锁对象默认选this,那么锁对象根本不是同一个
while (true) {
synchronized (ticket) {
ticket.sale();
}
}
}
};
Thread t2 = new Thread("窗口二") {
public void run() {
while (true) {
synchronized (ticket) {
ticket.sale();
}
}
}
};
Thread t3 = new Thread(new Runnable() {
public void run() {
synchronized (ticket) {
ticket.sale();
}
}
}, "窗口三");


t1.start();
t2.start();
t3.start();
}
}

//1、编写资源类
class Ticket {
private int total = 1000;

public void sale() {//也可以直接给这个方法加锁,锁对象是this,这里就是Ticket对象
if (total > 0) {
System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余:" + --total);
} else {
throw new RuntimeException("没有票了");
}
}

public int getTotal() {
return total;
}
}

五、等待唤醒机制

  • 等待唤醒机制:多个线程间的一种协作机制,在一个线程满足某个条件时,就进入等待状态,等待其他线程执行完或指定时间到期后自动唤醒,有多个线程进行等待时,可以唤醒所有的等待线程

  • 被唤醒的线程如果成功获取锁,则从 WAITING(等待)状态变成RUNNABLE(准备运行)状态,在当初中断的地方恢复执行;反之,则从WAITING(等待)状态又变成 BLOCKED(等待监视器锁) 状态

  • 等待唤醒机制通过Object类的实例方法实现(因为锁对象可以是任意对象)

    • wait()/ wait(时间):释放锁,线程不再参与调度,因此不会浪费 CPU 资源
    • notify():释放一个所通知对象的线程
    • notifyAll():释放全部所通知对象的线程

wait()方法与notify()方法必须要由同一个锁对象调用,且必须在同步代码块或同步方法中使用

(3)sleep()在Thread类中声明的静态方法,wait方法在Object类中声明

六、线程生命周期

1、5种状态(JDK1.5之前)

  • 新建(New):一个Thread类或其子类的对象被声明并创建

    • 此时和其他Java对象一样,仅仅由JVM为其分配了内存,并初始化了实例变量的值
    • 此时的线程对象并没有任何线程的动态特征,程序也不会执行它的线程体run()
  • 就绪(Runnable):线程对象调用了start()方法(只能对新建状态的线程调用,且只能调用一次)

    • JVM会为其创建方法调用栈和程序计数器,表示已具备了运行的条件,随时可以被调度
  • 运行(Running):处于就绪状态的线程获得了CPU,调用了run()方法的线程体代码

    • 如果计算机只有一个CPU,在任何时刻只有一个线程处于运行状态,如果计算机有多个CPU,将会有多个线程并行(Parallel)执行
    • 对于抢占式策略的系统而言,系统会给每个可执行的线程一个小时间段来处理任务,当该时间用完,系统会剥夺该线程所占用的资源,让其回到就绪状态等待下一次被调度
  • 阻塞(Blocked):

    • 在运行过程中的线程遇到如下情况时,线程会进入阻塞状态
      • 线程调用了sleep()方法,主动放弃所占用的CPU资源
      • 线程试图获取一个同步监视器,但该监视器正被其他线程持有
      • 线程同步监视器调用了wait()wait(time),让它等待通知
      • 其他线程对象加塞,调用了join()方法;
      • 线程被调用suspend方法挂起(已过时,因为容易发生死锁)
    • 发生如下情况时会解除阻塞,让该线程重新进入就绪状态,等待线程调度器再次调度
      • 线程的sleep()时间到
      • 线程成功获得了同步监视器
      • 线程等到了通知notify()或线程已到达等待时间
      • 加塞的线程结束了
      • 被挂起的线程又被调用了resume恢复(已过时,因为容易发生死锁)
  • 死亡(Dead):遇到如下情况时,线程就处于死亡状态

    • run()方法执行完成,线程正常结束
    • 线程执行过程中抛出了一个未捕获的异常(Exception)或错误(Error
    • 直接调用该线程的stop()来结束该线程(已过时,因为容易发生死锁)

img

2、6种状态(JDK1.5之后)

  • 新建(New)

  • 准备运行(Runnable)

  • 阻塞-等待同步监视器(Blocked)

  • 阻塞-等待通知(Wating):

    • 进入等待状态:Object类的wait(),唤醒:Object的notify()notifyAll()
    • 进入等待状态:Condition类的await(),唤醒:Conditon类的signal()
    • 进入等待状态:LockSupport类的park(),唤醒:LockSupport类的unpark()
    • 进入等待状态:Thread类的join(),唤醒:调用join方法的线程对象结束
  • 阻塞-定时等待(Time_Wating):

    • 进入定时等待状态:Object类的wait(时间)
    • 进入定时等待状态:LockSupport类的park(时间)
    • 进入定时等待状态:Thread类的sleep(时间)join(时间)
  • 结束(Terminated)

当线程从WAITING或TIMED_WAITING恢复到Runnable状态时,如果发现当前线程没有得到同步监视器,那么会立刻转入BLOCKED状态

img

七、释放锁操作与死锁

  • 可以释放锁的操作

    • 当前线程正常执行结束
    • 当前线程出现了未处理的ErrorException,导致当前线程异常结束
    • 当前线程执行了锁对象的wait()方法,被挂起并释放锁
  • 不会释放锁的操作

    • 程序调用sleep()yield()方法暂停当前线程的执行
    • 其他线程调用了该线程的suspend()方法将该该线程挂起(已过时)
  • 死锁:不同的线程分别锁住对方需要的同步监视器不释放,都在等待对方先放弃。一旦出现死锁,整个程序既不会发生异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续

八、代码练习

  • 简介:丈夫存钱,妻子取钱,余额不足时需要等待
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
package com.sguigu.homework3;

public class Account {
private String id;
private double balance;

public String getId() {
return id;
}

public double getBalance() {
return balance;
}

public Account(String id,double balance) {
this.id = id;
this.balance = balance;
}

@Override
public String toString() {
return "账户:" + id + ",余额:" + balance;
}

public synchronized void save (double money) {
if (money >= 0) {
balance += money;
System.out.println("丈夫" + Thread.currentThread().getName() + "本次存钱" + money + "元," + this);
}
this.notify();
}

public synchronized void withDraw (double money) {
while (balance < money) {
System.out.println("妻子" + Thread.currentThread().getName() + "本次想取钱" + money + "元,余额不足,请等待..");
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}

if (money > 0) {
balance -= money;
System.out.println("妻子" + Thread.currentThread().getName() + "本次取钱" + money + "元," + this);
}
}
}
package com.sguigu.homework3;

import javax.swing.*;
import java.util.Random;
import java.util.TreeMap;

public class Husband extends Thread{
private Account account;

public Husband (String name,Account account) {
super(name); // 线程名
this.account = account;
}

@Override
public void run() {
while (true) {
account.save(Math.random() * 9900 + 100);
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
package com.sguigu.homework3;

public class Wife extends Thread{
private Account account;

public Wife (String name,Account account) {
super(name);
this.account = account;
}

@Override
public void run() {
while (true) {
account.withDraw(Math.random() * 19000 + 1000);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
package com.sguigu.homework3;

public class TestAccount {
public static void main(String[] args) {
Account account = new Account("4645",0);
new Husband("老王",account).start();
new Wife("小李",account).start();
}
}