1,并发,并行,串行的区别。
串行是指是时间上不可能发生重叠,上一个任务没结束,下一个任务就不会执行。
并行是指在时间上是重叠的,两个任务互不干扰。
并发是指两个任务互相干扰,一个时间点只有一个任务的执行,交替执行。
2,谈谈你对线程安全的理解。
准确的说应该是内存安全,堆是内存共享的,是可以被所有线程访问到的,当多个线程访问同一个对象时,如果不用进行额外的同步控制其他的协调工作,调用这个对象的行为都可以获得正确的结论,那么可以认为这是线程安全的。我认为所谓的正确的结论是指在单机操作下得到的结论。
3,什么是强引用,弱引用,软引用,虚引用。
强引用(StrongReference):通过关键字new创建的对象所关联的引用就是强引用。垃圾回收机制是绝对不会回收它的,就算是内存空间不足时,jvm宁愿抛出OutOfMemoryError,使程序异常终止也不愿意将强引用回收。
Object obj = new Object();
软引用(
SoftReference):
如果一个对象只具有软引用,则内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。
// 软引用
String str = new String("abc");
SoftReference<String> softReference = new SoftReference<String>(str);
弱引用(WeakReference):弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
String str = new String("abc");
WeakReference<String> weakReference = new WeakReference<>(str);
虚引用(PhantomReference): 虚引用顾名思义,就是形同虚设。与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
String str = new String("abc");
ReferenceQueue queue = new ReferenceQueue();
// 创建虚引用,要求必须与一个引用队列关联
PhantomReference pr = new PhantomReference(str, queue);
4,说说你对ThreadLocal 的理解?
ThreadLocal即线程变量,它将需要并发访问的资源复制多份,让每个线程拥有一份资源,由于每个资源都有自己的资源副本,所以没有必要再对该变量进行同步了。每一个Thread中有一个threadLocals的变量,它的类型是ThreadLocalMap 这个ThreadLocalMap是维护在ThreadLocal类中的类部类,ThreadLocal中有一个set方法,以下是set方法的源码
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
set方法需要传进来一个值,这个值以后回设置到ThreadLocalMap中的value中,调用set方法时会先拿到这个线程,然后调用ThreadMap中的getMap方法拿到这个线程中的ThreadLocalMap,判断是否为空,如果不为空就以threadlocal这个对象为键,value为值传入到threadlocalmap中,为空则调用createMap方法创建这个map。
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
get方法不需要传入任何值,首先也是拿到当前线程,再调用getMap方法拿到此线程的ThreadLocalMap,因为每个ThreadLocalMap中都维护了一个Entry(以ThreadLocal为键,这里需要注意的是引用的ThreadLocal为弱引用,以我们传入的值为value),当这个map不为空时会以当前ThreadLocal为键拿到value值。如果这个map为空的话会调用setInitialValue方法,创建这个map并赋初始值,以当前ThreadLocal为键,以null为值。
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}
内存泄漏问题:
首先看看Entry的源码:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocalMap中的Entry弱引用了一个ThreadLocal,上面也提到了弱引用机制,随时都有可能会被gc,THreadLocal的根源就在于ThreadLocalMap的生命周期是和ThreadLocal一样长的,但是它里面的Entry中以threadLocal为键是弱引用,随时都有可能会被回收,如果没有手动去删除对应的key就会导致内存泄漏,解决办法就是每次在调用完ThreadLocal之后都调用它的remove()方法。
5,ThreadLocal和Synchronized的区别。
6,多线程有哪几种创建方式。
继承自Thread:重写run方法。在run方法中写具体任务,调用start方法启动线程。
实现Runnable:调用这样子的线程时还是需要new一下Thread类,将实现runnable的类放入其中,再通过thread调用start方法。注意,如果直接使用run方法是达不到多线程的目的,他还是一样的会等上一个线程结束之后再开始执行。
实现Callable:Callable规定的方法是call(),任务执行后有返回值,可以抛出异常。
通过线程池创建线程:线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。
7,java中线程的生命周期?
新建状态:就是线程刚被new出来的时候,还没有被start。
就绪状态:线程创建之后,其他线程调用了它的start方法,此时线程变成可运行状态,等待cpu的调度。
运行状态:线程被cpu调度,执行里面的方法。
阻塞状态:阻塞是指线程由于某些原因,放弃了cpu的使用权,暂时停止了运行,直到线程进入就绪状态才有机会转为运行状态。阻塞状态又分为以下三种:
- 等待阻塞:运行中的线程执行wait方法之后,会被放入到等待池当中,进入这个状态是不能主动唤醒的,必须依靠其他线程调用notify方法或notifyAll才能被唤醒,wait是Object中的方法。
- 同步阻塞:运行中的线程获取某个对象的同步锁时,这个锁已被别的线程占用,这时此线程就会被jvm放入到锁池当中。
- 其他阻塞:运行中的线程执行sleep或者join方法,或者发出了I/O请求,jvm就会把这个线程设置为阻塞状态,只有当sleep过时,join等待结束,I/O处理完毕,才会重新进入到就绪状态。
死亡状态:线程执行完毕,或者因为异常退出了run方法,该线程生命周期结束。
8,线程池的底层工作原理
线程池的内部是通过队列+线程实现的,当我们利用线程池执行:
当有一个线程需要执行时:
如果线程池中的线程数小于corePoolSize(核心线程数),即时此时线程池中的线程都是空闲状态,也会创建线程来执行此任务。
如果线程池中的线程数大于或等于corePoolSize,但是workQueue(缓冲队列)未满,那么就会将此线程添加到workQueue中等待执行。
如果线程池中的线程数大于corepoolSize,且workQueue已满,但是线程池中的线程总数小于maxmumPoolSize,那么会创建临时线程来代为执行。
如果线程池中的线程数大于corePoolSize,且线程池总数等于maxmumPoolSize,那么会通过handler所指定的策略来处理此任务。
如果线程池中的线程数大于corePoolSize,且线程等待时间超过了keepAliveTime,线程将被终止,这样,线程池可以动态的调整线程池中的线程数。
9,线程池中的线程复用原理
线程池将线程进行解耦,线程是线程,任务是任务,同一个线程可以从阻塞队列中不断的获取新任务来执行,其核心原理在于线程池对Thread进行了封装,并不是每次执行任务都调用start方法来创建新线程,而是让每个线程去执行一个循环任务,在这个循环任务中,不断的检索是否有新任务需要被执行,如果有则执行,即调用任务中的run方法。将run方法当做一个普通的方法执行。
10,为什么要用线程池,解释一下线程池中的参数?
因为线程是我们程序中的稀缺资源。通过线程池可以做到:
1,降低资源的消耗,提高线程的利用率,降低创建和销毁线程的消耗。
2,提高响应速度,任务一旦来了,马上就有线程响应执行。
3,提高线程线程的可管理性,线程是稀缺资源,使用线程可以统一分配调用监控。
- corePoolSize:代表核心线程数,也就是正常情况下能够创建创建个工作线程,这些线程创建之后不会销毁,常驻线程。
- maxnumPoolSize: 代表的是最大线程数,与核心线程数相对应,表示允许线程池中所有能够工作的线程总数。
- keepAliveTime:表示超出核心线程那部分线程能够存活的时间,意思是当我们核心线程数满了的时候,创建的一些临时线程,这些线程都有一个空闲时间,一旦超过了这个时间,就会被销毁,可以通过setkeepAliveTime来进行设置过期时间。
- workQueue:用来存放任务的队列,当核心线程满了的时候,任务就会被加入到这个队列中进行排队。
- ThreadFactory:实际上是一个线程工厂,用来生产线程执行任务,我们可以使用默认的创建工厂,产生的线程都在同一个组内,拥有相同的优先级,且都不是守护线程。我们也可以自定义工厂。
- Handler:任务拒绝策略,有两种情况,第一种是当我们手动调用shutdown方法关闭线程池时,这时候哪怕是线程池还有没有执行完的任务,我们再想向线程池提交任务就会遭到拒绝。另一种情况就是当达到最大线程数,线程池已经没有能力继续处理任务时,也是一种拒绝策略。
11,sleep(),wait(),join(),yield()的区别
锁池:所有需要竞争锁的线程都会被放入到锁池当中,比如a对象的锁已被其中一个线程拿到,其他竞争线程都会进入到锁池中,等待a对象释放锁,当某个对象得到锁之后会进入到就绪状态。
等待池:线程调用wait方法之后,会进入到等待池当中,等待池中的线程不会去竞争同步锁的,只有调用notify或者notifyAll之后,线程才会开始竞争锁,notify是随机将等待池中的一个线程放到锁池当中,而notifyAll是将等待池中所有的线程放入到锁池当中。
1,sleep只是将线程进行休眠,不会释放锁,而wait会释放锁,使得其他线程可以竞争锁。
2,sleep是可以在任何地方使用的,但是wait,notify,notifyAll只能在同步块中使用。
3,sleep必须捕获异常,wait不用。
yield:调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间。
join:等待某个线程执行结束。
12,谈谈对守护线程的理解?
守护线程为所有非守护线程提供服务,任何一个守护线程都是jvm中所有非守护线程的保姆。它并不能只单独服务于某个线程。可以通过thread.setDaemon(true)设置,但必须是在thread.start上面。
13,并发中的三大特性?
- 原子性:一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行。在 Java 中,对基本数据类型的变量的读取和赋值操作是原子性操作(64位处理器)。不采取任何的原子性保障措施的自增操作并不是原子性的。
- 可见性:当一个线程对一个共享变量发生修改后,其他线程可以马上看到。
- 有序性:有序性是指在java并发中,执行编码的两条指令按照代码的先后顺序执行。
14,谈谈你对volatile的理解?
volatile用来修饰对象的属性,在对被volatile修饰的属性进行修改时,会直接将高级缓存区中的值刷到住内存当中,这样子就保证了可见性,同样读的时候也会是直接从主内存中读取。底层是通过操作系统的内存屏障实现的,由于是内存屏障所以也会禁止指令重排,也就保证了有序性。
15,谈谈你对synchronized的理解?
synchronized是java中的一个关键字,也叫同步锁,一般可以用在实例方法上,静态方法上,以及代码块上,主要维护的是一个状态,这个状态就是同一时刻,只能有一个线程去访问synchronized修饰的方法或代码块,用在实例方法上,锁调用该方法的对象,用在静态方法上,锁当前类的所有对象,用在代码块上如果修饰的是对象则锁的是对象,如果修饰的是类,锁的是该类的所有对象。
16,synchronized可以用来修饰构造方法吗?
不能用来修饰构造方法,但是能修饰构造方法里面的代码块。
17,sychronized的偏向锁,轻量级锁,重量级锁。
1,偏向锁:在锁对象的对象头中记录改锁的线程id,下次这个线程再来获取锁时,可以直接获取到了。
2,轻量级锁:有偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁,此时如果有第二个线程来竞争,偏向锁会升级成轻量级锁,之所以叫轻量级锁是为了和重量级锁区分开来。底层有自旋锁实现,并不会阻塞线程。
3,如果自旋次数过多,仍然没有获取到锁则会升级成重量级锁。重量级锁会导致线程阻塞。
3,自旋锁:自旋锁是线程获取锁的过程中,不会去阻塞线程,也就是无所谓唤醒,阻塞和唤醒这两步都是需要操作系统去进行的,比较消耗时间,自旋锁是线程通过CAS获取预期的一个标记,如果没有获取到,则继续循环获取,如果获取到则表示获取到了锁,这个过程线程一直在运行。
18,volatile和synchonized有什么区别?Volatile能不能保证线程安全?DCL单例为什么要加Volatile?
synchonized用来加锁,而Volatile用来只是保证线程的可见性,通常使用与一个线程写,多个线程读的情况。
volatile不能保证线程安全,因为Volatile不能保证原子性。
Volatile防止指令重排,在DCL中,防止高并发情况,指令重排造成的线程安全的问题。
19,谈谈你对AQS的理解,AQS如何实现可重入锁?
- AQS是一个java线程的同步框架,是jdk中很多锁工具的核心实现框架。
- 在AQS中,维护了一个信号量state和一个线程组成的线程队列。这个线程队列就是用来给线程排队的,而state就像是一个红绿灯,用来控制线程排队或者放行,不同场景下应用不同。
- 在可重入锁场景下,state就用来表示锁的状态。0表示无锁,没加一次锁,state就加1,释放锁state减1。