10.线程间的等待唤醒机制
- void wait () :在其他线程调用此对象的 notify () 方法或 notifyAll () 方法前,导致当前线程等待。
- void wait (long timeout) :在其他线程调用此对象的 notify () 方法或 notifyAll () 方法,或者超过指定的时间量前,导致当前线程等待。
- void notify () :唤醒在此对象监视器上等待的单个线程,随机唤醒。
- void notifyAll () :唤醒在此对象监视器上等待的所有线程。
- 整个对待唤醒机制的过程注意事项:
- 1.notify()需要在同步方法或同步块中调用,即在调用前,线程必须获得该对象的对象级别锁;
- 2.在调用wait()方法之前,线程必须获得该对象的对象级别锁,即只能在同步方法或同步块中调用wait方法;
- 3.在某个线程方法中对wait()和notify()的调用必须指定一个Object对象,而且该线程必须拥有该Object对象的monitor;
- 4.获取对象monitor最简单的办法就是,在对象上使用synchronized关键字。当调用wait()方法以后,该线程会释放掉对象锁,并进入sleep状态;
- 5.在其它线程调用notify()方法时,必须使用同一个Object对象,notify()方法调用成功后,所在这个对象上的相应的等侍线程将被唤醒。
对于被一个对象锁定的多个方法,在调用notify()方法时将会任选其中一个进行唤醒,而notifyAll()则是将其所有等待线程唤醒。
wait()和sleep()的区别
- 最大本质的区别是,Sleep()不释放同步锁,Wait()释放同步锁;
- 用法的上的不同:Sleep(milliseconds)可以用时间指定来使他自动醒过来,如果时间不到你只能调用Interreput()来强行打断;Wait()可以用Notify()直接唤起;
- 这两个方法来自不同的类分别是Thread和Object;
- 最主要是Sleep方法没有释放锁,而Wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
11.内存可见性问题 volatile
-
Java内存模型
- Java内存模型规定了所有的变量都存储在主内存中。每条线程中还有自己的工作内存,线程的工作内存中保存了被该线程所使用到的变量(这些变量是从主内存中拷贝而来)。线程对变量的所有操作(读取,赋值)都必须在工作内存中进行。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
- Java内存模型规定了所有的变量都存储在主内存中。每条线程中还有自己的工作内存,线程的工作内存中保存了被该线程所使用到的变量(这些变量是从主内存中拷贝而来)。线程对变量的所有操作(读取,赋值)都必须在工作内存中进行。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
-
Java中的可见性
- 对于可见性,Java提供了volatile关键字来保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
-
volatile 关键字:当多个线程进行操作共享数据时,可以保证内存中的数据可见。相较于 synchronized 是一种较为轻量级的同步策略。
-
volatile 变量:用来确保将变量的更新操作通知到其他线程。可以将 volatile 看做一个轻量级的锁,但是又与锁有些不同:
- 对于多线程,不是一种互斥关系
- 不能保证变量状态的“原子性操作”
12.CAS 算法
-
CAS(Compare-And-Swap) 算法
- CAS 算法是一种硬件对并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问;
- CAS 是一种无锁的非阻塞算法的实现;
- CAS 包含了 3 个操作数:
- 需要读写的内存值 V
- 进行比较的值 A
- 拟写入的新值 B
-
当且仅当 V 的值等于 A 时, CAS 通过原子方式用新值 B 来更新 V 的值,否则不会执行任何操作。jdk5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁。JDK 5之前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁。
13.线程的状态转换图及常见执行情况
- 线程的状态转换图及常见执行情况:新建 , 就绪 , 运行 , 冻结 , 死亡
- 新建:线程被创建出来
- 就绪:具有CPU的执行资格,但是不具有CPU的执行权
- 运行:具有CPU的执行资格,也具有CPU的执行权
- 阻塞:不具有CPU的执行资格,也不具有CPU的执行权
- 死亡:不具有CPU的执行资格,也不具有CPU的执行权
线程几个状态
Java中线程的状态分为6种:
- 初始(New)
- 新创建了一个线程对象,但还没有调用start()方法。
- 可运行(Runnable)
- 调用线程的start()方法,此线程进入就绪状态。就绪状态只是说你资格运行,调度程序没有给你CPU资源,你就永远是就绪状态;
- 当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态;
- 当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态;
- 锁池里的线程拿到对象锁后,进入就绪状态。
- 运行中(Running)
- 就绪状态的线程在获得CPU时间片后变为运行中状态(running)。这也是线程进入运行状态的唯一的一种方式。
- 阻塞(Blocked)
- 阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。
- 等待(Waiting) 和超时等待(Timed_Waiting)
- 处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒(通知或中断),否则会处于无限期等待的状态;
- 处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。
- 终止(Terminated):
- 当线程正常运行结束或者被异常中断后就会被终止。线程一旦终止了,就不能复生。
- 注意事项:
- 调用 obj.wait 的线程需要先获取 obj 的 monitor,wait会释放 obj 的 monitor 并进入等待态。所以 wait()/notify() 都要与 synchronized 联用。
- 线程从阻塞/等待状态 到 可运行状态都涉及到同步队列跟等待队列的。
阻塞与等待的区别
-
阻塞:
-
当一个线程试图获取对象锁(非JUC库中的锁,即synchronized),而该锁被其他线程持有,则该线程进入阻塞状态。它的特点是使用简单,由JVM调度器来决定唤醒自己,而不需要由另一个线程来显式唤醒自己,不响应中断。
-
等待:
- 当一个线程等待另一个线程通知调度器一个条件时,该线程进入等待状态。它的特点是需要等待另一个线程显式地唤醒自己,实现灵活,语义更丰富,可响应中断。例如调用:Object.wait()、**Thread.join()**以及等待 Lock 或 Condition。
-
注意事项:
- 虽然 synchronized 和 JUC 里的 Lock 都实现锁的功能,但线程进入的状态是不一样的。synchronized 会让线程进入阻塞态,而 JUC 里的 Lock是用park()/unpark() 来实现阻塞/唤醒 的,会让线程进入等待状态。虽然等锁时进入的状态不一样,但被唤醒后又都进入Runnable状态,从行为效果来看又是一样的。
14.线程池的概述和使用
-
为什么要使用线程池?
- 在java中,如果每个请求到达就创建一个新线程,开销是相当大的。在实际使用中,服务器在创建和销毁线程上花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多。除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。如果在一个jvm里创建太多的线程,可能会使系统由于过度消耗内存或“切换过度”而导致系统资源不足。为了防止资源不足,服务器应用程序需要采取一些办法来限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务,这就是“池化资源”技术产生的原因。
- 线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重复使用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使用应用程序响应更快。另外,通过适当的调整线程中的线程数目可以防止出现资源不足的情况。
-
线程池的组成部分
-
一个比较简单的线程池至少应包含线程池管理器、工作线程、任务列队、任务接口等部分。其中线程池管理器的作用是创建、销毁并管理线程池,将工作线程放入线程池中;工作线程是一个可以循环执行任务的线程,在没有任务是进行等待;任务列队的作用是提供一种缓冲机制,将没有处理的任务放在任务列队中;任务接口是每个任务必须实现的接口,主要用来规定任务的入口、任务执行完后的收尾工作、任务的执行状态等,工作线程通过该接口调度任务的执行。
-
线程池管理器至少有下列功能:创建线程池,销毁线程池,添加新任务。
-
工作线程是一个可以循环执行任务的线程,在没有任务时将等待。
-
任务接口是为所有任务提供统一的接口,以便工作线程处理。任务接口主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等。
-
线程池适合应用的场合
-
当一个服务器接受到大量短小线程的请求时,使用线程池技术是非常合适的,它可以大大减少线程的创建和销毁次数,提高服务器的工作效率。但是线程要求的运动时间比较长,即线程的运行时间比较长。
必须手动实现自己的线程池,从JDK5开始,Java内置支持线程池 -
内置线程池的使用概述
JDK5新增了一个Executors工厂类来产生线程池,有如下几个方法-
public static ExecutorService newCachedThreadPool()
根据任务的数量来创建线程对应的线程个数 -
public static ExecutorService newFixedThreadPool(int nThreads)
固定初始化几个线程 -
public static ExecutorService newSingleThreadExecutor()
初始化一个线程的线程池
-
-
这些方法的返回值是ExecutorService对象,该对象表示一个线程池,可以执行Runnable对象或者Callable对象代表的线程。它提供了如下方法
- Future<?> submit(Runnable task)
- Future submit(Callable task)
- 使用步骤:
- 1.创建线程池对象;
- 2.创建Runnable实例;
- 3.提交Runnable实例;
- 4.关闭线程池
15.定时器的概述和使用
- 定时器的概述:定时器是一个应用十分广泛的线程工具,可用于调度多个定时任务以后台线程的方式执行。在Java中,可以通过Timer和TimerTask类来实现定义调度的功能。
- Timer和TimerTask
- Timer:
- public Timer()
- public void schedule(TimerTask task, long delay)
- public void schedule(TimerTask task,long delay,long period)
- public void schedule(TimerTask task, Date time)
- public void schedule(TimerTask task, Date firstTime, long period)
- TimerTask:
- public abstract void run()
- public boolean cancel()
- Timer:
- Timer和TimerTask
- 开发中,Quartz是一个完全由java编写的开源调度框架。
练习:定时删除指定的带内容目录
代码如下:
public class Test {
public static void main(String args[])throws ParseException {
Timer timer = new Timer();
DeleteFolder deleteFolder = new DeleteFolder();
String dateStr = "2021-06-19 14:26:00";
Date date = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(dateStr);
timer.schedule(deleteFolder, date, 1000);
}
}
class DeleteFolder extends TimerTask {
@Override
public void run() {
File srcFolder = new File("需要删除的文件夹根路径");
deleteFolder(srcFolder);
}
private void deleteFolder(File srcFolder) {
if(srcFolder.isFile()){
srcFolder.delete();
}else{
File[] files = srcFolder.listFiles();
for (File file : files) {
File file1 = new File(file.getAbsolutePath());
deleteFolder(file1);
}
srcFolder.delete();
}
}
}
练习:交替打印ABC
代码如下:
public class Test {
public static void main(String[] args) {
MyObject obj = new MyObject();
AThread th1 = new AThread(obj);
BThread th2 = new BThread(obj);
CThread th3 = new CThread(obj);
th2.start();
th1.start();
th3.start();
}
}
class MyObject {
//定义一个标记
// boolean flag=false;
public int flag = 1;
}
class AThread extends Thread {
private MyObject obj;
public AThread(MyObject obj) {
this.obj = obj;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
synchronized (obj) {
while (obj.flag != 1) {
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("A");
obj.flag = 2;
obj.notifyAll();
}
}
}
}
class BThread extends Thread {
private MyObject obj;
public BThread(MyObject obj) {
this.obj = obj;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
synchronized (obj) {
while (obj.flag != 2) {
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("B");
obj.flag = 3;
obj.notifyAll();
}
}
}
}
class CThread extends Thread {
private MyObject obj;
public CThread(MyObject obj) {
this.obj = obj;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
synchronized (obj) {
while(obj.flag != 3) {
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("C");
obj.flag = 1;
obj.notifyAll();
}
}
}
}
多线程常见的面试题
-
多线程有几种实现方案?分别是哪几种? 三种
- 继承Thread类
- 实现Runnable接口
- 扩展一种 实现Callable接口。这个得和线程池结合
-
同步有几种方式,分别是什么?两种
- 同步代码块
- 同步方法
-
启动一个线程是run()还是start()?它们的区别?
- 启动一个线程是调用start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM调度并执行。这并不意味着线程就会立即运行。run()方法可以产生必须退出的标志来停止一个线程。
- 区别:
- run():封装了被线程执行的代码,直接调用仅仅是普通方法的调用;
- start():启动线程,并有JVM自动调用run()方法。