# 线程

# 进程

在操作系统中,一个运行中的任务通常对应一个进程。
进程通常包含以下三个特征:

  • 独立性:进程是系统中的独立单位,具有独立的资源、私有的空间
  • 动态性:进程具有自己的生命周期
  • 并发性:多个进程可以并发执行而不会相互影响

# 多进程

多进程就是计算机同时执行多个进程(软件)。
现代操作系统均支持多线程并发运行,用户可以随意开启多个软件,并让它们同时运行在计算机之上。

对于 CPU 而言,它在同一时间只能执行一个程序。因此 CPU 会在多个进程之间轮换执行,从而达到宏观上同时运行的效果。

# 多线程

多线程扩展了多进程的概念,使得同一个进程可以同时并发处理多个任务。
特点

  • 线程是进程的组成部分
  • 进程中必须有一个主线程
  • 进程中可以有很多个线程
  • 当进程被创建,主线程也就被创建了
  • 线程的执行是抢占式的
  • 与进程相比,线程之间的分隔程度更小,它们共享进程的状态

线程与进程的相同点
线程之于进程,就像进程之于操作系统。

  • 线程在进程中是独立的
  • 线程具有生命周期
  • 线程可以并发执行
  • 进程可以并发执行

多线程的优势

  • 线程会共享进程的状态,因此线程具有更好的性能
  • 线程会共享进程的状态,因此线程更容易实现相互通信

# Thread 类

JAVA 用 Thread 类代表线程,所有的线程对象都必须是 Thread 类或其子类的实例。

# 构造方法

Thread Thread(Runnable target, String name)
1
  • 传入实现了 Runnable 接口的对象
  • 传入线程的名字

# 类方法

Thread currentThread()
1

获取当前的线程

# 实例方法

void run() {
    if (target != null) {
    	target.run();
    }
}
1
2
3
4
5

线程所执行的"任务",为线程指定任务一般有两种做法:

  • 继承 Thread 类,重写 run() 方法
  • 传入实现了 Runnable 接口,实现了 run() 方法的类的对象
void start()
1

启动线程

不要 直接调用 run() 方法

String getName()
1

获取线程的名字

boolean isAlive()
1

线程是否启动

提示

当线程处于就绪、运行、堵塞时,返回 true 当线程处于新建、死亡时,返回 false

# 线程的创建和启动

# 继承 Thread 类创建线程类

  • 定义 Thread 的子类,重写 run() 方法
  • 创建该类的对象
  • 调用对象的 start() 方法,启动线程
// 定义 Thread 的子类,重写 run() 方法
class 类名 extends Thread{
    @Override
    public void run() {
    		···
        }
    }
}
// 创建该类的对象
类名 对象名 = new 类名();
// 调用对象的 `start()` 方法,启动线程
对象名.start()
1
2
3
4
5
6
7
8
9
10
11
12
 public class FirstThread extends Thread{
  private int i;
 
  @Override
  public void run() {
      for ( ; i < 100; i++) {
          System.out.println(getName() + " " + i);
      }
  }
 }
 public class Test {
  public static void main(String[] args) {
      new FirstThread().start();
      new FirstThread().start();
  }
 }
// 输出结果
  Thread-1 0
  Thread-1 1
  Thread-0 0
  Thread-1 2
  Thread-0 1
  Thread-1 3
  Thread-0 2
  Thread-1 4
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

# 实现 Runnable 接口创建线程类

  • 定义类,实现 Runnable 接口,重写 run() 方法
  • 实例化接口实现类
  • 以接口实现类的对象作为 Thread 的参数创建 Thread 的对象
  • 调用对象的 start() 方法,启动线程
