Java多线程学习笔记

创建线程的两种方式:

  • 继承Thread类;
  • 实现Runnable接口。
  1. 继承Thread类:
1
2
3
4
5
6
7
8
9
10
public class MyThread extends Thread {
public void run() {
System.out.println("My thread.");
}

public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
  1. 当一个类已经继承了其他类时,就只能实现Runnable接口:
1
2
3
4
5
6
7
8
9
10
11
12
public class MyThread extends OtherClass implements Runnable {
@Override
public void run() {
System.out.println("my thread");
}

public static void main(String[] args) {
// 先创建一个Thread实例,再传入自己的MyThread实例
MyThread myThread = new MyThread();
new Thread(myThread).start();
}
}

Thread类实现了Runnable接口。

1
2
3
4
5
public class Thread implements Runnable {
private volatile String name;
private int priority;
...
}

当线程有返回值时,必须实现Callable接口,执行Callable任务后返回Future对象,调用Future对象的get()方法获取返回值:

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
public class MyThread2 implements Callable {

private int i;

MyThread2(int i) {
this.i = i;
}

@Override
public Object call() throws Exception {
return i * i * i;
}

public static void main(String[] args) throws ExecutionException, InterruptedException {
int taskSize = 10;
ExecutorService pool = Executors.newFixedThreadPool(taskSize);
List<Future> list = new ArrayList<>();

for (int i = 0; i < taskSize; i++) {
Callable callable = new MyThread2(i);
Future future = pool.submit(callable);
list.add(future);
}
pool.shutdown();

for (Future future : list) {
System.out.println(future.get().toString());
}
}
}

new Thread的弊端如下:

a. 每次new Thread新建对象性能差。
b. 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。
c. 缺乏更多功能,如定时执行、定期执行、线程中断。

相比new Thread,Java提供的四种线程池的好处在于:

a. 重用存在的线程,减少对象创建、消亡的开销,性能佳。
b. 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
c. 提供定时执行、定期执行、单线程、并发数控制等功能。

参见 >> Java 四种线程池的用法分析

Java提供的4种线程池:

  • newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
  • newFixedThreadPool:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  • newSchuduledThreadPool:创建一个定长线程池,支持定时及周期性任务执行
  • newSingleThreadPool:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行

newCachedThreadPool

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyThread4 {
public static void main(String[] args) {
ExecutorService pool = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
final int index = i;
try {
Thread.sleep(i * 500);
} catch (InterruptedException e) {
e.printStackTrace();
}
pool.execute(() -> System.out.println(Thread.currentThread().getName() + " " + index));
}
}
}

线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。

1
2
3
4
5
6
7
8
9
10
pool-1-thread-1 0
pool-1-thread-1 1
pool-1-thread-1 2
pool-1-thread-1 3
pool-1-thread-1 4
pool-1-thread-1 5
pool-1-thread-1 6
pool-1-thread-1 7
pool-1-thread-1 8
pool-1-thread-1 9

newFixedThreadPool

1
2
3
4
5
6
7
8
9
10
11
12
ExecutorService pool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
final int x = i;
pool.execute(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " " + x);
});
}

任务数超出最大线程数,超出的线程会等待:

1
2
3
4
5
6
7
8
9
10
pool-1-thread-2  1
pool-1-thread-3 2
pool-1-thread-1 0
pool-1-thread-2 3
pool-1-thread-1 5
pool-1-thread-3 4
pool-1-thread-2 6
pool-1-thread-1 7
pool-1-thread-3 8
pool-1-thread-2 9

newSchuduledThreadPool

1
2
3
ScheduledExecutorService pool = Executors.newScheduledThreadPool(3);
// 延迟 1 秒后每 3 秒执行一次
pool.scheduleAtFixedRate(() -> System.out.println(Thread.currentThread().getName()), 1, 3, TimeUnit.SECONDS);

newSingleThreadPool

1
2
3
4
5
ExecutorService pool = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int index = i;
pool.execute(() -> System.out.println(Thread.currentThread().getName() + " " + index));
}
1
2
3
4
5
6
7
8
9
10
pool-1-thread-1 0
pool-1-thread-1 1
pool-1-thread-1 2
pool-1-thread-1 3
pool-1-thread-1 4
pool-1-thread-1 5
pool-1-thread-1 6
pool-1-thread-1 7
pool-1-thread-1 8
pool-1-thread-1 9

