1、进程与线程的区别
- 进程:在内存中分配自己独立的运行空间,彼此之间不会相互影响。这种独立的应用空间称为进程。也可以通用的理解为进程就是一个运行的程序
- 线程:位于进程中,负责当前进程中某个具备独立运行资格的空间。一个进程中至少有一个线程。
2、引入多线程目的
- 更加合理的利用计算机资源
- 提高程序运行效率
3、synchronized的缺点(一般与Lock做比较)
- synchronized是java的关键字,如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行了代码块时,其他线程只能一直等待,直到该线程释放锁
- 无法实现读写锁分离,如果一个线程获取锁,其他线程将无法再次获取
3、Lock与synchronized的区别
- Lock是java语言申明的接口,由jdk1.5引入,synchronize是java关键字
- Lock需要手动释放锁,而synchronized不需要手动释放,代码执行完毕或者抛出异常,jvm会要求线程主动释放锁
- Lock支持更多的获取锁机制,如可以设置等待时长(tryLock(longtime,unit)),或者非阻塞获取锁(tryLock)或者可设置中断(lockinterruptibly)
- 通过Lock可以轻松实现读写锁分离
- synchronized下如果一个线程处于等待某个锁状态,是无法被中断的
- 从性能角度,如果竞争不激烈,两者性能差不多,而当竞争激烈时,Lock的性能要远远优于synchronized
特别注意
- 使用Lock接口时,如果有异常发生,jvm不会释放锁,这样会造成死锁,最佳实践是在finally中释放锁
- 可中断是指等待线程,并不是正在执行的线程
读写锁
读写锁主要是对读操作与写操作获取不同的锁,当进行写操作时,需要获取写锁,其他线程将进行等待,直到写操作结束,释放锁。读操作时先获取读锁,此时写操作(写锁)将无法获取,必须等待当前线程释放锁,但是其他读操作仍然可以获取读锁,执行后续逻辑。
- 如果一个线程占用了读锁,此时其他线程需要申请写锁,则该线程进入等待,直到读锁释放
- 如果一个线程占用了写锁,测试其他线程都将等待锁的释放,不论获取读锁还是写锁。
java.util.concurrent包下线程池种类及其特性
- Single Thread Executor:只有一个线程的线程池,所有提交的任务都是顺序执行的
- Cached Thread Pool:如果线程池有空闲的线程,则使用空闲线程去执行新加入任务,否则将创建新的线程执行新建任务。如果任务执行完毕,没有新任务进入,线程池中线程超过60秒等待时间后,线程将被从线程池删除。
- Fixed Thread Pool:拥有固定线程数的线程池,如果没有任务执行,那么线程会一直等待。如果任务集中进入,线程不能及时处理,会放入等待队列。
- Scheduled Thread Pool:用来调度即将执行任务的线程池,可以设置多长时间执行一次
- Single Thread Scheduled Pool:只有一个线程的线程池,用来调度任务在指定时间执行。
Runnable与Callable区别
- Runnable任务结束后,Future对象返回null,Callable返回的Future实例可以包括返回值
- Runnable通过execute提交任务,Callable通过submit提交任务
- Runnable中的run方法无返回值,Callable的call方法具有返回值,通过Future会获取,但是如果要获取返回值,则会进入阻塞状态,直到线程结束。
阻塞消息队列(BlockingQueue)入队方式对比
- add(Object):如果队列可容纳该对象压入,否则抛出异常
- offer(Object):如果可能,将对象放入队列,并返回true。如果队列已满,则返回false
- put(time):如果可能,将对象放入队列。如果队列已满,则阻塞当前线程,直到队列有空间放入队列后返回。
ArrayBlockingQueue与LinkedBlockingQueue比较
- ArrayBlockingQueue是由数组支持的有界阻塞队列,其队列大小是在构造时确定的,放入该队列的对象是以fifo排序的。
- LinkedBlockingQueue:无固定大小,构造时可设置队列容量,如果构造时未设置大小,则默认为Integer.MAX_VALUE
- LinkedBlockingQueue主要使用put和take,所以在消息队列容量已满进行入队或者容量为空获取数据时会阻塞
- 由于底层使用数据结构不同,LinkedBlockingQueue拥有比较大的吞吐量,但是如果线程数很大时性能要低于ArrayBlockingQueue
为什么使用线程池
涉及到多线程开发,如果并发量较小、或者业务执行频率比较分散,使用new Thread().start()模式是没有问题的。但是如果请求量比较集中,而且并发比较集中、数量较高。如:每一个线程执行1秒,并发tps是10000,即在1秒内,需要创建10000个线程。这会给jvm带来很大压力。此时引入线程池是必要的。具体原因涉及到以下几个方面
- 新建线程的开销,对于jvm新建与销毁线程消耗资源代价比较大,使用线程池可以重复利用现有线程,不需要为每个请求新建线程处理
- 资源消耗量:对每一个请求都新建一个线程,而每个线程是有一定资源消耗的,如果新建线程过多,对jvm运行会带来一定风险。线程池线程可重复利用,而且一定时间空闲后,可自动回收。
- 稳定性:每个jvm或者操作系统对线程数、资源(CPU、内存)有限制,如果无限制的创建线程,可能导致系统异常。