多线程_1

参考文档:https://baijiahao.baidu.com/s?id=1601950239093344022&wfr=spider&for=pc

https://www.cnblogs.com/toria/p/11234323.html

https://www.cnblogs.com/qlsem/p/11487783.html

https://www.cnblogs.com/sachen/p/7401959.html

https://blog.csdn.net/yan88888888888888888/article/details/83927609

今天被顺丰的大佬问到一个尖锐的问题:

解释一下你们的程序停止的时候,正在运行的线程要如何处理?

在 Java 中有以下 3 种方法可以终止正在运行的线程:

  1. 使用退出标识,使线程正常退出,也就是当 run() 方法完成后线程终止。
  2. 使用 stop() 方法强行终止线程,但是不推荐使用这个方法,因为 stop() 和 suspend() 及 resume() 一样,都是作废过期的方法,使用它们可能产生不可预料的结果。
  3. 使用 interrupt() 方法中断线程。

  interrupt() 方法的作用是用来停止线程,但 intermpt() 方法的使用效果并不像循环结构中 break 语句那样,可以马上停止循环。调用 intermpt() 方法仅仅是在当前线程中打了一个停止的标记,并不是真的停止线程。 

  interrupted()是静态方法:内部实现是调用的当前线程的isInterrupted(),并且会重置当前线程的中断状态

    isInterrupted()是实例方法,是调用该方法的对象所表示的那个线程的isInterrupted(),不会重置当前线程的中断状态。

  如果在休眠(sleep)状态下停止某一线程则会拋出进入 InterruptedException 异常,所以会进入 catch 语句块清除停止状态值,使之变成 false。

  调用 stop() 方法时会抛出 java.lang.ThreadDeath 异常,但在通常情况下,此异常不需要显式地捕捉。使用 stop() 释放锁将会给数据造成不一致性的结果。

  不过还是建议使用“拋异常”的方法来实现线程的停止,因为在 catch 块中还可以将异常向上拋,使线程停止的事件得以传播 

 

 CountDownLunch和Cyclic Barrier的区别?

  CountDown表示减法计数,Latch表示门闩的意思,计数为0的时候就可以打开门闩了。Cyclic Barrier表示循环的障碍物。两个类都含有这一个意思:对应的线程都完成工作之后再进行下一步动作,也就是大家都准备好之后再进行下一步。然而两者最大的区别是,进行下一步动作的动作实施者是不一样的。这里的“动作实施者”有两种,一种是主线程(即执行main函数),另一种是执行任务的其他线程,后面叫这种线程为“其他线程”,区分于主线程。对于CountDownLatch,当计数为0的时候,下一步的动作实施者是main函数;对于CyclicBarrier,下一步动作实施者是“其他线程”。

 

1.解释下CAS:

  首先CAS是源自操作,CAS属于乐观锁,因为他会一致尝试去修改,并默认的认为会修改成功,CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
  在内存地址V当中,存储着值为10的变量。线程1想把变量的值增加1.对线程1来说,旧的预期值A=10,要修改的新值B=11.
2.什么时候用自旋锁,什么时候用重量级锁?
  自旋锁:会占用CPU,但是处于用户态,不需要加锁解锁,不占用操作系统。
  重量级锁:需要加锁解锁,不占用CPU,什么时候需要调用,什么时候加锁,占用CPU开始执行。
  因此:
    长时间执行,线程数比较多的线程,需要使用系统锁(重量级锁)
    线程数量少,执行时间短的时候,适合使用自旋锁。
synchronized中方法锁,类锁,对象锁
  通过在方法声明中加入synchronized关键字来声明synchronized方法。
  synchronized 方法锁控制对类成员变量的访问:每个类实例对应一把锁,每个synchronized方法都必须获得调用该方法的类实例的”锁“方能执行,否则所属线程阻塞。火车站卖票
  当一个对象中有synchronized method 或synchronized block 的时候,调用此对象的同步方法或进入其同步区域时,就必须先获得对象锁。
