java多线程

java中创建线程方式:

继承Thread类

实现Runnable接口

Callable接口:可以拿到任务的结果;

线程池

不建议用Executors的方式创建线程池;

为什么:newFixThreadPool:LinkedBlockingQueue,(这个队列是无数的),线程池线程数量少,但是进来的线程数量多,导致OOM。

线程池的状态:

  1. RUNNING:正常运行;
  2. SHUTDOWN:正在关闭状态,线程池不会接受新任务,但是会吧队列中的任务处理完;
  3. STOP:停止状态,线程池不会接受新任务,也不会处理队列中的任务;
  4. TIDYING
  5. TERMINATED

线程池核心参数:

corePoolSize:核心线程数;

maximmPoolSize:最大线程数目;

workQueue:阻塞队列;

keepAliveTime:针对救急线程;

handle:拒绝策略;

Sychronized和ReentrantLock不同点:

sychronized ReentrantLock
Java中的关键字 JDK提供的一个类
自动加锁与释放锁 需要手动加锁和释放锁
JVM层面的锁 API层面的锁
非公平锁 公平锁和非公平锁
锁的是对象,锁信息保存在对象头中 int类型和state标识来标识锁状态
底层有锁升级过程 没有锁升级过程

ThreadLocal有哪些应用场景?它底层是如何实现的?

  1. Threadlocal是Java中所提供的线程本地存储机制,可以利用该机制将数据缓存在某个线程内部,该线程可以在任意时刻、任意方法中获取缓存的数据

  2. Threadlocal底层是通过 ThreadLocalMap来实现的,每个 Thread对象(注意不是ThreadLocal对象)中都存在一个 Threadlocalmap,Map的key为 Threadlocal对象,Map的value为需要缓存的值。

  3. 可能导致内存泄漏,(因为是线程池指向),每次使用之后需要使用remove释放掉。

ReentrantLock分为公平锁和非公平锁,那底层分别是如何实现的?

首先不管是公平锁和非公平锁,它们的底层实现都会使用AQS来进行排队,它们的区别在于线程在使用lock0方法加锁时

  1. 如果是公平锁,会先检查AQS队列中是否存在线程在排队,如果有线程在排队,则当前线程也进行排队
  2. 如果是非公平锁,则不会去检查是否有线程在排队,而是直接竞争锁。

Tomcat为什么要自定义类加载器:

针对每一个类,都有一个自定义类加载器,不同的类判别:名字和类加载器,隔离应用。

线程安全的理解:

多个线程运行时,运行结果是否正确,相互之间是否会造成干扰。

守护线程:

JVM的后台线程,垃圾回收线程。

一直在运行,所有线程运行结束之后,结束运行。

死锁:

互斥、占有并等待、非剥夺、循环等待。

谈谈你对AQS的理解,AQS如何实现可重入锁?

  1. AQS是一个JAVA线程同步的框架。是JK中很多锁工具的核心实现框架。
  2. 在AQS中,维护了一个信号量 state和一个线程组成的双向链表队列。其中,这个线程队列,就是用来给线程排队的,而 state就像是一个红绿灯,用来控制线程排队或者放行的。在不同的场景下,有不用的意义。
  3. 在可重入锁这个场下, state就用来表示加锁的次数。0标识无锁,每加次锁, state就加1。释放锁 state就减1。

线程池的底层工作原理:

线程池内部是通过队列+线程实现的,当我们利用线程池执行任务时,

  1. 如果此时线程池中的线程数量小于 core poolsize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
  2. 如果此时线程池中的线程数量等于 corepoolsize,但是缓冲队列 workqueue未满,那么任务被放入缓冲队列。
  3. 如果此时线程池中的线程数量大于等于 corepoolsize,缓冲队列 workqueue满,并且线程池中的数量小于 maximum Poolsize,建新的线程来处理被添加的任努。
  4. 如果此时线程池中的线程数量大于 corepoolsize,缓冲队列 workqueue满,并且线程池中的数量等于 maximum Poolsize,那么通过 handler所指定的策略来处理此任
  5. 当线程池中的线程数量大于 corepoolsize时,如果某线程空闲时间超过 keepalivetime,线程将被终止。这样,线程池可以动态的调整池中的线程数

core不足,来了新任务,创建新线程;

core足够,队列未满,来了新任务,加入队列;

core满了,队列满了,来了新任务,创建新线程。

HashMap的put方法:

  1. 根据key通过哈希算法的“与运算”得到数组下标;

  2. 如果数组下标位置为空,将key和value封装成Entry对象(1.7位Entry,1.8位Node对象);

  3. 如果不为空:

    1.7:是否需要扩容(是否存在key),生成Entry对象,头插法;

    1.8:判断是红黑树Node还是链表Node:

    红黑树;

    链表:尾插法,大于等于8个,需要插入,转换成红黑树。

​ 最后判断是否扩容。

1.7、1.8HashMap区别:

1.7:数组+链表;头插法;

1.8:数组+链表+红黑树;尾插法(判断链表节点个数);

HashMap的扩容机制:

https://zhuanlan.zhihu.com/p/114363420

Colections.synchronized...

Lock vs Synchronized:

都是悲观锁;可重入锁(可以给多个对象加多道锁)

lock:接口,java实现,自己释放(wait、signal)

支持获取等待状态、公平锁、可打断、可超时、多条件变量

有多个场景的实现:ReentranLock,ReentrantReadWriteLock

synchronized:关键字,c++实现;自动释放,

乐观锁和悲观锁:

悲观锁:获取锁失败的线程,进入阻塞状态,(实际上,获取的锁被占用时,或尝试重复操作)

乐观锁:需要多CPU的支持,如果碰到锁被占用,一直尝试。

CAS保证了原子性

compareAndSet:

比如说,n = o + 5;
U.compareAndSetInt(account, BALANCE,o,n);

如果初始值o为10,那么这个会比较是否是15,如果是,那么返回true,如果不是,那么返回False,这样保证了操作的原子性,也就是如果其他线程干扰导致结果不同,那么就返回false,这种情况适用于乐观锁,不断尝试直到获取成功。

但是存在ABA问题,(先+5,再-5,好像没变一样,但是实际其他线程干扰了),使用版本号问题解决。

concurrentHashMap:

1.7:扩容是指segment下面的小数组,segment并不扩容;

1.8:链表扩容迁移时,需要重新创建链表,因为如果碰见查找的时候,应该查找原来的链表,如果按照原链表迁移,那么查找会出现问题,比如它的next节点等等发生改变。

put的时候,在put链表的时候,会阻塞。

posted @ 2023-04-03 15:06  maxzhangxiaotao  阅读(23)  评论(0编辑  收藏  举报