【深入问题,拓展广度】JAVA多线程问答

73 阅读12分钟

线程

线程的通信方式

  • 共享变量:使用volatile
  • 消息传递:使用wait、Notify
  • 管道流:输入输出,使用的比较少,使用缓存循环数组实现

进程的通信方式

  • 管道
  • 信号量
  • 消息队列
  • 共享内存
  • 套接字

线程安全了解吗,线程安全的容器原理,创建线程的方式

线程安全是一份数据,指在多个线程访问下,可以保持一致性和正确性

Concurrent容器线程安全原理

创建线程的方式:

  1. 继承Thread类,重写run方法
  2. 实现Runnable接口,重写run方法 Thread thread = new Thread(myRunnable);
  3. 使用匿名内部类重写run方法,创建Thread子类对象
  4. 使用匿名内部类重写run方法,实现Runnable方法
  5. lambda代替内部类
  6. 实现Callable接口,重写call方法,带有返回值,创建FutureTask【传入callable实例,两个构造方法,一个只需要实例,另一个需要泛型的result】,将其传入new thread
  7. 使用线程池创建线程,
  • 创建方式:
    • 使用Executors工具类中有可创建多类型的FixedThreadpoll、SingleThreadPool、CachedThreadPool、ScheduleThreadPool。其中fixed和Single用的都是无界LinkedBlockedQueue,堆积大量任务,会产生OOM问题,Cached使用的同步队列,任务数量多之后速度较慢产生OOM问题。Shcedule用的无界延迟队列问题,堆积请求,OOM问题。总之,都是使用的队列最大长度为INTEGER.MAX_VALUE,会堆积请求,发生OOM问题
    • 使用ThreadPoolExecutor创建,有3个核心参数corePoolSize\maximumPoolSize\workQueue。
  • 饱和策略:AbortPolicy直接返回拒绝异常,DiscardPolicy直接丢弃,DiscardOldestPolicy丢弃最早未处理的任务,CacheRunsPolicy是调用执行自己的线程运行任务,若执行程序已经被关闭,则丢弃该任务
  • 常用的阻塞队列:SingleThreadExecutorl\FixedThreadPool使用LinkedBlokingQueue无界队列CachedThreadPool\使用SynchronousQueue同步队列ScheduleThreadPool\SingleThreadScheduledExecutor使用DelayedWorkQueueDelayedWorkQueue 添加元素满了之后会自动扩容原来容量的 1/2,即永远不会阻塞,最大扩容可达 Integer.MAX_VALUE,所以最多只能创建核心线程数的线程。
多线程,线程池参数以及作用

多线程指的是在一个程序中执行多个独立的任务和操作,每个任务都是由一个线程来完成的。多线程共享堆、方法区,每个线程有自己的程序计数器、本地方法栈、虚拟机栈。进程往往由多个线程组成,这样更适合于我们当下多CPU的处理逻辑,提高了并发量。

线程参数主要有3个重要corePoolSize、maximumPoolSize、workQueue,其中corePoolSize指的是核心线程数,未达到任务队列容量时可以运行的线程数,maximumPoolSize指的是最大线程数,指的是线程达到了任务队列最大容量时,当前可以同时运行的线城市,workQueue指的是来新的线程后,判断线程数是否达到了最大线程数,如果达到了则放入任务队列中。还有其他4个参数,分别是keepAliveTime、unit、ThreadFactory、handler,其中keepAliveTime为线程在大于核心线程数时,多余的空闲线程存活时间,unit为时间单位、ThreadFactory为线程工程、handler为饱和策略。

synchronized底层原理、和lock、volatile的区别

底层原理:

synchronized用于解决多线程之间同步的问题,在java的早期版本之中,synchronized是比较重的,在后续的自旋锁、锁消除、锁粗化、偏向锁、轻量锁的一系列优化减少系统开销,让synchronized的效率提升了许多。

对于synchronized用于代码块的情况,底层是用moniter实现的,其中monitorenter指令指向代码开始的位置,monitorexit指向代码块结束的位置,并且有两个monitorexit从而保证出现异常了也可以释放锁。monitorenter在每次执行后首先判断锁是否为0,若是则+1获锁成功,若不是则获取锁失败。执行monitorexit后首先判断锁是否锁所有者如果是则-1释放锁,若不是则结束

对于synchronized用于方法的情况,底层是用ACC_SYNCHRONIZED标识进而标注是否是一个同步方法lock。JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。如果是实例方法,JVM 会尝试获取实例对象的锁。如果是静态方法,JVM 会尝试获取当前 class 的锁。

Volatile与Synchronized的区别:

  1. Votaile可以保证数据可见性,但是不能保证原子性,但是Synchronized都可以
  2. Votaile用于变量,而Synchronized用在方法、代码块
  3. Votaile比较轻,效率比Synchronized好一些
  4. Votiale使用变量在多个线程之间的可见性,Synchronized用于解决多个线程之间的资源同步性

