文章目录
- 三、多线程
- 何为线程
- 守护线程是什么
- 线程的生命周期和状态
- 上下文切换
- 创建线程有哪几种方式
- 说一说自己对于 synchronized 关键字的了解
- 构造方法可以使用synchronized 关键字修饰吗?
- 说一下 Runnable 和 Callable 有什么区别
- 执行execute()方法和submit()方法的区别是什么呢?
- volatile和synchronized区别
- sleep() 和 wait() 有什么区别
- notify()和 notifyAll()有什么区别
- 线程的 run() 和 start() 有什么区别
- 线程池都有哪些状态
- volatile是什么?可以保证有序性吗
- 线程池中 submit() 和 execute() 方法有什么区别
- 在 Java 程序中怎么保证多线程的运行安全
- 什么是死锁
- 怎么防止死锁
- 为什么我们调用start()方法时会执行run()方法,为什么不直接调用run()方法
- 之后的synchronized关键字底层做了哪些优化
- 谈谈 AtomicInteger
- 谈谈 synchronized 和 ReentrantLock 的区别
三、多线程
何为线程
一个进程在其执行的过程中可以产生多个线程,与进程不同的是,同一个类i中的多个线程共享进程的堆和方法区,但是每个线程有自己的程序计数器、虚拟机栈、本地方法栈。
线程与进程之间的关系
一个进程可以有多个线程,多个线程共享进程的堆和方法区(JDK1.8之后,方法区变成了元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈、本地方法栈。
程序计数器为什么是私有的
程序计数器主要有两个作用:
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,例如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器相当于记录当前线程执行的位置,从而当线程被切换回来的时候,就能够知道线程上次运行到哪儿了。
如果执行的是native方法,那么程序计数器记录的是undefined地址,只有执行的是java代码时,程序计数器记录的才是下一条指令的地址。
所以程序计数器私有化主要是为了线程切换后能够恢复到正确的执行位置。
虚拟机栈和本地方法栈为什么时私有的
- 虚拟机栈:每个Java方法在执行时会创建出一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用到执行完成的过程就对应着一个栈帧在Java虚拟机中入栈和出栈的过程。
- 本地方法栈:和虚拟机栈发挥的作用非常类似,但是虚拟机栈为虚拟机执行的Java方法(字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务。
所以为了保证线程池中的局部变量不能被其他线程访问到,虚拟机栈和本地方法栈是线程私有的。
堆和方法区
堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象。
方法区主要用于存放已被加载的类的信息、常量、静态变量等数据。
守护线程是什么
守护线程是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。在 Java 中垃圾回收线程就是特殊的守护线程
线程的生命周期和状态
Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态。
上下文切换
多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
概括来说就是当前任务执行完1CPU时间片切换到另一个任务之前会先保存自己的状态以便下一次再切换回这个任务时,可以再加载这个任务的状态。任务从保存再到加载的过程就是一次上下文切换。
创建线程有哪几种方式
- 继承 Thread 重写 run 方法。
- 实现 Runnable 接口。
- 实现 Callable 接口。
- 线程池创建线程。
说一说自己对于 synchronized 关键字的了解
synchronized
关键字解决的是多个线程之间访问资源的同步性,他可以保证被他修饰的方法或者代码块在任意时刻内只有一个线程执行。
构造方法可以使用synchronized 关键字修饰吗?
**构造方法不能使用 synchronized 关键字修饰。**因为构造方法本身就是属于线程安全的,不存在同步的构造方法。
说一下 Runnable 和 Callable 有什么区别
-
Runnable 和 Callable都是接口。
-
Runnable 接口中的
run()
方法的返回值是void,它做的事情只是单纯执行run()
方法中的代码,没有返回值且不会抛出异常检查。 -
Callable中的
call()
方法是有返回值的,是一个泛型,是一个泛型,可以和Future、FutureTask配合可以用来获取异步执行的结果,且可以抛出异常检查。
执行execute()方法和submit()方法的区别是什么呢?
- execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否。
- submit()方法用于提交需要返回值的任务,线程池会返回一个Future类型的对象,通过这个Future对象可以判断任务是否会执行成功。我们可以通过Future的
get()
方法来获取返回值,get()方法会阻塞当前线程直到任务结束,而使用get(long timeout,TimeUnit unit)
方法会阻塞当前线程一段时间后立即返回,这个时候有可能任务没有执行完。
volatile和synchronized区别
-
volatile
关键字是线程同步的轻量级实现,所以volatile
的性能肯定比synchronized
关键字要好。 -
volatile
关键字只能用于修饰变量,而synchronized
关键字可以修饰方法以及代码块。 -
多线程访问
volatile
关键字不会发生阻塞,而多线程访问synchronized
关键字可能会发生阻塞。 -
volatile
关键字能保证数据的可见性,但是不能保证数据的原子性,而synchronized
关键字两者都可以保证。 -
volatile
关键字主要用于解决变量在多个线程之间的可见性,而synchronized
关键字解决的是多个线程之间访问资源的同步性。
sleep() 和 wait() 有什么区别
- 类的不同:sleep() 来自 Thread,wait() 来自 Object。
- 释放锁:sleep() 不释放锁;wait() 释放锁。
- 用法不同:sleep() 时间到会自动恢复;wait() 可以使用 notify()/notifyAll()直接唤醒。
notify()和 notifyAll()有什么区别
notifyAll()会唤醒所有的线程,notify()之后唤醒一个线程
notifyAll() 调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而 notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制
线程的 run() 和 start() 有什么区别
start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次
线程池都有哪些状态
- RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务
- SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务
- STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程
- TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING状态时,会执行钩子方法 terminated()
- TERMINATED:terminated()方法结束后,线程池的状态就会变成terminated
volatile是什么?可以保证有序性吗
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰后,那么就具备了两层语义:
(1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这个新值对其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存。
(2)禁止进行指令重排序。
线程池中 submit() 和 execute() 方法有什么区别
- execute():只能执行 Runnable 类型的任务,所以execute()方法的返回类型是void。
- submit():可以执行 Runnable 和 Callable 类型的任务,所以submit()方法可以返回持有计算结果的Future对象。
在 Java 程序中怎么保证多线程的运行安全
- 使用安全类,比如 Java. util. concurrent 下的类
- 使用自动锁 synchronized
- 使用手动锁 Lock
//手动锁 Java 示例代码
Lock lock = new ReentrantLock();
lock. lock();try {
System. out. println("获得锁");
} catch (Exception e) {
// TODO: handle exception
} finally {
System. out. println("释放锁");
lock. unlock();
}
什么是死锁
线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
如图,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
产生死锁必须具备以下四个条件:
- 互斥条件:该资源任意一个时刻只由一个线程占用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
怎么防止死锁
为了避免死锁,我们只要破坏产生死锁的四个条件中的其中一个就可以了。现在我们来挨个分析一下:
- 破坏互斥条件 :这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
- 破坏请求与保持条件 :一次性申请所有的资源。
- 破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
- 破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
为什么我们调用start()方法时会执行run()方法,为什么不直接调用run()方法
new 一个 Thread,线程进入了新建状态。调用 start()
方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start()
会执行线程的相应准备工作,然后自动执行 run()
方法的内容,这是真正的多线程工作。
但是,直接执行 run()
方法,会把 run()
方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结: 调用 start()
方法方可启动线程并使线程进入就绪状态,直接执行 run()
方法的话不会以多线程的方式执行。
之后的synchronized关键字底层做了哪些优化
JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。
锁主要存在四种状态,他们会随着竞争的激烈而主键升级,但是锁可以升级不可以降级,这种策略是为了提高获得锁和释放锁的效率:
- 无锁状态。
- 偏向锁状态。
- 轻量级锁状态。
- 重量级锁状态。
谈谈 AtomicInteger
AtomicInteger 类常用方法
public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
AtomicInteger 类的使用示例
使用 AtomicInteger 之后,不用对 increment() 方法加锁也可以保证线程安全。
class AtomicIntegerTest {
private AtomicInteger count = new AtomicInteger();
//使用AtomicInteger之后,不需要对该方法加锁,也可以实现线程安全。
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
AtomicInteger 类的原理
AtomicInteger 类的部分源码:
// setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替换”的作用)
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value
AtomicInteger 类主要是利用CAS(compare and swap)+volatile和native方法来保证原子操作,从而避免了synchronized的高开销,执行效率大幅度提升。
CAS的原理是拿期望的值和原本的值做一个比较,如果相同则更新成期望的值,如果不相同则不更新。UnSafe类的objectFieldOffset()方法是一个本地方法,这个方法是用来拿到原来的值的内存地址,返回值是valueObject。而且value是一个volatile变量,在内存中可见,因此JVM可以保证任何试可任何线程总能拿到该变量的最新值。
谈谈 synchronized 和 ReentrantLock 的区别
-
两者都是可重入锁
可重入锁指的是自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的。如果不可重入锁的话,会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,直到锁的计数器下降为0时才能释放锁。
-
synchronized 依赖于JVM,而ReentrantLock 依赖于API
synchronized 是依赖于JVM实现的,JDK1.6为synchronized 关键字做了很多的优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。
ReentrantLock 是JDK层面实现的,也就是API层面,需要lock()和unlock()方法配合try/finally语句块来完成。
-
ReentrantLock 比synchronized 增加了一些高级功能,主要来说有三点:
(1):等待可中断:ReentrantLock 提供了一种能够中断等待锁的线程机制,通过
lock.lockInterruptibly()
来实现这个机制,也就是说正在等待的线程可以选择放弃等待,改为处理其他的事情。 (2):可实现公平锁:ReentrantLock可以指定是公平锁还是非公平锁,而synchronized 只能是非公平锁。公平锁就是先等待的线程先获得锁。ReentrantLock默认是非公平的,可以通过
ReentrantLock
类的ReentrantLock(boolean fair)
构造方法来制定是否是公平的。 (3):可选择性通知:锁可以绑定多个条件。