多线程难点

多线程问题

守护线程

通过setDaemon(true)方法设置为 true,将普通线程转换为守护线程。

DDOS攻击怎么解决

DDOS攻击指控制多台机器对于服务器进行大幅度统一攻击导致服务器异常。
1.使用负载均衡(Nignx)去处理,降低单机的压力
2.使用限流算法,对整个接口进行限流,对ip进行限流
3.加入请求验证码,如果ip大量请求加入验证码
4.封闭多请求IP
join的作用(实现三个线程顺序执行)
针对线程T1,T2,在T2代码里面提前写入T1.join,这样T2在T1.join后面的代码必须等到T1执行完
notifyAll和notify区别和作用,唤醒wait的线程,一个是所有,一个是随机唤醒一个
wait必须在获取锁的块里面使用,其本质就是将已经获取的到锁当时没有获取到其他的资源的线程做一个等待放弃锁和cpu使用权

打断线程的几种方式

1.interrupt,可以在线程对象设置这个参数,然后再Thread的run代码判断,如果这个true了就停止
2.stop,这个不推荐,因为一次性释放锁,可能导致数据不一致性问题
3.正常停止
synchronized的底层原理
通过对象监控锁进行锁定monitor、monitorenter锁定、monitorexit解锁,synchronized有两次解锁,就是为了避免程序异常导致解锁失败

锁升级:首先是偏向锁,也就是没有竞争认为只有某一个线程用,在线程栈中存储一个对象和锁的对象头关联,通过CAS设置值来锁定
如果发生竞争就马上升级为轻量级锁,这个锁由之前执行线程拥有,而后续竞争的线程就会自旋CAS等待锁的释放。
当自旋到一定时间,或者竞争加大,就会变成重量级锁,通过对象监控器来锁定,将竞争线程变为阻塞等待锁的释放
Monitor的原理
monitor是jvm层次的对象,里面有三个变量,一个是当前持有的对象,一个是阻塞的对象列表,一个是对象等待列表
当持有对象释放锁,就从阻塞对象列表里面抢占锁,如果调用notify就从等待对象中唤醒抢占,同时不成功就进阻塞
这个属于重量级锁
创建线程的方式(4种)
1.集成Tread
2.实现runnable,这是任务实际上还是放到Thread执行
3.实现Callable,这是任务实际上还是放到Thread执行
4.线程池创建,创建runnable或者callable丢到Thread执行
线程上下文
线程在cpu上进行切换的时候,需要记住执行的位置和环境,这就是线程上下文。如果线程数量太多切换次数过多,就会导致大量计算时间被浪费在切换这个过程中,应该针对cpu核心数,和线程是cpu处理密集、IO密集类型进行设置县城数
死锁产生的条件(4)和解决方式
1.线程资源只能由一个资源持有
2.线程获取资源后阻塞会一直持有这个资源
3.不可以强行剥夺线程资源
4.若干线程之间形成一种头尾相接的循环等待资源关系

解决方式:
1.针对2,线程如果阻塞就放弃资源,从头获取
2.针对3,对资源按统一顺序获取,必须获取第一个才能继续获取第二个,释放资源则反序释放
3.一次性获取所有资源,不然就获取不到释放

wait和sleep的差异和原因
sleep不释放资源,wait释放资源和锁
wait是在对象锁上使用,处理的是获取了当前对象锁的线程,而sleep直接在对象使用,处理的是使用了sleep方法的对象
wait必须主动去唤醒,使用notify(),notifyAll()

用户线程和内核线程的关系和对应用程序影响
用户线程不能用多核,内核可以,所以IO读写时候本质上线程态变化,就是吧用户态线程变成调用内核态线程
JVM线程和内核线程的关系
java线程和内核线程 有1对1 n-1 n-n,一个java线程对应的就是一个内核线程,如果是所有java线程对应一个内核线程,那么一个线程阻塞将会导致进程阻塞,

用户态和内核态,不只是线程也是cpu状态,用户态的时候执行的是业务逻辑,而对系统的操作,必须交给内核线程去做处理。
凡是与共享资源有关的操作(比如内存分配、I/O 操作、文件管理等),都必须通过系统调用的方式向操作系统内核提出请求,由操作系统内核代为完成。内核线程创建消耗大