volatile:volatile可让变量共享可见,让变量从本地内存进入内主存中,指示JVM这个变量是共享不稳定的,每次获取都需要去主内存获取。但是volatile是无法保证原子性的,synchronized既可以保证可见性又可以保证原子性。并且volatile可以防止指令重排序的问题,比如对于使用双重校验锁实现的单例模式中,instance = new Singleton() 在编译后会被分解为 3 个指令,1分配内存空间,2初始化,3赋内存地址,需要对uniqueInstance加上volatile关键字才可以保证顺序,否则线程1执行了分配内存空间和内存地址赋值,线程2调用getInstance发现不为空,但是未被初始化就会出现问题嘞。

public calss Singleton{

    private vlotile static Singleton instance = null;
    
    public Singleton(){
    }
    
    public static Singleton getInstance(){
        if(instance == null){
            synchronized(Singleton.class){
                if(instance == null){
                    return new Singleton();
                }
            }
        }
    }

}

但是volatile不能保证原子性,比如i++实际上是三个操作,读取、加一、写回,所以可以使用Synchronized、AtomicInteger、ReetrantLock进行优化

Lock与Synchronized的区别:

  1. Synchronized是java关键字,是JVM层面上的,Lock是API接口
  2. Synchronized可以再执行完同步代码后或者异常自动释放,而LOCK必须要在finally中执行unlock手动释放,否则就会出现死锁的情况
  3. Synchronized粒度粗只能锁方法或者代码块,而Lock粒度较细,比如可以锁对象的某个成员变量
  4. Synchronized是不公平锁,悲观锁,而Lock可以自定义选择公平锁或非公平锁,基于AQS机制
  5. Synchronized不可以判断锁的属性,而Lock可以通过tryLock判断锁的属性
  6. Synchronized在获得锁过程中,线程被中断自动抛出InterruptionException异常,而Lock需要手动捕获异常

Lock实现的原理:使用对象的锁进行同步,如果lock的锁被获取,那么其他线程如果想获取则被阻塞,等待锁释放。Lock的锁是与对象无关的,相比Synchronized关键字,具有更好的灵活性,支持可中断的获取锁状态,但是也需要更在关注于手动加锁与释放,因为他不会像Synchronized那样自动释放锁,往往都需要在finally中释放。

比如ReentrantLock就是实现Lock接口的可重入且独占的锁,他里面有个内部类Sync,它继承了AQS,可以通过构造函数的方式指定是非公平锁还是公平锁,其中非公平锁的意思是可以在被阻塞的线程中按优先级执行,但是有可能会发生某个线程一直无法执行的情况。公平锁就是先到先得,但是需要维护时间上下文,所以性能较差。

ReentrantLock与Synchronized的区别:

  1. ReentrantLock可以实现公平与非公平锁,而Synchronized只能实现非公平锁
  2. ReentrantLock可实现等待中断,通过lockInterruptibly实现等待的线程放弃等待区做别的事情,而Synchronized不可以
  3. ReentrantLock是基于API的,而Synchronized是java关键字,在JVM层面的
  4. ReentrantLock可以选择通知,使用Condition与newCondition方法实现选择性通知,而Synchronized与wait和notify/notifyAll方法可以实现等待/通知机制,被通知的是由JVM选择的。synchronized(a) 只能有一个等待条件。只能用a.wait(),Lock.NewCondition() 可以有多个等待条件。

AQS原理:全名为抽象队列同步器,他是基于CLH实现的,CLH的全称是由3个人的人名组成,他是一个抽象的双向队列,其中每个节点包含有线程的引用,同步状态,后继节点,前驱节点。AQS使用被volatile修饰的int state作为同步状态,使用内置的FIFO线程等待获取资源。

比如可重入的ReentrantLock就是维护了一个state,他每次占有这个state的线程,如果没有占有到那么就会进入CLH中,如果占用了,而且后面如果需要再次需要,那么就直接累加,这就是可重入锁的提现,也意味着每次释放也要减一。比如CountDownLatch计数器中,对state初始化为子线程的个数n,多个子线程会使用CAS对state减一,直到0后会启动uppark(),主线程从await()中返回,继续执行后续操作。

AQS共有两种共享方式,一种是独占比如reentrantLock一种是共享semaphonre\CountDownLatch。自定义的同步器中,可以同时实现独占+共享两种方式,比如ReentrantReadWriteLock。

