深入浅出 Java 多线程(1)

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;

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的内部工作原理:

  1. 判断核心线程是否已满,是进入队列,否:创建线程

  2. 判断等待队列是否已满,是:查看线程池是否已满,否:进入等待队列

  3. 查看线程池是否已满,是:拒绝,否创建线程

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到底是个什么东东?有啥用?一番研究得出如下结论:

ctl属性包含两个概念:

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

private static int ctlOf(int rs, int wc) { return rs | wc; }

  1. **runState:**即rs 表明当前线程池的状态,是否处于Running,Shutdown,Stop,Tidying,

  2. **workerCount:**即wc 表明当前有效的线程数

我们点击workerCount即工作状态记录值,以RUNNING为例,RUNNING = -1 << COUNT_BITS;,即-1无符号左移COUNT_BITS位,进一步我们得知COUNT_BITS位29,因为Integer位数为31位(2的五次方减一)

private static final int COUNT_BITS = Integer.SIZE - 3;

既然是29位那么就是Running的值为

1110 0000 0000 0000 0000 0000 0000 0000  ||| 31~29位

那低28位呢,就是记录当前线程的总线数啦

private static int runStateOf(int c) { return c & ~CAPACITY; }

private static int workerCountOf(int c) { return c & CAPACITY; }

private static int ctlOf(int rs, int wc) { return rs | wc; }

从上述代码可以看到workerCountOf这个函数传入ctl之后,是通过ctl&CAPACITY操作来获取当前运行线程总数的。也就是RunningState|WorkCount&CAPACITY,算出来的就是低28位的值。因为CAPACITY得到的就是高3位(29-31位)位0,低28位(0-28位)都是1,所以得到的就是ctl中第28位的值。

而runStateOf这个方法的话,算的就是RunningState|WorkCount&CAPACITY,高3位的值,因为CAPACITY是CAPACITY的取反,所以得到的就是高3位(29-31位)为1,低28位(0-28位)为0,所以通过&运算后,所得到的值就是高3位的值。

简单来说就是ctl中是高3位作为状态值,低28位作为线程总数值来进行存储。

总之这是个

2.3.2 shutdownNow和shutdown的区别

看源码发现有两种近乎一样的方法,shutdownNow和shutdown,设计者这么设计自然是有它的道理,那么这两个方法的区别在哪呢?

shutdown会把线程池的状态改为SHUTDOWN,而shutdownNow把当前线程池状态改为STOP

shutdown只会中断所有空闲的线程,而shutdownNow会中断所有的线程。

shutdown返回方法为空,会将当前任务队列中的所有任务执行完毕;而shutdownNow把任务队列中的所有任务都取出来返回。

2.3.3 线程复用原理

final void runWorker(Worker w) {

Thread wt = Thread.currentThread();

Runnable task = w.firstTask;

w.firstTask = null;

w.unlock();

boolean completedAbruptly = true;

try {

while (task != null || (task = getTask()) != null) {

w.lock();

if ((runStateAtLeast(ctl.get(), STOP) ||

(Thread.interrupted() &&

runStateAtLeast(ctl.get(), STOP))) &&

!wt.isInterrupted())

wt.interrupt();

try {

beforeExecute(wt, task);

Throwable thrown = null;

try {

task.run();

} catch (RuntimeException x) {

thrown = x; throw x;

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

言尽于此,完结

无论是一个初级的 coder,高级的程序员,还是顶级的系统架构师,应该都有深刻的领会到设计模式的重要性。

  • 第一,设计模式能让专业人之间交流方便,如下:

程序员A:这里我用了XXX设计模式

程序员B:那我大致了解你程序的设计思路了

  • 第二,易维护

项目经理:今天客户有这样一个需求…

程序员:明白了,这里我使用了XXX设计模式,所以改起来很快

  • 第三,设计模式是编程经验的总结

程序员A:B,你怎么想到要这样去构建你的代码

程序员B:在我学习了XXX设计模式之后,好像自然而然就感觉这样写能避免一些问题

  • 第四,学习设计模式并不是必须的

程序员A:B,你这段代码使用的是XXX设计模式对吗?

程序员B:不好意思,我没有学习过设计模式,但是我的经验告诉我是这样写的

image

从设计思想解读开源框架,一步一步到Spring、Spring5、SpringMVC、MyBatis等源码解读,我都已收集整理全套,篇幅有限,这块只是详细的解说了23种设计模式,整理的文件如下图一览无余!

image

搜集费时费力,能看到此处的都是真爱!
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
取!!(备注Java获取)**

img

言尽于此,完结

无论是一个初级的 coder,高级的程序员,还是顶级的系统架构师,应该都有深刻的领会到设计模式的重要性。

  • 第一,设计模式能让专业人之间交流方便,如下:

程序员A:这里我用了XXX设计模式

程序员B:那我大致了解你程序的设计思路了

  • 第二,易维护

项目经理:今天客户有这样一个需求…

程序员:明白了,这里我使用了XXX设计模式,所以改起来很快

  • 第三,设计模式是编程经验的总结

程序员A:B,你怎么想到要这样去构建你的代码

程序员B:在我学习了XXX设计模式之后,好像自然而然就感觉这样写能避免一些问题

  • 第四,学习设计模式并不是必须的

程序员A:B,你这段代码使用的是XXX设计模式对吗?

程序员B:不好意思,我没有学习过设计模式,但是我的经验告诉我是这样写的

[外链图片转存中…(img-u9PIzaiy-1713747700209)]

从设计思想解读开源框架,一步一步到Spring、Spring5、SpringMVC、MyBatis等源码解读,我都已收集整理全套,篇幅有限,这块只是详细的解说了23种设计模式,整理的文件如下图一览无余!

[外链图片转存中…(img-Fpy7rf5F-1713747700209)]

搜集费时费力,能看到此处的都是真爱!
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 9
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值