如果此对象的对象锁已被其他调用者占用,则需要等待此锁被释放。(方法锁也是对象锁)
  java的所有对象都含有一个互斥锁,这个锁由jvm自动获取和释放。
  线程进入synchronized 方法的时候获取该对象的锁,当然如果已经有线程获取了这个对象的锁,那么当前线程会等待;
  synchronized方法正常返回或者抛异常而终止,jvm会自动释放对象锁。
  类锁(synchronized修饰静态的方法或者代码块)
    由于一个class不论被实例化多少次,其中的静态方法和静态变量在内存中都只有一份。所以,一旦一个静态的方法被声明为synchronized。此类所有的实例对象在调用此方法,共用同一把锁,我们称之为类锁。
  对象锁是用来控制实例方法之间的同步,而类锁是用来控制静态方法(或者静态变量互斥体)之间的同步的。
  java类可能会有很多对象,但是只有一个Class(字节码)对象,也就是说类的不同实例之间共享该类的Class对象。Class对象其实也仅仅是1个java对象,只不过有点特殊而已。
    由于每个java对象都有1个互斥锁,而类的静态方法是需要Class对象。所以所谓的类锁,只不过是Class对象的锁而已。
多线程CAS中ABA问题
   一个值从1变成了2,再从2变成了1,正常情况是不需要关心这个问题的,一定要关系,就可以使用值+版本号一起check。
 ThreadLocal有了解吗,实际工作中的使用?
  每个线程持有一个ThreadLocalMap对象。
  1. 对于某一ThreadLocal来讲,他的索引值i是确定的,在不同线程之间访问时访问的是不同的table数组的同一位置即都为table[i],只不过这个不同线程之间的table是独立的。
  2. 对于同一线程的不同ThreadLocal来讲,这些ThreadLocal实例共享一个table数组,然后每个ThreadLocal实例在table中的索引i是不同的。

ThreadLocal和Synchronized都是为了解决多线程中相同变量的访问冲突问题,不同的点是

  • Synchronized是通过线程等待,牺牲时间来解决访问冲突
  • ThreadLocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突,并且相比于Synchronized,ThreadLocal具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值。

ThreadLocal是否是父子线程共享的,如果是,为什么?如果不是,如何实现子进程可以获取到父进程的内容?

  首先,肯定不是父子线程间共享的,其次如何实现共享?需要使用InheritableThreadLocal这个类,使用InheritableThreadLocal,保存的所有东西在一个新的t.inheritableThreadLocals变量中了。copy父线程parent的map,创建一个新的map赋值给当前线程的inheritableThreadLocals。copy过程中是浅拷贝,key和value都是原来的引用地址。

  过程如下:

    在创建InheritableThreadLocal对象的时候赋值给线程的t.inheritableThreadLocals变量。

    在创建新线程的时候会check父线程中t.inheritableThreadLocals变量是否为null,如果不为null则copy一份ThradLocalMap到子线程的t.inheritableThreadLocals成员变量中去。

    因为覆写了getMap(Thread)和CreateMap()方法,所以get的时候,就可以在getMap(t)的时候就会从t.inheritableThreadLocals中拿到map对象,从而实现了可以拿到父线程ThreadLocal中的值。

  但是在使用线程池的过程中,使用inheritableThreadLocal就会出现问题。

  线程池的特点:

    为了减小创建线程的开销,线程池会缓存已经使用过的线程生命周期统一管理,合理的分配系统资源对于第一点,如果一个子线程已经使用过,并且会set新的值到ThreadLocal中,那么第二个task提交进来的时候还能获得父线程中的值吗?

    线程在执行完毕的时候并没有清除ThreadLocal中的值,导致后面的任务重用已有的threadLocalMap。

  在使用完这个线程的时候清除所有的threadLocalMap,在submit新任务的时候在重新从父线程中copy所有的Entry。然后重新给当前线程的t.inhertableThreadLocal赋值。这样就能够解决在线程池中每一个新的任务都能够获得父线程中ThreadLocal中的值而不受其他任务的影响,因为在生命周期完成的时候会自动clear所有的数据。Alibaba的一个库解决了这个问题github:alibaba/transmittable-thread-local。

synchronized 关键字和 volatile 关键字的区别

  • volatile关键字是线程同步的轻量级实现,所以volatile性能比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块
  • 多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞。、
  • volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性
  • volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。

synchronized和 Lock 的区别?(重要)

1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;

2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁

3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断

4)通过Lock可以知道有没有成功获取锁(tryLock()方法:如果获取锁成功,则返回true),而synchronized却无法办到

