Dawn's Blogs

分享技术 记录成长

0%

Java高级 (1) 多线程

线程的创建和使用

Java 语言的 JVM 允许程序运行多个线程,它通过 java.lang.Thread 类(或者 Runnable 接口)来体现。Thread 类的特性:

  • 每个线程都是通过某个特定 Thread 对象的 run 方法来完成操作的,经常把 run 方法的主体称为线程体
  • 通过 Thread 对象的 start 方法来启动线程。

创建线程方式

JDK1.5 之前创建新执行线程有两种方法:

  • 继承 Thread 类。
  • 实现 Runnable 接口

JDK1.5 之后新增的线程创建方式:

  • 实现 Callable 接口。
  • 使用线程池。

image-20230129180135020

继承 Thread 类

创建方法如下:

  • 定义子类继承 Thread 类
  • 子类中重写 Thread 类中的 run 方法
  • 创建子类对象,调用 start 方法启动线程

注意:一个线程对象只能调用一次 start 方法启动,如果重复调用了会抛出异常 IllegalThreadStateException

实现 Runnable 接口

创建方法如下:

  • 定义子类实现 Runnable 接口
  • 子类中重写 Runnable 接口中的 run 方法
  • 通过 Thread 类含参构造器创建线程对象,将实现 Runnable 接口的对象作为实际参数传递给 Thread 类的构造器中
  • 调用 Thread 类对象的 start 方法开启线程

实现 Runnable 接口的好处:

  • 避免了单继承的局限性。
  • 多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源

实现 Callable 接口

与使用 Runnable 相比, Callable 功能更强大些

  • 相比 Runnable接口中的 run 方法,Callable 接口中的 call 方法可以有返回值。
  • 方法可以抛出异常。
  • 支持泛型的返回值。
  • 需要借助 FutureTask 类,比如获取返回结果。

Future 接口:

  • 可以对具体 Runnable、Callable 任务的执行结果进行取消、查询是否完成、获取结果等操作。
  • FutureTask 是 Future 的唯一实现类。
  • FutureTask 同时实现了 Runnable、Future 接口。它既可以作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值。

创建方法:

  • 创建一个 Callable 的实现类,实现 call 方法
  • 创建 Callable 实现类对象,将此对象作为参数传递到 FutureTask 构造器中,创建 FutureTask 对象
  • 将 FutureTask 对象作为参数传递到 Thread 构造器中,创建 Thread 对象并 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
class Num implements Callable {
private int num = 1;

@Override
public Object call() throws Exception {
int total = 0;
while (num <= 100) {
total += num;
num ++;
}

return total;
}
}

public class CallableTest {

public static void main(String[] args) {
Num num = new Num(); // 创建 Callable 实现类对象
FutureTask futureTask = new FutureTask(num); // 创建 FutureTask 对象
Thread thread = new Thread(futureTask); // 创建 Thread 对象

thread.start();

try {
// 查看返回值
Object total = futureTask.get();
System.out.println("1~100 的和为:" + total);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
}

线程池

提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用线程池相关的 API:ExecutorService 和 Executors

ExecutorService:真正的线程池接口。常见子类 ThreadPoolExecutor:

  • void execute(Runnable command):执行任务,没有返回值,一般用来执行 Runnable。
  • Future submit(Callable task):执行任务,有返回值,一般用来执行 Callable。
  • void shutdown():关闭连接池。

Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池。

  • Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池。
  • Executors.newFixedThreadPool(n):创建一个可重用固定线程数的线程池。
  • Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池。
  • Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

线程池的好处:

  • 提高响应速度(减少了创建新线程的时间)。
  • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)。
  • 便于线程管理:
    • corePoolSize:核心池大小。
    • maximumPoolSize:最大线程数。
    • keepAliveTime:线程没有任务时最多保持多长时间后会终止。

线程池创建线程方法:

  • 首先利用 Executors 工具类创建线程池
  • 调用 execute 或者 submit 方法执行操作。
  • 关闭连接池。
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
class Num implements Runnable {
private int num = 1;

@Override
public void run() {
int total = 0;
while (num <= 100) {
total += num;
num ++;
}

System.out.println("1 ~ 100 的和为:" + total);
}
}

public class ThreadPoolTest {
public static void main(String[] args) {
// 创建线程池
ExecutorService service = Executors.newFixedThreadPool(10);
// 执行
service.execute(new Num());
// 关闭连接池
service.shutdown();
}
}

Thread 类

构造器

Thread 类有以下几个构造器:

  • Thread():创建新的 Thread 对象。
  • Thread(String threadname):创建线程并指定线程实例名
  • Thread(Runnable target):指定创建线程的目标对象,target 为实现了 Runnable 接口中的 run 方法的对象
  • Thread(Runnable target, String threadname):创建新的 Thread 对象,指定实例名。

相关方法

  • String getName():返回线程的名称。
  • void setName(String name):设置线程名称。
  • static Thread currentThread():返回当前线程,即返回执行这条代码的线程。
  • static void yield():线程让步,让出 CPU。
    • 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程。
    • 若队列中没有同优先级的线程,忽略此方法。
  • join(): 当线程 A 调用其他 B 线程的 join 方法时,A 进入阻塞,直到被调用的 B 线程将插队直到执行完成。
  • static void sleep(long millis):令当前线程睡眠,可能会抛出 InterruptedException 异常。
  • stop():强制线程生命期结束,弃用不推荐。
  • boolean isAlive():判断线程是否存活。

优先级

线程的优先级如下:

  • MAX_PRIORITY:10
  • MIN_PRIORITY:1
  • NORM_PRIORITY:5,默认优先级

可以通过 Thread 类的 getPriority 和 setPriority 方法获取、设置优先级

线程创建时继承父线程的优先级

守护线程和用户线程

Java 中分为守护线程用户线程,一个线程默认是用户线程。通过 setDaemon 方法,可以将用户线程变为守护线程。

  • 当 JVM 中剩余运行的都是守护线程时,JVM 会退出。只要存在至少一个用户线程,JVM 就不会退出

  • 只能在 Thread 未开始运行之前设置 daemon 属性,如果 Thread 已经开始运行后再设置则会抛出 IllegalThreadStateException 异常。

当新建线程为用户线程时,主线程退出用户线程继续运行。当新建线程为守护线程时,主线程退出用户线程随之停止运行。

GC 线程就是一个守护线程,保持低优先级进行垃圾回收,不依赖系统资源,当所有用户线程退出之后,GC 线程也就没有什么用了,会随即退出。因为如果没有用户线程了,也就代表没有垃圾会继续产生,也就不需要 GC 线程了。

可以简单理解为守护线程为用户线程服务,当所有用户线程结束,也就不需要守护线程了

线程调度策略

Java 线程的调度策略为:

  • 同优先级线程组成先进先出队列,使用时间片进行调度。
  • 高优先级,使用优先调度的抢占式策略,高优先级的线程获得的执行机会更多。

注意:

  • 线程优先级会提示调度器优先调度该线程,它仅仅是一个提示,调度器可以忽略它。
  • 如果 CPU 比较忙,那么优先级高的线程会获得更多的时间片,但是 CPU 闲时,优先级几乎没作用。

线程同步和通信

Synchronized 同步机制

Java 对于多线程提供了 Synchronized 同步机制。分为:

  • 同步代码块,同步监视器(对象)就是锁。
    • 要求多个线程共用同一把锁
1
2
3
synchronized(同步监视器) {
// 需要被同步的代码
}
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
class Window implements Runnable {
private int ticket = 100;

@Override
public void run() {
while (true) {
synchronized(this) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + " 票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
}
}

public class ThreadTest {
public static void main(String[] args) {
Window window = new Window();
Thread t1 = new Thread(window, "窗口1");
Thread t2 = new Thread(window, "窗口2");
Thread t3 = new Thread(window, "窗口3");

t1.start();
t2.start();
t3.start();
}
}
  • 同步方法。对于同步方法而言:
    • 静态方法的锁:ClassName.class。
    • 非静态方法的锁:this。
1
2
3
public synchronized void show(String name) {
// ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Windows implements Runnable {

private int ticket = 100;

@Override
public void run() {
while (sale());
}

private synchronized boolean sale() {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + " 票号为:" + ticket);
ticket--;
return true;
}

return false;
}
}

改造单例模式 - 懒汉式

第一种方法是同步方法,此时的锁是 Singleton.class

1
2
3
4
5
6
7
8
9
10
11
12
13
class Singleton {
private Singleton() {}

private static Singleton instance;

public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}

return instance;
}
}

第二种方法是同步代码块,效率更高。当 instance 不为 null 时,不用进入同步代码块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Singleton {
private Singleton() {}

private static Singleton instance;

public static Singleton getInstance() {
if (instance == null) { // instance 不为 null 时,不用进入同步代码块
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}

return instance;
}
}

释放锁

在 Synchronized 机制中会释放锁的操作:

  • 当前线程的同步方法、同步代码块执行结束
  • 当前线程在同步代码块、同步方法中遇到 break、return 终止了代码块、方法的执行。
  • 出现了未处理的异常,导致异常结束。
  • 执行了线程对象的 wait 方法,当前线程暂停,会释放锁。

在Synchronized 机制中不会释放锁的操作:

  • 调用 Thread.sleep、Thread.yield 方法暂停当前线程的执行,不会释放锁。
  • 线程执行同步代码块时,其他线程调用了该线程的 suspend 方法将该线程挂起,该线程不会释放锁。

尽量避免使用 suspend 和 resume 来控制线程。

Lock 锁机制

java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,线程开始访问共享资源之前应先获得 Lock 对象

ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义。在实现线程安全的控制中,比较常用的是 ReentrantLock,可以显式加锁、释放锁

Synchronized vs. Lock:

  • Lock 是显式锁,synchronized 是隐式锁。
  • Lock 只有代码块锁,synchronized 有代码块锁和方法锁。
  • 使用 Lock 锁,JVM 将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Window implements Runnable {
private int ticket = 100;

private ReentrantLock lock = new ReentrantLock();

@Override
public void run() {
while (true) {
try {
lock.lock();
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + " 票号为:" + ticket);
ticket--;
} else {
break;
}
} finally {
lock.unlock();
}
}
}
}

线程通信 - 信号量机制

Java 中线程通信的信号量机制包含三个方法 wait、notify 和 notifyAll 方法,这三个方法只有在 synchronized 方法或者代码块中才能使用,否则会报 java.lang.IllegalMonitorStateException 异常。

这三个方法中的调用者必须是同步代码块或者同步方法中的同步监视器。

wait 方法

wait 方法使得当前线程进入阻塞(同时释放 synchronized 锁),直到另一线程对该对象发出 notify 或者 notifyAll 方法为止。

notify & notifyAll 方法

notify 方法用于唤醒正在排队等待同步资源的线程中,优先级最高者结束等待。

notifyAll 方法用于唤醒等待该对象监控权的所有线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Num implements Runnable {
private int num = 1;

@Override
public void run() {
while (true) {
synchronized (this) {
notify();
if (num <= 100) {
// 交替打印 1 ~ 100
System.out.println(Thread.currentThread().getName() + ":" + num);
num ++;
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
}