// 定义类,实现 Runnable 接口,重写 run() 方法
class 类名 implements Runnable{
    @Override
    public void run() {
    		···
        }
    }
}
// 实例化接口实现类
类名 对象名1 = new 类名();
// 以接口实现类的对象作为 Thread 的参数创建 Thread 的对象
Thread 对象名2 = new Thread(对象名1);
// 调用对象的 `start()` 方法,启动线程
对象名2.start();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SecondThread   implements Runnable{
  @Override
  public void run() {
      for (int i = 0; i < 100; i++) {
          System.out.println(Thread.currentThread().getName() + " " + i);
      }
  }
}
public class Test {
  public static void main(String[] args) {
      SecondThread secondThread = new SecondThread();
      new Thread(secondThread).start();
      new Thread(secondThread).start();
  }
}
// 输出结果
  Thread-0 72
  Thread-1 8
  Thread-0 73
  Thread-0 74
  Thread-1 9
  Thread-1 10
  Thread-1 11
  Thread-1 12
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 线程的生命周期

# 新建

如果仅仅是新建一个线程对象,它和普通 JAVA 一样,由虚拟机分配空间、初始化。此时线程对象没有任何动态特征,其线程执行体也不会被执行。

# 就绪

使用 start() 可以激活线程对象,虚拟机会为其创建方法调用栈和程序计数器,为线程的运行做好准备,线程进入就绪状态。就绪状态的线程只是可以运行了,并不会立刻开始运行。线程何时能够抢到 CPU 的执行权,线程才能进入运行状态。

# 运行

如果处于就绪状态的线程抢到了 CPU 的执行权,则线程会进入运行状态。
程序开始运行后,它不可能始终处于运行状态,它会在运行时被其它线程抢走 CPU 的执行权,并重新进入就绪状态。

# 堵塞

但发生以下情况时,线程将会进入堵塞状态

  • 调用 sleep() 方法
  • 调用了堵塞式的 IO 方法
  • 试图获得一个同步监视器,但该同步监视器正在被其它线程调用
  • 在等待通知
  • 调用了线程的 suspend() 方法 当线程的堵塞接触后,线程会重新进入就绪状态,等待进入运行状态。

# 死亡

线程会以三种方式结束,进入死亡状态

  • 线程执行体执行完毕
  • 抛出未捕获的异常或错误
  • 调用 stop() 方法结束线程

# 控制线程

# join()

Thread 提供了 join() 方法,它能够让一个线程等待另外一个线程完成后再继续进行。 如果在线程中调用了另一个线程的 join() 方法,则线程将被堵塞,直至另一个线程执行完毕。 未使用 join() 方法

public class RelaxThread extends Thread{
  @Override
  public void run() {
     for (int i = 0; i < 20; i++) {
         System.out.println("休息了" + i + "下");
     }
  }
}
public class RunThread extends Thread{
  @Override
  public void run() {
     for (int i = 0; i < 100; i++) {
         System.out.println("走了" + i + "步");
         if (i == 20) {
             RelaxThread relaxThread = new RelaxThread();
             relaxThread.start();
         }
     }
  }
}
public static void main(String[] args) {
	RunThread runThread = new RunThread();
	runThread.start();
}
// 输出结果
  走了77步
  走了78步
  休息了4下
  休息了5下
  休息了6下
  走了79步
  休息了7下
  休息了8
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

使用 join() 方法

public class RelaxThread extends Thread{
  @Override
  public void run() {
     for (int i = 0; i < 20; i++) {
         System.out.println("休息了" + i + "下");
     }
  }
}
public class RunThread extends Thread{
  @Override
  public void run() {
     for (int i = 0; i < 100; i++) {
         System.out.println("走了" + i + "步");
         if (i == 20) {
             RelaxThread relaxThread = new RelaxThread();
             relaxThread.start();
             relaxThread.join();
         }
     }
  }
}

public static void main(String[] args) {
	RunThread runThread = new RunThread();
	runThread.start();
}
// 输出结果
  走了18步
  走了19步
  走了20步
  休息了0下
  休息了1下
  休息了2下
  休息了3下
  休息了4
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

# setDaemon()