所以用户线程和内核线程有:
多对1 没人用了,不能并发,会阻塞死,虽然用户线程可以用,但是对系统调用直接阻塞,连内存都分配不了能干啥呢
1对1 java用的,可以并发,用户线程和内核线程一起生死,
多对多 java后面优化实际上就是内核线程一致,但是用户线程不受限制

后面java就根据操作系统来确定了,但是没有操作系统使用多对1了
volatile的本质
本质是告诉线程,当前变量禁用cpu缓存,直接去内存读,同时对这个变量的操作禁止指令重排
在JMM中是每次直接去主内存中读,不去工作空间读
指令重排的机制原理和影响(cpu和内存缓存机制)
指令重排是jvm对于代码执行顺序指令的一种优化,也可以是多核情况下对指令的并行,针对jvm认为不影响结果的执行进行重排序,加快速度
指令重排有编译器重排、处理器重排、内存重排(主存和cpu内存不一致)

计算机存储本身有硬盘、内存和cpu缓存,cpu缓存是为了隔离cpu速度和内存速度不一致而产生的。所有数据要先加载到cpu缓存中,cpu处理后同步到内存。这样在新计算机时代的多核cpu情况下,多核cpu有多个自己的cpu缓存,这会导致多线程的时候,一个cpu读取的数据刚读完就被另一个cpu给修改了,这样就出现了缓存不一致的问题。volatile就是解决这个问题,映射到JMM就是本地内存和主内存的问题。同样为了保持缓存一致性有了很多协议。
JMM主内存和本地内存的概念
JMM是java内存模型,针对于缓存,因为java基于不同平台有着可移植性和cpu缓存机制,所以他自己虚拟实现了一个针对线程的内存模型。
线程分有本地内存和主内存,对象实例,共享变量,类信息等都在主内存中,而线程只能操作自己的本地内存,本地内存是主内存中共享变量的副本。当线程要读取/操作变量的时候,先要讲主内存的共享变量锁定变成独占线程,然后加载值进入自己的本地内存创建副本,然后通过执行引擎assign赋值给引用,一般来说不会即时的写回值,但是volatile对象会马上写回,如果要写回值,必须要有赋值操作,做完处理后写回内存,然后清空其他线程的对该对象本地内存,必须要解锁后其他线程才能接着处理。
指令重排的-happen-before的原则
happen-before本质就是发生在前,也就两个操作发生顺序的定义。首先如果一个操作A,happen-before在另一个操作B之前,那么就说明A执行的结果对于B是可见的。
而JMM对于重排序处理规则是,如果两个操作没有happen-before那么就可以重排,或者两个操作有happen-before但是重排后不影响结果那么也可以重排。
大多数情况下重排只对单线程的代码happen-before负责,但是其实多线程也是遵循,只是针对的是主线程和子线程之间的happen-before

原子性、可见性、顺序性(多线程问题的产生原因)
原子性:线程操作对象的时候并不是原子的,比如int i=1; i=3;第二是读取i地址,然后给i的内存修改为3,如果有一个判读是当i!=3的时候去做处理,因为不是原子的,所以在第一步后面就做了判断,这个时候i==1,所以除了程序异常

可见性:因为有cpu缓存和主内存的区分,线程修改共享变量的时候不是即时可见的,这种情况下,i = 3 可能修改后没有刷新其他线程的本地内存,导致i还是历史数据

顺序性:比如int i=1;首先要分配内存,初始化对象为1,然后将对象地址赋值给i,然后进行三步,如果不用volatile修饰或者加锁,在多线程的时候进行了可能有雨指令重排,其他线程拿到的i是没有初始化成功的,也就是i!=1这样旧数据异常了
单例模式的实现(双重校验锁)
// 懒汉式
public class OneObject{
  
  // 使用volatile禁止指令重排,因为创建对象有三步 1.分配内存 2.初始化 3.复制给引用 如果是指令重排有可能1-3-2 这样就会导致多线程的时候get获取到一个
  private static volatile OneObject oneObject;
  