5)Lock可以提高多个线程进行读操作的效率

  在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

  Lock的性能要远远优于synchronized,原因:Lock在竞争的时候,默认是非公平的,不是直接放入队列的,如果释放锁的时候,存在cas自旋的线程,会直接去竞争锁,就不需要再去队列中去获取,但是synchronized高并发下是重量级锁,必须要等待被唤醒,才会去从队列中获取线程开始执行。

synchronized和ReentrantLock(重入锁) 的区别?

  • 两者都是可重进入锁,就是能够支持一个线程对资源的重复加锁。sychnronized关键字隐式的支持重进入,比如一个sychnronized修饰的递归方法,在方法执行时,执行线程在获取了锁之后仍能连续多次地获取该锁。ReentrantLock虽然没能像sychnronized关键字一样隐式的重进入,但是在调用lock()方法时,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞
    • 线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于0时表示锁已经成功被释放
  • synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API。ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成)
  • ReentrantLock 比 synchronized 增加了一些高级功能,主要有3点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)
    • ReentrantLock提供了一种能够中断等待锁的线程的机制,也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。通过lock.lockInterruptibly()来实现这个机制。
    • ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。(公平锁就是先等待的线程先获得锁)
    • synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制。ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。用ReentrantLock类结合Condition实例可以实现“选择性通知” 。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程
 AQS是什么?原理?
 
AQS:AbstractQueuedSynchronizer,即队列同步器。它是构建锁或者其他同步组件的基础框架(如ReentrantLock、ReentrantReadWriteLock、Semaphore等)
  AbstractQueuedSynchronizer维护了一个volatile int类型的变量,用户表示当前同步状态。volatile虽然不能保证操作的原子性,但是保证了当前变量state的可见性。
  AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。
  读写锁(ReentrantReadWriteLock): 读锁与读锁可以共享;读锁与写锁不可以共享(排他); 写锁与写锁不可以共享(排他)。
  
    AQS 队列内部维护的是一个 FIFO 的双向链表(每个数据结构都有两个指针),分别指向直接的后继节点和直接前驱节点。可以从任意一个节点开始很方便的访问前驱和后继。每个 Node 其实是由线程封装,当线程争抢锁失败后会封装成 Node 加入到 ASQ 队列中去;当获取锁的线程释放锁以
后,会从队列中唤醒一个阻塞的节点(线程)。
  当出现锁竞争以及释放锁的时候,AQS 同步队列中的节点会发生变化,首先看一下添加节点的场景。
  这里会涉及到两个变化
    1. 新的线程封装成 Node 节点追加到同步队列中,设置 prev 节点以及修改当前节点的前置节点的 next 节点指向自己
    2. 通过 CAS 将 tail 重新指向新的尾部节点(因为存在同时多个线程去修改tail节点,因此需要使用cas,保证当前线程修改成功)
  head 节点表示获取锁成功的节点,当头结点在释放同步状态时,会唤醒后继节点,如果后继节点获得锁成功,会把自己设置为头结点
  这个过程也是涉及到两个变化
    1. 修改 head 节点指向下一个获得锁的节点
    2. 新的获得锁的节点,将 prev 的指针指向 null
  设置 head 节点不需要用 CAS,原因是设置 head 节点是由获得锁的线程来完成的,而同步锁只能由一个线程获得,所以不需要 CAS 保证,只需要把 head 节点设置为原首节点的后继节点,并且断开原 head 节点的 next 引用即可
 线程池的参数?
 
Java线程池的完整构造函数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
//...
}
corePoolSize(线程池基本大小)
当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时,(除了利用提交新任务来创建和启动线程(按需构造),也可以通过 prestartCoreThread() 或 prestartAllCoreThreads() 方法来提前启动线程池中的基本线程。)
maximumPoolSize(线程池最大大小)
线程池里允许有的最大线程数量
keepAliveTime(线程存活保持时间)
当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0
unit
keepAliveTime的时间单位,有7种单位:
TimeUnit.DAYS; //天
TimeUnit.HOURS; //小时
TimeUnit.MINUTES; //分钟
TimeUnit.SECONDS; //秒
TimeUnit.MILLISECONDS; //毫秒
TimeUnit.MICROSECONDS; //微妙
TimeUnit.NANOSECONDS; //月
BlockingQueue<Runnable> workQueue
用于传输和保存等待执行任务的阻塞队列。
threadFactory(线程工厂)
用于创建新线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。
handler(线程饱和策略)
当线程,队列都满了,之后采取的策略,比如抛出异常等策略
○ ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
○ ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,不做任何处理
○ ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务
○ ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
 

