线程
线程的通信方式
- 共享变量:使用volatile
- 消息传递:使用wait、Notify
- 管道流:输入输出,使用的比较少,使用缓存循环数组实现
进程的通信方式
- 管道
- 信号量
- 消息队列
- 共享内存
- 套接字
线程安全了解吗,线程安全的容器原理,创建线程的方式
线程安全是一份数据,指在多个线程访问下,可以保持一致性和正确性
Concurrent容器线程安全原理
创建线程的方式:
- 继承
Thread类,重写run方法 - 实现
Runnable接口,重写run方法 Thread thread = new Thread(myRunnable); - 使用匿名内部类重写run方法,创建Thread子类对象
- 使用匿名内部类重写run方法,实现Runnable方法
- lambda代替内部类
- 实现Callable接口,
重写call方法,带有返回值,创建FutureTask【传入callable实例,两个构造方法,一个只需要实例,另一个需要泛型的result】,将其传入new thread中 - 使用线程池创建线程,
- 创建方式:
-
- 使用Executors工具类中有可创建多类型的FixedThreadpoll、SingleThreadPool、CachedThreadPool、ScheduleThreadPool。其中
fixed和Single用的都是无界LinkedBlockedQueue,堆积大量任务,会产生OOM问题,Cached使用的同步队列,任务数量多之后速度较慢产生OOM问题。Shcedule用的无界延迟队列问题,堆积请求,OOM问题。总之,都是使用的队列最大长度为INTEGER.MAX_VALUE,会堆积请求,发生OOM问题
- 使用Executors工具类中有可创建多类型的FixedThreadpoll、SingleThreadPool、CachedThreadPool、ScheduleThreadPool。其中
-
- 使用ThreadPoolExecutor创建,有3个核心参数corePoolSize\maximumPoolSize\workQueue。
- 饱和策略:AbortPolicy直接返回拒绝异常,DiscardPolicy直接丢弃,DiscardOldestPolicy丢弃最早未处理的任务,CacheRunsPolicy是调用执行自己的线程运行任务,若执行程序已经被关闭,则丢弃该任务
- 常用的阻塞队列:
SingleThreadExecutorl\FixedThreadPool使用LinkedBlokingQueue无界队列;CachedThreadPool\使用SynchronousQueue同步队列;ScheduleThreadPool\SingleThreadScheduledExecutor使用DelayedWorkQueue。DelayedWorkQueue添加元素满了之后会自动扩容原来容量的 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的区别:
- Votaile可以保证数据可见性,但是不能保证原子性,但是Synchronized都可以
- Votaile用于变量,而Synchronized用在方法、代码块
- Votaile比较轻,效率比Synchronized好一些
- 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的区别:
- Synchronized是java关键字,是JVM层面上的,Lock是API接口
- Synchronized可以再执行完同步代码后或者异常自动释放,而LOCK必须要在finally中执行unlock手动释放,否则就会出现死锁的情况
- Synchronized粒度粗只能锁方法或者代码块,而Lock粒度较细,比如可以锁对象的某个成员变量
- Synchronized是不公平锁,悲观锁,而Lock可以自定义选择公平锁或非公平锁,基于AQS机制
- Synchronized不可以判断锁的属性,而Lock可以通过tryLock判断锁的属性
- Synchronized在获得锁过程中,线程被中断自动抛出InterruptionException异常,而Lock需要手动捕获异常
Lock实现的原理:使用对象的锁进行同步,如果lock的锁被获取,那么其他线程如果想获取则被阻塞,等待锁释放。Lock的锁是与对象无关的,相比Synchronized关键字,具有更好的灵活性,支持可中断的获取锁状态,但是也需要更在关注于手动加锁与释放,因为他不会像Synchronized那样自动释放锁,往往都需要在finally中释放。
比如ReentrantLock就是实现Lock接口的可重入且独占的锁,他里面有个内部类Sync,它继承了AQS,可以通过构造函数的方式指定是非公平锁还是公平锁,其中非公平锁的意思是可以在被阻塞的线程中按优先级执行,但是有可能会发生某个线程一直无法执行的情况。公平锁就是先到先得,但是需要维护时间上下文,所以性能较差。
ReentrantLock与Synchronized的区别:
- ReentrantLock可以实现公平与非公平锁,而Synchronized只能实现非公平锁
- ReentrantLock可实现等待中断,通过lockInterruptibly实现等待的线程放弃等待区做别的事情,而Synchronized不可以
- ReentrantLock是基于API的,而Synchronized是java关键字,在JVM层面的
- 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为饱和策略。
饱和策略:
- AbortPolicy:抛出异常并拒绝新任务
- CallerRunPolicy:调用运行自己的线程执行任务,如果执行程序已被关闭则直接抛弃任务,会影响整体性能
- DiscardPolicy:直接拒绝新任务
- 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)。