Semaphore:Syncrhonized和ReentrantLock都是一次只允许一个线程访问某个资源,Semaphore可以控制同时获得资源的线程数量。若假设有5个线程来获取Semaphore中的资源那么直接final Semaphore semaphore = new Semaphore(5);初始共享数量,每次获得时候semaphore.acquire,每次释放的时候semaphore.release;,剩1的时候就会退化成排他锁。它有两种模式一个是遵循FIFO的公平模式,一个是抢占模式。他的原理是基于AQS,当state>0时,通过CAS获得许可,state--,如果State<=0时就要进入CLH中挂起线程。可以通过如下方式构造,不过Semaphore只限于单机模式,实际中推荐使用Redis+Lua限流。

public Semaphore(int permits, boolean fair){
    sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}

多线程创建,线程池优势,怎么用spring创建线程

多线程可以通过Executor创建,共有4类:

  • FixedThreadExecutor:固定线程数量的线程池,当新任务提交时,如果线程池有空闲线程则执行,没有则阻塞,阻塞队列使用的是LinkedBocledQueue,是个无界队列,会发生OOM问题。
  • SingleThreadExecutor:单个线程数量的线程池,当新任务提交时,如果线程池里是空线程则执行,否则则阻塞,用的也是LinkedBlockedQueue,会发生OOM问题
  • ShceduleThreadExecutor:返回一个给定延迟任务执行的线程池,使用的阻塞队列是无界的DelayQueue延时队列,最大长度是Integer.MAX_VALUE,产生大量任务堆积问题,发生OOM
  • CachedThreadExecutor:线程的数量是跟着线程请求动态的,每当有任务提交,则判断是否有空线程,没有则创建线程,执行任务结束后,若60s没有新任务执行则销毁,使用的SynchronousQueue同步队列,允许的线程数量为Integer.MAX_VALUE,会产生大量任务堆积问题,从而导致OOM问题

使用ThreadPoolExecutor创建,其中的有3个核心参数,corePoolSize、maxmiumPoolSize、workQueue,还有额外4个参数,keepAliveTime、unit、ThreadFactory、handler。workQueue为任务队列,在新任务提交时,判断线程数量是否达到corePoolSize,如果达到了则任务放在任务队列中。在任务队列未达到队列容量时,corePoolSize为最大可以同时执行的线程数,如果达到了任务队列中最大容量,maxmiumPoolSize可以同时运行的最大线程数。keepAliveTime为线程池中的线程大于corePoolSize时,如果没有新的任务提交,存活的时间,unit为时间单元。ThreadFactory为线程工厂,可以为Callable或者Runnable,handler为饱和策略。

饱和策略:

  1. AbortPolicy:抛出异常并拒绝新任务
  2. CallerRunPolicy:调用运行自己的线程执行任务,如果执行程序已被关闭则直接抛弃任务,会影响整体性能
  3. DiscardPolicy:直接拒绝新任务
  4. DiscardOldestPolicy:丢弃最早提交的新任务

线程池的优势:

减少资源的损耗:每次任务提交,会从线程池中拿到线程去执行。 提高线程的可管理性:线程如果无限制的创建,不仅会消耗资源,还会降低系统的稳定性,使用线程池可以进行统一的分配、监控、调优。 降低反应时间:每次提交任务不需要等待线程的创建,直接就可以执行。

线程池处理任务流程:

在提交新任务后,判断线程池中的线程是否达到corePoolSize,如果没达到则新建线程执行任务,如果达到了,则判断workQueue是否已满,如果没满则进入workQueue中阻塞等待,如果满了则判断线程池中的线程数是否达到了maxmiumPoolSize,如果没达到则新建线程执行任务,如果达到了则任务被拒绝按照对应的饱和策略处理。

在Springboot中创建线程池的方式:

首先在启动类上打上@EnableAsync的注解,然后创建一个线程配置类Pool,实现AsyncConfigurer接口,打上@Configuration和@EnabelAsync的注解,在之中可以创建一个返回类型为Executor类型的taskExecutor方法,打上@Bean注解,其中的值即为该线程池的名称。在方法中,在使用ThreadPoolTaskExecutor创建完executor对象后,配置setCorePoolSize、setMaxPoolSize、setQueueCapacity、setKeepAliveSeconds、setRejectedExecutiionHandler、setThreadNamePrefix、以及等待线程结束后关闭线程池的参数,最后返回executor对象。

在使用中对于没有返回值的只需要在其方法上打上@Async注解交给Spring管理即可,如果有返回值则需要实现Future方法,返回new AsyncResult(x),然后调用.get方法即可获得。

在Springboot中创建线程监视器的方式:

创建一个实现了ThreadPoolTaskExecutor接口的类monitor,打上@Configuration注解,在这之中创建一个线程安全的ConcurrentHashMap类型的threadPoolMap用于存储监控信息,在pool中注入monitor类,在bean名称为thread方法中配置monitor.threadMap.put("thread", executor)