最后
分布式技术专题+面试解析+相关的手写和学习的笔记pdf
还有更多Java笔记分享如下:
1. 相同 : sleep()和yield()都会释放CPU。
2. 不同: sleep()使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会执行;yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。
sleep()可使优先级低的线程得到执行的机会,当然也可以让同优先级和高优先级的线程有执行的机会;yield()只能使同优先级的线程有执行的机会。
1.5 补充:死锁的概念
死锁:指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
死锁产生的四个必要条件(缺一不可):
-
**互斥条件:**顾名思义,线程对资源的访问是排他性,当该线程释放资源后下一线程才可进行占用
-
**请求和保持:**简单来说就是自己拿的不放手又等待新的资源到手。
线程T1至少已经保持了一个资源R1占用,但又提出对另一个资源R2请求,而此时,资源R2被其他线程T2占用,于是该线程T1也必须等待,但又对自己保持的资源R1不释放。
-
**不可剥夺:**在没有使用完资源时,其他线性不能进行剥夺
-
循环等待:一直等待对方线程释放资源
我们可以根据死锁的四个必要条件破坏死锁的形成
1.6 补充:并发和并行的区别
**并发:**是指在某个时间段内,多任务交替的执行任务。当有多个线程在操作时,把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行。在一个时间段的线程代码运行时,其它线程处于挂起状。
并行:是指同一时刻同时处理多任务的能力。当有多个线程在操作时,cpu同时处理这些线程请求的能力。
区别就在于CPU是否能同时处理所有任务,并发不能,并行能
1.7 补充:线程安全三要素
**原子性:**Atomic包、CAS算法、synchronized、Lock
**可见性:**synchronized、volatile(不能保证原子性)
**有序性:**happens-before规则
1.8 补充:如何实现线程安全
-
**互斥同步:**synchronized、lock
-
**非阻塞同步:**CAS
-
**无需同步的方案:**如果一个方法本来就不涉及共享数据,那它自然就无需任何同步操作去保证正确性
1.9 补充:保证线程安全的机制:
-
synchronized关键字
-
lock
-
CAS、原子变量
-
ThreadLocl:简单来说就是让每个线程,对同一个变量,都有自己的独有副本,每个线程实际访问的对象都是自己的,自然也就不存在线程安全问题了。
-
volatile
-
CopyOnWrite写时复制
多线程
随着CPU核心的增多以及互联网迅速发展,单线程的程序处理速度越来越跟不上发展速度和大数据量的增长速度,多线程应运而生,充分利用CPU资源的同时,极大提高了程序处理速度。
2 创建线程的方法
- 继承Thread类
public class ThreadCreateTest {
public static void main(String[] args) {
new MyThread().start();
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + “\t” + Thread.currentThread().getId());
}
}
- 实现Runable接口
public class RunableCreateTest {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
new Thread(runnable).start();
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + “\t” + Thread.currentThread().getId());
}
}
- 通过Callable和Future创建线程
public class CallableCreateTest {
public static void main(String[] args) throws Exception {
MyCallable callable = new MyCallable();
FutureTask futureTask = new FutureTask<>(callable);
new Thread(futureTask).start();
Integer sum = futureTask.get();
System.out.println(Thread.currentThread().getName() + Thread.currentThread().getId() + “=” + sum);
}
}
class MyCallable implements Callable {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName() + “\t” + Thread.currentThread().getId() + “\t” + new Date() + " \tstarting…");
int sum = 0;
for (int i = 0; i <= 100000; i++) {
sum += i;
}
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName() + “\t” + Thread.currentThread().getId() + “\t” + new Date() + " \tover…");
return sum;
}
}
- 线程池方式创建
实现Runnable接口这种方式更受欢迎,因为这不需要继承Thread类。在应用设计中已经继承了别的对象的情况下,这需要多继承(而Java不支持多继承,但可以多实现啊),只能实现接口。同时,线程池也是非常高效的,很容易实现和使用。
实际开发中,阿里巴巴开发插件一直提倡使用线程池创建线程,原因在下方会解释,所以上面的代码我就只简写了一些demo
2.1 线程池创建线程
线程池,顾名思义,线程存放的地方。和数据库连接池一样,存在的目的就是为了较少系统开销,主要有以下几个特点:
-
降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗(主要)。
-
提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
-
提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性。
Java提供四种线程池创建方式:
-
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
-
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
-
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
-
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
通过源码我们得知ThreadPoolExecutor继承自AbstractExecutorService,而AbstractExecutorService实现了ExecutorService
public class ThreadPoolExecutor extends AbstractExecutorService
public abstract class AbstractExecutorService implements ExecutorService
2.2 ThreadPoolExecutor介绍
实际项目中,用的最多的就是ThreadPoolExecutor这个类,而《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 new ThreadPoolExecutor 实例的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
我们从 ThreadPoolExecutor入手多线程创建方式,先看一下线程池创建的最全参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
参数说明如下:
-
corePoolSize: 线程池的核心线程数,即便线程池里没有任何任务,也会有corePoolSize个线程在候着等任务。
-
maximumPoolSize: 最大线程数,不管提交多少任务,线程池里最多工作线程数就是maximumPoolSize。
-
keepAliveTime: 线程的存活时间。当线程池里的线程数大于corePoolSize时,如果等了keepAliveTime时长还没有任务可执行,则线程退出。
-
unit: 这个用来指定keepAliveTime的单位,比如秒:TimeUnit.SECONDS。
-
BlockingQueue: 一个阻塞队列,提交的任务将会被放到这个队列里。
-
threadFactory: 线程工厂,用来创建线程,主要是为了给线程起名字,默认工厂的线程名字:pool-1-thread-3。
-
handler: 拒绝策略,当线程池里线程被耗尽,且队列也满了的时候会调用。
2.2.1BlockingQueue
对于BlockingQueue个人感觉还需要单独拿出来说一下
**BlockingQueue:阻塞队列,有先进先出(注重公平性)和先进后出(注重时效性)两种,常见的有两种阻塞队列:**ArrayBlockingQueue和LinkedBlockingQueue
队列的数据结构大致如图:
队列一端进入,一端输出。而当队列满时,阻塞。BlockingQueue核心方法:1. 放入数据 put2. 获取数据take。常见的两种Queue:
2.2.2 ArrayBlockingQueue
基于数组实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。
一段代码来验证一下:
package map;
import java.util.concurrent.*;
public class MyTestMap {
private static final int maxSize = 5;
public static void main(String[] args){
ArrayBlockingQueue queue = new ArrayBlockingQueue(maxSize);
new Thread(new Productor(queue)).start();
new Thread(new Customer(queue)).start();
}
}
class Customer implements Runnable {
private BlockingQueue queue;
Customer(BlockingQueue queue) {
this.queue = queue;
}
@Override
public void run() {
this.cusume();
}
private void cusume() {
while (true) {
try {
int count = (int) queue.take();
System.out.println(“customer正在消费第” + count + “个商品===”);
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Productor implements Runnable {
private BlockingQueue queue;
private int count = 1;
Productor(BlockingQueue queue) {
this.queue = queue;
}
@Override
public void run() {
this.product();
}
private void product() {
while (true) {
try {
queue.put(count);
System.out.println(“生产者正在生产第” + count + “个商品”);
count++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
2.2.3 LinkedBlockingQueue
基于链表的阻塞队列,内部也维护了一个数据缓冲队列。需要我们注意的是如果构造一个LinkedBlockingQueue对象,而没有指定其容量大小,LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。
2.2.4 LinkedBlockingQueue和ArrayBlockingQueue的主要区别
-
ArrayBlockingQueue的初始化必须传入队列大小,LinkedBlockingQueue则可以不传入
-
ArrayBlockingQueue用一把锁控制并发,LinkedBlockingQueue两把锁控制并发,锁的细粒度更细。即前者生产者消费者进出都是一把锁,后者生产者生产进入是一把锁,消费者消费是另一把锁。
-
ArrayBlockingQueue采用数组的方式存取,LinkedBlockingQueue用Node链表方式存取
2.2.5 handler拒绝策略
java提供了4种丢弃处理的方法,当然你也可以自己实现,主要是要实现接口:RejectedExecutionHandler中的方法
-
**AbortPolicy:**不处理,直接抛出异常。
-
**CallerRunsPolicy:**只用调用者所在线程来运行任务,即提交任务的线程。
-
**DiscardOldestPolicy:**LRU策略,丢弃队列里最近最久不使用的一个任务,并执行当前任务。
-
**DiscardPolicy:**不处理,丢弃掉,不抛出异常。
2.2.6 线程池五种状态
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
-
**RUNNING:**在这个状态的线程池能判断接受新提交的任务,并且也能处理阻塞队列中的任务
-
**SHUTDOWN:**处于关闭的状态,该线程池不能接受新提交的任务,但是可以处理阻塞队列中已经保存的任务,在线程处于RUNNING状态,调用shutdown()方法能切换为该状态。
-
STOP:线程池处于该状态时既不能接受新的任务也不能处理阻塞队列中的任务,并且能中断现在线程中的任务。当线程处于RUNNING和SHUTDOWN状态,调用shutdownNow()方法就可以使线程变为该状态
-
**TIDYING:**在SHUTDOWN状态下阻塞队列为空,且线程中的工作线程数量为0就会进入该状态,当在STOP状态下时,只要线程中的工作线程数量为0就会进入该状态。
-
TERMINATED:在TIDYING状态下调用terminated()方法就会进入该状态。可以认为该状态是最终的终止状态。
回到线程池创建ThreadPoolExecutor,我们了解了这些参数,再来看看ThreadPoolExecutor的内部工作原理:
-
判断核心线程是否已满,是进入队列,否:创建线程
-
判断等待队列是否已满,是:查看线程池是否已满,否:进入等待队列
-
查看线程池是否已满,是:拒绝,否创建线程
2.3 深入理解ThreadPoolExecutor
进入execute方法可以看到:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf© < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning© && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
addWorker方法:
-
创建Worker对象,同时也会实例化一个Thread对象。在创建Worker时会调用threadFactory来创建一个线程。
-
启动启动这个线程
2.3.1线程池中ctl属性的作用是什么?
看源码第一反应就是这个CTL到底是个什么东东?有啥用?一番研究得出如下结论:
最后
俗话说,好学者临池学书,不过网络时代,对于大多数的我们来说,我倒是觉得学习意识的觉醒很重要,这是开始学习的转折点,比如看到对自己方向发展有用的信息,先收藏一波是一波,比如如果你觉得我这篇文章ok,先点赞收藏一波。这样,等真的沉下心来学习,不至于被找资料分散了心神。慢慢来,先从点赞收藏做起,加油吧!
另外,给大家安排了一波学习面试资料:
以上就是本文的全部内容,希望对大家的面试有所帮助,祝大家早日升职加薪迎娶白富美走上人生巅峰!
rker(command, false))
reject(command);
}
addWorker方法:
-
创建Worker对象,同时也会实例化一个Thread对象。在创建Worker时会调用threadFactory来创建一个线程。
-
启动启动这个线程
2.3.1线程池中ctl属性的作用是什么?
看源码第一反应就是这个CTL到底是个什么东东?有啥用?一番研究得出如下结论:
最后
俗话说,好学者临池学书,不过网络时代,对于大多数的我们来说,我倒是觉得学习意识的觉醒很重要,这是开始学习的转折点,比如看到对自己方向发展有用的信息,先收藏一波是一波,比如如果你觉得我这篇文章ok,先点赞收藏一波。这样,等真的沉下心来学习,不至于被找资料分散了心神。慢慢来,先从点赞收藏做起,加油吧!
另外,给大家安排了一波学习面试资料:
[外链图片转存中…(img-nnd75KSN-1714964311897)]
[外链图片转存中…(img-yFA5xMCB-1714964311897)]
以上就是本文的全部内容,希望对大家的面试有所帮助,祝大家早日升职加薪迎娶白富美走上人生巅峰!