面试题:线程与进程区别
进程:我们把运行中的程序叫做进程(概念)。进程与进程之间各自占用各自的内存资源,互相独立(独立性)。
对于单核CPU的同一个时刻,其实计算机中只能有一个进程在执行,我们看到了很多进程在一起执行,原因是CPU通过时间片分配算法来循环执行任务,轮询依次为每个进程服务,因为轮询的速度非常快,所以给我们的感觉好像这些进程同时执行。
而在多个 CPU 系统中,这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行。核越多,并行处理的程序越多,能大大的提高电脑运行的效率。
我们电脑上会安装很多的应用程序比如微信,QQ,Word等这些正在运行的程序就叫做进程。
线程:线程就是进程中的一个执行单元,负责当前进程中程序的执行。一个进程可以包含多个线程。一个进程包含了多个线程就是多线程。多线程可以提高程序的并行运行效率。
优点:
- 在多核CPU中,通过并行计算提高程序性能. 比如一个方法的执行比较耗时,现在把这个方法逻辑拆分,分为若干个线程并发执行,提高程序效率。
- 可以解决网络等待、io响应导致的耗时问题。
- 可以随时停止任务
- 可以分别设置各个任务的优先级以优化性能
- 提高CPU的使用率.提高网络资源的利用率
缺点:
- 线程也是程序,所以线程需要占用内存,线程越多占用内存也越多;
- 线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题;
- 多线程存在上下文切换问题
Java 程序的进程里面至少包含两个线程,主进程也就是 main()方法线程,另外一个是垃圾回收机制线程。每当使用 java 命令执行一个类时,实际上都会启动一个 JVM,JVM 实际上就是在操作系统中启动了一个线程,java 本身具备了垃圾的收集机制,所以在 Java 运行时至少会启动两个线程。
面试题1:创建线程有几种方式?
方式1:继承Thread创建线程
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start();
MyThread t2 = new MyThread();
t2.start();
}
}
方式2:通过Runnable
public class App2 {
public static void main(String[] args) {
new Thread(()->{
for (int i = 0; i < 20; i++) {
System.out.println("i = " + i);
}
}).start();
}
}
方式3:通过Callable创建线程
一个可取消的异步计算。FutureTask提供了对Future的基本实现,可以调用方法去开始和取消一个计算,可以查询计算是否完成并且获取计算结果。只有当计算完成时才能获取到计算结果,一旦计算完成,计算将不能被重启或者被取消,除非调用runAndReset方法。
package cn.test;
import java.util.concurrent.*;
public class App3 {
public static void main(String[] args) {
//1、计算任务,实现Callable接口
Callable<String> callable = ()->{
int sum = 0;
for (int i = 0; i < 20; i++) {
sum += i;
// 耗时操作
Thread.sleep(100);
}
return "计算结果:" + sum;
};
//2、创建FutureTask,传入callable对象
FutureTask<String> futureTask = new FutureTask<>(callable);
//3、创建启动线程
Thread thread = new Thread(futureTask);
thread.start();
try {
String result = futureTask.get(1, TimeUnit.SECONDS);
System.out.println("result = " + result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
// 超时中断执行
futureTask.cancel(true);
System.out.println("超时中断执行");
}
}
}
方式4:通过线程池
概述
线程过多会带来额外的开销,频繁创建和销毁大量线程需要占用系统资源,消耗大量时间。其中包括创建销毁线程的开销、调度线程的开销等等,同时也降低了计算机的整体性能。线程池维护多个线程,等待监督管理者分配可并发执行的任务。这种做法,一方面避免了处理任务时创建销毁线程开销的代价,另一方面避免了线程数量膨胀导致的过分调度问题,保证了对内核的充分利用。
当然,使用线程池可以带来一系列好处:
- 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
- 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
- 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
- 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。
如何使用?
ThreadPoolExecutor
API
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
corePoolSize :核心池的大小,如果调用了prestartAllCoreThreads()或者prestartCoreThread()方法,会直接预先创建corePoolSize指定大小的线程,否则当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;这样做的好处是,如果任务量很小,那么甚至就不需要缓存任务,corePoolSize的线程就可以应对;
maximumPoolSize:线程池最大线程数,表示在线程池中最多能创建多少个线程,如果运行中的线程超过了这个数字,那么相当于线程池已满,新来的任务会使用RejectedExecutionHandler 进行处理;
keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止,然后线程池的数目维持在corePoolSize 大小;
unit:参数keepAliveTime的时间单位;
workQueue:一个阻塞队列,用来存储等待执行的任务,如果当前对线程的需求超过了corePoolSize大小,才会放在这里;
threadFactory:线程工厂,主要用来创建线程,比如可以指定线程的名字;
handler:如果线程池已满,新的任务的处理方式
代码实现
public class App4 {
// 线程池的核心线程数
private static final int CORE_POOL_SIZE = 5;
// 线程池的最大线程数
private static final int MAX_POOL_SIZE = 10;
// 当线程数大于核心线程数时,多余的空闲线程存活的最长时间
private static final int KEEP_ALLOW_TIME = 100;
// 任务队列大小,用来存储等待执行任务的队列
private static final int QUEUE_CAPACITY = 100;
public static void main(String[] args) {
// handler 指定拒绝策略,当提交的任务过多不能及时处理,我们通过定制的策略处理任务
ThreadPoolExecutor executor = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALLOW_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.CallerRunsPolicy()
);
//executor.prestartAllCoreThreads();
for (int i = 0; i < 10; i++) {
Runnable runnable = () -> {
System.out.println(Thread.currentThread().getName() + ":start");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":end");
};
// 运行线程
executor.execute(runnable);
}
// 终止线程池
executor.shutdown();
while (!executor.isTerminated()) {}
System.out.println("Finish All");
}
}
源码分析
/*The main pool control state, ctl, is an atomic integer packing
* two conceptual fields
* workerCount, indicating the effective number of threads
* runState, indicating whether running, shutting down etc
*/
// 存放线程池的线程池内有效线程的数量 (workerCount)和运行状态 (runState)
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static int workerCountOf(int c) {
return c & CAPACITY;
}
private final BlockingQueue<Runnable> workQueue;
public void execute(Runnable command) {
// 如果任务为null,则抛出异常。
if (command == null)
throw new NullPointerException();
// ctl 中保存的线程池当前的一些状态信息
int c = ctl.get();
// 下面会涉及到 3 步 操作
// 1.首先判断当前线程池中之行的任务数量是否小于 corePoolSize
// 如果小于的话,通过addWorker(command, true)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 2.如果当前之行的任务数量大于等于 corePoolSize 的时候就会走到这里
// 通过 isRunning 方法判断线程池状态,线程池处于 RUNNING 状态才会被并且队列可以加入任务,该任务才会被加入进去
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 再次获取线程池状态,如果线程池状态不是 RUNNING 状态就需要从任务队列中移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略。
if (!isRunning(recheck) && remove(command))
reject(command);
// 如果当前线程池为空就新创建一个线程并执行。
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//3. 通过addWorker(command, false)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
//如果addWorker(command, false)执行失败,则通过reject()执行相应的拒绝策略的内容。
else if (!addWorker(command, false))
reject(command);
}
工作原理:
- 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
当调用 execute() 方法添加一个任务时,线程池会做如下判断:
- 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
- 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列。
- 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建线程运行这个任务;
- 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常,告诉调用者“我不能再接受任务了”。
3,当一个线程完成任务时,它会从队列中取下一个任务来执行。
4,当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
这样的过程说明,并不是先加入任务就一定会先执行。假设队列大小为 10,corePoolSize 为 3,maximumPoolSize 为 6,那么当加入 20 个任务时,执行的顺序就是这样的:首先执行任务 1、2、3,然后任务 4~13 被放入队列。这时候队列满了,任务 14、15、16 会被马上执行,而任务 17~20 则会抛出异常。最终顺序是:1、2、3、14、15、16、4、5、6、7、8、9、10、11、12、13。
线程池的阻塞队列选择
如果线程数超过了corePoolSize,则开始把线程先放到阻塞队列里,相当于生产者消费者的一个数据通道,有以下一些阻塞队列可供选择:
ArrayBlockingQueue
ArrayBlockingQueue是一个有边界的阻塞队列,它的内部实现是一个数组。有边界的意思是它的容量是有限的,我们必须在其初始化的时候指定它的容量大小,容量大小一旦指定就不可改变。DelayQueue
DelayQueue阻塞的是其内部元素,DelayQueue中的元素必须实现 java.util.concurrent.Delayed接口,该接口只有一个方法就是long getDelay(TimeUnit unit),返回值就是队列元素被释放前的保持时间,如果返回0或者一个负值,就意味着该元素已经到期需要被释放,此时DelayedQueue会通过其take()方法释放此对象,DelayQueue可应用于定时关闭连接、缓存对象,超时处理等各种场景;LinkedBlockingQueue
LinkedBlockingQueue阻塞队列大小的配置是可选的,如果我们初始化时指定一个大小,它就是有边界的,如果不指定,它就是无边界的。说是无边界,其实是采用了默认大小为Integer.MAX_VALUE的容量 。它的内部实现是一个链表。PriorityBlockingQueue
PriorityBlockingQueue是一个没有边界的队列,它的排序规则和 java.util.PriorityQueue一样。需要注意,PriorityBlockingQueue中允许插入null对象。所有插入PriorityBlockingQueue的对象必须实现 java.lang.Comparable接口,队列优先级的排序规则就是按照我们对这个接口的实现来定义的。SynchronousQueue
SynchronousQueue队列内部仅允许容纳一个元素。当一个线程插入一个元素后会被阻塞,除非这个元素被另一个线程消费。
使用的最多的应该是LinkedBlockingQueue,注意一般情况下要配置一下队列大小,设置成有界队列,否则JVM内存会被撑爆!
线程池已满又有新任务?
如果线程池已经满了可是还有新的任务提交怎么办?
线程池已满的定义,是指运行线程数==maximumPoolSize,并且workQueue是有界队列并且已满(如果是无界队列当然永远不会满);
这时候再提交任务怎么办呢?线程池会将任务传递给最后一个参数RejectedExecutionHandler来处理,比如打印报错日志、抛出异常、存储到Mysql/redis用于后续处理等等,线程池默认也提供了几种处理方式,详见下一章:
拒绝策略
拒绝策略指的就是线程池已满情况下任务的处理策略,默认有以下几种:
1、ThreadPoolExecutor.AbortPolicy 中,处理程序遭到拒绝将抛出运行时RejectedExecutionException。
/**
* A handler for rejected tasks that throws a
* {@code RejectedExecutionException}.
*/
public static class AbortPolicy implements RejectedExecutionHandler {
/**
* Creates an {@code AbortPolicy}.
*/
public AbortPolicy() { }
/**
* Always throws RejectedExecutionException.
*
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
* @throws RejectedExecutionException always
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
}
2、在 ThreadPoolExecutor.CallerRunsPolicy,交给线程池调用所在的线程进行处理。
/**
* A handler for rejected tasks that runs the rejected task
* directly in the calling thread of the {@code execute} method,
* unless the executor has been shut down, in which case the task
* is discarded.
*/
public static class CallerRunsPolicy implements RejectedExecutionHandler {
/**
* Creates a {@code CallerRunsPolicy}.
*/
public CallerRunsPolicy() { }
/**
* Executes task r in the caller's thread, unless the executor
* has been shut down, in which case the task is discarded.
*
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}
3、在 ThreadPoolExecutor.DiscardPolicy 中,直接丢弃后来的任务
/**
* A handler for rejected tasks that silently discards the
* rejected task.
*/
public static class DiscardPolicy implements RejectedExecutionHandler {
/**
* Creates a {@code DiscardPolicy}.
*/
public DiscardPolicy() { }
/**
* Does nothing, which has the effect of discarding task r.
*
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
}
4、在 ThreadPoolExecutor.DiscardOldestPolicy 丢弃队列里最老的任务,将当前这个任务继续提交给线程池。
/**
* A handler for rejected tasks that discards the oldest unhandled
* request and then retries {@code execute}, unless the executor
* is shut down, in which case the task is discarded.
*/
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
/**
* Creates a {@code DiscardOldestPolicy} for the given executor.
*/
public DiscardOldestPolicy() { }
/**
* Obtains and ignores the next task that the executor
* would otherwise execute, if one is immediately available,
* and then retries execution of task r, unless the executor
* is shut down, in which case task r is instead discarded.
*
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
}
5、当然也可以自己实现处理策略类,继承RejectedExecutionHandler接口即可,该接口只有一个方法:
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
如何优化线程池配置?
如何合理配置线程池大小,仅供参考。
一般需要根据任务的类型来配置线程池大小:
如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为【(CPU总核数)】 或者 【(CPU总核数+1)】
如果是IO密集型任务,类似 网络I/O、数据库、磁盘I/O 等,参考值可以设置为【(2 * CPU总核数)】
当然,这只是一个参考值,具体的设置还需要根据实际情况进行调整,比如可以先将线程池大小设置为参考值,
再观察任务运行情况和系统负载、资源利用率来进行适当调整。
其中NCPU的指的是CPU的核心数,可以使用下面方式来获取;
public static void main(String[] args) {
int ncpu = Runtime.getRuntime().availableProcessors();
System.out.println("cpu核数 = " + ncpu);
}
Executors
通过Executors类提供四种线程池。创建方法为静态方式创建。
Executors.newFixedThreadPool();
返回线程池对象。创建的是有界线程池,也就是池中的线程个数可以指定最大数量。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
可见该方法让keepAliveTime为0,即限制了线程数必须小于等于corePoolSize。而多出的线程则会被无界队列所存储,在其中排队。
Executors.newCachedThreadPool();
创建一个可缓存线程池,线程池长度超过处理需要时,可灵活回收空闲线程,若无可回收线程则新建线程。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
该方法中所有线程均由SynchronousQueue管理,且不设置线程数量上限。对于SynchronousQueue,每个插入线程必须等待另一线程的对应移除操作。(即该队列没有容量,仅试图取得元素时元素才存在)因而,该方法实现了,如果有线程空闲,则使用空闲线程进行操作,否则就会创建新线程。
Executors.newScheduledThreadPool();
创建一个定长线程池,相对于FixedThreadPool,它支持周期性执行和延期执行。
1、延迟3秒执行
public static void main(String[] args) {
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2);
executorService.schedule(()->{
System.out.println(Thread.currentThread().getName()+":线程启动");
},3, TimeUnit.SECONDS);
executorService.shutdown();
}
2、每三秒隔一秒执行
public static void main(String[] args) {
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2);
executorService.scheduleAtFixedRate(()->{
System.out.println(Thread.currentThread().getName()+":线程启动");
},1,3, TimeUnit.SECONDS);
}
Executors.newSingleThreadExecutor();
创建一个单线程线程池,只会用唯一的工作线程执行任务,保证所有任务按FIFO,LIFO的优先级执行。
在实现上,其相当于一个线程数为1的FixedThreadPool
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
面试题2:wait和notify方法作用?
为什么wait和notify方法放在Object对象?
wait()使当前线程阻塞,前提是 必须先获得锁,一般配合synchronized 关键字使用,即,一般在synchronized 同步代码块里使用 wait()、notify/notifyAll() 方法。
当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状态。
只有当 notify/notifyAll() 被执行时候,才会唤醒一个或多个正处于等待状态的线程,然后继续往下执行,直到执行完synchronized 代码块的代码或是中途遇到wait() ,再次释放锁。
notify/notifyAll() 的执行只是唤醒沉睡的线程,而不会立即释放锁,锁的释放要看代码块的具体执行情况。所以在编程中,尽量在使用了notify/notifyAll() 后立即退出临界区,以唤醒其他线程让其获得锁
wait与sleep区别?
因为wait和notify需要使用锁对象来调用,而任何对象都可以作为锁,所以放在Object类中.
public class App7 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
App7 app = new App7();
Thread t1 = new Thread(() -> {
synchronized (app) {
try {
System.out.println(Thread.currentThread().getName()+":start");
app.wait();
System.out.println(Thread.currentThread().getName()+":end");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (app) {
System.out.println(Thread.currentThread().getName()+":nofify");
app.notify();
}
}, "t2");
t1.start();
TimeUnit.SECONDS.sleep(1);
t2.start();
}
}
public final native void wait(long timeout)
public final native void notify();
native 后的函数的实现不是用java写的。使用native关键字说明这个方法是原生函数,也就是这个方法是用C/C++语言实现的,并且被编译成了DLL,由java去调用。这些函数的实现体在DLL中,JDK的源代码中并不包含,你应该是看不到的。对于不同的平台它们也是不同的。这也是java的底层机制,实际上java就是在不同的平台上调用不同的native方法实现对操作系统的访问的。
上面两个代码的实现时hotspot源码中实现:hotspot/src/share/vm/runtime/objectMonitor.cpp,看ObjectMonitor::wait方法。
面试题3:线程状态?
NEW 尚未启动的线程处于此状态。
RUNNABLE 在Java虚拟机中执行的线程处于此状态。
BLOCKED 被阻塞等待监视器锁定的线程处于此状态。
WAITING 正在等待另一个线程执行特定动作的线程处于此状态。
TIMED_WAITING 正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。
TERMINATED 已退出的线程处于此状态。
面试题4:如何控制线程执行顺序?
现在有 T1、T2、T3 三个线程,你怎样保证 T2 在 T1 执行完后执行,T3 在 T2 执行完后执行?
thread.join()
1、当thread执行完后才会执行其他线程。能够使得线程之间的并行执行变成串行执行。
2、会使主线程进入等待池并等待thread线程执行完毕后才会被唤醒。并不影响同一时刻处在运行状态的其他线程。
public class App {
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(() -> {
System.out.println("执行t1");
}, "t1");
Thread t2 = new Thread(() -> {
System.out.println("执行t2");
}, "t2");
Thread t3 = new Thread(() -> {
System.out.println("执行t3");
}, "t3");
t3.start();
//t.join()方法只会使主线程(或者说调用t.join()的线程)进入等待池并等待t线程执行完毕后才会被唤醒。并不影响同一时刻处在运行状态的其他线程。
t3.join();
t2.start();
t2.join();
t1.start();
t1.join();
}
}
源码:
/**
*等待该线程终止的时间最长为 millis 毫秒。超时为 0 意味着要一直等下去。
*millis - 以毫秒为单位的等待时间。
*/
public final synchronized void join(long millis)
throws InterruptedException {
//获取启动时的时间戳,用于计算当前时间
long base = System.currentTimeMillis();
//当前时间
long now = 0;
if (millis < 0) {//等待时间不能小于0则抛出IllegalArgumentException
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {//等待时间为0,则无限等待
//需要注意,如果当前线程未被启动或者终止,则isAlive方法返回false
//即意味着join方法不会生效
while (isAlive()) {
wait(0);
}
} else {
//需要注意,如果当前线程未被启动或者终止,则isAlive方法返回false
//即意味着join方法不会生效
while (isAlive()) {
//计算剩余的等待时间
long delay = millis - now;
if (delay <= 0) {//如果剩余的等待时间小于等于0,则终止等待
break;
}
//等待指定时间
wait(delay);
//获取当前时间
now = System.currentTimeMillis() - base;
}
}
}
join源码中,可以看出它是利用wait方法来实现的,上面的例子当main方法主线程调用线程t的时候,main方法获取到了t的对象锁,而t调用自身wait方法进行阻塞,只要当t结束或者到时间后才会退出,接着唤醒主线程继续执行。millis为主线程等待t线程最长执行多久,0为永久直到t线程执行结束。
join源码中,只会调用wait方法,并没有在结束时调用notify,这是因为线程在die的时候会自动调用自身的notifyAll方法,来释放所有的资源和锁。
查看hostspot中c++代码中 JavaThread::exit() 调用了 ensure_join(this); thread.cpp
//一个c++函数:
void JavaThread::exit(bool destroy_vm, ExitType exit_type) {
...
//ensure_join()是一个线程执行完毕之后,jvm会做的事,做清理等收尾工作,
//翻译成中文叫 确保_join(这个);代码如下:
ensure_join(this);
...
}
static void ensure_join(JavaThread* thread) {
Handle threadObj(thread, thread->threadObj());
ObjectLocker lock(threadObj, thread);
thread->clear_pending_exception();
java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED);
java_lang_Thread::set_thread(threadObj(), NULL);
//thread就是当前线程main线程啊。
lock.notify_all(thread);
thread->clear_pending_exception();
}
面试题5:(JUC)多个线程在某一时刻同时开始执行?
countDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。
是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。
强调的是多个线程在某一时刻同时开始执行。类似于赛跑。
不足:CountDownLatch是一次性的,计算器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当CountDownLatch使用完毕后,它不能再次被使用。
public class App2 {
/**
* CountDownLatch
* 1、作用:等待其他的线程都执行完任务,然后主线程才继续往下执行
* 2、await()方法则使调用该方法的线程处于等待状态,一般是主线程调用。
* 3、countDown()方法每次都会使计数器减一,减为0时处于阻塞等待的主线程将会被唤醒。
* 需求:乌克兰、美国、欧洲战败后,中国统一江湖
*/
public static void main(String[] args) throws InterruptedException {
String[] country = {"乌克兰","美国","欧洲"};
CountDownLatch countDownLatch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(()->{
countDownLatch.countDown();
System.out.println(Thread.currentThread().getName() + "执行完毕");
},country[i]).start();
}
countDownLatch.await();
System.out.println("中国统一江湖!");
}
}
枚举:
public enum CountryEnum {
Ukraine(0,"乌克兰"),Europe(1,"欧洲"),America(2,"美国");
private Integer code;
private String country;
private CountryEnum(Integer code,String country){
this.code = code;
this.country = country;
}
public static CountryEnum getCountry(Integer code){
CountryEnum[] values = CountryEnum.values();
for (CountryEnum countryEnum : values) {
if (countryEnum.code == code) {
return countryEnum;
}
}
return null;
}
public Integer getCode() {
return code;
}
public String getCountry() {
return country;
}
}
面试题5:(JUC)CyclicBarrier 与 CountDownLatch 区别
public class App2 {
/**
* 1:`CyclicBarrier(可重用屏障/栅栏) 类似于 CountDownLatch(倒计数闭锁),它能阻塞一组线程直到某个事件的发生`。
* 2:与闭锁的关键区别在于,所有的线程必须同时到达屏障位置,才能继续执行。
* 3:闭锁用于等待事件,而屏障用于等待其他线程。
* 4:CyclicBarrier 可以使一定数量的线程反复地在屏障位置处汇集。当线程到达屏障位置时将调用 await() 方法,这个方法将阻塞直到所有线程都到达屏障位置。如果所有线程都到达屏障位置,那么屏障将打开,此时所有的线程都将被释放,而屏障将被重置以便下次使用。
* 5:CyclicBarrier 是一个同步辅助类,它允许一组线程相互等待直到所有线程都到达一个公共的屏障点。
* 6:在程序中有固定数量的线程,这些线程有时候必须等待彼此,这种情况下,使用 CyclicBarrier 很有帮助。
* 7:这个屏障之所以用循环修饰,是因为在所有的线程释放彼此之后,这个屏障是可以重新使用的。
*/
public static void main(String[] args) throws InterruptedException {
//说明:当10个线程到达屏障时候,所有到达屏障的线程才可以执行。10个线程执行完后,再执行barrierAction
CyclicBarrier cyclicBarrier = new CyclicBarrier(10,new Thread(()->{
System.out.println("开始比赛!");
}));
Runnable runnable = ()->{
System.out.println(Thread.currentThread().getName() + ":到达会篮球场");
try {
// 到达屏障,阻塞当前线程
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
};
for (int i = 0; i < 10; i++) {
new Thread(runnable).start();
}
}
}
CountDownLatch 的计数器只能使用一次。而 CyclicBarrier的计数器可以多次使用。reset()方法重置;
CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次。
CountDownLatch 采用减计数方式;CyclicBarrier 采用加计数方式。
面试题6:(JUC) Semaphore ?
Semaphore 通常我们叫它信号量, 可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。线程限流,如连接池、停车场。
比如:数据库连接池,同时进行连接的线程有数量限制,连接不能超过一定的数量,当连接达到了限制数量后,后面的线程只能排队等前面的线程释放了数据库连接才能获得数据库连接。
比如:停车场场景,车位数量有限,同时只能容纳多少台车,车位满了之后只有等里面的车离开停车场外面的车才可以进入。
public class App {
public static void main(String[] args) throws InterruptedException {
//1. 创建信号量设置并发线程数,允许最大并发线程数是3
Semaphore semaphore = new Semaphore(3);
//2. 循环创建6个线程,会看到每次执行3个线程
for (int i = 0; i < 6; i++) {
new Thread(() -> {
try {
//3. 获取许可,在达到限制并发线程数之前将可以正常执行线程;否则要等待其他线程释放许可
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + ":进入停车场");
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName() + ":离开停车场");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//4. 释放许可
semaphore.release();
}
}, "车辆" + i).start();
}
}
}
面试题7:(JUC)线程间数据交换?
Exchange:Exchanger是一个用来进行线程之间的数据交换的工具类,它提供了一个同步点,在这个同步点,两个线程可以互相交换数据,当一个线程执行exchange()方法时,会等待第二个线程执行exchange()方法,这个时刻就应该是书中所说的同步点,这两个线程就可以互相交换数据。
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
class ResultData{
private String name;
private String msg;
}
public class App {
public static void main(String[] args) throws InterruptedException {
Exchanger<ResultData> exchanger = new Exchanger<>();
new Thread(()->{
ResultData dataA = ResultData.builder().name("小张").msg("鸡腿").build();
try {
ResultData dataB = exchanger.exchange(dataA);
System.out.println("t1线程结果:" + dataB);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1").start();
new Thread(()->{
ResultData dataB = ResultData.builder().name("小丽").msg("汉堡").build();
try {
ResultData dataA = exchanger.exchange(dataB);
System.out.println("t2线程结果:" + dataA);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t2").start();
}
}
应用场景:实战场景
设计一个定时任务,每日凌晨执行。在定时任务中启动两个线程,一个线程负责对业务明细表(xxx_info)进行查询统计,把统计的结果放置在内存缓冲区,另一个线程负责读取缓冲区中的统计结果并插入到业务统计表(xxx_statistics)中。
面试题8:(JUC) ForkJoin
分治法:把一个规模大的问题划分为规模较小的子问题,然后分而治之,最后合并子问题的解得到原问题的解。
if(任务很小){
直接计算得到结果
}else{
分拆成N个子任务
调用子任务的fork()进行计算
调用子任务的join()合并计算结果
}
在分治法中,子问题一般是相互独立的,因此,经常通过递归调用算法来求解子问题。
Fork/Join框架是将一个任务分成多个子任务,再将子任务压入到线程中,形成一个个线程队列,与传统线程池不一样的地方是,Fork/Join采用的是“工作窃取”模式,简单理解为:当执行新的任务时它可以将其拆分成更小的任务执行,并将小任务加到线程队列中,然后再从一个随机线程的队列中偷一个并把它放在自己的队列中。
如果某个子问题由于阻塞而无法继续执行的时候,那么处理该子问题的线程会主动寻找其他尚未运行的子问题来执行,这种方式减少了线程的等待时间,提高了性能。再说的直白一点就是,其他线程完事之后不会变成空闲状态,而是去别的线程队列上“偷”一个来“帮助”执行(因为线程队列是双端队列,所以可以从尾部进行“偷取”),更好的利用了CPU资源。
//RecursiveTask代表有返回值的任务
//RecursiveAction代表没有返回值的任务。
//CountedCompleter 在任务完成执行后会触发执行一个自定义的钩子函数
class ForkJoinSumTask extends RecursiveTask<Long> {
private Long start;
private Long end;
// 临界值
private Long temp = 100000L;
public ForkJoinSumTask(Long start,Long end){
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
if ((end-start) < temp) {
Long sum = 0L;
for (Long i = start; i <= end ; i++) {
sum+=i;
}
return sum;
} else {
Long middle = (start + end) / 2;
ForkJoinSumTask task1 = new ForkJoinSumTask(start,middle);
ForkJoinSumTask task2 = new ForkJoinSumTask(middle+1,end);
// 执行子任务
task1.fork();
task2.fork();
// 等待任务执行结束合并其结果
return task1.join() + task2.join();
}
}
}
public class App {
public static void main(String[] args) throws Exception {
//test01();
test2();
//test3();
}
private static void test01() {
long start = System.currentTimeMillis();
Long sum = 0L;
for (Long i = 1L; i <=10_0000_0000; i++) {
sum += i;
}
System.out.println("sum = " + sum);
long end = System.currentTimeMillis();
System.out.println("结果:" + sum + ",耗时:" + (end-start));
}
private static void test2() throws InterruptedException, ExecutionException {
long start = System.currentTimeMillis();
ForkJoinSumTask task = new ForkJoinSumTask(0L,10_0000_0000L);
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Long> forkJoinTask = forkJoinPool.submit(task);
Long sum = forkJoinTask.get();
long end = System.currentTimeMillis();
System.out.println("结果:" + sum + ",耗时:" + (end-start));
}
private static void test3(){
long start = System.currentTimeMillis();
Long total = LongStream.rangeClosed(0,10_0000_0000).parallel().reduce(0,Long::sum);
long end = System.currentTimeMillis();
System.out.println("结果:" + total + ",耗时:" + (end-start));
}
}
面试题9:synchronized、volatile 保障可见性
可见性:synchronize、volatile
原子性:synchronize
volatile保证数据的可见性,但是不保证原子性(多线程进行写操作,不保证线程安全);而synchronized是一种排他(互斥)的机制。
主线程无法感知t1线程修改了flag值:
private static Boolean flag = false;
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(()->{
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
});
t1.start();
while (true) {
if (flag) {
System.out.println("线程修改了flag");
break;
}
}
}
解决1:synchronized。 程序执行synchronized后,会清空工作内存副本数据,之后就从新从主内存获取数据
while (true) {
synchronized (flag) {
if (flag) {
System.out.println("线程修改了flag");
break;
}
}
}
解决2:volatile 也可以保证多线程之间访问共享变量时的可见性。
public class App10 {
private static volatile Boolean flag = false;
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
});
t1.start();
while (true) {
if (flag) {
System.out.println("线程修改了flag");
break;
}
}
}
}
面试题10:原子性保障?
可见性:synchronize、volatile
原子性:synchronize、AtomicInteger
volatile保证数据的可见性,但是不保证原子性(多线程进行写操作,不保证线程安全);而synchronized是一种排他(互斥)的机制。
原子性
如果要把一个变量从主内存中复制到工作内存中,就需要按顺序地执行read和load操作,如果把变量从工作内存中同步到主内存中,就需要按顺序地执行store和write操作。但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。
对应如下的流程图:
数据操作的原子性,可以通过八种操作中的lock和unlock来达到目的。但是JVM并没有把lock和unlock操作直接开放给用户使用,我们的java代码中,就是大家所熟知的synchronized关键字保证原子性。
1、非原子操作存在的问题
public class App10 {
private static Integer num = 0;
public static void main(String[] args) throws Exception {
Runnable runnable = () -> {
for (int i = 0; i < 100; i++) {
num++;
}
};
for (int i = 0; i < 100; i++) {
new Thread(runnable).start();
}
Thread.sleep(20);
// 结果不一定是10000
System.out.println("num = " + num);
}
}
2、synchronize 解决原子性问题
public class App{
private static Integer num = 0;
public static void main(String[] args) throws Exception {
Runnable runnable = () -> {
for (int i = 0; i < 100; i++) {
synchronized (App11.class) { // 解决原子性问题
num++;
}
}
};
for (int i = 0; i < 100; i++) {
new Thread(runnable).start();
}
Thread.sleep(20);
System.out.println("num = " + num);
}
}
3、AtomicInteger 解决原子性问题
public class App {
private static AtomicInteger atomicInteger= new AtomicInteger(0);
public static void main(String[] args) throws Exception {
Runnable runnable = () -> {
for (int i = 0; i < 100; i++) {
// 原子方式实现递增,线程安全
atomicInteger.getAndIncrement();
}
};
for (int i = 0; i < 100; i++) {
new Thread(runnable).start();
}
Thread.sleep(20);
System.out.println("num = " + atomicInteger.get());
}
}
CAS
面试题11:(JUC)对象原子更新?
AtomicReference
package cn.test3;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
@Data
@NoArgsConstructor
@AllArgsConstructor
class CartItem{
private Long itemId;
private Long num;
}
public class App {
/**
* AtomicReference
* 1、原子更新引用类型
* 2、AtomicReference是对对象进行原子操作,保证多线程操作同一个对象时候的线程安全性
*/
public static void main(String[] args) throws InterruptedException {
//1. 创建原子更新引用类型,保证多线程操作对象的原子性
AtomicReference<CartItem> atomicReference = new AtomicReference<>();
CartItem cartItem = new CartItem(1L,100L);
//2. 存入对象
atomicReference.set(cartItem);
//3. 原子方式更新对象,会与set时候存入的对象进行对比,如果是同一个对象更新成功返回true
boolean result = atomicReference.compareAndSet(cartItem, new CartItem(2L, 200L));
System.out.println("result = " + result);
System.out.println(atomicReference.get());
}
}
AtomicLongFieldUpdater
/**
* AtomicLongFieldUpdater
* 1、原子更新字段类型
* 2、保证对象属性原子操作线程安全
*/
public static void main(String[] args) throws InterruptedException {
AtomicLongFieldUpdater atomicIntegerFieldUpdater =
AtomicLongFieldUpdater.newUpdater(CartItem.class,"num");
CartItem user = new CartItem(1L,100L);
boolean flag = atomicIntegerFieldUpdater.compareAndSet(user, user.getNum(), 101);
System.out.println("flag = " + flag);
System.out.println("原子更新后的值:"+atomicIntegerFieldUpdater.get(user));
}
ABA问题
ABA问题: 如果另一个线程修改V值假设原来是A,先修改成B,再修改回成A。当前线程的CAS操作无法分辨当前V值是否发生过变化。
危害:
小明在提款机,提取了50元,因为提款机问题,有两个线程,同时把余额从100变为50
线程1(提款机):获取当前值100,期望更新为50,
线程2(提款机):获取当前值100,期望更新为50,
线程1成功执行,线程2某种原因block了,这时,某人给小明汇款50
线程3(默认):获取当前值50,期望更新为100,
这时候线程3成功执行,余额变为100,
线程2从Block中恢复,获取到的也是100,compare之后,继续更新余额为50!!!
此时可以看到,实际余额应该为100(100-50+50),但是实际上变为了50(100-50+50-50)这就是ABA问题带来的成功提交。
解决方法: 在变量前面加上版本号,每次变量更新的时候变量的版本号都+1
,即A->B->A
就变成了1A->2B->3A
。
public static void main(String[] args) throws InterruptedException {
AtomicStampedReference<Integer> asr = new AtomicStampedReference<>(50,0);
Thread thread1 = new Thread(()->{
asr.compareAndSet(50,100,asr.getStamp(),asr.getStamp()+1);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
asr.compareAndSet(100,50,asr.getStamp(),asr.getStamp()+1);
});
Thread thread2 = new Thread(()->{
int stamp = asr.getStamp();
System.out.println("thread2获取版本 = " + stamp);
boolean result = asr.compareAndSet(50, 200, stamp, asr.getStamp() + 1);
System.out.println("result = " + result);
},"thread2");
thread1.start();
thread2.start();
}
面试题12:Java中锁
锁分类总结
1、乐观锁
乐观锁
是一种乐观思想,假定当前环境是读多写少,遇到并发写的概率比较低,读数据时认为别的线程不会正在进行修改(所以没有上锁)。写数据时,判断当前 与期望值是否相同,如果相同则进行更新(更新期间加锁,保证是原子性的)。
Java中的乐观锁
: CAS
,比较并替换,比较当前值(主内存中的值),与预期值(当前线程中的值,主内存中值的一份拷贝)是否一样,一样则更新,否则继续进行CAS操作。
2、悲观锁
悲观锁
是一种悲观思想,即认为写多读少,遇到并发写的可能性高,每次去拿数据的时候都认为其他线程会修改,所以每次读写数据都会认为其他线程会修改,所以每次读写数据时都会上锁。其他线程想要读写这个数据时,会被这个线程block,直到这个线程释放锁然后其他线程获取到锁。
Java中的悲观锁
: synchronized
修饰的方法和方法块、ReentrantLock
。
如图所示,只能有一个线程进行读操作或者写操作,其他线程的读写操作均不能进行。
3、自旋锁
自旋锁
是一种技术: 为了让线程等待,我们只须让线程执行一个忙循环(自旋)。
现在绝大多数的个人电脑和服务器都是多路(核)处理器系统,如果物理机器有一个以上的处理器或者处理器核心,能让两个或以上的线程同时并行执行,就可以让后面请求锁的那个线程“稍等一会”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。
自旋锁
的优点: 避免了线程切换的开销。挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给Java虚拟机的并发性能带来了很大的压力。
自旋锁
的缺点: 占用处理器的时间,如果占用的时间很长,会白白消耗处理器资源,而不会做任何有价值的工作,带来性能的浪费。因此自旋等待的时间必须有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程。
自旋
次数默认值:10次,可以使用参数-XX:PreBlockSpin来自行更改。
自适应自旋
: 自适应意味着自旋的时间不再是固定的,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。有了自适应自旋,随着程序运行时间的增长及性能监控信息的不断完善,虚拟机对程序锁的状态预测就会越来越精准。
Java中的自旋锁
: CAS操作中的比较操作失败后的自旋等待。
4、可重入锁
可重入锁
是一种技术: 任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞。
可重入锁
的原理: 通过组合自定义同步器来实现锁的获取与释放。
- 再次获取锁:识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。获取锁后,进行计数自增,
- 释放锁:释放锁时,进行计数自减。
Java中的可重入锁
: ReentrantLock、synchronized修饰的方法或代码段。
可重入锁
的作用: 避免死锁。
面试题1: 可重入锁如果加了两把,但是只释放了一把会出现什么问题?
答:程序卡死,线程不能出来,也就是说我们申请了几把锁,就需要释放几把锁。
面试题2: 如果只加了一把锁,释放两次会出现什么问题?
答:会报错,java.lang.IllegalMonitorStateException。
5、读写锁
读写锁
是一种技术: 通过ReentrantReadWriteLock
类来实现。为了提高性能, Java 提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由 jvm 自己控制的。
读锁: 允许多个线程获取读锁,同时访问同一个资源。
写锁: 只允许一个线程获取写锁,不允许同时访问同一个资源。
如何使用:
/**
* 创建一个读写锁
* 它是一个读写融为一体的锁,在使用的时候,需要转换
*/
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
获取读锁和释放读锁
// 获取读锁
rwLock.readLock().lock();
// 释放读锁
rwLock.readLock().unlock();
获取写锁和释放写锁
// 创建一个写锁
rwLock.writeLock().lock();
// 写锁 释放
rwLock.writeLock().unlock();
Java中的读写锁:ReentrantReadWriteLock
6、公平锁
公平锁
是一种思想: 多个线程按照申请锁的顺序来获取锁。在并发环境中,每个线程会先查看此锁维护的等待队列,如果当前等待队列为空,则占有锁,如果等待队列不为空,则加入到等待队列的末尾,按照FIFO的原则从队列中拿到线程,然后占有锁。
7、非公平锁
非公平锁
是一种思想: 多个线程获取锁的顺序,不是按照先到先得的顺序,有可能后申请锁的线程比先申请的线程优先获取锁。
优点: 非公平锁的性能高于公平锁。
缺点: 有可能造成线程饥饿(某个线程很长一段时间获取不到锁)
Java中的非公平锁:synchronized是非公平锁,ReentrantLock通过构造函数指定该锁是公平的还是非公平的,默认是非公平的。
8、共享锁
共享锁
是一种思想: 可以有多个线程获取读锁,以共享的方式持有锁。和乐观锁、读写锁同义。
Java中用到的共享锁: ReentrantReadWriteLock
。
9、独占锁
独占锁
是一种思想: 只能有一个线程获取锁,以独占的方式持有锁。和悲观锁、互斥锁同义。
Java中用到的独占锁: synchronized,ReentrantLock
10、重量级锁
重量级锁是一种称谓: synchronized
是通过对象内部的一个叫做监视器锁(monitor
)来实现的,监视器锁本身依赖底层的操作系统的 Mutex Lock
来实现。操作系统实现线程的切换需要从用户态切换到核心态,成本非常高。这种依赖于操作系统 Mutex Lock
来实现的锁称为重量级锁。为了优化synchonized
,引入了轻量级锁
,偏向锁
。
Java中的重量级锁: synchronized
11、轻量级锁
轻量级锁
是JDK6时加入的一种锁优化机制: 轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量。轻量级是相对于使用操作系统互斥量来实现的重量级锁而言的。轻量级锁在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。如果出现两条以上的线程争用同一个锁的情况,那轻量级锁将不会有效,必须膨胀为重量级锁。
优点: 如果没有竞争,通过CAS操作成功避免了使用互斥量的开销。
缺点: 如果存在竞争,除了互斥量本身的开销外,还额外产生了CAS操作的开销,因此在有竞争的情况下,轻量级锁比传统的重量级锁更慢。
12、偏向锁
偏向锁
是JDK6时加入的一种锁优化机制: 在无竞争的情况下把整个同步都消除掉,连CAS操作都不去做了。偏是指偏心,它的意思是这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作(例如加锁、解锁及对Mark Word的更新操作等)。
优点: 把整个同步都消除掉,连CAS操作都不去做了,优于轻量级锁。
缺点: 如果程序中大多数的锁都总是被多个不同的线程访问,那偏向锁就是多余的。
13、分段锁
分段锁
是一种机制: 最好的例子来说明分段锁是ConcurrentHashMap。ConcurrentHashMap原理:它内部细分了若干个小的 HashMap,称之为段(Segment)。默认情况下一个 ConcurrentHashMap 被进一步细分为 16 个段,既就是锁的并发度。如果需要在 ConcurrentHashMap 添加一项key-value,并不是将整个 HashMap 加锁,而是首先根据 hashcode 得到该key-value应该存放在哪个段中,然后对该段加锁,并完成 put 操作。在多线程环境中,如果多个线程同时进行put操作,只要被加入的key-value不存放在同一个段中,则线程间可以做到真正的并行。
线程安全:ConcurrentHashMap 是一个 Segment 数组, Segment 通过继承ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。
14、互斥锁
互斥锁与悲观锁、独占锁同义,表示某个资源只能被一个线程访问,其他线程不能访问。
- 读-读互斥
- 读-写互斥
- 写-读互斥
- 写-写互斥
Java中的同步锁: synchronized
15、同步锁
同步锁与互斥锁同义,表示并发执行的多个线程,在同一时间内只允许一个线程访问共享数据。
Java中的同步锁: synchronized
16、死锁
死锁
死锁是一种现象:如线程A持有资源x,线程B持有资源y,线程A等待线程B释放资源y,线程B等待线程A释放资源x,两个线程都不释放自己持有的资源,则两个线程都获取不到对方的资源,就会造成死锁。
Java中的死锁不能自行打破,所以线程死锁后,线程不能进行响应。所以一定要注意程序的并发场景,避免造成死锁。
17、锁粗化
锁粗化
是一种优化技术: 如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作都是出现在循环体体之中,就算真的没有线程竞争,频繁地进行互斥同步操作将会导致不必要的性能损耗,所以就采取了一种方案:把加锁的范围扩展(粗化)到整个操作序列的外部,这样加锁解锁的频率就会大大降低,从而减少了性能损耗。
举例:
public void doSomethingMethod(){
synchronized(lock){
//do some thing
}
//这是还有一些代码,做其它不需要同步的工作,但能很快执行完毕
synchronized(lock){
//do other thing
}
}
public void doSomethingMethod(){
//进行锁粗化:整合成一次锁请求、同步、释放
synchronized(lock){
//do some thing
//做其它不需要同步但能很快执行完的工作
//do other thing
}
}
==========================================================
for(int i=0;i<size;i++){
synchronized(lock){ //每次循环都会进行锁的请求、同步与释放。jdk内部会对锁的请求做一些优化
}
}
18、锁消除
锁消除
锁消除
是一种优化技术: 就是把锁干掉。当Java虚拟机运行时发现有些共享数据不会被线程竞争时就可以进行锁消除。
那如何判断共享数据不会被线程竞争?
利用逃逸分析技术
:分析对象的作用域,如果对象在A方法中定义后,被作为参数传递到B方法中,则称为方法逃逸;如果被其他线程访问,则称为线程逃逸。
在堆上的某个数据不会逃逸出去被其他线程访问到,就可以把它当作栈上数据对待,认为它是线程私有的,同步加锁就不需要了。
19、synchronized
synchronized
synchronized
是Java中的关键字:用来修饰方法、对象实例。属于独占锁、悲观锁、可重入锁、非公平锁。
- 1.作用于实例方法时,锁住的是对象的实例(this);
- 2.当作用于静态方法时,锁住的是 Class类,相当于类的一个全局锁, 会锁所有调用该方法的线程;
- 3.synchronized 作用于一个非 NULL的对象实例时,锁住的是所有以该对象为锁的代码块。它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。
每个对象都有个 monitor 对象, 加锁就是在竞争 monitor 对象,代码块加锁是在代码块前后分别加上 monitorenter 和 monitorexit 指令来实现的,方法加锁是通过一个标记位来判断的。
20、Lock和synchronized的区别
自动挡和手动挡的区别
Lock
: 是Java中的接口,可重入锁、悲观锁、独占锁、互斥锁、同步锁。
- 1.Lock需要手动获取锁和释放锁。就好比自动挡和手动挡的区别
- 2.Lock 是一个接口,而 synchronized 是 Java 中的关键字, synchronized 是内置的语言实现。
- 3.synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁。
- 4.Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断。
- 5.通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
- 6.Lock 可以通过实现读写锁提高多个线程进行读操作的效率。
synchronized的优势:
- 足够清晰简单,只需要基础的同步功能时,用synchronized。
- Lock应该确保在finally块中释放锁。如果使用synchronized,JVM确保即使出现异常,锁也能被自动释放。
- 使用Lock时,Java虚拟机很难得知哪些锁对象是由特定线程锁持有的。
21、ReentrantLock 和synchronized的区别
Lock、ReentrantLock、shnchronzied
ReentrantLock
是Java中的类 : 继承了Lock类,可重入锁、悲观锁、独占锁、互斥锁、同步锁。
相同点:
- 1.主要解决共享变量如何安全访问的问题
- 2.都是可重入锁,也叫做递归锁,同一线程可以多次获得同一个锁,
- 3.保证了线程安全的两大特性:可见性、原子性。
不同点:
- 1.ReentrantLock 就像手动汽车,需要显示的调用lock和unlock方法, synchronized 隐式获得释放锁。
- 2.ReentrantLock 可响应中断, synchronized 是不可以响应中断的,ReentrantLock 为处理锁的不可用性提供了更高的灵活性
- 3.ReentrantLock 是 API 级别的, synchronized 是 JVM 级别的
- 4.ReentrantLock 可以实现公平锁、非公平锁,默认非公平锁,synchronized 是非公平锁,且不可更改。
- 5.ReentrantLock 通过 Condition 可以绑定多个条件
面试题13:Java中锁升级
看一段代码:
public class App {
public static void main(String[] args) throws InterruptedException {
Calculate cal = new Calculate();
long start = System.currentTimeMillis();
Thread t1 = new Thread(()->{
for (int i = 0; i < 1000_0000; i++) {
cal.increase();
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 1000_0000; i++) {
cal.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("time = " + (System.currentTimeMillis()-start));
System.out.println("cal.getNum() = " + cal.getNum());
}
}
public class Calculate {
private int num;
public int getNum() {
return num;
}
// 多线程执行:public void increase() 会有线程安全问题
// synchronized 锁解决,锁的是什么?
public void increase(){
synchronized (this) {
num++;
}
}
}
分析:
对象组成:
Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,占用内存大小与虚拟机位长一致。Mark Word对应的类型是markOop
。源码位于markOop.hpp
中。在64位虚拟机下,Mark Word是64bit大小的,其存储结构如下:
对象头在64位虚拟机 8个字节
package cn.wn.juc;
import org.openjdk.jol.info.ClassLayout;
import java.util.concurrent.TimeUnit;
public class App {
// 锁升级演示
public static void main(String[] args) throws InterruptedException {
User user01 = new User();
System.out.println("无状态(001):" + ClassLayout.parseInstance(user01).toPrintable());
// 从jdk6开始,jvm默认延迟4s自动开启开启偏向锁。通过-XX:BiasedLockingStartupDelay=0设置取消延迟
// 如果不要偏向锁:-XX:-UseBiasedLocking=false
TimeUnit.SECONDS.sleep(5);
User user02 = new User();
System.out.println("启用偏向锁(101):" + ClassLayout.parseInstance(user02).toPrintable());
for (int i = 0; i < 2; i++) {
synchronized (user02) {
System.out.println("偏向锁(101)带线程ID:" + ClassLayout.parseInstance(user02).toPrintable());
}
// 偏向锁释放,对象头不会变化,一直存在, (偏向线程id) 下次执行判断是否同一个线程如果是直接执行
System.out.println("偏向锁(101)释放线程ID:" + ClassLayout.parseInstance(user02).toPrintable());
}
// 多个线程加锁,升级为轻量级锁
new Thread(() -> {
synchronized (user02) {
System.out.println("轻量级锁(00):" + ClassLayout.parseInstance(user02).toPrintable());
try {
System.out.println("=====休眠3秒======");
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("轻量-->重量(10):" + ClassLayout.parseInstance(user02).toPrintable());
}
}).start();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> {
synchronized (user02) {
System.out.println("重量级锁(10):" + ClassLayout.parseInstance(user02).toPrintable());
}
}).start();
}
}