1. 线程和进程的区别
进程:资源分配的基本单位,是程序(硬盘上的QQ.exe)的一个实例,比如双击QQ.exe,程序启动,进入内存
线程:程序执行的基本单位,是一个任务,比如软件管家可以同时清理垃圾和扫描病毒
- 一个进程可以有多个线程
- 进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段。多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,创建/切换线程的开销比进程小
- 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行(管道,信号,消息队列,共享内存,套接字等通信机制)。不过如何处理好同步与互斥是编写多线程程序的难点。
2. 线程间通信
同一进程下的线程虽然能共享全局变量、静态变量等数据是没错,但是由于JMM的限制,一个线程改变全局变量的值,其他线程并不能感知到,因为JMM中有规定,线程不能直接操作主存中的值,只能拷贝到自己的工作内存再进行操作。也就是说大家都知道有个private int a = 5,但是线程一将其改为6后线程二不能得知。这个问题我们在volatile一文中探究过。是的,volatile就是线程间通信的其中一种方式。
- volatile
- 使用Object类的wait() 和 notify() / notifyAll() 方法
- JUC下的一些包(CountDownLatch/ReentrantLock/LockSupport)的锁机制
3. 线程的状态
网上说的很乱,到处乱传,还分什么runnable和running什么的,很多都是错的!本着不造谣不信谣不传谣的原则,我们来看一下源码就知道了
1 | public enum State { |
6种!!没有什么Running状态!!!记好了!!
wait()和sleep()的区别
它们都可以让线程阻塞。不同的是:
- wait方法必须在synchronized保护的代码中使用,而 sleep 方法并没有这个要求
- 在同步代码中执行 sleep 方法时,并不会释放monitor锁,但执行wait方法时会主动释放monitor锁
- sleep方法中会要求必须定义一个时间,时间到期后会主动恢复,而对于没有参数的wait方法而言,意味着永久等待,直到被中断或被唤醒才能恢复,它并不会主动恢复
- wait/notify 是 Object 类的方法,而 sleep 是Thread类的方法
4. start方法到底是什么
为什么要调用start而不是run
1 | public synchronized void start() { |
4. 线程池
- 降低资源消耗:重复利用已创建的线程,降低线程创建和销毁造成的消耗。
- 提高响应速度:当任务到达时,任务可以不需要要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
4.1 线程池的参数
1 | public ThreadPoolExecutor(int corePoolSize, |
corePoolSize:核心线程数线。线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁,除非设置了allowCoreThreadTimeOut。这里的最小线程数量即是corePoolSize
maximumPoolSize:最大线程数。一个任务被提交到线程池以后,首先会找有没有空闲存活线程,如果有则直接执行,如果没有则会缓存到工作队列(后面会介绍)中,如果工作队列满了,才会创建一个新线程,然后从工作队列的头部取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列尾部。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize指定
keepAliveTime:一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定
TimeUnit:时间单位
workQueue:当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中,有四种任务队列
①ArrayBlockingQueue
基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。
②LinkedBlockingQueue
基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到maxPoolSize,因此使用该工作队列时,参数maxPoolSize其实是不起作用的。
③SynchronousQueue
一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。说白了就是个假队列
④PriorityBlockingQueue
具有优先级的无界阻塞队列,优先级通过参数Comparator实现
ThreadFactory:线程工厂,创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等
RejectedExecutionHandler:拒绝策略
- AbortPolicy:抛出
RejectedExecutionException
来拒绝新任务的处理。 - CallerRunsPolicy:调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。(说白了,谁管理任务的,谁就负责帮忙)
- DiscardPolicy:不处理新任务,直接丢弃掉。
- DiscardOldestPolicy:此策略将丢弃最早的未处理的任务请求。
- AbortPolicy:抛出
4.2 常见线程池
newFixedThreadPool
1 | // core和max是一样的 |
newSingleThreadExecutor
1 | // core和max无非都是1而已 |
newCachedThreadPool
1 | // core 0 |
还有些别的比如newScheduledThreadPool(带定时器的线程池),就不介绍了
可以看到这些都太极端了,要么core等于max,要么直接为0,队列也是无界,所以阿里巴巴的规范会提示自定义线程池
4.3 线程池的线程数量怎么确定
- 一般来说,如果是CPU密集型应用,则线程池大小设置为N+1。
- 一般来说,如果是IO密集型应用,则线程池大小设置为2N+1。
- 在IO优化中,线程等待时间所占比例越高,需要越多线程,线程CPU时间所占比例越高,需要越少线程。这样的估算公式可能更适合:最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目
参考
线程间通信:https://blog.csdn.net/jisuanji12306/article/details/86363390
线程的状态:https://blog.csdn.net/pange1991/article/details/53860651