深入浅出 Java 多线程

**进程:**指在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程——资源分配的最小单位

线程:系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元执行流。线程——程序执行的最小单位

也就是,进程可以包含多个线程,而线程是程序执行的最小单位。

我把 Java 相关的文章整理成了 PDF ,关注微信公众号 Java后端 回复 666 下载。

1.2 线程的状态

  1. NEW:线程刚创建

  2. RUNNABLE: 在JVM中正在运行的线程,其中运行状态可以有运行中RUNNING和READY两种状态,由系统调度进行状态改变。

  3. BLOCKED: 线程处于阻塞状态,等待监视锁,可以重新进行同步代码块中执行

  4. WAITING : 等待状态

  5. TIMED_WAITING: 调用sleep() join() wait()方法可能导致线程处于等待状态

  6. TERMINATED: 线程执行完毕,已经退出

1.3 Notify 和 wait :

Notify 和 wait 的作用

首先看源码给出的解释,这里翻译了一下:

Notify

唤醒一个正在等待这个对象的线程监控。如果有任何线程正在等待这个对象,那么它们中的一个被选择被唤醒。选择是任意的,发生在执行的酌情权。一个线程等待一个对象通过调用一个{@code wait}方法进行监视。

notify()需要在同步方法或同步块中调用,即在调用前,线程也必须获得该对象的对象级别锁

wait

导致当前线程等待,直到另一个线程调用

{@link java.lang.Object#notify()}方法或

{@link java.lang.Object#notifyAll()}方法。

换句话说,这个方法的行为就像它简单一样

执行调用{@code wait(0)}。

当前线程必须拥有该对象的监视器。线程

释放此监视器的所有权,并等待另一个线程

通知等待该对象的监视器的线程唤醒

通过调用{@code notify}方法或

{@code notifyAll}方法。然后线程等待,直到它可以重新取得监视器的所有权,然后继续执行。

wait()的作用是使当前执行代码的线程进行等待,它是Object类的方法,该方法用来将当前线程置入预执行队列中,并且在wait所在的代码行处停止执行,直到接到通知或被中断为止。在调用wait方法之前,线程必须获得该对象的对象级别锁,即只能在同步方法或同步块中调用wait方法。

wait 和 sleep 的区别。

它们最大本质的区别是:sleep()不释放同步锁,wait()释放同步锁.

还有用法的上的不同是:sleep(milliseconds)可以用时间指定来使他自动醒过来,如果时间不到你只能调用interreput()来强行打断;wait()可以用notify()直接唤起.

wait和sleep的区别还有:

  • 这两个方法来自不同的类分别是Thread和Object

  • 最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。

1.4 Thread.sleep() 和Thread.yield()的异同

1. 相同 : sleep()和yield()都会释放CPU。

2. 不同: sleep()使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会执行;yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。

sleep()可使优先级低的线程得到执行的机会,当然也可以让同优先级和高优先级的线程有执行的机会;yield()只能使同优先级的线程有执行的机会。

1.5 补充:死锁的概念

死锁:指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

死锁产生的四个必要条件(缺一不可):

  1. **互斥条件:**顾名思义,线程对资源的访问是排他性,当该线程释放资源后下一线程才可进行占用

  2. **请求和保持:**简单来说就是自己拿的不放手又等待新的资源到手。

线程T1至少已经保持了一个资源R1占用,但又提出对另一个资源R2请求,而此时,资源R2被其他线程T2占用,于是该线程T1也必须等待,但又对自己保持的资源R1不释放。

  1. **不可剥夺:**在没有使用完资源时,其他线性不能进行剥夺

  2. 循环等待:一直等待对方线程释放资源

我们可以根据死锁的四个必要条件破坏死锁的形成

1.6 补充:并发和并行的区别

**并发:**是指在某个时间段内,多任务交替的执行任务。当有多个线程在操作时,把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行。在一个时间段的线程代码运行时,其它线程处于挂起状

并行:是指同一时刻同时处理多任务的能力。当有多个线程在操作时,cpu同时处理这些线程请求的能力。

区别就在于CPU是否能同时处理所有任务,并发不能,并行能

1.7 补充:线程安全三要素

**原子性:**Atomic包、CAS算法、synchronized、Lock

**可见性:**synchronized、volatile(不能保证原子性)

**有序性:**happens-before规则

1.8 补充:如何实现线程安全

  • **互斥同步:**synchronized、lock

  • **非阻塞同步:**CAS

  • **无需同步的方案:**如果一个方法本来就不涉及共享数据,那它自然就无需任何同步操作去保证正确性

1.9 补充:保证线程安全的机制:

  1. synchronized关键字

  2. lock

  3. CAS、原子变量

  4. ThreadLocl:简单来说就是让每个线程,对同一个变量,都有自己的独有副本,每个线程实际访问的对象都是自己的,自然也就不存在线程安全问题了。

  5. volatile

  6. 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提供四种线程池创建方式:

  1. newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

  2. newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

  3. newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。

  4. 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 LinkedBlockingQueueArrayBlockingQueue的主要区别

  • 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;

最后

我还通过一些渠道整理了一些大厂真实面试主要有:蚂蚁金服、拼多多、阿里云、百度、唯品会、携程、丰巢科技、乐信、软通动力、OPPO、银盛支付、中国平安等初,中级,高级Java面试题集合,附带超详细答案,希望能帮助到大家。

新鲜出炉的蚂蚁金服面经,熬夜整理出来的答案,已有千人收藏

还有专门针对JVM、SPringBoot、SpringCloud、数据库、Linux、缓存、消息中间件、源码等相关面试题。

新鲜出炉的蚂蚁金服面经,熬夜整理出来的答案,已有千人收藏

存取

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;

最后

我还通过一些渠道整理了一些大厂真实面试主要有:蚂蚁金服、拼多多、阿里云、百度、唯品会、携程、丰巢科技、乐信、软通动力、OPPO、银盛支付、中国平安等初,中级,高级Java面试题集合,附带超详细答案,希望能帮助到大家。

[外链图片转存中…(img-e4MIv2jZ-1714739325946)]

还有专门针对JVM、SPringBoot、SpringCloud、数据库、Linux、缓存、消息中间件、源码等相关面试题。

[外链图片转存中…(img-jCedq66F-1714739325947)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值