1、线程的生命周期
-
线程状态转换图:
阻塞状态分为三种:
- 等待阻塞: 运行的线程执行wait()方法,JVM会把该线程放入等待池中。
- 同步阻塞: 运行的线程在获取对象同步锁时,若该同步锁被别的线程占用,则JVM会把线程放入锁池中。
- 其他阻塞: 运行的线程执行Sleep()方法,或者发出I/O请求时,JVM会把线程设为阻塞状态。当Sleep()状态超时、或者I/O处理完毕时,线程重新转入就绪状态。
2、哪些场景会用到多线程
- 比如订单创建成功后,需要调另一个系统给用户发送短信,且这类任务比较耗时,且就算失败了也不是特别重要的。
- 后台定期执行一些特殊任务,比如更新数据、监控数据的采集等等。
- 验证1万条url是否存在
3、实现接口 VS 继承 Thread
实现接口会更好一些,因为:
- Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;
- 类可能只要求可执行就行,继承整个 Thread 类开销过大。
4、锁的分类
公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁,非公平锁则不一定;
非公平锁相比公平锁,性能更好;
synchronized是非公平锁,ReentrantLock默认非公平锁,可以通过构造函数指定为公平锁;
可重入锁
Synchronized 和 ReentrantLock都是可重入锁
如下代码就是一个可重入锁的一个特点,如果不是可重入锁的话,setB可能不会被当前线程执行,可能造成死锁。
synchronized void setA() throws Exception{
Thread.sleep(1000);
setB();
}
synchronized void setB() throws Exception{
Thread.sleep(1000);
}
乐观锁/悲观锁
synchronized、ReentrantLock都是悲观锁,
乐观锁有两种方式:
- 版本控制
- CAS(compare and swap) 引用
会带来ABA问题
自旋锁
获取锁的线程不会立即阻塞,而且循环的方式尝试获取锁;核心操作是CAS
优点:如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
缺点:缺点是循环会消耗CPU
偏向锁/轻量级锁/重量级锁
这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
- 偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。如果一个线程获得了锁,那么锁就进入偏向模式。当这个线程再次请求锁时,无须再做任何同步操作。这样就节省了大量有关锁申请的操作,从而提高了程序性能降低获取锁的代价,适用于锁竞争比较小的场景。
- 偏向锁存在的意义?
偏向锁可设置开启关闭,偏向锁是一种非常轻的锁,JVM在上锁时顺便做的一件事,代价最小,在程序执行同步代码块的大多数情况下,是不会发生锁竞争的,如果我们一上来就直接执行轻量级锁或者重量级锁,代价太大并且没必要,只有存在锁竞争的时候才有必要升级锁来保证线程同步。 - 轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
- 重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
5、Java中wait()方法为什么要放在同步块中
Reference
https://www.jianshu.com/p/b8073a6ce1c0
6、synchronized VS lock(ReentrantLock)
实现
synchronized 是JVM实现的,ReentrantLock 是jdk实现的.
性能
新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。
等待可中断
这是两者最大的区别,synchronized如果没有获得锁,会一直阻塞没法中断,而lock可以通过编码放弃等待,执行其他事;
使用选择
除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。
7、java线程池:
几个重要参数:
- corePoolSize:
核心池的大小,如果没有设置prestartCoreThread,那么初始线程数为0,来一个创建一个,直到当前线程数=corePoolSize,新来的任务会放到缓存队列 - maximumPoolSize:
线程池最大线程数,如果当前线程数小于这个值,那么会新创建线程 - keepAliveTime:
表示线程没有任务执行时最多保持多久时间会终止,默认只有当前线程数大于corePoolSize才会起作用; - workQueue:
一个阻塞队列,用来存储等待执行的任务(LinkedBlockingQueue基于链表的先进先出队列、Synchronous直接新建一个线程来执行新来的任务,前提是没有达到最大线程数)
Java通过Executors提供四种线程池
- newFixedThreadPool
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
corePoolSize=maximumPoolSize=初始化一个值,使用的LinkedBlockingQueue。 - newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。corePoolSize=0,maximumPoolSize=Integer.MAX_VALUE,使用的SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程 - newScheduledThreadPool
创建一个定长线程池,支持定时及周期性任务执行。 - newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)
执行,线程死了可以重启一个继续执行。
corePoolSize=maximumPoolSize=1,使用的LinkedBlockingQueue。
线程池执行任务的过程
ThreadPoolExecutor.execute(Runnable command)
方法执行过程
注意:newFixedThreadPool 使用的是无界队列LinkedBlockingQueue作为线程池的工作队列(队列的容量为Integer.MAX_VALUE),当maxPollSize已满时还是能继续接任务(maxPollSize和keepAliveTime将无效)
9、单核cpu设置多线程有意义么 ?能提高并发性能么?
能。通常一个任务不光 cpu 上要花时间, IO(磁盘IO、网络IO) 上也要花时间(比如去数据库查数据,去抓网页等)。
一个进程在等 io 的时候, cpu 是闲置的,另一个进程正好可以利用 cpu 把 cpu 该做的事做完。
多几个进程一起跑,可以把 io 和 cpu 都跑满了。所以单核开多线程可以防止阻塞
10、线程数究竟设多少合理
N核服务器,通过执行业务的单线程分析出本地计算时间为x,等待时间(IO)为y,则工作线程数(线程池线程数)设置为 N*(x+y)/x,能让CPU的利用率最大化。
如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 NCPU+1
如果是IO密集型任务,参考值可以设置为2*NCPU
11、volatile
作用:
保证多线程情况下,对变量的可见性;“轻量级”synchronized
被volatile修饰的变量可以保证每个线程能够获取到最新值,避免出现数据脏读
适用于读多写少
volatile底层实现原理
引用
- 被volatile修饰的共享变量在执行写操作时,会发出一个LOCK前缀指令
- lock前缀指令会将当前处理器的缓存区数据同步到内存中
- 这个操作会使其他CPU缓存了该数据的内存地址无效(通知的方式)
- 当其他CPU发现本地缓存失效时,会重新拉取内存中的数据;
不能保证操作的原子性:
多线程下,对volatile修饰的变量做++操作,不能保证原子性,必须得加锁,因为++是多操作先get后++再set
12、ThreadLocal
用于在多线程情况下,获取当前线程独有的变量,这个变量与其他线程无关,只有当前线程能操作;
实现方式:
- Thread类里有个ThreadLocalMap,用于保存只属于当前线程的一些东西(ThreadLocal);
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
}
- 当ThreadLocal执行Set的时候,实际上是对当前线程的ThreadLocalMap属性新增操作;
- ThreadLocalMap 里面是一个entry数组(同样size、有扩容操作),entry的属性由key和value组成,key对应ThreadLocal对象,value则是这个ThreadLocal对象的泛型的值;
- set的时候根据ThreadLocal对象算出一个hashCode来和entry数组的长度-1进行与运行得到一个数组下标,来决定放到entry数组的哪个位置;
5.而get操作呢,同样是根据当前ThreadLocal对象去Thread类下的ThreadLocalMap里找对应的值;
这种存储结构的好处:
线程死去的时候,线程共享变量ThreadLocalMap则销毁
(线程池除外,线程池里的线程不会死去)
存在的问题:
- 弱引用导致的内存泄露
引用
当threadlocal被设置为null的时候,除了theadlocalmap没有办法再获得该变量了,但是又没有办法直接从theadlocalmap获取到,因为他是private的,所以就永远访问不到,造成内存泄露 - 为什么用弱引用
entry的key之所以设计成弱引用是了更好的被gc回收:如果设计成强引用,threadlocalmap里的entry是一直引用着threadlocal对象的,强引用导致其永远无法被回收,更容易造成内存泄漏,除非这个线程死掉。 - 解决办法
- 手动调用ThreadLocal.remove();
- 其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。
13. ExecutorService,等待所有线程执行完毕
- 方式一
ExecutorService taskExecutor = Executors.newFixedThreadPool(4);
while(...) {
taskExecutor.execute(new MyTask());
}
taskExecutor.shutdown(); // 非阻塞,任务可能并没有执行完
try {
taskExecutor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); // 阻塞,等待所有任务执行完
} catch (InterruptedException e) {
...
}
- 方式二
ExecutorService executor = Executors.newFixedThreadPool(10);
CountDownLatch latch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
executor.execute(() -> {
try{
// do something...
}finally {
latch.countDown();
}
});
}
latch.await();