一.Java多线程基础
1.进程和线程的区别
程序是由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU中,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的。
当一个程序被运行,从磁盘加载这个程序的代码到内存中,这时就开启了一个进程。
什么叫做进程:当一个程序被允许,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。(比如:打开QQ软件,QQ运行后就是一个进程)
什么叫做线程:线程就是一个指令流,将指令流中的一条条指令以一定顺序交给CPU执行。
进程和线程的区别:
- 进程是正在运行程序的实例,进程中包含了多个线程,每个线程执行不同的任务。
- 不同的进程使用不同的内存空间(QQ进程与微信进程使用的内存空间是不同的),当前进程下的所有线程可以共享内存空间。
- 线程比进程更轻量,线程上下文切换成本一般比进程低(上下文切换指的是一个线程切换到另一个线程)
2.并行和并发的区别
两种情况:单核CPU和多核CPU
1.单核CPU
- 单核CPU下线程实际还是串行执行的
- 操作系统中有一个组件叫做任务调度器,将cpu的时间片(windows下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于cpu在线程间(时间片很短)切换的速度非常快,给人的感觉是同时运行的 。
- 总结为一句话就是: 微观串行,宏观并行
- 一般会将这种线程轮流使用CPU的做法称为并发(concurrent)
2.多核CPU
每个核(core)都可以调度运行线程,这时候线程可以是并行的。
3.并行和并发有什么区别?
并发(concurrent)是同一时间应对(dealing with)多件事情的能力,(在同一刻时间上,还是只做一件事)
并行(parallel)是同一时间动手做(doing)多件事情的能力,(同一刻时间内,多核CPU同时做多件事)
4.总结
现在都是多核CPU,在多核CPU下
并发是同一时间应对多件事情的能力,多个线程轮流使用一个或多个CPU并行是同一时间动手做多件事情的能力,4核CPU同时执行4个线程
3.创建线程的方式有哪些?
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
- 线程池创建线程(项目中常用的方式)
1.runnable与callable有什么区别?
- runnable没有方法返回值,而callable有方法返回值类型,返回值类型与接口泛型保持一致。
- runnable不能向上抛出异常,只能内部消化,callable可以抛出异常。
- callable可以用Future接口、FutureTask类获取线程的返回值。
2.线程的run和start有什么区别?
- run相当于一个普通方法,可以在一个线程中多次执行,而start是开启一个线程,一个线程只能开启一次,多次调用就会抛出异常。
- run(): 封装了要被线程执行的代码,可以被调用多次。
start(): 用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次。
4.线程包括哪些状态,状态之间如何变换?
线程的状态可以参考JDK中Thread类中的枚举State。
public enum State {/*** Thread state for a thread which has not yet started.*/NEW,/*** Thread state for a runnable thread. A thread in the runnable* state is executing in the Java virtual machine but it may* be waiting for other resources from the operating system* such as processor.*/RUNNABLE,/*** Thread state for a thread blocked waiting for a monitor lock.* A thread in the blocked state is waiting for a monitor lock* to enter a synchronized block/method or* reenter a synchronized block/method after calling* {@link Object#wait() Object.wait}.*/BLOCKED,/*** Thread state for a waiting thread.* A thread is in the waiting state due to calling one of the* following methods:* <ul>* <li>{@link Object#wait() Object.wait} with no timeout</li>* <li>{@link #join() Thread.join} with no timeout</li>* <li>{@link LockSupport#park() LockSupport.park}</li>* </ul>** <p>A thread in the waiting state is waiting for another thread to* perform a particular action.** For example, a thread that has called {@code Object.wait()}* on an object is waiting for another thread to call* {@code Object.notify()} or {@code Object.notifyAll()} on* that object. A thread that has called {@code Thread.join()}* is waiting for a specified thread to terminate.*/WAITING,/*** Thread state for a waiting thread with a specified waiting time.* A thread is in the timed waiting state due to calling one of* the following methods with a specified positive waiting time:* <ul>* <li>{@link #sleep Thread.sleep}</li>* <li>{@link Object#wait(long) Object.wait} with timeout</li>* <li>{@link #join(long) Thread.join} with timeout</li>* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>* </ul>*/TIMED_WAITING,/*** Thread state for a terminated thread.* The thread has completed execution.*/TERMINATED;}
简化:
1.线程的6种状态
- 新建(new)。
- 可执行(runnable)。
- 阻塞(blocked)。
- 等待(waiting)。
- 时间等待(timed_waiting)。
- 终止(terminated)。
2.线程状态之间的变化
1.创建线程对象是新建状态。
2.调用start()方法转变为可执行状态。
3.线程获取到了CPU的执行权,执行结束是终止状态。
4.在可执行状态的过程中,如果没有获取到CPU的执行权,可能会切换(出现)其它的状态。
(*).如果没有获取到锁(synchronized或lock),线程进入阻塞状态,获得锁之后再切换为可执行状态。
(*).如果线程调用了wait()方法,线程进入等待状态,其它线程调用notify()唤醒后可切换为可执行状态。
(*).如果线程调用了sleep()方法,线程进入计时等待状态,时间结束后可切换为可执行状态。
* 表示没有先后顺序
5.新建T1、T2、T3三个线程,如何保证它们按顺序执行?
可以使用线程中的join方法解决。
在Java中,
join()
方法是Thread
类中的一个方法,它允许一个线程等待另一个线程终止。当在某个线程上调用join()
方法时,调用线程将会阻塞,直到被调用的线程(即目标线程)终止。
package com.hmtest;public class Demo01 {public static void main(String[] args) {Thread thread1 = new Thread(() -> {for (int i = 0; i < 5; i++) {System.out.println("Thread 1: " + i);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});Thread thread2 = new Thread(() -> {try {// 等待thread1完成thread1.join();} catch (InterruptedException e) {e.printStackTrace();}for (int i = 0; i < 5; i++) {System.out.println("Thread 2: " + i);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});// 启动线程thread1.start();thread2.start();// 主线程等待thread2完成try {thread2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Main thread finished.");}
}
6.notify和notifyAll有什么区别?
- notify:只随机唤醒一个wait方法。
- notifyAll:唤醒所有wait方法。
7.在java中wait()和sleep()方法有什么区别?
共同点
wait() 和 sleep() 的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态
不同点
1.方法归属不同
wait()
方法是Object
类的一部分,因此所有对象都可以调用该方法。sleep()
方法是Thread
类的一部分,因此只能在当前运行的线程上调用。2.醒来时机不同
执行 sleep() 和 wait() 的线程都会在等待相应毫秒后醒来。
wait()
通常用于多线程之间的通信,一个线程在等待某个条件成立时调用wait()
,另一个线程在条件成立时调用notify()
或notifyAll()
来唤醒等待的线程。sleep()
通常用于在指定的一段时间后暂停当前线程的执行,不涉及线程间的通信。3. 锁的特性不同(重点)
wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制。wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃 cpu ,但你们还可以用)。而 sleep 方法 如果是在 synchronized 代码块中执行,并不会释放对象锁(我放弃 cpu ,你们也用不了)。
8.如何停止一个正在运行的线程
java 终止线程的4种方式
- 使用布尔标志位标志退出法,使线程正常的退出,也就是run方法完成后线程终止。
- 使用stop强行终止(不推荐,方法作废)
- 使用interrupt方法中断线程。
- 打断阻塞的线程(join、wait、sleep),线程会抛出异常。
- 打断正常的线程,可以根据打断状态来标记是否退出线程。
1.使用布尔标志位退出法
在线程的执行代码中,使用一个布尔类型的标志位来标识线程是否需要终止。线程在执行过程中,不断地检查这个标志位,如果标志位为true,则主动退出线程执行的循环或方法,从而终止线程的执行。
package com.hmtest;public class MyThread implements Runnable {private volatile boolean flag = true;@Overridepublic void run() {while (flag){System.out.println("线程执行了");try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}}}public void stopThread(){flag = false;}public static void main(String[] args) {MyThread mt = new MyThread();Thread t2 = new Thread(mt);t2.start();try {Thread.sleep(9000);} catch (InterruptedException e) {e.printStackTrace();}// mt.flag=false;mt.stopThread();}}
二.Java多线程安全
1.synchronized关键字的底层原理
基本使用方法:
没有使用synchronized配合Object类型对象(没有对象锁)
问题:超卖现象(一共10张票,买了15张票)
package com.hmtest.threadpackage;public class TicketDemo {// lock是一个Object类型的对象static Object lock = new Object();int ticket = 10;public void getTicket() {// lock配合synchronized实现线程同步,是为对象锁
// synchronized (lock) {// 无票票,直接返回if (ticket <= 0){return;}System.out.println("当前线程:" + Thread.currentThread().getName() + ",抢到了第" + ticket + "票");ticket--;
// }}public static void main(String[] args) {TicketDemo ticket = new TicketDemo();for (int i = 0; i < 15; i++){new Thread(() -> ticket.getTicket()).start();}}
}
卖票案例:加锁(Object类型对象配合synchronized实现线程同步,是为对象锁)
不会导致超卖现象
package com.hmtest.threadpackage;public class TicketDemo {// lock是一个Object类型的对象static Object lock = new Object();int ticket = 10;public void getTicket() {// lock配合synchronized实现线程同步,是为对象锁synchronized (lock) {// 无票票,直接返回if (ticket <= 0){return;}System.out.println("当前线程:" + Thread.currentThread().getName() + ",抢到了第" + ticket + "票");ticket--;}}public static void main(String[] args) {TicketDemo ticket = new TicketDemo();for (int i = 0; i < 15; i++){new Thread(() -> ticket.getTicket()).start();}}
}
在Java中,
synchronized
关键字是用来实现线程同步,是保证多线程访问共享资源时的正确性的一个机制。其底层原理主要涉及Java对象头中的Mark Word以及监视器(Monitor)。
每个Java对象都有一个对象头,对象头中包含了一些标记信息,其中就包括了锁状态标记。对于32位系统,对象头有以下两部分组成:
- Mark Word(标记字段):存储对象的hashCode或锁信息等。
- Class Metadata Address(类型指针):指向对象的类元数据的指针。
1.Monitor
Monitor是Java中实现
synchronized
的基础,可以把它理解为一个同步工具。每个对象都可以关联一个Monitor,如果使用synchronized
给对象上锁(重量级锁),该对象头的Mark Word中就被设置指向锁记录的指针。
在Java虚拟机(JVM)中,Monitor(监视器)是一种同步机制,用来确保在多线程环境中对共享资源的互斥访问。Monitor是由JVM内部对象(如所有对象)的一部分来实现的,主要由以下几部分组成:
Owner(拥有者):
- 指向持有Monitor的线程的指针。当没有线程持有Monitor时,该字段为null。当有线程持有Monitor时,该字段会指向那个线程。
Entry Set(入口集):
- 所有等待获取Monitor的线程都会被存放在这个集合中。这些线程处于阻塞状态,等待Monitor被释放,以便它们可以尝试获取Monitor。
Wait Set(等待集):
- 当线程调用了
Object.wait()
方法后,它会被加入到等待集中。这些线程在等待某个条件,当其他线程调用Object.notify()
或Object.notifyAll()
方法时,它们可能会从等待集中被唤醒。Count(计数器):
- 记录获取Monitor的次数。对于重入锁,这个计数器会递增,以允许持有锁的线程多次进入同步块而不会阻塞。
以下是Monitor的组成结构的详细描述:
Owner:
- 任何时刻只能有一个线程持有Monitor,这个线程就是Monitor的拥有者。只有拥有者才能退出同步块,释放Monitor,从而允许其他线程进入。
Entry Set:
- 当一个线程尝试进入一个同步块,但该Monitor已经被其他线程持有时,这个线程会被加入到入口集中。它将保持阻塞状态,直到Monitor变为可用状态,并且它能够成功获取Monitor。
Wait Set:
- 当一个线程在同步块内部调用了
wait()
方法时,它会释放Monitor并进入等待集。它将保持在这个集合中,直到其他线程在同一个Monitor上调用notify()
或notifyAll()
方法。Lock(锁):
- Monitor内部有一个锁机制,用来实现线程间的互斥。这个锁可以是轻量级锁、偏向锁或重量级锁,取决于锁的状态和竞争情况。
1.Monitor的工作流程大致如下:
- 当一个线程尝试进入一个
synchronized
块时,它会尝试获取与该块关联的Monitor的所有权。- 如果Monitor未被其他线程持有,则当前线程成为Monitor的拥有者。
- 如果Monitor已被其他线程持有,则当前线程会进入入口集,等待Monitor的释放。
- 当Monitor的拥有者线程调用
wait()
方法时,它会被加入到等待集,并释放Monitor,允许其他线程获取Monitor。- 当其他线程调用
notify()
或notifyAll()
方法时,等待集中的线程可能会被唤醒,并重新尝试获取Monitor。
- Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住
Synchronized底层原理答案
- Synchronized【对象锁】采用互斥的方式让同一时刻最多只有一个线程能持有【对象锁】
- Synchronized的底层由monitor实现的,monitor是jvm内部的对象( C++实现),线程获得锁需要使用对象(锁)关联monitor
- 在monitor内部有三个属性,分别是owner、entrylist、waitset
- 其中owner是关联获得锁的线程,并且只能关联一个线程;entrylist关联的是处于阻塞状态的线程;waitset关联的是处于Waiting状态的线程
Synchronized底层原理进阶答案
1.Monitor实现的锁属于重量级锁,你了解过锁升级吗?
- Monitor实现的锁属于重量级锁,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。
- 在JDK 1.6引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。
补充:
用户态:
- 用户态是处理器的一种运行模式,用于运行普通应用程序。在这个模式下,程序不能直接访问硬件资源或者执行特权指令,必须通过系统调用(System Call)来请求内核的服务。(权限比较低)
内核态:
- 内核态是处理器的另一种运行模式,用于运行操作系统的核心代码。在这个模式下,程序具有最高的权限,可以执行所有CPU指令,包括特权指令,并且可以直接访问任何内存地址和硬件设备。(权限比较高)
2.对象锁lock怎么关联上的Monitor的?(重量级锁)
static Object lock = new Object();// lock配合synchronized实现线程同步,是为对象锁synchronized (lock) {// 无票票,直接返回}
1. 先看对象的内存模型
MarkWord
- lptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针,占30位。
- 每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上重量级锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针。
3.轻量级锁
在很多的情况下,在Java程序运行时,同步块中的代码都是不存在竞争的,不同的线程交替的执行同步块中的代码。这种情况下,用重量级锁是没必要的。因此JVM引入了轻量级锁的概念。
如:
static final Object obj = new Object(); public static void method1() {synchronized( obj ) {// 同步块 Amethod2();} } public static void method2() { synchronized( obj ) { // 同步块 B} }
两个代码块,都是同一把对象锁obj。此时,一个线程去调用了method1,而method1中又去调用method2,此时线程进入了同一把锁两次(该现象称为锁重入),因为是同一个线程持有同一个锁,不存在竞争,其实没必要使用重量级锁。
轻量级锁
加锁流程
1.在线程栈中创建一个Lock Record,将其obj字段指向锁对象。
2.通过CAS指令将Lock Record的地址存储在对象头的mark word中,如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。
3.如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为null,起到了一个重入计数器的作用。
4.如果CAS修改失败,说明发生了竞争,需要膨胀为重量级锁。
解锁过程
1.遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record。
2.如果Lock Record的Mark Word为null,代表这是一次重入,将obj设置为null后continue。
3.如果Lock Record的 Mark Word不为null,则利用CAS指令将对象头的mark word恢复成为无锁状态。如果失败则膨胀为重量级锁。
4.偏向锁
- 轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
- Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头。后面只是判断线程id是否是自己的。
- 这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有。
Java中的synchronized有偏向锁(锁只被一个线程持有)、轻量级锁(不同线程交替持有锁)、重量级锁(多线程竞争锁)三种锁。
重量级锁:
- 底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。
轻量级锁:
- 线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是CAS操作,保证原子性。
偏向锁
- 一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只需要判断mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令。
一旦锁发生了竞争,都会升级为重量级锁
2.谈谈JMM(Java内存模型)
JMM(Java Memory Model)Java内存模型,定义了共享内存中多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性。
- 主内存:这是所有线程共享的内存区域,它存储了Java程序中的共享变量。
- 工作内存:每个线程都有自己的工作内存,(每个线程只能访问自己的工作内存)用于存储线程执行时需要的局部变量、方法调用的参数、返回值以及线程私有的其他数据。工作内存不存在线程安全问题。
Java内存模型答案
- JMM(Java Memory Model)Java内存模型,定义了共享内存中多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性。
- JMM把内存分为两块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享区域(主内存)。
- 线程跟线程之间是相互隔离的,线程跟线程的交互需要通过主内存实现。
3.CAS你知道吗?
- CAS的全称是:Compare And Swap(比较再交换),它体现的一种乐观锁的思想,在无锁情况下保证线程操作共享数据的原子性。
- CAS使用的地方很多:AQS框架、Atomicxxx类、synchronized。
- 在操作共享变量的时候使用的自旋锁,效率上更高。
- CAS的底层是调用的Unsafe类中的方法,都是操作系统提供的,其它语言实现。
在JUC( java.util.concurrent )包下实现的很多类都用到了CAS操作
- AbstractQueuedSynchronizer(AQS框架)
- AtomicXXX类
CAS数据交换流程
- 线程A和线程B从主内存中获取共享变量V:int a = 100(当前的内存值V) ,假设线程A比线程B先获得CPU的执行权,此时线程A的工作内存中的变量 int a = 100(旧的预期值A),然后a++,得到新的预期值int a = 101 (新的预期值B)。 CAS交换规则:拿旧的预期值A去与主内存中的当前内存值V进行比较,如果相等,则把主内存中的当前预期值V替换为线程A中的新预期值B。
- 线程A终止后,主内存中的当前内存值V变为了 v: int a = 101。
一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当旧的预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。如果CAS操作失败,通过自旋的方式等待并再次尝试,直到成功。
3.此时,线程B获取到了CPU的执行权,此时主内存中的当前内存值V:int a = 101,线程B中的数据为 旧的预期值A:int a =100 ,执行代码a--后,新的预期值B: int a = 99 , 此时主内存中的内存值V与线程B中旧的预期值A不相等。失败,开始自旋。规则:线程B会重新从主内存中读取共享变量V:int a = 101 ,那么此时线程B中的旧的预期值A:int a =101 执行代码 a-- 后,新的预期值B:int a = 101 。然后拿线程B中旧的预期值A去与主内存中的内存值V进行比较,相等,替换成功。
4.乐观锁和悲观锁的区别
悲观锁和乐观锁的区别
1、悲观锁
顾名思义,就是比较悲观的锁,总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中
synchronized
和ReentrantLock
等独占锁就是悲观锁思想的实现。
2、乐观锁
反之,总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中
java.util.concurrent.atomic
包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
5.谈谈你对volatile的理解
- 保证线程间的可见性:用volatile修饰的共享变量,能够防止即时编译器优化的发生,让一个线程对共享变量的修改对另一个线程可见。
- 禁止指令重排序:用volatile修饰共享变量会在读、写共享变量时加入不同屏障,阻止其它读写操作越过屏障,从而达到阻止重排序的效果。
1.保证线程间的可见性:
执行这段代码,会出现一个问题:线程1已经把stop的值改为了true,线程2打印的stop值也为true,但线程3中的 !stop的值却为true,陷入死循环。这是因为即时编译器JIT做了优化