多线程基础

多线程概述

为什么使用多线程

  1. 摩尔定律失效(硬件方面):

    • 集成电路上可以容纳的晶体管数目在大约每经过18个月便会增加一倍,可是从2003年开始CPU主频已经不再翻倍,而是采用多核而不是更快的主频
    • 在主频不再提高且核数不断增加的情况下,要想让程序更快就要用到并行或并发编程
  2. 高并发系统,异步+回调的生产需求(软件方面)

进程、线程、管程(monitor 监视器)

比较官方解释:进程是系统资源分配的单位,线程是 cpu 调度的单位。

线程就是程序执行的一条路径,一个进程中可以包含多条线程,多线程并发执行可以提高程序的效率,可以同时完成多项工作。

举例 : 进程是一个工厂,占用着一定的空间资源,在里边有很多条生产线,生产线上有很多工人。生产线可以看作是 cpu 核数,工人可以看作是线程。

管程:Monitor(监视器),也就是我们平时所说的锁

  1. Monitor 其实是一种同步机制,它的义务是保证(在同一时间)只有一个线程可以访问被保护的数据和代码
  2. JVM 中同步时基于进入和退出的监视器对象(Monitor,管程),每个对象实例都有一个 Monitor 对象。
  3. Monitor 对象和 JVM 对象一起销毁,底层由C来实现

多线程并行和并发的区别

  1. 并行就是两个任务同时运行,就是甲任务进行的同时,乙任务也在进行(需要多核CPU);
  2. 并发是指两个任务都请求运行,而处理器只能接收一个任务,就是把这两个任务安排轮流进行,由于时间间隔较短,使人感觉两个任务都在运行;

简单说:并行就是在某一个时间点上,cpu 在同时执行两个任务。并发就是在某一个时间段内,cpu 在有序执行两个任务。

wait | sleep的区别

功能都是当前线程暂停,有什么区别?

  1. wait 放开手去睡,放开手里的锁 ; wait是 Object 类中的方法
  2. sleep 握紧手去睡,醒了手里还有锁 ; sleep是 Thread 中的方法

synchronized 和 lock的区别

  1. 原始构成
    1. synchronized 是关键字属于 JVM 层面, monitor 对象,每个 java 对象都自带了一个 monitor ,需要拿到 monitor 对象才能做事情。monitorenter (底层是通过 monitor 对象来完成,其实 wait/notify 等方法也依赖 monitor 对象,只能在同步块或方法中才能调用 wait/notify 等方法)进入 ,monitorexit:退出;
    2. lock 是 api 层面的锁,主要使用 ReentrantLock 实现
  2. 使用方法
    1. synchronized 不需要用户手动释放锁,当 synchronized 代码完成后系统会自动让线程释放对锁的占用
    2. ReentrantLock 则需要用户手动释放锁若没有主动释放锁,就有可能会导致死锁的现象
    3. 等待是否可中断?
      1. synchronized 不可中断,除非抛出异常或者正常运行完成
      2. ReentrantLock 可中断 (设置超时时间tryLock(long timeout,TimeUnit unit),调用 interrupt 方法中断)
    4. 加锁是否公平
      1. synchronized 非公平锁
      2. ReentrantLock 两者都可以,默认是非公平锁,构造方法可以传入 boolean 值, true 为公平锁, false 为非公平锁
    5. 锁绑定多个Condition
      1. synchronized 没有
      2. ReentrantLock 用来实现分组唤醒需要唤醒线程们,可以精确唤醒, 而不是像 synchronized 要么随机唤醒一个,要么多个

线程实现方式

  • thread 实现方式一: 继承 Thread
  • thread 实现方式二: 实现 Runnable
  • thread 实现方式三: 实现 Callable
  • 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
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
package tech.chen.juccode.a01;

import java.util.concurrent.*;

/**
* @Date 2022/8/19 18:53
* @Author c-z-k
* thread 实现方式一: 继承 Thread
* thread 实现方式二: 实现 Runnable
* thread 实现方式三: 实现 Callable
* thread 实现方式四: 线程池
*/
public class ThreadImpl {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// ThreadOne threadOne = new ThreadOne();
// threadOne.start();//当前线程 :Thread-0

// Thread thread = new Thread(new ThreadTwo(),"implRunable");
// thread.start();//当前线程 :implRunable

// FutureTask<String> futureTask = new FutureTask<>(new ThreadThree());
// Thread thread = new Thread(futureTask,"implCallable");
// thread.start();//当前线程 :implCallable
// System.out.println("futureTask.get() = " + futureTask.get());//futureTask.get() = hello

// ExecutorService executorService = Executors.newFixedThreadPool(1);
// executorService.submit(()->{
// System.out.println("当前线程 :"+Thread.currentThread().getName());//当前线程 :pool-1-thread-1
// });
}
static class ThreadOne extends Thread{
@Override
public void run() {
System.out.println("当前线程 :"+Thread.currentThread().getName());
}
}

