- 线程池问题
- 什么是线程池,线程池工作原理和好处
线程池:管理一组工作线程,同时放置了一组等待执行的任务队列。
主要解决问题:当请求数量非常多时,为每个请求都创建一个线程,导致时间和资源上开销巨大。
好处:①重复利用已经创建的线程,减少在创建过程中时间和资源上的开销 ②提高响应时间 ③统一的管理
工作原理:
第一步:如果线程池中线程的线程数量小于核心线程池(corepoolSize),那么来一个任务就会创建一个线程
第二步:如果线程数量大于等于核心线程池,但是缓存队列(workQueue)未满,则将任务放入缓冲队列,等待空闲下来的线程。
第三步:如果缓冲队列也满了,则判断线程池中的数量是否小于线程池最大线程数(maxmumPoolSize),如果是,则创建新线程。如果不是则采取任务拒绝策略。
第四步:如果任务没有那么多了,则会控制线程数量小于核心线程池。
- 常见的几种线程池
Executors类:
①newCachedThreadPool:创建一个可以缓存的线程池。如果线程池的大小超过了任务的大小则回收部分空闲线程,反之,当任务增加时,线程池增加线程数量。
②newFixedThreadPool:创建固定大小的线程池。上限是线程池最大线程数量。
③newSingleThreadExecutor:创建一个单线程的线程池。只有一个线程在工作,所有任务按照提交顺序执行。
④Intrger.Max_VALUE:无界线程池
ThreadPoolExecutor类:
①corePoolSize:核心线程池大小。
②maximumPoolSize:线程池最大线程数。
③keepAliveTime:当线程池中的线程数量大于核心线程池大小的时候,keepAliveTime生效,计算一个线程的空闲时间到达keepAliveTime时,会终止该线程,直到线程池数量小于核心线程池数量。
④unit:参数keepAliveTime的时间单位
⑤workQueue:阻塞队列,任务缓存队列,类型为blockingQueue<Runnable>,通常可以取下面三种类型:
ArrayBlockingQueue:基于数组的先进先出,必须制定大小(有界队列,maximumPoolSizes 有效)
LinkedBlockingQueue:基于链表的先进先出,如果没有制定队列大小,默认为无界线程池(Intrger.Max_VALUE)(无界队列,maximumPoolSizes 无效)
synchronousQueue:同步队列,不会保存任务,而是创建新的线程来执行新的任务,必须等待其他线程取走后才能继续添加。(直接提交,maximumPoolSizes 无效)
⑥threadFactory:线程工程主要用来创建线程。
⑦handler:任务拒绝策略。有一下四种取值
AbortPolicy:直接抛出异常
DiscardPolicy:直接丢弃任务
DiscardOldestPolicy:丢弃最旧的任务,执行当前任务
CallerRunsPolicy:不用线程池中的线程执行,用调用者所在线程执行
- 线程池风险(注意事项)
虽然线程池是构建多线程应用程序的强大机制,但使用它并不是没有风险的。
①死锁:多线程无尽的等待
②资源不足:线程并非越多越好,要考虑软硬件资源情况,可以采用自适应算法来动态调整线程池的大小。
③并发错误:多线程情况下排队机制产生并发性错误。
④线程泄露:当任务执行完毕而线程没有返回线程池
- 阻塞队列问题BlockingQueue
①ArrayBlockingQueue:由数组支持的有界缓存阻塞队列,读写操作都要锁住整个容器,是线程安全的,生产者消费者公用一把锁。
②linkedBlockingQueue:基于链表的阻塞队列,没有制定界限时,默认是无界线程池,生产者和消费者分别采用独立的锁控制数据同步,提高整个队列的并发性。
③PriorityBlockingQueue 和 PriorityQueue:基于数组的无界阻塞队列,安装优先级进行排序,每次出队的元素都是最优先的。
④DelayQueue:无界队列,只有在延迟期满时才能从中提取元素,头部是延迟期满后保存时间最长的delayed元素。
③synchronousQueue:缓存值为1的无界阻塞队列。直接提交
- 多线程专题
- 实现多线程的四种方法(创建多线程的四种方法)
①继承Thread类创建线程类:
②通过Runnable接口创建线程类
③通过callable和future创建线程
④通过线程池创建线程
扩展:runnable和callable有什么不同
- callable定义的方法是call(),runnable定义的方法是run()
- callable的call()方法可以有返回值,而runnable的run()方法不能有返回值
- Call()方法可以抛出异常,run()方法不可以
扩展:一个类可以同时基础thread 和实现 runnable
扩展:thread类和runnable接口的区别
实现多个runnable接口,继承单个Thread类
如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
实现Runnable接口比继承Thread类所具有的优势:
1):适合多个相同的程序代码的线程去处理同一个资源
2):可以避免java中的单继承的限制
3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
- Start()方法和run()区别
①start()方法启动线程,斌仔新线程中运行run()方法,真正实现多线程运行
②直接调用run()方法的话,会把run()方法当做普通方法来调用,会在当前线程中执行run()方法,而不会启动新线程来运行run()方法
- volatile多线程保持变量内存可见性(volatile关键字修饰变量)
一旦一个共享变量(类的成员变量,类的静态变量)被volatile修饰的时候,就具有如下两层语义:
①线程共享变量的可见性:一个线程修改了某个变量的值,其新值对其他线程来说是可见的。
因为使用volatile关键字会强制将修改的值立即写入主存。同时其他线程缓存中对应的缓存行无效,是的其他线程必须从主存读取最新的值。
②禁止指令重排序,禁止编译器对代码的优化:前面的操作和后面的操作不能更换位置。
volatile的应用场景:DCL双重校验
- 实现多线程同步(synchronized,lock,wait,CAS)
java中通过四种方法来实现同步互斥访问:①synchronized ② lock ③wait()/notify()/notifyAll() ④CAS
- Synchronized(悲观锁):可以修饰方法(粗粒度并发控制),只能一个线程执行该方法和修饰块(细粒度并发控制),只会将快中的代码同步,块之外的可以被多线程访问。
修饰非静态方法:将该对象上锁
修饰静态方法:将该方法所在的类class上锁
- lock(悲观锁):必须在try-catch-fianlly块中进行,并且将释放锁的操作反倒finally块中进行,保证一定释放锁,防止死锁。
- wait()/notify()/notifyAll():都是针对对象的,必须在synchronized的方法和块中被调用。调用wait()线程阻塞,释放对象的锁。调用notify()解除阻塞,重新获得锁。
- CAS(乐观锁):硬件CPU同步原语,是一种乐观锁。当多个线程同时使用CAS更改同一个变量的时候,其中一个线程更改了变量的值,而其他线程失败了,失败的线程并不会被挂起而是不断尝试。Java的Atomic包使用CAS算法来更新数据,而不需加锁
缺点:循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。.
只能保证一个共享变量的原子操作
- Lock和synchronized的区别,Lock的优势
①lock是一个接口,synchronized是一个关键字修饰方法和块的
②synchronized发生异常时会自动释放线程占有的锁,而lock必须把释放锁放到finally中,这样不会导致死锁。
③Lock等待锁的线程响应中断,而synchronized会一直等待下去无法中断。
④Lock可以知道有没有成功获得锁,而synchronized不可以
⑤Lock可以提高多个线程进行读操作
⑥Lock实现公平锁,synchronized不保证公平性
性能上来讲,如果竞争不激烈,两者都可以,如果竞争激烈Lock效率比synchronized高。
- Volatile和synchronized的区别
①Volatile是修饰变量的,synchronized是修饰方法和块的
②Volatile不对变量加锁,不会参数阻塞。synchronized会对变量加锁,会参数阻塞
③Volatile实现变量可见性,不保证原子性。synchronized会保证修改变量的可见性和原子性
④Volatile禁止指令重排,不允许编译器优化变量。synchronized可以被编译器优化
- Wait(),sleep(),notify(),notifyAll()
wait():使一个线程处于等待状态,并且释放所持有的对象的lock。
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException异常,没有释放锁。是thread类中的与wait()不同
notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。
notifyAll():唤醒所有进入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。
- 锁的分类
①代码层次上的,java中的同步锁,可重入锁,公平锁,读写锁
②数据库层次上的:乐观锁,悲观锁,表锁,行锁,页锁
同步锁的分类:synchronized和lock是悲观锁。乐观锁包括CAS同步原语,如原子类和非阻塞同步方式。
- 多线程中锁的种类
①可重入锁:如果当前线程已经获得某个监视器对象所持有的锁,那么该线程在该方法中调用另外一个同步方法也同样持有该锁
②可中断锁
③公平锁
④读写锁
- 多线程的优点和缺点
优点:充分利用cpu,避免cpu空转,程序响应更快
缺点:
①上下文切换开销:当CPU从执行一个线程切换到执行另一个线程的时候,需要存储当前线程的本地的数据,程序指针等,然后载入拎一个线程的本地数据,程序指针等,最后才开始执行。切换过程需要开销。
②增加资源消耗:线程需要一些内存来维持它本地的堆栈,需要占用操作系统一些资源。
③编程更加复杂:并发访问资源的时候需要考虑安全问题
- 实现线程安全(threadlocal锁)
ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。ThreadLocal相当于一个容器,相当于存放每个线程的局部变量,为每个线程提供一个初始值的副本,保证不同线程有个拷贝。
(1)隐式锁(线程同步synchronized)
线程同步synchronized有两种方式,一种是放在同步方法上,一种是放在同步代码块里。
(2)显式锁Lock和ReentrantLock
ReentrantLock是Lock的实现类,是一个互斥的同步器。 在竞争条件下ReentrantLock的实现要比synchronized的实现更具有伸缩性,这意味着当许多线程都竞争相同锁定时,使用ReentrantLock的吞吐量通常要比synchronized好。 ReentrantLock是对方法块加锁使用频率最高点。
3)显示锁ReadWriteLock和ReentrantReadWriteLock(读写锁)
ReadWriteLock也是一个接口,提供了readLock和writeLock两种锁的操作机制,一个用于只读操作,另一个用于写入操作。只要没有 writer,读取锁可以由多个 reader 线程同时保持。写入锁是独占的。读写锁使用的场景是一个共享资源被大量的读取操作而只有少量的写操作。(读读不互斥,读写互斥,写写互斥;)。
(4)显示锁StampedLock
StampedLock可大幅度提高程序的读取锁的吞吐量。在大量都是读取、很少写入的情况下,乐观读锁可以极大的提高吞吐量,也可以减少这种情况下写的饥饿现象。
- 多线程异步断点重传
多线程下载,即是一个文件能过多个线程进行下载;而断点续传说的是当一个文件下载到一半时突然由于某个原因下载中断了,比如突然电脑关机了,那么当再开机时已经下载到一半的文件不需要重头开始,而是接着下载;其原理很简单:首先,下载中断时记住上一个时点下载的位置,然后接着这个位置继续下载,这个继续下载可以是人手工触发的也可以是程序运行时自动识别进行下载的.
- 如果让你用多线程优化你会在哪里优化
使用多线程无非是为了提高性能,但如果多线程使用不当,不但性能提升不明显,而且会使得资源消耗更大。下面列举一下可能会造成多线程性能问题的点:(1)死锁;(2)过多串行化;(3)过多锁竞争;(4)切换上下文;(5)内存同步
锁的优化策略
编码过程中可采取的锁优化的思路有以下几种:
(1)减少锁持有时间
例如:对一个方法加锁,不如对方法中需要同步的几行代码加锁;
(2)减小锁粒度
例如:ConcurrentHashMap采取对segment加锁而不是整个map加锁,提高并发性;
(3)锁分离
根据同步操作的性质,把锁划分为的读锁和写锁,读锁之间不互斥,提高了并发性。
(4)锁粗化
这看起来与思路1有冲突,其实不然。思路1是针对一个线程中只有个别地方需要同步,所以把锁加在同步的语句上而不是更大的范围,减少线程持有锁的时间;
而锁粗化是指:在一个间隔性地需要执行同步语句的线程中,如果在不连续的同步块间频繁加锁解锁是很耗性能的,因此把加锁范围扩大,把这些不连续的同步语句进行一次性加锁解锁。虽然线程持有锁的时间增加了,但是总体来说是优化了的。
(5)锁消除
锁消除是编译器做的事:根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程(即不会影响线程空间外的数据),那么可以认为这段代码是线程安全的,不必要加锁。
- 线程与进程
- 线程通信与进程通信的区别
进程:是操作系统运行的一个任务(比如电脑同时开启QQ和微信等)
线程:线程是进程的一个顺序执行流(比如在QQ上同时进行多个任务)
进程间的通信:由于每个进程都有自己的独立空间和地址,所以他们之间的通信要通过操作系统。通信方法有套接字,管道,信号等。可以不局限于单台计算机,实现网络通信。
线程间的通信:多个线程共享地址空间和数据空间,他们间的通信不用经过操作系统。通信方式:wait()/notify()/notifyAll()方法,锁机制等
- 进程间的通信
①管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
②有名管道 (named pipe) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
③信号量( semophore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
③消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
④信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
共享内存( shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
④套接字( socket ) : 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。
- 其他问题
- HashMap线程安全吗?HashMap和conrrentHashmap区别?
HashMap不是线程安全的
conrrentHashmap是线程安全的
- Servlet是线程安全的吗?
不是!
当Tomcat接收到Client的HTTP请求时,Tomcat从线程池中取出一个线程,之后找到该请求对应的Servlet对象并进行初始化,之后调用service()方法。要注意的是每一个Servlet对象再Tomcat容器中只有一个实例对象,即是单例模式。如果多个HTTP请求请求的是同一个Servlet,那么着两个HTTP请求对应的线程将并发调用Servlet的service()方法。
- I++是否线程安全?
不是线程安全的,因为i++不是原子操作。volatile解决了线程间共享变量的可见性问题使用volatile会增加性能开销,但volatile并不能解决线程同步问题
解决i++或者++i这样的线程同步问题需要使用synchronized或者AtomicXX系列的包装类,同时也会增加性能开销