有一种进程,它运行于后台,为其它进程提供服务,这种进程被称为“后台进程”。
后台进程有一个特征:如果所有的前台进程死亡,后台进程也将自动死亡。
调用 Thread 的 setDaemon() 方法,该方法需要一个 Boolean 类型的参数,填入 true ,即可将指定的进程设置为后台进程。
需要注意的是,当前台线程死亡以后,后台线程并非立即死亡,其接收指令到做出响应也需要一定的时间。

public class BackEndThread extends Thread{
  @Override
  public void run() {
   while(true) {
       System.out.println("后台");
   }
  }
}
public class TestThread extends Thread{
  @Override
  public void run() {
     BackEndThread backEndThread = new BackEndThread();
     backEndThread.setDaemon(true);
     backEndThread.start();
     for (int i = 0; i < 99; i++) {
         System.out.println(getName() + " " + i);
   }
}

public static void main(String[] args) {
 TestThread testThread = new TestThread();
 testThread.start();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# sleep()

将当前进程设置为堵塞状态。

Thread.sleep(time);
1

当线程被 “sleep” 之后,在其睡眠时间段内,它都不会获得执行的机会。

# yield()

将当前进程设置为就绪状态。

Thread.yield()
1

当进程被 yield() 设置为就绪状态后,它便可以开始和其它进程一起争夺 CPU 的执行权,如果抢到便可以再次进入执行状态。

# 线程优先级

提示

默认优先级 NORM_PRIORITY:5 最大优先级 MAX_PRIORITY:10 最小优先级 MIN_PRIORITY:1

每个线程都具有优先级,在争夺 CPU 的执行权时,优先级高的线程可以获得更多的执行机会。
每个线程线程默认的优先级与创建它的父线程的优先级相同。Thread 类提供了 setPriority(num)getPriority() 方法,用于设置和获取指定线程的优先级。
需要注意的是,优先级高的线程可以获得更多的执行机会,优先被执行,而不是一定先执行。

# 线程同步

# 为什么要线程同步?

当多个线程同时访问一个属性时,往往会因为并发访问导致属性的状态出现混乱。 会出现问题的三个条件:

  • 多线程环境
  • 有共享属性
  • 多个线程操作共享属性
public class Account implements Runnable {
  private int balance = 1000;
  @Override
  public void run() {
      while (balance >= 10) {
          try {
              Thread.sleep(100);
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
          balance -= 10;
          System.out.println("还剩" + balance);
      }
  }
}
public static void main(String[] args) {
  Account account = new Account();
  Thread thread1 = new Thread(account);
  Thread thread2 = new Thread(account);
  thread1.start();
  thread2.start();
}
// 输出
  还剩50
  还剩40
  还剩30
  还剩20
  还剩10
  还剩0
  还剩-10
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

# 同步代码块

JAVA 引入了同步监视器来解决同步问题,其语法格式如下:

synchronized (对象) {
	代码体;
}
1
2
3
  • synchronized 为关键字,用于修饰代码块
  • obj 为同步监视器,可以为任意对象
  • 同一时刻只能有一个线程执行代码块

提示

  • 任何时刻只能有一个线程可以获得对同步监视器的锁定
  • 当线程锁定同步监视器时,线程便可以执行代码块
  • 当同步监视器被锁定时,其它试图访问代码块的线程将被堵塞
  • 当线程的代码块执行完成,会自动释放对同步监视器的锁定
public class Account implements Runnable {
private int balance = 1000;
private Object object = new Object();
@Override
public void run() {
    synchronized (object) {
        while (balance >= 10) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
>             balance -= 10;
            System.out.println("还剩" + balance);
        }
    }
}
}

public class Test {
  public static void main(String[] args) {
      Account account = new Account();
      Thread thread1 = new Thread(account);
      Thread thread2 = new Thread(account);
      thread1.start();
      thread2.start();
  }
}
// 输出结果
  还剩70
  还剩60
  还剩50
  还剩40
  还剩30
  还剩20
  还剩10
  还剩0
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

# 同步方法

可以使用 synchronized 修饰方法,使方法成为同步方法。

TIP

同步监视器为对象或类

synchronized 返回值类型 方法名 {
	···
}
1
2
3
public class Account implements Runnable {
  private int balance = 1000;
  @Override
  public synchronized void run() {
      while (balance >= 10) {
          try {
              Thread.sleep(10);
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
          balance -= 10;
          System.out.println("还剩" + balance);
      }
  }
}
public class Test {
  public static void main(String[] args) {
      Account account = new Account();
      Thread thread1 = new Thread(account);
      Thread thread2 = new Thread(account);
      thread1.start();
      thread2.start();
  }
}
// 输出结果
  还剩70
  还剩60
  还剩50
  还剩40
  还剩30
  还剩20
  还剩10
  还剩0
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

# 同步锁

Lock 和 synchronized 一样都可以用于设置线程同步,但与 synchronized 相比,Lock 提供了更多的方法和更灵活的同步控制。
Lock 和 ReadWriteLock 是 JAVA 提供的两个接口,ReentrantLock 和 ReentrantReadWriteLock 分别是它们的实现类。
语法

// 首先获得Lock的lock对象
Lock lock = ···

// 通过lock对象的lock()方法加锁
lock.lock()

// 需要线程同步的代码
···

// 通过lock对象的unlock()方法解锁
lock.unlock()
1
2
3
4
5
6
7
8
9
10
11

为了保证 lock 锁始终被解开,应该使用 try--catch 写法,并将 unlock() 放置到 finally 中

// 首先获得Lock的lock对象
Lock lock = ···

try 
{
    // 通过lock对象的lock()方法加锁
    lock.lock()

    // 需要线程同步的代码
    ···
}
finally
{
	// 通过lock对象的unlock()方法解锁
	lock.unlock()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 死锁

当两个线程相互等待对方释放同步监视器时,就会发生死锁。具体来讲:

  • 当前线程拥有其它线程需要的资源
  • 当前线程等待其它线程的资源 JAVA 并没有检测,也没有措施处理死锁问题,一旦出现死锁,程序并不会发生错误或异常,线程将处于堵塞状态无法继续。

死锁实例

public class DeadLock {
	// 用于被线程"抢占"的资源
    private static final Object left = new Object();
    private static final Object right = new Object();

	// 先拿left,再拿right的方法
    public static void leftRight() {
        // 得到left锁
        synchronized (left) {
            // 得到right锁
            synchronized (right) {
                System.out.println("我先左,我再右");
            }
        }
    }

	// 先拿right,再拿left的方法
    public static void rightLeft() {
        // 得到right锁
        synchronized (right) {
            // 得到left锁
            synchronized (left) {
                System.out.println("我先右,我再左");
            }
        }
    }
}

// Left线程类
public class Left extends Thread {
    @Override
    public synchronized void run() {
        while (true) {
            DeadLock.leftRight();
        }
    }
}

// right线程类
public class Right extends Thread {
    @Override
    public synchronized void run() {
        while (true) {
            DeadLock.rightLeft();
        }
    }
}

// 执行两个线程
public class Test {
    public static void main(String[] args) {
        Thread left = new Left();
        Thread right = new Right();
        left.start();
        right.start();
    }
}
// 输出结果
  我先左,我再右
  我先左,我再右
  我先左,我再右
  我先左,我再右
  我先左,我再右
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

程序开始后不久,线程就会进入堵塞状态而无法继续。
其原因是,在程序中的某一刻:

  • right 线程占据 right 对象,并期望获得 left 对象
  • left 线程占据 left 对象,并期望获得 right 对象
    两个线程相互僵持,导致了死锁。

避免死锁
避免死锁有以下几种方法:

  • 避免多次锁定:避免同一个线程锁定多个同步监视器
  • 固定加锁的顺序:如果有多个线程需要对多个同步监视器进行锁定,应该保证它们以相同的顺序加锁

提示

例如: 所有线程都以 对象1、对象2、···、对象n 的顺序对多个对象加锁,从而避免了死锁

  • 使用定时锁:通过 tryLock() 方法,当等待超时后将自动解锁