1、多线程方法
1.1、创建线程的方式
- 继承Thread类
- 实现Runnable
- 使用Executor框架创建线程池
1.2、线程的生命周期(状态)
新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5 种状态
1.3、sleep()方法与wait()方法区别
- 在等待时wait会释放锁,而sleep会一直持有锁
- wait通常被用于线程间交互,sleep通常被用于暂停执行
1.4、start与run区别
- start方法用于启动线程,run()方法用于执行线程的运行时代码
- run可以重复调用,而start()只能调用一次
1.5、线程常用的方法
- start():开启线程
- stop():停止线程
- join():调用join方法的线程强制执行,其他线程处于阻塞状态,等该线程执行完后,其他线程再执行。
- sleep():在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。休眠的线程进入阻塞状态。
- wait():导致线程等待,进入堵塞状态。该方法要在同步方法或者同步代码块中才使用的
- isAlive():判断线程是否处于活动状态 (线程调用start后,即处于活动状态)
- notify():唤醒当前线程,进入运行状态,该方法要在同步方法或者同步代码块中才使用的
- notifyAll():唤醒所有等待的线程,该方法要在同步方法或者同步代码块中才使用的
- yield():调用yield方法的线程,会礼让其他线程先运行。(大概率其他线程先运行,小概率自己还会运行)
- getPriority():获取当前线程的优先级
- setPriority():设置当前线程的优先级
- interrupt():中断线程
- Thread.currentThead():获取当前线程对象
2、线程池
2.1、线程池原理
- 判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务,如果核心线程都在执行任务,则进入下个流程。
- 线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里,如果工作队列满了,则进入下个流程。
- 判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务,如果已经满了,则交给饱和策略来处理这个任务。
2.2、线程池的创建
Executors类提供了4种不同的线程池:newCachedThreadPool, newFixedThreadPool, newScheduledThreadPool, newSingleThreadExecutor
- newCachedThreadPool:用来创建一个可以无限扩大的线程池,适用于负载较轻的场景,执行短期异步任务。(可以使得任务快速得到执行,因为任务时间执行短,可以很快结束,也不会造成cpu过度切换),用于处理大量短时间工作任务的线程池。
- newFixedThreadPool:创建一个固定大小的线程池,因为采用无界的阻塞队列,所以实际线程数量永远不会变化,适用于负载较重的场景,对当前线程数量进行限制。(保证线程数可控,不会造成线程过多,导致系统负载更为严重)
- newSingleThreadExecutor:创建一个单线程的线程池,适用于需要保证顺序执行各个任务。
- newScheduledThreadPool:适用于执行延时或者周期性任务
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit);
command:执行的任务 Callable或Runnable接口实现类
initialDelay:第一次执行任务延迟时间
period:连续执行任务之间的周期,从上一个任务开始执行时计算延迟多少开始执行下一个任务,
但是还会等上一个任务结束之后。
unit:initialDelay和period时间单位
package com.zhw.learning.thread;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* @author zhw
* 创建一个可定期或者延时执行任务的定长线程池,支持定时及周期性任务执行
*
* Executors.newScheduledThreadPool(3):
* public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
* return new ScheduledThreadPoolExecutor(corePoolSize);
* }
* public ScheduledThreadPoolExecutor(int corePoolSize) {
* super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
* new DelayedWorkQueue());
* }
*/
public class ScheduledThreadPoolTest {
public static void main(String[] args) throws InterruptedException {
final SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
System.out.println("提交时间: " + sdf.format(new Date()));
//延迟3秒钟后执行任务
// scheduledThreadPool.schedule(new Runnable() {
// @Override
// public void run() {
// System.out.println("运行时间: " + sdf.format(new Date()));
// }
// }, 3, TimeUnit.SECONDS);
//延迟1秒钟后每隔3秒执行一次任务
scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("运行时间: " + sdf.format(new Date()));
}
}, 1, 3, TimeUnit.SECONDS);
Thread.sleep(10000);
scheduledThreadPool.shutdown();
}
}
2.3、线程池参数
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize:指定了线程池中核心线程数。
- maximumPoolSize:指定了线程池中的最大线程数量。
- keepAliveTime:当前线程池数量超过 corePoolSize 时,多余的空闲线程的存活时间,即多次时间内会被销毁。
- unit:keepAliveTime 的单位。
- workQueue:任务队列,被提交但尚未被执行的任务。
- threadFactory:线程工厂,用于创建线程,一般用默认的即可。
- handler:拒绝策略,当任务太多来不及处理,如何拒绝任务
2.4、线程池中有哪几种工作队列
- ArrayBlockingQueue:基于数组结构的有界阻塞队列,先进先出原则对元素进行排序
- LinkedBlockingQueue:基于链表结构的无界阻塞队列,先进先出原则对元素进行排序,吞吐量大于ArrayBlockingQueue,方法Executors.newFixedThreadPool使用了这个队列
- SynchronousQueue:是一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态, 吞吐量通常要高于Linked-BlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列
- 一个具有优先级的无限阻塞队列
2.5、线程池工作过程
- 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
- 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
- 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
- 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
- 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
- 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出RejectExecutionException;
- 当一个线程完成任务时,它会从队列中取下一个任务来执行。
- 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
2.6、线程池的好处
- 降低资源消耗。通过复用已存在的线程和降低线程关闭的次数来尽可能降低系统性能损耗;
- 提升系统响应速度。通过复用线程,省去创建线程的过程,因此整体上提升了系统的响应速度;
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,因此,需要使用线程池来管理线程
2.7、线程池之阻塞队列
- 非阻塞队列
- 入队:当队列中数据满的时候,放入数据,数据丢失
- 出队:如果队列中没有数据,取出数据值为null
- 阻塞队列:
- 入队:当队列中的数据满的时候,进行等待,有出队数据的时候,再进行放数据
- 出队:等待,什么时候放进去,再取出来
- 线程池(阻塞队列):当队列中的数据满的时候,进行等待,有出队数据的时候,再进行放数据(先进先出)
2.8、线程池的应用场景
线程池中获取线程异步执行代码
例如:上传文件页面显示上传结果,通过异步执行解析、校验、写入数据库(修改上传状态),再次刷新页面显示上传结果,减少用户的等待时间
2.9、execute()和submit()方法区别
- execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否.
- submit() 方法用于提交需要返回值的任务。线程池会返回一个Future类型的对象,通过这个Future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
2.10、如果提交任务时,线程池已满,会发生什么
- 如果使用的是无界队列LinkedBlockingQueue,继续添加任务到队列等待执行
- 如果使用的是有界队列ArrayBlockingQueue,队列满了,会根据最大线程数增加线程数量,如果增加线程还是处理不过来,会使用拒绝策略
3、锁
3.1、volatile关键字作用
保证变量的可见性,被volatile修饰的变量,如果值发生变化,其他线程立马可见,避免出现脏读现象。
3.2、synchronized和volatile的区别
- volatile是变量修饰符,synchronized是修饰类、方法、代码块
- volatile仅实现变量的修改可见性,不能保证原子性,而synchronized可以保证变量的修改的可见性和原子性
- volatile不会造成线程阻塞,而synchrinized可能会造成线程的阻塞
3.3、synchronized和lock的区别
- synchronized可以给类、方法、代码块加锁;而lock只能给代码块加锁
- synchronized不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁,而lock需要自己加锁和释放锁,如果使用不当没有unLock去释放锁会造成死锁
- lock可以知道有没有成功获取锁,而synchronized却无法办到
- ReentrantLock和Lock作用差不多
3.4、悲观锁与乐观锁
- 悲观锁:每次查询数据都认为别人会修改,所以每次查询数据的时候都会上锁,别人查询数据就会阻塞直到拿到锁(共享资源每次只给一个线程使用,其他的线程阻塞,用完后再把资源转让给其他线程),适用于多写的场景
- 乐观锁:每次查询数据的时候都认为别人不会修改,所以不会上锁,但在更新的时候会判断别人有没有更新数据,会通过版本号机制和cas算法实现,适用于多读的场景
3.5、死锁
当线程A持有独占锁a,并尝试获取独占锁b的同时,线程B持有独占锁b,并尝试获取独占锁a的情况下,就会发生AB两个线程由于互相持有对方的锁,而发生的阻塞现象,叫死锁。(两个或多个线程被永久阻塞)
防止死锁的办法:尽量使用tryLock()方法,设置超时时间,超时可以退出防止死锁
3.6、多线程同步实现方式
Synchronized关键字,Lock锁实现,分布式锁
3.7、怎么唤醒一个阻塞的线程
如果线程是因为调用了wait()、sleep()、join()方法而导致的阻塞,可以中断线程(interrupt()),并且通过抛出InterruptedException来唤醒它;如果线程遇到IO阻塞,那就没办法了
3.8、什么是多线程的上下文切换
CPU的控制权由一个已经正在运行的线程切换到另一个就绪并等待获取CPU执行权的线程过程。
3.9、并发编程的三要素
- 原子性:要么全部执行,要么全部不执行。
- 可见性:多个线程操作一个共享的变量时,其中一个线程对变量进行修改后,其他的线程可立即看到结果
- 有序性:程序的执行顺序按照代码的先后顺序执行