  public static OneObject get() {
    // 判断对象是否为空,如果不为空直接跳过获取锁的代价
    if(oneObject == null) {
      synchronized(OneObject.class) {
        // 主要是避免当前线程拿到锁了,但是在之前已经有另外一个线程提前获取锁创建了
        if(oneObject == null) {
          oneObject = new OneObject();
        }
      }
    }
    return oneObject;
  } 
  
}

AQS(AbstractQueuedSynchronizer)实现
AQS的本质就是维护一个数据变state和一个CLH队列,通过这个实现独占锁、共享锁两种锁的方式

1.ReentrantLock(独占锁)
将state初始值设置为0,如果cas设置1成功就是获取锁,其他的先线程就会放到CLH中阻塞,等待线程唤醒。重入特质就是一直在state上面自增1,然后释放的时候就对应的释放,等到为0的时候就唤醒下一个线程

2.CountDownLatch(共享锁)
将state设置成对应线程数n,当一个线程执行到coutDown的时候就去对state进行cas -1 ,然后等state等于0的时候就是所有线程执行完。就调用await()方法

AQS可以有公平和非公平锁,公平的话就是直接进队列里面,非公平的话就是先抢锁不行再进队列

AQS为什么是双向队列

因为他吧CLH的自旋变阻塞了,要解除阻塞
CLH队列锁
AQS的CLH是一个双向队列,用于AQS中阻塞等待的队列,有前后的节点和线程的状态,在AQS实际上是进行扩展的,首先让前后node显式出来了,同时把自旋变成阻塞,因为阻塞,所以换起后面线程执行是前个线程解锁的时候,而不是后线程自旋的时候


其实原生CLH就是一个链表,基于自旋锁变化而来,自旋锁会有1.有的线程可能长时间获取不多锁、2.线程竞争一个变量导致性能开销大这两个问题。而为了解决这两个问题,就有了CLH。

CLH的原理是一个链表,优先由第一个变量获取到锁,而后面的一个链表获取锁依赖前一个变量的锁状态,如果前置node释放了锁就自动获取锁,然后后面依次如此。第一解决了不公平分配锁的问题,第二个竞争的不再是一个变量,而是各有各的变量,减少了消耗,而且不需要cas降低了消耗

性能优异,获取和释放锁开销小。CLH 的锁状态不再是单一的原子变量,而是分散在每个节点的状态中,降低了自旋锁在竞争激烈时频繁同步的开销。在释放锁的开销也因为不需要使用 CAS 指令而降低了。
公平锁。先入队的线程会先得到锁。
实现简单,易于理解。



CLH队列锁实现
public class CLHLock{
  private final ThreadLock<node> cur = ThreadLocal.withInitial(Node::new);
  private final AtomicReference<Node> tail = new AtomicReference<>(new Node());
	public static class Node{
    // 这个volatile目的是为禁止指令排序,避免多线程情况下读取到不可用的数据lock
    private volatile boolean lock;
  }
  
  public void lock() {
    // 给自己加上锁定状态
    cur.get().lock = true; 
    // 讲tail设置成最新锁定的数据 同时去获取之前的tail就是前缀
    Node pre= tail.getAndSet(cur.get());
    // 自循环获取前一个节点lock状态,直到释放才标识锁定完成
    while(pre.lock) ;
  }
  