线程池都有哪几种工作队列?(重要)

阻塞队列

  • ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序。
  • LinkedBlockingQueue:是一个基于链表结构的无界阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
  • SynchronousQueue:是一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于Linked-BlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
  • PriorityBlockingQueue:一个具有优先级的无界阻塞队列
 线程池的种类:
newCachedThreadPool:
  • 通俗:当有新任务到来,则插入到SynchronousQueue中,由于SynchronousQueue是同步队列,因此会在池中寻找可用线程来执行,若有可以线程则执行,若没有可用线程则创建一个线程来执行该任务;若池中线程空闲时间超过指定大小,则该线程会被销毁。
  • 适用:执行很多短期异步的小程序或者负载较轻的服务器

newFixedThreadPool:

  • 底层:返回ThreadPoolExecutor实例,接收参数为所设定线程数量nThread,corePoolSize为nThread,maximumPoolSize为nThread;keepAliveTime为0L(不限时);unit为:TimeUnit.MILLISECONDS;WorkQueue为:new LinkedBlockingQueue<Runnable>() 无解阻塞队列
  • 通俗:创建可容纳固定数量线程的池子,每隔线程的存活时间是无限的,当池子满了就不在添加线程了;如果池中的所有线程均在繁忙状态,对于新任务会进入阻塞队列中(无界的阻塞队列)
  • 适用:执行长期的任务,性能好很多

newSingleThreadExecutor:

  • 底层:FinalizableDelegatedExecutorService包装的ThreadPoolExecutor实例,corePoolSize为1;maximumPoolSize为1;keepAliveTime为0L;unit为:TimeUnit.MILLISECONDS;workQueue为:new LinkedBlockingQueue<Runnable>() 无解阻塞队列
  • 通俗:创建只有一个线程的线程池,且线程的存活时间是无限的;当该线程正繁忙时,对于新任务会进入阻塞队列中(无界的阻塞队列)
  • 适用:一个任务一个任务执行的场景

NewScheduledThreadPool:

  • 底层:创建ScheduledThreadPoolExecutor实例,corePoolSize为传递来的参数,maximumPoolSize为Integer.MAX_VALUE;keepAliveTime为0;unit为:TimeUnit.NANOSECONDS;workQueue为:new DelayedWorkQueue() 一个按超时时间升序排序的队列
  • 通俗:创建一个固定大小的线程池,线程池内线程存活时间无限制,线程池可以支持定时及周期性任务执行,如果所有线程均处于繁忙状态,对于新任务会进入DelayedWorkQueue队列中,这是一种按照超时时间排序的队列结构
  • 适用:周期性执行任务的场景
 创建线程池的方式:
public static void main(String[] args) {
    ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
    for (int i = 0; i < 10; i++) {
     final int index = i;
     try {
     Thread.sleep(index * 1000);
    } catch (InterruptedException e) {
     e.printStackTrace();
    }
    cachedThreadPool.execute(new Runnable() {
     public void run() {
      System.out.println(index);
     }
    });
   }

public static void main(String[] args) {
  ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
  for (int i = 0; i < 10; i++) {
   final int index = i;
   fixedThreadPool.execute(new Runnable() {
    public void run() {
     try {
      System.out.println(index);
      Thread.sleep(2000);
     } catch (InterruptedException e) {
      e.printStackTrace();
     }
    }
   });
  }

这样创建线程池存在OOM的风险,因为LinkedBlockingQueue:是一个基于链表结构的无界阻塞队列,也就是队列可以存在无数个等待线程。正确的打开方式是:

private static ExecutorService executor = new ThreadPoolExecutor(10, 10,
        60L, TimeUnit.SECONDS,
        new ArrayBlockingQueue(10));

或者:

public class ExecutorsDemo {
 
    private static ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
        .setNameFormat("demo-pool-%d").build();
 
    private static ExecutorService pool = new ThreadPoolExecutor(5, 200,
        0L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());
 
    public static void main(String[] args) {
 
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            pool.execute(new SubThread());
        }
    }
}

 

 
posted @ 2020-11-27 19:15  c++c鸟  阅读(112)  评论(0编辑  收藏  举报