线程的生命周期:

新建New:JVM分配内存,并初始化成员变量的值;

就绪Runnable:创建方法调用栈和程序计数器;

运行 Running:获得CPU,执行run()方法;

阻塞 Blocked:让出CPU时间片,停止运行;

死亡 Dead:正常结束,异常结束或调用stop()方法。

wait()和sleep()的关键的区别在于,sleep()方法属于Thread类,wait()方法属于Object类,wait()是用于线程间通信的,而sleep()是用于短时间暂停当前线程。更加明显的一个区别在于,当一个线程调用wait()方法的时候,会释放它锁持有的对象的管程和锁,但是调用sleep()方法的时候,不会释放他所持有的管程。

而当调用 wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。

终止线程的3种方式 > Java中如何优雅正确的终止线程

  1. 使用stop()方法

    thread.stop()调用之后,创建子线程的线程就会抛出 ThreadDeatherror 的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因此,并不推荐使用 stop 方法来终止线程。

  2. 使用volatile标志位:

    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
    public class MyRunnable implements Runnable {

    public volatile boolean flag = true;

    @Override
    public void run() {
    System.out.println(Thread.currentThread().getName() + "创建");
    try {
    Thread.sleep(1000L);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    while (flag) {

    }
    System.out.println(Thread.currentThread().getName() + "终止");
    }

    public static void main(String[] args) throws InterruptedException {
    MyRunnable myRunnable = new MyRunnable();
    for (int i = 0; i < 3; i++) {
    Thread thread = new Thread(myRunnable, i + " ");
    thread.start();
    }
    Thread.sleep(2000L);
    System.out.println("-------");
    myRunnable.flag = false;
    }
    }

    output

    1
    2
    3
    4
    5
    6
    7
    0 创建
    2 创建
    1 创建
    -------
    2 终止
    0 终止
    1 终止
  3. 使用interrupt方式

    1. 线程处于阻塞状态时:

      如使用了 sleep,同步锁的 wait,socket 中的 receiver,accept 等方法时,会使线程处于阻塞状态。当调用线程的 interrupt()方法时,会抛出 InterruptException 异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后 break 跳出循环状态,从而让我们有机会结束这个线程的执行。通常很多人认为只要调用 interrupt 方法线程就会结束,实际上是错的, 一定要先捕获 InterruptedException 异常之后通过break 来跳出循环,才能正常结束 run 方法。

      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
      public class MyThread5 extends Thread {

      @Override
      public void run() {
      try {
      Thread.sleep(3000L);
      } catch (InterruptedException e) {
      e.printStackTrace();
      }
      }

      public static void main(String[] args) {
      MyThread5 thread5 = new MyThread5();
      thread5.start();
      boolean interrupted = thread5.isInterrupted();
      if (!interrupted) {
      thread5.interrupt();
      }
      }
      }

      output
      java.lang.InterruptedException: sleep interrupted
      at java.base/java.lang.Thread.sleep(Native Method)
      at ch4.MyThread5.run(MyThread5.java:12)
    2. 线程未处于阻塞状态:

      使用 isInterrupted()判断线程的中断标志来退出循环。当使用
      interrupt()方法时,中断标志就会置 true,和使用自定义的标志来控制循环是一样的道理。

      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
      public class MyThread5 extends Thread {
      public void run() {
      super.run();

      try {
      System.out.println("线程开始。。。");
      Thread.sleep(200000);
      System.out.println("线程结束。");
      } catch (InterruptedException e) {
      System.out.println("在沉睡中被停止, 进入catch, 调用isInterrupted()方法的结果是:" + this.isInterrupted());
      e.printStackTrace();
      }
      }

      public static void main(String[] args) {
      MyThread5 thread5 = new MyThread5();
      thread5.start();
      thread5.interrupt();
      }
      }

      output
      线程开始。。。
      在沉睡中被停止, 进入catch, 调用isInterrupted()方法的结果是:false
      java.lang.InterruptedException: sleep interrupted
      at java.base/java.lang.Thread.sleep(Native Method)
      at ch4.MyThread5.run(MyThread5.java:13)

线程基本方法:

yield:线程的礼让指的是先将线程的资源让出去,让别的线程先执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ThreadMethod extends Thread {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int x = 0; x < 10; x++) {
if (x % 3 == 0) {
Thread.yield(); //线程礼让
System.out.println("玩耍线程礼让执行 ***********************");
}
System.out.println(Thread.currentThread().getName() + "、 x = " + x);
}
}, "玩耍的线程");
thread.start();
for (int x = 0; x < 10; x++) {
System.out.println("【霸道的main主线程】 x = " + x);
}
}
}