  public void unlock() {
    cur.get().lock = false;
    // 将当前的node刷新掉,避免出现死锁,因为如果不刷新,当旧node再lock的时候就会出先T1.pre ->T2 T2.pre = T1,T2等待T1变为false,T1等待T2变成false
    cur.set(new Node());
  }
  
}
CountDownLatch实现
用来将多个线程阻塞在一个节点,直到所有的线程被处理了才进行下一步
原理是通过state赋值为线程数量,当线程执行完的时候调用countDown()就将state通过CAS减1,直到state=0的时候调用await()方法执行下面的操作
CyclicBarrier
循环线程屏障,用来将多个线程阻塞到一道屏障前,再让线程一起执行。
通过count计数器来计数到了调用CyclicBarrier对象的awit()方法的线程数量,当到达创对象的数量时候才执行下线程的下一步代码
设置state=n,当一个线程执行到了屏障就-1,直到为0
Semaphore简单了解
用来限制一个共享资源的数量
其原理是用AQS的state属性(volatile修饰)的值作为许可证,只有通过CAS获取到许可证的线程才可以获取到共享资源锁,所以创建的时候必须指定数量
Condition、ReentrantLock、synchronized怎么实现通知机制
可中断锁(ReentrantLock锁等待可以中断去做其他事情)、不可中断锁(synchronized不可以中断做其他事情)
公平锁(先申请的先拿到锁)、不公平锁(synchronized其他排序或者优先级进行拿锁)
ThreadLocal的作用和原理
ThreadLocal用来分割线程的变量,也就是每个线程可以通过ThreadLocal存储自己的变量
实现是通过ThreadLocal的set方法实现的,这个方法的本质Thread对象里面有个map,ThreadLocal的set就是将自身作为key,value作为值存储进去
ThreadLocal的内存泄漏问题
因为ThreadLocal是弱引用,而value是强引用,如果没有外界强引用的情况下,可能导致ThreadLocal被回收了,但是value没有,最后内存泄漏
乐观锁-CAS算法-ABA问题和自循环过得会导致性能问题
乐观锁是默认不加锁,在修改前用原值或者版本号判断是否值没有被其他线程变更多,如果没有就修改,有就重试
ABA问题:就是其他线程将A变为B然后变为A,这样我们CAS的时候就发觉不了,解决办法是加入时间搓或者加入编辑版本号

线程池

Executor线程池的种类(4种)
1.SingleThreadExecutor单例线程池,任务队列无限(link队列)
2.CachedThreadPool无限创建线程线程池,没有任务队列
3.FixedThreadPool 定额线程线程池,任务队列无限
4.ScheduledThreadPool定时线程池:给定的延迟后运行任务或者定期执行任务的线程池。(延迟阻塞队列,根据延期时间吐出,使用堆数据结构)
为什么不能用内置Executor的来创建线程,而是用ThreadPoolExecutor自己创建
第一增强程序员对线程池的理解,避免资源被耗尽
线程池的重要参数(7个)
核心线程数
最大线程数
线程等待时间
时间单位
任务队列
拒绝策略:
线程工厂: 创建新线程的时候会用到,使用这个去创建线程

线程池执行任务的任务队列饱和策略(4种),线程最大了,而且任务满了
1.直接拒绝新任务,但是不抛异常
2.抛出异常并且拒绝新任务
2.直接删除最早的任务
3.用任务当前线程执行新任务

怎么实现线程池的根据优先级处理
PriorityBlockingQueue 使用优先级阻塞队列
线程池最佳实践(后期)

阻塞队列和非阻塞队列区别
阻塞队列就是当队列是空的时候或者队列满的时候,线程会阻塞 使用锁实现
非阻塞队列在上两种情况就是返回null或者报错 使用CAS实现
BlockingQueue(典型阻塞队列,用加锁实现)、ConcurrentLinkedQueue(典型非阻塞队列,用CAS实现)
阻塞队列几种类型(后期)
ConcurrentSkipListMap跳表实现(后期)
阅读(后期)
https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html
https://mp.weixin.qq.com/s?__biz=Mzg3NjU3NTkwMQ==&mid=2247505103&idx=1&sn=a041dbec689cec4f1bbc99220baa7219&source=41#wechat_redirect
https://www.cnblogs.com/waterystone/p/4920797.html
Future的使用和Callable、Runnable区别
Future是一个可以判断线程返回结果和判断是否执行完成,一般使用futureTask,使用有两种方式,第一把Callable、Runnable放入futureTask中获取结果,还有一个就是线程池提交任务后,用线程池对象.sumbit()获取到futureTask


CountDownLatch使用
本质上就是多线程加速,并且获取到一致性的执行结果
控制业务的线程并发数
通过nignx用流量控制法去处理数据数量
通过Semaphore的AQS的方式去设置共享线程锁,锁定线程数量
JDBC链接跟线程关系
每个线程都有自己的数据库连接,通过ThreadLocal存储,就是为避免数据库出现串丽娜姐操作的问题
ReentrantLock的原理和底层
实际上还是通过AQS的state变量(CAS)和CHL队列实现

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值