多线程基础

一个程序同时执行多个任务。通常,每一个任务称为一个线程( thread ), 它是线程控制的简称。可以同时运行一个以上线程的程序称为多线程程序(multithreaded)。

进程与线程的区别

区别 进程 线程
根本区别 作为资源分配的单位 调度和执行的单位
开销 每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销 线程可以看成是轻量级的进程,同一类线程共享代码和数据空间,每一个线程有独立的运行栈和程序计数器(PC),线程切换的开销小
所处环境 在操作系统中能同时运行多个任务(程序) 在同一个应用程序中有多个顺序流同时执行
分配内存 系统在运行时会为每个进程分配不同的内存区域 除了CPU之外,不会为线程分配内存(线程所使用的资源是它所属进程的资源),线程组只能共享资源
包含关系 没有线程的进程是可以被看作单线程的,如果一个进程内拥有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的 线程是进程的一部分,所以线程有的时候被称为是轻权进程或者轻量级进程

Java实现多线程的方法

1. 继承Thread类

  • Java中负责多线程功能的类是 java.lang.Thread
  • 通过创建 Thread 的实例来创建新的线程
  • 每个线程都是通过特定的Thread对象所对应的方法run()(线程体)来完成操作
  • 调用 Thread 类的start()方法来启动一个线程
  • 缺点:如果类已经继承了一个类(如小程序必须继承自 Applet 类),则无法再继承 Thread 类
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
public class TestThread extends Thread{ // 继承Thread类
public void run(){
for(int i=0; i< 5; i++){
System.out.println(this.getName() + ":" + i); // getName() 返回线程名称
}
}

public static void main(String[] args){
TestThread thread1 = new TestThread(); // 创建线程对象
thread1.start(); // 启动线程
TestThread thread2 = new TestThread();
thread2.start();
}
}
/*输出
Thread-0:0
Thread-0:1
Thread-0:2
Thread-0:3
Thread-0:4
Thread-1:0
Thread-1:1
Thread-1:2
Thread-1:3
Thread-1:4
*/