output

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
玩耍线程礼让执行 ***********************
【霸道的main主线程】 x = 0
【霸道的main主线程】 x = 1
【霸道的main主线程】 x = 2
【霸道的main主线程】 x = 3
【霸道的main主线程】 x = 4
【霸道的main主线程】 x = 5
【霸道的main主线程】 x = 6
【霸道的main主线程】 x = 7
【霸道的main主线程】 x = 8
【霸道的main主线程】 x = 9
玩耍的线程、 x = 0
玩耍的线程、 x = 1
玩耍的线程、 x = 2
玩耍线程礼让执行 ***********************
玩耍的线程、 x = 3
玩耍的线程、 x = 4
玩耍的线程、 x = 5
玩耍线程礼让执行 ***********************
玩耍的线程、 x = 6
玩耍的线程、 x = 7
玩耍的线程、 x = 8
玩耍线程礼让执行 ***********************
玩耍的线程、 x = 9

join: 线程强制执行,指的当满足某些条件之后,某一个线程对象将一直可以独占资源,一直到该线程的程序执行结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ThreadMethod extends Thread {
public static void main(String[] args) throws InterruptedException {
Thread mainThread = Thread.currentThread(); //获得主线程
Thread thread = new Thread(() -> {
for (int x = 0; x < 10; x++) {
if (x == 3) { //如果x == 3,就强制执行主线程
try {
mainThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "、 x = " + x);
}
}, "玩耍的线程");
thread.start();
for (int x = 0; x < 10; x++) {
Thread.sleep(100L);
System.out.println("【霸道的main主线程】 x = " + x);
}
}
}

output

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
玩耍的线程、 x = 0
玩耍的线程、 x = 1
玩耍的线程、 x = 2
【霸道的main主线程】 x = 0
【霸道的main主线程】 x = 1
【霸道的main主线程】 x = 2
【霸道的main主线程】 x = 3
【霸道的main主线程】 x = 4
【霸道的main主线程】 x = 5
【霸道的main主线程】 x = 6
【霸道的main主线程】 x = 7
【霸道的main主线程】 x = 8
【霸道的main主线程】 x = 9
玩耍的线程、 x = 3
玩耍的线程、 x = 4
玩耍的线程、 x = 5
玩耍的线程、 x = 6
玩耍的线程、 x = 7
玩耍的线程、 x = 8
玩耍的线程、 x = 9

sleep: 休眠的主要特点是可以自动实现线程的唤醒,以继续进行后续的处理。但是需要注意的是,如果现在有多个线程对象,休眠也是有多个先后顺序的。

wait和notify:

  • synchronized关键词用于互斥访问。
  • 为了让一个方法是同步的,简单的添加synchronized关键词到它们的声明上。那么同一个对象上没有两个同步方法的调用可以相互交错。
  • 同步语句必须指定提供内在锁的对象。当使用synchronized(this)时候,必须避免使用同步其他对象方法的调用。
  • wait()告诉调用的线程放弃监视器和进入休眠状态,直到其他线程进入相同的监视器和调用notify()。
  • notify()唤醒在相同对象上第一个调用wait()的线程。
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
public class ThreadA {
public static void main(String[] args) {
ThreadB b = new ThreadB();
b.start();

synchronized (b) {
try {
System.out.println("Waiting for b to complete...");
b.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("Total is: " + b.total);
}
}
}

class ThreadB extends Thread {
int total;

@Override
public void run() {
synchronized (this) {
for (int i = 0; i < 100; i++) {
total += i;
}
notify();
}
}
}