static class ThreadTwo implements Runnable{

@Override
public void run() {
System.out.println("当前线程 :"+Thread.currentThread().getName());

}
}
static class ThreadThree implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("当前线程 :"+Thread.currentThread().getName());
return "hello";
}
}
}

常用API

线程名称

设置

1
2
1. Thread thread = new Thread(new ThreadTwo(),"implRunable");//构造
2. thread.setName("implRunable");//setter

获取

1
thread.getName();

线程优先级

线程有两种调度模型:

  • 分时调度模式:所有线程轮流使用 CPU 的使用权,平均分配每个线程占有CPU的时间片
  • 抢占式调度模型:优先让优先级高的线程使用 CPU ,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一些 [ Java使用的是抢占式调度模型 ]

设置和获取线程优先级:

1
2
thread.setPriority(int newPriority);//设置
thread.getPriority();//获取

线程默认优先级是5;线程优先级范围是:1-10; 线程优先级高仅仅表示线程获取的 CPU 时间的几率高,但是要在次数比较多,或者多次运行的时候才能看到效果。

线程控制

  1. sleep(long millis) : 使当前正在执行的线程停留(暂停执行)指定的毫秒数 (休眠线程)
  2. join() : 当前线程暂停,等待指定的线程执行结束后,当前线程再继续 (相当于插队加入),void join(int millis):可以等待指定的毫秒之后继续 (相当于插队,有固定的时间)
  3. yield() : 让出 cpu 的执行权(礼让线程)
  4. setDaemon(boolean on) : 将此线程标记为守护线程,当运行的线程都是守护线程时 , Java虚拟机将退出(守护线程)
    1. 守护线程是区别于用户线程,用户线程即我们手动创建的线程,而守护线程是程序运行的时候在后台提供一种通用服务的线程。垃圾回收线程就是典型的守护线程
    2. 守护线程拥有自动结束自己生命周期的特性,非守护线程却没有。如果垃圾回收线程是非守护线程,当JVM 要退出时,由于垃圾回收线程还在运行着,导致程序无法退出,这就很尴尬。这就是为什么垃圾回收线程需要是守护线程
    3. t1.setDaemon(true) 一定要在start()方法之前使用

线程的生命周期

  • 新建 : 就是刚使用new方法,new出来的线程

  • 就绪 : 就是调用的线程的 start() 方法后,这时候线程处于等待 CPU 分配资源阶段,谁先抢的 CPU 资源,谁开始执行

  • 运行 : 当就绪的线程被调度并获得 CPU 资源时,便进入运行状态, run 方法定义了线程的操作和功能

  • 阻塞 : 在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态。比如 sleep()、wait() 之后线程就处于了阻塞状态,这个时候需要其他机制将处于阻塞状态的线程唤醒,比如调用 notify 或者 notifyAll() 方法。唤醒的线程不会立刻执行 run 方法,它们要再次等待 CPU 分配资源进入运行状态

  • 销毁 : 如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源

1660908336363

线程同步

synchronized

为什么出现问题?(这也是我们判断多线程程序是否会有数据安全问题的标准)

  1. 是否有多线程坏境
  2. 是否有共享数据
  3. 是否有多条语句操作共享数据

如何解决多线程安全问题?

  1. 基本思想 : 让程序没有安全问题的坏境
  2. 把多条语句操作的共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可

怎么锁起来呢?

synchronized(任意对象){} : 相当于给代码加锁了,任意对象就可以看成是一把锁

同步的好处和弊端

  1. 好处 : 解决了多线程的数据安全问题
  2. 弊端 : 当线程很多时,因为每个线程都会判断同步上的锁,这是很浪费资源的,无形中会降低程序的运行效率

同步方法

同步方法:就是把synchronized 关键字加到方法上

同步方法的锁对象是什么呢? this

格式 : 修饰符 synchronized 返回值类型 方法名(方法参数){ }

同步静态方法:就是把synchronized关键字加到静态方法上

格式:修饰符 static synchronized 返回值类型 方法名(方法参数){ }

同步静态方法的锁对象是什么呢?类名.class