多线程的实现方式:
继承Thread
scala
复制代码
public class MyThread extends Thread{ @Override public void run(){ for (int i=0;i<100;i++){ System.out.println(i); } } }
直接调用就行
实现runable,
整理了一份面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记的【点击此处即可】即可免费获取
csharp
复制代码
public class MyThread3 implements Runnable{ @Override public void run(){ for (int i=0;i<100;i++){ System.out.println(i); } } }
需要把实现类传入Thread里面去才能开启start方法。
ini
复制代码
MyThread3 myThread3=new MyThread3(); Thread thread=new Thread(myThread3); thread.start();
多线程的几个方法:
start:启动线程
setPriority():设置优先级
setDaemon()设置为守护线程(别人结束之后守护线程会立马结束)
yield()礼让线程,这个线程结束之后会避免抢夺资源,大概率会让别人,但是自己也有几率抢夺到
join()设置之后就会让这个线程执行完才会执行别的。
多线程的生命周期:
首先创建线程:然后调用start方法获取执行资格,但是没有执行权,此时需要去抢夺执行权
如果此时抢到了执行权就有执行资格也有执行权了,此时运行程序run方法,借宿之后就会线程死亡。但是当抢夺到执行权过后,用了sleep或者其他阻塞的方法就会变成阻塞状态,没有执行权,没有执行资格。这时sleep,或阻塞结束之后就会继续处于就绪状态。
synchronize方法里面的必须写唯一的,静态的。
如果是继承thread必须是唯一的。如果是实现Runable的话就不用
lock定义对象的时候必须是静态的,因为静态才是公用的。
synchronize和lock区别:
1.lock是jdk代码实现,synchronize是jvm层面实现的。
2.synchronize可以在类上也可以在方法里。lock只能在方法里。
3.lock在性能需求高的时候更好。sy在不激烈的时候更好。
wait和sleep的区别。
wait和sleep都可以让线程执行,差异一般是,wait一般是在sy方法里面,用于线程之间的协调,会释放锁。sleep没有限制,会获取锁。wait是obj类的,sleep是thread类中的静态方法。
wait只能用notify或者notifyAll来唤醒
线程池:
线程池
什么时候会让临时线程去执行?当队伍排满的时候。
线程池参数:1.当前线程数
2.最大线程数
3.临时线程超过多长时间没有使用,销毁的时间60?120
4.时间的单位秒?
5.线程排队数量
6.拒绝策略
7.创建线程的方式
自定义线程池;
线程池中提交一个任务的流程是怎么样的?
1.会首先判断我们的线程池里面的当前线程数是不是小于核心线程数。
2.小于的话就会创建线程,然后把该任务作为线程要执行的第一任务
3.大于的话就会.把线程放到阻塞队列里面,
4.如果队列满了就会判断当前线程数是不是小于最大线程数。
5.如果小于就会创建新线程去执行这个任务。
6.如果大于等于(假如最大是10个就当前9个就可以创建,10个就证明已经满了)就会执行拒绝策略,默认的是丢弃任务,抛出异常。
最后一个否就会创建线程
为啥先添加队列而不是先添加核心线程?
因为最开始的核心线程都如果缑用了,就没必要再创建新线程来加快工作了
因为多一个线程会多消耗一个资源。
线程池的状态:
running:运行状态,会接受新任务,会处理队列的任务
shutdown: 不会接受新任务,会处理队列的任务 runing调用shundown方法就会变成shutdown状态
stop:不会接受新任务,不会处理队列的任务。 runing调用shuntdownnow方法。方法里会先把状态变成stop再停掉线程。为什么?因为先停掉就不会有线程进来。
tidying:线程停完就会变成这个状态。然后会调用terminated方法。
terminated;terminated()方法执行完之后就会调用这个方法。
为什么不建议使用stop来停掉线程?
因为当线程加锁的时候,加了lock锁,被强制关掉的时候不会关掉锁,就会导致接下来别的加锁加不上。synchronize就不会有这个问题,因为他可以自动解锁
使用中断会好一点
调用interrupt方法,再在逻辑里判断这个方法的使用情况。
如果睡眠的时候报异常了,那个标记就会变成false
线程池核心线程数怎么设置?
1.假如是cpu密集型任务,比如查10000个中的素数,最好线程数等于自己的cpu核心线程数。不过一般可以多设置一个线程来应对阻塞的情况。核心数+1
2.如果是io密集型任务,我们花在io的时间就会很多,cpu就会空闲下来。就会2*cpu核心数
3.但是也可能20个线程都阻塞了,cpu还是空闲了。
我们可以用公式
线程数=cpu核心数*(1+线程等待时间/线程运行总时间)
线程使用时间就是线程没有使用cpu的时间,比如阻塞在io了。
线程运行总时间=线程执行完某个任务的总时间。
最大线程数怎么设置呢?
用压测测试出来多少秒最合适
java并发可见性问题
当一个线程在读取内存中的数据的时候,会在cpu缓存中存放一份数据,发生修改之后,是先修改cpu的缓存的,再回写到内存。这个时候第二个线程去读取的时候就会读到之前的数据。这就是可见性问题。
如何避免?
使用volatile关键字:
这个关键字作用就是可以同时让cpu修改cpu缓存数据的时候,同时修改内存数据,这样就避免了cpu的可见性问题。
如何避免死锁?
注意加锁顺序,保证每个线程按照同样的顺序加锁。
注意加锁实现,可以针对所设置一个超时时间。
注意检查死锁。
避免两个锁互持自己的资源。
hashmap扩容机制:
1.7:
初始为16.扩容就是2倍扩容
1.7之前就直接先生成新数组,然后遍历每个老数组中每个位置上的每个元素。计算在新数组中的下标位置。
把元素添加到新数组里面去
最后把新数组赋值给hashmap的table属性
1.8:也是先生成新数组,
然后遍历数组中的所有的链表或者红黑树
然后如果是链表就直接计算元素的下标,然后添加到数组去
如果是红黑树的话,先统计红黑树每个元素对应在新数组中的下标位置
a.计算每个下标位置的数量,超过8的话就生成新的红黑树,再把红黑树添加到新数组对应位置。不超过就生成链表。
然后再把元素转移过去
什么是非公平锁和公平锁?
非公平就是后来的线程可能比先来的抢到锁资源。
如果是公平锁,会先检查aqs队列中是否存在线程在排队,如果有线程在排队就直接排队。
如果是公平锁的话就不会去检查是否有线程在排队,而是直接竞争锁。
如果没竞争到锁就会排队,当锁释放的时候,都是唤醒排在最前面的线程,所以非公平锁只是体现在了线程加锁阶段,而不是唤醒状态。
什么是偏向锁?
当同步锁只有一个线程访问的时候就会给这个线程加一个偏向锁,线程第二次到达代码块的时候就会判断持有锁是否是自己,如果是就继续往下执行。加了偏向锁第二次到达的时候就不用重复加锁了,效率高。
什么是自旋锁(轻量级锁)?
当偏向锁的线程和别的线程争抢资源的时候,就会把偏向锁变成自旋锁。
自旋锁就是在抢占锁资源的时候没有抢到然后就会一次又一次的尝试获取资源。这样会很消耗cpu资源的。当自旋多了,某个线程达到最大自旋次数的线程,就会升级成为重量级锁。好处就是反应快,但是吃cpu
重量级锁:当一个线程获取到这个重量级锁的时候就会被阻塞。其他锁线程只有等锁资源被释放的时候才会被唤醒。好处就是cpu消耗低,但是反应慢。
ThreadLocal:
底层就是一个map集合。threadLocal是一个强引用,在垃圾回收的时候不会被回收掉。当使用线程池的时候,线程会被重复利用set方法,内存没有得到释放就会可能造成内存泄漏的问题。
假如保存的都是一个new user 就会每次都占用内存。又因为线程池很可能多次是同一个线程,没有关掉。会导致ThreadLocal没有被释放掉。导致内存泄漏。
解决办法就是在使用完成之后手动调用remove方法。
还有一种就是为什么key要设置成为弱引用?value要设置成为一个强引用?
弱引用是每次会被垃圾回收进行回收。
强引用不会被垃圾回收。
如果key不设置弱引用,设置成强引用就会一直存在,越积越多导致内存泄漏,而value的话值会在
key设置为弱引用就是为了让他能够回收。
包括我们的HttpRequest和httpResponse都是使用的threadlocal实现的
什么是可重入锁?
同一个线程可以重复获得这个锁对象。
为什么要调用start方法而不是调用run方法?
1.start调用它可以直接启动一个新线程,如果调用run就只是简单的启动一个方法而已。并不会开启一个新线程。
2.start的话实现了多线程,run没有。
3.start不可以重复多次调用,run可以
4.start中的run可以不执行完就可以继续执行下面的方法,也就是线程切换。然而,如果直接调用run方法的话,就必须等待润执行完才能执行下面的。