(六)Java多线程
1、Java中的多线程
1)通过 JDK 中的 java.lang.Thread 可以实现多线程。
2)Java 中多线程运行的程序可能是并发也可能是并行,取决于操作系统对线程的调度和计算机硬件资源( CPU 的个数和 CPU 的核数)。
3)CPU 资源比较充足时,多线程被分配到不同的 CPU 资源上,即并行;CPU 资源比较紧缺时,多线程可能被分配到同个 CPU 的某个核上去执行,即并发。
4)不管多线程是并行还是并发,都是为了提高程序的性能。
2、程序、进程和线程的联系与区别
关系:
1)一个程序至少一个进程,一个进程至少一个线程,进程中的多个线程是共享进程的资源
2)Java 中当我们启动 main 函数时候就启动了一个 JVM 的进程,而 main 函数所在线程就是这个进程中的一个线程,也叫做主线程
3)一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器,栈区域
区别:
1)本质:进程是操作系统资源分配的基本单位;线程是任务调度和执行的基本单位
2)内存分配:系统在运行的时候会为每个进程分配不同的内存空间,建立数据表来维护代码段、堆栈段和数据段;除了 CPU 外,系统不会为线程分配内存,线程所使用的资源来自其所属进程的资源
3)资源拥有:进程之间的资源是独立的,无法共享;同一进程的所有线程共享本进程的资源,如内存,CPU,IO 等
4)开销:每个进程都有独立的代码和数据空间,程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行程序计数器和栈,线程之间切换的开销小
5)通信:进程间 以IPC(管道,信号量,共享内存,消息队列,文件,套接字等)方式通信 ;同一个进程下,线程间可以共享全局变量、静态变量等数据进行通信,做到同步和互斥,以保证数据的一致性
6)调度和切换:线程上下文切换比进程上下文切换快,代价小
7)执行过程:每个进程都有一个程序执行的入口,顺序执行序列;线程不能够独立执行,必须依存在应用程序中,由程序的多线程控制机制控制
8)健壮性:每个进程之间的资源是独立的,当一个进程崩溃时,不会影响其他进程;同一进程的线程共享此线程的资源,当一个线程发生崩溃时,此进程也会发生崩溃,稳定性差,容易出现共享与资源竞争产生的各种问题,如死锁等
9)可维护性:线程的可维护性,代码也较难调试,bug 难排查
3、守护线程
1)守护线程时程序在运行的时候在后台提供一种通用服务的线程;所有用户线程停止,进程会停掉所有守护线程,退出程序
2)Java中把线程设置为守护线程的方法;在start线程之前用线程的setDaemon(true)方法
注意:
1)setDaemon(true) 必须在 start() 之前设置,否则会抛出IllegalThreadStateException异常,该线程仍默认为用户线程,继续执行
2)守护线程创建的线程也是守护线程
3)守护线程不应该访问、写入持久化资源,如文件、数据库,因为它会在任何时间被停止,导致资源未释放、数据写入中断等问题
4、创建、启动线程的4种方法
1、重写Thread类的run()方法
1)new Thread对象匿名重写run()方法
2)继承Thread类,重写run()方法
2、实现Runnable接口,重写run()方法
1)new Runnable对象,匿名重写run()方法
2)实现Runnable接口,重写run()方法
3、实现Callable接口,使用Future Task类创建线程
4、使用线程池创建、启动线程
5、线程三个特点
原子性、可见性、有序性
6、线程的生命周期及转换
1)NEW(初始化状态)
2)RUNNABLE(可运行 / 运行状态)
3)BLOCKED(阻塞状态)
4)WAITING(无限时等待)
5)TIMED_WAITING(有限时等待)
6)TERMINATED(终止状态) 在操作系统层面,Java 线程中的 BLOCKED、WAITING、TIMED_WAITING 是一种状态(休眠状态)。即只要 Java 线程处于这三种状态之一,就永远没有 CPU 的使用权。
1. NEW 到 RUNNABLE 状态
Java 刚创建出来的 Thread 对象就是 NEW 状态,不会被操作系统调度执行。从 NEW 状态转变到 RUNNABLE 状态调用线程对象的 start() 方法就可以了。
2. RUNNABLE 与 BLOCKED 的状态转变
1)synchronized 修饰的方法、代码块同一时刻只允许一个线程执行,其他线程只能等待,等待的线程会从 RUNNABLE 转变到 BLOCKED 状态。
2)当等待的线程获得 synchronized 隐式锁时,就又会从 BLOCKED 转变到 RUNNABLE 状态。
3)在操作系统层面,线程是会转变到休眠状态的,但是在 JVM 层面,Java 线程的状态不会发生变化,即 Java 线程的状态会保持 RUNNABLE 状态。JVM 层面并不关心操作系统调度相关的状态,因为在 JVM 看来,等待 CPU 使用权(操作系统层面处于可执行状态)与等待 I/O(操作系统层面处于休眠状态)没有区别,都是在等待某个资源,都归入了 RUNNABLE 状态。
4)Java 在调用阻塞式 API 时,线程会阻塞,指的是操作系统线程的状态,并不是 Java 线程的状态。
3. RUNNABLE 与 WAITING 的状态转变
1)获得 synchronized 隐式锁的线程,调用无参数的 Object.wait() 方法,状态会从 RUNNABLE 转变到 WAITING;调用 Object.notify()、Object.notifyAll() 方法,线程可能从 WAITING 转变到 RUNNABLE 状态。
2)调用无参数的 Thread.join() 方法。join() 是一种线程同步方法,如有一线程对象 Thread t,当调用 t.join() 的时候,执行代码的线程的状态会从 RUNNABLE 转变到 WAITING,等待 thread t 执行完。当线程 t 执行完,等待它的线程会从 WAITING 状态转变到 RUNNABLE 状态。
3)调用 LockSupport.park() 方法,线程的状态会从 RUNNABLE 转变到 WAITING;调用 LockSupport.unpark(Thread thread) 可唤醒目标线程,目标线程的状态又会从 WAITING 转变为 RUNNABLE 状态。
4. RUNNABLE 与 TIMED_WAITING 的状态转变
1)Thread.sleep(long millis)
2)Object.wait(long timeout)
3)Thread.join(long millis)
4)LockSupport.parkNanos(Object blocker, long deadline)
5)LockSupport.parkUntil(long deadline)
TIMED_WAITING 和 WAITING 状态的区别,仅仅是调用的是超时参数的方法。
5. RUNNABLE 到 TERMINATED 状态
1)线程执行完 run() 方法后,会自动转变到 TERMINATED 状态
2)执行 run() 方法时异常抛出,也会导致线程终止
3)Thread类的 stop() 方法已经不建议使用
7、线程池(一种生产者---消费者模式)
1)定义:线程池就是创建若干个可执行的线程放入一个池中,有任务需要处理时,会提交到线程池中的任务队列,处理完之后线程并不会被销毁,而是仍然在线程池中等待下一个任务
2)方法:
1)poolSize 是线程池工作线程的个数
2)BlockingQueue taskQueue 是用有界阻塞队列存储 Runnable 任务
3)execute(Runnable task) 提交任务
4)线程池对象被创建,就自动启动 poolSize 个工作线程
5)工作线程一直从任务队列 taskQueue 中取任务
6)corePoolSize:线程池保有的最小线程数。
7)maximumPoolSize:线程池创建的最大线程数。
8)keepAliveTime:上面提到项目根据忙闲来增减人员,那在编程世界里,如何定义忙和闲呢?很简单,一个线程如果在一段时间内,都没有执行任务,说明很闲,keepAliveTime 和 unit 就是用来定义这个“一段时间”的参数。也就是说,如果一个线程空闲了keepAliveTime & unit这么久,而且线程池的线程数大于 corePoolSize ,那么这个空闲的线程就要被回收了。
9)unit:keepAliveTime 的时间单位
(1)workQueue:任务队列
(2)threadFactory:线程工厂对象,可以自定义如何创建线程,如给线程指定name。
(3)handler:自定义任务的拒绝策略。线程池中所有线程都在忙碌,且任务队列已满,线程池就会拒绝接收再提交的任务。handler 就是拒绝策略,包括 4 种(即RejectedExecutionHandler 接口的 4个实现类)。
(4)AbortPolicy:默认的拒绝策略,throws RejectedExecutionException
(5)CallerRunsPolicy:提交任务的线程自己去执行该任务
(6)DiscardPolicy:直接丢弃任务,不抛出任何异常
(7)DiscardOldestPolicy:丢弃最老的任务,加入新的任务
8、线程池的状态及转换
1)RUNNING:线程池一旦被创建,就处于 RUNNING 状态,任务数为 0,能够接收新任务,对已排队的任务进行处理。
2)SHUTDOWN:不接收新任务,但能处理已排队的任务。调用线程池的 shutdown() 方法,线程池由 RUNNING 转变为 SHUTDOWN 状态。
3)STOP:不接收新任务,不处理已排队的任务,并且会中断正在处理的任务。调用线程池的 shutdownNow() 方法,线程池由(RUNNING 或 SHUTDOWN ) 转变为 STOP 状态。
4)TIDYING:
(1)SHUTDOWN 状态下,任务数为 0, 其他所有任务已终止,线程池会变为 TIDYING 状态,会执行 terminated() 方法。线程池中的 terminated() 方法是空实现,可以重写该方法进行相应的处理。
(2)线程池在 SHUTDOWN 状态,任务队列为空且执行中任务为空,线程池就会由 SHUTDOWN 转变为 TIDYING 状态。
(3)线程池在 STOP 状态,线程池中执行中任务为空时,就会由 STOP 转变为 TIDYING 状态。
5)TERMINATED:线程池彻底终止。线程池在 TIDYING 状态执行完 terminated() 方法就会由 TIDYING 转变为 TERMINATED 状态。
状态转换如图
9、停止线程池的方法
shutdown()、shutdownNow()、shutdown() + awaitTermination(long timeout, TimeUnit unit) 方法。
10、synchronized关键字的作用
Java 中关键字 synchronized 表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程。
作用:
1)确保线程互斥地访问同步代码
2)保证共享变量的修改能够及时可见
3)有效解决重排序问题
用法:
1)修饰普通方法
2)修饰静态方法
3)指定对象,修饰代码块
特点:
1)阻塞未获取到锁、竞争同一个对象锁的线程
2)获取锁无法设置超时
3)无法实现公平锁
4)控制等待和唤醒需要结合加锁对象的 wait() 和 notify()、notifyAll()
5)锁的功能是 JVM 层面实现的
6)在加锁代码块执行完或者出现异常,自动释放锁
原理:
1)同步代码块是通过 monitorenter 和 monitorexit 指令获取线程的执行权
2)同步方法通过加 ACC_SYNCHRONIZED 标识实现线程的执行权的控制
11、synchronized和Lock之间的区别
1)实现层面不同。synchronized是Java关键字,JVM层面实现加锁和释放锁;Lock是一个接口,在代码层面实现加锁和释放锁
2)是否自动释放锁。synchronized在线程代码执行完成或出现异常时自动释放锁;Lock不会自动释放锁,需要在finally{}代码块显式的释放锁
3)是否一直等待。synchronized会导致线程拿不到锁一直等待;Lock可以设置尝试获取锁或者获取锁失败一定时间超时
4)获取锁成功是否可知。synchronized无法得知是否获取锁成功;Lock可以通过tryLock获得加锁是否成功
5)功能复杂性。synchronized加锁可重入、不可中断、非公平;Lock可重入、可判断、可公平和不公平、细分读写锁提高效率
12、synchronized锁升级
1)在锁对象的对象头里面有一个 threadid 字段,未访问时 threadid 为空
2)第一次访问 jvm 让其持有偏向锁,并将 threadid 设置为其线程 id
3)再次访问时会先判断 threadid 是否与其线程 id 一致。如果一致则可以直接使用此对象;如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁
4)执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁
13、死锁
线程死锁是指由于两个或者多个线程互相持有所需要的资源,导致这些线程一直处于等待其他线程释放资源的状态,无法继续执行,如果线程都不主动释放所占有的资源,将产生死锁。
当线程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。
产生原因:
1)持有系统不可剥夺资源,去竞争其他已被占用的系统不可剥夺资源,形成程序僵死的竞争关系。
2)持有资源的锁,去竞争锁已被占用的其他资源,形成程序僵死的争关系。
3)信号量使用不当
死锁发生的条件
1)互斥,共享资源只能被一个线程占用
2)占有且等待,线程t1已经取得共享资源s1,尝试获取共享资源s2的时候,不释放共享资源s1
3)不可抢占,其他线程不能强行抢占线程t1占有的资源s1
4)循环等待,线程t1等待线程t2占有的资源,线程t2等待线程t1占有的资源
避免死锁的方法
1)互斥条件是不能破坏的
2)一次性申请所有的资源,破坏“占有且等待”条件
3)占有部分资源的线程进一步申请其他资源时,如果申请不到,主动释放它占有的资源,破坏“不可抢占”条件
4)按序申请资源,破坏“循环等待”条件
14、Java内存模型
1)Java内存模型时JVM的一种规范
2)定义了共享内存在多线程程序中读写操作行为的规范
3)屏蔽了各种硬件和操作系统的访问差异,保证了Java程序在各种平台下对内存的访问效果一致
4)解决并发问题采用的方式:限制处理器优化和使用内存屏障
5)增强了三个同步原语(synchronized、volatile、final)的内存语义
6)定义了happens-before规则
15、sleep()方法和wait()方法的区别
1)sleep() 是 Thread 类的静态本地方法;wait() 是Object类的成员本地方法
2)sleep() 方法可以在任何地方使用;wait() 方法则只能在同步方法或同步代码块中使用,否则抛出异常Exception in thread "Thread-0" java.lang.IllegalMonitorStateException
3)sleep() 会休眠当前线程指定时间,释放 CPU 资源,不释放对象锁,休眠时间到自动苏醒继续执行;wait() 方法放弃持有的对象锁,进入等待队列,当该对象被调用 notify() / notifyAll() 方法后才有机会竞争获取对象锁,进入运行状态
4)JDK1.8 sleep()、 wait() 均需要捕获 InterruptedException 异常
16、Runnable接口和Callable接口的区别
1)Runnable接口run()方法无返回值;Callable接口call()方法有返回值,支持泛型
2)Runnable接口run()方法只能抛出运行时异常,且无法捕获处理;Callable接口call()方法可以获取异常信息
17、synchronized和volatile的区别
作用:
1)synchronized 表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程。
2)volatile 表示变量在 CPU 的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的可见性;禁止指令重排序。
区别:
1)synchronized 可以作用于变量、方法、对象;volatile 只能作用于变量。
2)synchronized 可以保证线程间的有序性(个人猜测是无法保证线程内的有序性,即线程内的代码可能被 CPU 指令重排序)、原子性和可见性;volatile 只保证了可见性和有序性,无法保证原子性。
3)synchronized 线程阻塞,volatile 线程不阻塞。
4)volatile 本质是告诉 jvm 当前变量在寄存器中的值是不安全的需要从内存中读取;sychronized 则是锁定当前变量,只有当前线程可以访问到该变量其他线程被阻塞。
5)volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。
18、乐观锁和悲观锁以及优缺点
悲观锁(Pessimistic Lock):线程每次在处理共享数据时都会上锁,其他线程想处理数据就会阻塞直到获得锁。
乐观锁(Optimistic Lock):线程每次在处理共享数据时都不会上锁,在更新时会通过数据的版本号等机制判断其他线程有没有更新数据。乐观锁适合读多写少的应用场景
两种锁各有优缺点:
1)乐观锁适用于读多写少的场景,可以省去频繁加锁、释放锁的开销,提高吞吐量
2)在写比较多的场景下,乐观锁会因为版本不一致,不断重试更新,产生大量自旋,消耗 CPU,影响性能。这种情况下,适合悲观锁
19、使用对象的wait()方法注意事项
1)wait() 方法是线程间通信的方法之一
2)必须在 synchronized 方法或 synchronized 修饰的代码块中使用,否则会抛出 IllegalMonitorStateException
3)只能在加锁的对象调用 wait() 方法
4)加锁的对象调用 wait() 方法后,线程进入等待状态,直到在加锁的对象上调用 notify() 或者 notifyAll() 方法来唤醒之前进入等待的线程
20、sleep()方法和yield()方法的区别
1)sleep() 方法给其他线程运行机会时不考虑线程的优先级;yield() 方法只会给相同优先级或更高优先级的线程运行的机会
2)线程执行 sleep() 方法后进入超时等待状态;线程执行 yield() 方法转入就绪状态,可能马上又得到执行
3)sleep() 方法声明抛出 InterruptedException;yield() 方法没有声明抛出异常
4)sleep() 方法需要指定时间参数;yield() 方法出让 CPU 的执行权时间由 JVM 控制
21、线程的run()方法和start()方法的区别
1)启动一个线程需要调用 Thread 对象的 start() 方法
2)调用线程的 start() 方法后,线程处于可运行状态,此时它可以由 JVM 调度并执行,这并不意味着线程就会立即运行
3)run() 方法是线程运行时由 JVM 回调的方法,无需手动写代码调用
4)直接调用线程的 run() 方法,相当于在调用线程里继续调用方法,并未启动一个新的线程
22、synchronized同步锁的理解
1)每个 Java 对象都有一个内置锁
2)线程运行到非静态的 synchronized 同步方法上时,自动获得实例对象的锁
3)持有对象锁的线程才能运行 synchronized 同步方法或代码块
4)一个对象只有一个锁
5)一个线程获得该锁,其他线程就无法获得锁,直到第一个线程释放锁。任何其他线程都不能进入该对象上的 synchronized 方法或代码块,直到该锁被释放。
6)释放锁是指持锁线程退出了 synchronized 同步方法或代码块
7)类可以同时拥有同步和非同步方法
8)只有同步方法,没有同步变量和类
9)在加锁时,要明确需要加锁的对象
10)线程可以获得多个锁
11)同步应该尽量缩小范围
23、Java中实现线程通信的方式
1)对象的 wait(long timeout)、wait(long timeout, int nanos)、wait() 方法,组合对象的 notify()、notifyAll()
2)显示锁:Lock.newCondition()、Condition await 系列方法、Condition signal()、signalAll()
3)信号量:Semaphore acquire 系列方法、release()系列方法