2. 实现Runnable接口

  • 实现 Runnable 接口的同时可以继承某个类,此方法更通用一些
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class TestThread2 implements Runnable{ // 实现 Runnable 接口
public void run(){
for(int i=0; i < 5; i++){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}

public static void main(String[] args){
// 创建线程对象,将实现了Runnable接口的对象作为参数传入
Thread thread1 = new Thread(new TestThread2());
thread1.start();
Thread thread2 = new Thread(new TestThread2());
thread2.start();
}
}
// 输出与继承Thread的类似

线程的状态

​ 线程有以下5中状态(ps:《Java核心技术I 》中是6种)

​ 要确定一个线程的当前状态,可调用 getState 方法

  • 新生状态(NEW)
    new关键字(如new Thread(r))建立一个线程对象后,该线程就处于新生状态
  • 就绪状态(Runnable)
    具备运行条件,但没被分配到CPU,处于“线程就绪状态”,等待系统为其分配CPU。就绪状态并不是执行状态,当系统选定一个等待执行Thread对象后,它就会进入执行状态,一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。
    导致线程进入就绪状态的4个原因:
    • 新建线程:调用 start() 方法,进入就绪状态
    • 阻塞线程:阻塞解除,进入就绪状态
    • 运行线程:调用 yield()方法,直接进入就绪状态
    • 运行线程:JVM将CPU资源从本线程切换到其它线程
    在任何给定时刻,一个可运行的线程可能正在运行也可能没有运行
  • 运行状态(Running)
    在运行状态的线程执行自己run方法中的代码,直到调用其他方法而终止或等待某资源而阻塞或完成任务而死亡。如果在给定的时间片内没有执行结束,就会被系统给换下来回到就绪状态。也可能由于某些“导致阻塞的事件”而进入阻塞状态。
  • 阻塞状态(Blocked)
    暂停一个线程的执行以等待某个条件发生(如某资源就绪)。
    导致阻塞的4种原因:
    • 执行 sleep(int millsecond) 方法,使当前线程休眠,进入阻塞状态。当到了指定的时间后,线程进入就绪状态。
    • 执行 wait() 方法,使当前线程进入阻塞状态。当使用 notify() 方法唤醒该线程后,进入就绪状态
    • 线程运行时,某个操作进入阻塞状态,比如执行IO流操作( read()/write() 方法本身就是阻塞的方法)。只有当引起该操作阻塞的原因消失后,线程进入就绪状态
    • join()线程联合: 当某个线程等待另一个线程执行结束后,才能继续执行时,使用join()方法
  • 死亡状态(Terminated)
    死亡状态是线程生命周期中的最后一个阶段。
    线程死亡的两个原因:
    • 正常运行的线程完成了它 run() 方法内的全部工作
    • 线程被强制终止,如通过执行 stop() 或 destroy() 方法(这两个方法已过时)来终止一个线程
      当一个线程进入死亡状态以后,就不能再回到其它状态了

终止线程的典型方式

设置一个 boolean型的终止变量,当它为 false 时,终止线程的运行

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
public class TestThreadCiycle implements Runnable {
String name;
boolean live = true;// 标记变量,表示线程是否可中止;
public TestThreadCiycle(String name) {
super();
this.name = name;
}
public void run() {
int i = 0;
//当live的值是true时,继续线程体;false则结束循环,继而终止线程体;
while (live) {
System.out.println(name + (i++));
}
}
public void terminate() {
live = false;
}

public static void main(String[] args) {
TestThreadCiycle ttc = new TestThreadCiycle("线程A:");
Thread t1 = new Thread(ttc);// 新生状态
t1.start();// 就绪状态
for (int i = 0; i < 100; i++) {
if(i == 88){
ttc.terminate();
System.out.println("ttc stop!");
}
System.out.println("主线程" + i);
}
}
}

暂停线程sleep/yield

  • sleep():可以让正在运行的线程进入阻塞状态,直到休眠时间满了,进入就绪状态
  • yield():可以让正在运行的线程直接进入就绪状态,让出CPU的使用权
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
public class TestThreadState {
public static void main(String[] args) {
StateThread thread1 = new StateThread();
thread1.start();
StateThread thread2 = new StateThread();
thread2.start();
}
}
//使用继承方式实现多线程
class StateThread extends Thread {
// 方法一:sleep()
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(this.getName() + ":" + i);
try {
Thread.sleep(2000);//调用线程的sleep()方法,睡眠2秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//方法二:yield()
/*
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(this.getName() + ":" + i);
Thread.yield(); //调用线程的yield()方法;
}
}
*/
}

线程的联合 join()

线程A在运行期间,可以调用线程B的join()方法,让线程B和线程A联合。这样,线程A就必须等待线程B执行完毕后,才能继续执行。

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
package thread;
public class TestThreadState {
public static void main(String[] args) {
Thread a = new Thread(new AThread());
a.start();
}
}

/**
* A线程
*/
class AThread implements Runnable {
@Override
public void run() {
Thread b = new Thread(new BThread());
b.start();
try {
b.join();
} catch (InterruptedException e) {
e.printStackTrace();
// 结束JVM 如果是0则表示正常结束;如果是非0则表示非正常结束
System.exit(1);
}
System.out.println("A线程 联合 B线程 成功");
}
}

/**
* B线程
*/
class BThread implements Runnable {
@Override
public void run() {
try {
for (int i = 1; i <= 10; i++) {
System.out.println("第" + i + "分钟");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("B线程 结束");
}
}

获取线程基本信息的方法

方法 功能
isAlive() 判断线程是否还未终止
getPriority() 获取线程的优先级数值
setPriority() 设置线程的优先级数值
setName() 设置线程名称
getName() 获取线程名称
currentThread() 获取当前正在运行的线程对象,即取得自己本身

线程的优先级

  • 线程的优先级用数字表示,范围是 1 ~ 10,一个线程的缺省优先级是 5
  • 优先级低只是意味着获得调度的概率低。并不是绝对 先调用优先级高的线程 后调用优先级低的线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TestThread {
public static void main(String[] args) {
Thread t1 = new Thread(new MyThread(), "t1");
Thread t2 = new Thread(new MyThread(), "t2");
t1.setPriority(1);
t2.setPriority(10);
t1.start();
t2.start();
}
}
class MyThread extends Thread {
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}

线程同步

  • 多个线程访问同一个对象,且某些线程还想修改这个对象。此时,需要用到“线程同步”。
  • 线程同步其实是一个等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用

线程不同步——银行取钱

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
package thread;

public class TestSync {
public static void main(String[] args) {
Account a = new Account("A", 100);
Drawing drawing1 = new Drawing(a, 80, "我取钱");
Drawing drawing2 = new Drawing(a, 80, "她取钱");
drawing1.start();
drawing2.start();
}

/**
* 银行账户
*/
static class Account {
String name; // 账户名称
int money; // 金额

public Account(String name, int money) {
this.name = name;
this.money = money;
}
}

/**
* 模拟提款操作
*/
static class Drawing extends Thread {
Account account; // 提款的账户
int drawingNum; // 提取的金额
int expenseTotal; // 总共提取的金额

public Drawing(Account account, int drawingNum, String name) {
super(name);
this.account = account;
this.drawingNum = drawingNum;
}

@Override
public void run() {
if (account.money - drawingNum < 0) {
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.money -= drawingNum;
expenseTotal += drawingNum;
System.out.println(this.getName() + "--账户余额:" + account.money);
System.out.println(this.getName() + "--共取了:" + expenseTotal);
}
}
}
/*
她取钱--账户余额:20
她取钱--共取了:80
我取钱--账户余额:-60
我取钱--共取了:80
*/

实现线程同步——银行取钱

  • synchronized 方法

    • synchronized 方法控制对“对象的类成员变量”的访问
    • 每个对象对应一把锁,每个 synchronized 方法都必须获得调用该方法的对象的锁方能执行,否则所属线程阻塞
    • 方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态
    • 缺点:若将一个大的方法声明为synchronized 将会大大影响效率。
    1
    2
    3
    4
    public synchronized void method(int args)
    {
    // method body
    }
  • synchronized 块

    1
    2
    3
    synchronized(syncObject){
    // 允许访问控制的代码
    }
    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
    package thread;

    public class TestSync {
    public static void main(String[] args) {
    Account a = new Account("A", 100);
    Drawing drawing1 = new Drawing(a, 80, "我取钱");
    Drawing drawing2 = new Drawing(a, 80, "她取钱");
    drawing1.start();
    drawing2.start();
    }

    /**
    * 银行账户
    */
    static class Account {
    String name; // 账户名称
    int money; // 金额

    public Account(String name, int money) {
    this.name = name;
    this.money = money;
    }
    }

    /**
    * 模拟提款操作
    */
    static class Drawing extends Thread {
    Account account; // 提款的账户
    int drawingNum; // 提取的金额
    int expenseTotal; // 总共提取的金额

    public Drawing(Account account, int drawingNum, String name) {
    super(name);
    this.account = account;
    this.drawingNum = drawingNum;
    }

    @Override
    public void run() {
    draw();
    }

    public void draw() {
    // 可提高性能
    if(account.money<=0){
    return;
    }
    synchronized (account) { // 线程需获得account对象的“锁”才有资格运行同步块中的代码
    if (account.money - drawingNum < 0) {
    System.out.println(this.getName() + "-- 余额不足!");
    return;
    }
    try {
    Thread.sleep(1000); // 判断完后阻塞。其他线程开始运行。
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    account.money -= drawingNum;
    expenseTotal += drawingNum;
    }
    System.out.println(this.getName() + "--账户余额:" + account.money);
    System.out.println(this.getName() + "--总共取了:" + expenseTotal);
    }
    }

    }
    /*
    我取钱--账户余额:20
    她取钱-- 余额不足!
    我取钱--总共取了:80
    */

死锁

  • 多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
  • 死锁产生的四个必要条件:
    • 互斥使用。当资源被一个线程使用(占有)时,别的线程不能使用
    • 不可抢占。资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放
    • 请求和保持。当资源请求者在请求其它资源的同时保持对原有资源的占有
    • 循环等待。存在一个等待队列:P1占有P2的资源,P2占有P3的 资源;P3占有P1的资源,形成一个等待环路
  • 打破其中任一条件便可解决死锁问题

死锁问题

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
/**
* 两个资源obj1和obj2
* A锁住obj1,B锁住obj2,然后A再想锁住obj2,B再想锁住obj1,
* 但此时这两个资源都分别被A或B锁住了,所以就产生了死锁
*/
public class LockTest {
public static String obj1 = "obj1";
public static String obj2 = "obj2";
public static void main(String[] args) {
new Thread(new LockA()).start();
new Thread(new LockB()).start();
}
}
class LockA implements Runnable{
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " LockA 开始执行");
while(true){
synchronized (LockTest.obj1) {
System.out.println(Thread.currentThread().getName() + " LockA 锁住 obj1");
Thread.sleep(3000); // 此处等待是给B能锁住机会
synchronized (LockTest.obj2) {
System.out.println(Thread.currentThread().getName() + " LockA 锁住 obj2");
Thread.sleep(60 * 1000); // 为测试,占用了就不放
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
class LockB implements Runnable{
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " LockB 开始执行");
while(true){
synchronized (LockTest.obj2) {
System.out.println(Thread.currentThread().getName() + " LockB 锁住 obj2");
Thread.sleep(3000); // 此处等待是给A能锁住机会
synchronized (LockTest.obj1) {
System.out.println(Thread.currentThread().getName() + " LockB 锁住 obj1");
Thread.sleep(60 * 1000); // 为测试,占用了就不放
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

解决死锁

死锁是由于“同步块需要同时持有多个对象锁造成”的,所以同一个代码块,不要同时持有两个对象锁就可以解决了

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
public class LockTest {
public static String obj1 = "obj1";
public static String obj2 = "obj2";
public static void main(String[] args) {
new Thread(new LockA()).start();
new Thread(new LockB()).start();
}
}
class LockA implements Runnable{
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " LockA 开始执行");
while(true){
synchronized (LockTest.obj1) {
System.out.println(Thread.currentThread().getName() + " LockA 锁住 obj1");
Thread.sleep(3000); // 此处等待是给B能锁住机会
}
synchronized (LockTest.obj2) {
System.out.println(Thread.currentThread().getName() + " LockA 锁住 obj2");
Thread.sleep(60 * 1000); // 为测试,占用了就不放
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
class LockB implements Runnable{
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " LockB 开始执行");
while(true){
synchronized (LockTest.obj2) {
System.out.println(Thread.currentThread().getName() + " LockB 锁住 obj2");
Thread.sleep(3000); // 此处等待是给A能锁住机会
}
synchronized (LockTest.obj1) {
System.out.println(Thread.currentThread().getName() + " LockB 锁住 obj1");
Thread.sleep(60 * 1000); // 为测试,占用了就不放
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

线程并发协作(生产者/消费者模式)

  • 生产者:负责生产数据的模块(模块可能是方法、对象、线程、进程)
  • 消费者:负责处理数据的模块
  • 缓冲区:消费者不能直接使用生产者的数据,生产者将生产好的数据放入“缓冲区”,消费者从“缓冲区”拿取要处理的数据
  • 缓冲区是实现并发的核心,缓冲区的设置的3个好处
    • 实现线程的并发协作。有了缓冲区以后,生产者线程只需要往缓冲区里面放置数据,而不需要管消费者消费的情况;同样,消费者只需要从缓冲区拿数据处理即可,也不需要管生产者生产的情况。 这样,就从逻辑上实现了“生产者线程”和“消费者线程”的分离
    • 解耦了生产者和消费者。生产者不需要和消费者直接打交道
    • 解决忙闲不均,提高效率。生产者产生数据慢时,缓冲区任有数据,不影响消费者消费;消费者处理数据慢时,生产者然可以继续玩缓冲区里放置数据
  • wait()notify()notifyAll()均是java.lang.Object类的方法,只能在同步方法或同步代码中使用,否则会抛出异常
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
public class TestProduce {
public static void main(String[] args) {
SyncStack sStack = new SyncStack(); // 定义缓冲区对象;
Producer producer = new Producer(sStack); // 定义生产线程;
Consumer consumer = new Consumer(sStack); // 定义消费线程;
producer.start();
consumer.start();
}
}

class Mantou {// 馒头
int id;
Mantou(int id) {
this.id = id;
}
}

class SyncStack {// 缓冲区(相当于:馒头筐)
int index = 0;
Mantou[] ms = new Mantou[10];

public synchronized void push(Mantou m) {
while (index == ms.length) {//说明馒头筐满了
try {
//wait后,线程会将持有的锁释放,进入阻塞状态;
//这样其它需要锁的线程就可以获得锁;
this.wait();
//这里的含义是执行此方法的线程暂停,进入阻塞状态,
//等消费者消费了馒头后再生产。
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 唤醒在当前对象等待池中等待的第一个线程。
//notifyAll叫醒所有在当前对象等待池中等待的所有线程。
this.notify();
// 如果不唤醒的话。以后这两个线程都会进入等待线程,没有人唤醒。
ms[index] = m;
index++;
}

public synchronized Mantou pop() {
while (index == 0) {//如果馒头筐是空的;
try {
//如果馒头筐是空的,就暂停此消费线程
this.wait(); //等生产线程生产完再来消费;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.notify();
index--;
return ms[index];
}
}

class Producer extends Thread {// 生产者线程
SyncStack ss = null;

public Producer(SyncStack ss) {
this.ss = ss;
}

@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("生产馒头:" + i);
Mantou m = new Mantou(i);
ss.push(m);
}
}
}

class Consumer extends Thread {// 消费者线程;
SyncStack ss = null;

public Consumer(SyncStack ss) {
this.ss = ss;
}

@Override
public void run() {
for (int i = 0; i < 10; i++) {
Mantou m = ss.pop();
System.out.println("消费馒头:" + i);
}
}
}

任务定时调度

  • java.util.Timer: 定时或每隔一定时间触发一次线程。Timer类本身是一个线程,只是它用来调用其它的线程
  • java.util.TimerTask:TimerTask类是一个抽象类,实现了Runnable接口,具备多线程能力。
  • 在实际使用时,一个Timer可以启动任意多个TimerTask实现的线程,但是多个线程之间会存在阻塞。所以如果多个线程之间需要完全独立的话,最好还是一个Timer启动一个TimerTask实现
  • 可以使用开源框架quanz,更加方便的实现任务定时调度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.Timer;
import java.util.TimerTask;

public class TestTimer{
public static void main(String[] args){
Timer t = new Timer(); // 定义计时器
MyTask task = new MyTask(); // 定义任务
t.schedule(task,3000); // 3秒后执行
//t.schedule(task,5000,1000); // 5秒后每隔1秒执行一次
//GregorianCalendar calendar = new GregorianCalendar(2019,11,14,20,28);
//t.schedule(task,caldendar.getTime()); // 指定时间定时执行
}
}
class MyTask extends TimerTask{
public void run(){
for(int i=0;i<10;i++){
System.out.println("任务:"+i);
}
}
}

参考《 Java核心技术 卷 I 》及互联网