1.并行和并发有什么区别
并行:同一时刻执行多条指令,物理上的同时发生。
并发:同一时刻只能执行一条指令,但是多条指令被快速切换,在宏观上是同时发生的。一个处理器可以同时处理多个任务,在逻辑上是同时发生的。
2.线程和进程的区别
1)进程是资源分配和调度的基本单位,实现了操作系统的并发。
线程是cpu调度的基本单位,实现了进程内部的并发。
一个程序至少有一个进程,一个进程至少有一个线程
2)开销不一样
3.守护线程和本地线程的区别
java线程分为两种:守护线程(daemon)和用户线程(user)
任何线程都可以被设置为守护线程和用户线程,通过Thread.setDaemon(),必须在start之前调用,否则会报错
区别:唯一的区别在于虚拟机何时离开,守护线程是为其他线程服务的,。
当jvm中不存在任何一个正在运行的非守护线程时,则jvm进程会退出。典型的gc线程,finalizer线程
守护线程在不执行finally子句的情况下就会终止其run方法
4.上下文切换
5.死锁和活锁的区别,死锁和饥饿的区别
死锁:两个或两个以上的线程在执行过程中,因为争夺资源而造成的一直互相等待的现象。
产生死锁的必要条件:
1.互斥条件:资源在同一时间内只能被一个线程占有
2.不剥夺条件:进程已获取的资源在未使用完之前,不能强行剥夺
3.请求与保持条件:一个进程在请求其他资源的同时,会保持已持有的资源
4.循环等待条件:若干进程之间形成一种头尾相连的循环等待资源的关系
死活与活锁的区别:活锁和死锁类似,,不同之处在于处于活锁状态的进程或线程的状态是在一直不断改变的,活锁不会被阻塞,而是不停的检测一个永远不可能为真的条件。活锁产生的条件一般是由于对死锁的不正确处理引起的。由于处于死锁的多个线程同时采取行动。除去进程本身持有的资源外,活锁状态的进程还会消耗cpu资源。活锁有可能自行解开,而死锁不能。
饥饿:一个或多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态
导致饥饿的原因:
1.高优先级的线程会占有所有低优先级线程的cpu时间。
2.线程被永久阻塞在一个等待进入同步块的状态。
3.多个线程在wait()方法上执行,notify()方法不会保证哪一个线程会被唤醒,一个等待的线程可能永远不会被唤醒。
6.什么是线程组
ThreadGroup类,可以把一个线程归属到线程组。有很多安全隐患,如果需要使用推荐使用线程池。
7.java中用到的线程调度算法
抢占式。一个线程用完cpu之后,操作系统会根据线程的优先级,饥饿情况等算出一个总的优先级并分配下一个时间片给某个线程执行。
8.为什么使用Executor框架
1) new Thread()比较消耗性能,创建一个线程比较耗时、耗资源.
2)调用new Thread()创建的线程缺乏管理,而且可以无限制的创建,互相之间竞争,切换占用系统资源。
3)使用new Thread()启动的线程缺乏很多功能,比如定时执行,定期执行、线程中断
使用Executor的好处
1)重用存在的线程,减少对象的创建、消亡的开销
2)可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免阻塞
3)提供定时执行、定期执行、单线程、并发数控制等功能。
9.Executor和ExecutorService的区别
.1)ExecutorService是Executor的子接口
2)Executror定义了execut方法来接收Runnable接口,而ExecutorService的submit可以接收Callable和Runnable
3)ExecutorService提供了控制线程池的方法,shutdown关闭线程池
4)Executors提供工厂方法提供不同的线程池。cached,single,fixed,scheduled
10 .Executor简述
Executor框架实现的就是线程池的功能。
框架包括三部分
1)任务。也就是工作单元,实现Runnable接口或Callable接口
2)任务的执行,把任务分配给多个线程的执行机制,包括Executor和ExecutorService接口
3)异步计算的结果,包括Future接口和实现了Future接口的FutureTask类
1)Executor:一个接口,其定义了一个接收Runnable对象的方法,execute(Runnable command).
2)ExecutorService:是一个比Executor更广泛的子类接口,提供了生命周期管理的方法,返回Future对象,以及跟踪一个或多个异步任务执行状况。submit方法,shutdown平滑关闭线程池。
3)Executors类提供了一系列工厂方法用于创建线程池,线程池都实现了ExecutorService接口
newFiexedThreadPool(int Threads) 固定数量的线程池
newCachedThreadPool() 创建一个可缓存的线程池,没有可用线程会创建一个新的线程,终止并从缓存中移除哪些已有60s未被使用的线程。 线程池可以无限大
newSingleThreadExecutor()创建一个单线程化的Executor
newScheduledThreadPool(nt corePoolSize) 创建一个支持定时及周期性的任务执行的线程池,多数情况下用来代替Timmer
11.自定义线程池
可以使用ThreadPoolExecutor类创建
new ThreadPoolExecutor(corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue);
12.原子操作
原子操作意为不可被中断的一个或一系列操作
在java中通过锁或循环CAS的方式实现
JDK1.5 java.util.concurrent.atomic包提供了int和long类型的原子包装类
AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
解决了ABA问题的原子类
AtomicMarkableReference(通过引入一个boolean来反映中间有没有变过),AtomicStampedReference(通过引入一个int来累加来反映中间有没有变过)。
13.Lock接口对比同步的优势
1)可以使线程在等待锁的时候响应中断 lockInterruptibly()
2) 公平锁
3)可以有多个condition对象
4)可以尝试获取锁 tryLock()
14.阻塞队列
BlockingQueue。当队列为空时,从队列获取元素的操作会被阻塞,当队列满了的时候,添加元素的操作会被阻塞。阻塞队列最广泛的应用是生产者-消费者模式。
add,remove方法会抛出异常
offer poll会返回一个特殊值
put take offer(timeout) poll(timeout)会被阻塞
常见的阻塞队列有:
ArrayBlockingQueue 由数组结构组成的有界阻塞队列
LinkedBlockingQueue 由链表结构组成的有界阻塞队列
SynchronousQueue 单个元素的队列
阻塞队列的经典应用场景,线程池,socket客户端数据读取与解析
15.java实现线程的方式
1)继承Thread类
2)实现Runnable接口
3)实现Callable接口
使用callable的方式:使用FutureTask ft = new FutureTast(new Callable);
new thread(ft).start;
FutureTask实现了RunableFuture或者是ExecutorService.submit
16.如何停止一个正在运行的线程
1)使用共享变量。共享变量可以被多个执行相同任务的线程用来作为是否中断的信
2)使用Interrupt方法,当线程等待io,sleep,join、或ServerSocket.accept()方法导致线程阻塞时,可以被interrupt方法打断,产生interruptException
17.notify和notifyAll的区别
都可以唤醒处于wait状态的线程,notify只能唤醒一个,notifyAll可以唤醒多个
18.乐观锁和悲观锁
悲观锁:总是假设最坏的情况,认为只要不采取措施,就会出现问题。数据库中的读锁,写锁,互斥锁,共享锁,在操作之前先加锁。Synchronized。
乐观锁:基于冲突检测的乐观同步方法。先执行操作,执行完成后看有没有冲突产生,如果没有执行成功。如果由就重新执行。原子类就是使用了乐观锁的一种实现方式,CAS。
乐观锁实现方式
1)使用版本标识,来确定前后数据是否一致。提交后修改标识
2)CAS。cas的三个操作数,内存位置,原值,新值
CAS缺点
1.ABA问题 AtomicStampedReference,AtomicMarkableReference解决这个问题
2.对于竞争严重的情况,CAS自旋的概率比较大,会浪费cpu资源
3.只能保证一个共享变量的原子操作,多个变量时可以使用锁
19.SynchronizedMap和ConcurrentHashMap有什么区别?
SynchronizedMap是有条件的线程安全,所有的单个操作都是线程安全的。但是多个操作组成的操作序列却可能导致出现问题。比如说
if(shm.containsKey('key')){
shm.remove(key);
}
一个线程先判断有没有key,有,然后remove,在remove的同时,另一个线程判断有没有key,结果肯定是true,但是当remove时就没有了
SynchronizedMap是一次锁定一个hash表,同一时刻只有一个线程可以对其进行操作。ConcurrentHashMap采用分段锁来保证多线程下的性能,一次锁定一个桶,ConcurrentHashMap默认有16个桶,get,put,remove等操作只需要锁定当前的桶,可以同时有多个写线程。只有在size才需要锁住整个表。
20.CopyOnWriteArrayList
当多个迭代器同时遍历和修改这个列表时,不会抛出ConcurrentModificationException。写入会导致创建整个底层数组的副本,而源数据保留在原地,使得复制的数组在被修改时,读操作可以安全地执行。
1)读写分离,
2)最终一致性
3)使用额外的空间来解决并发冲突
add方法使用了ReentrantLock,可以防止多线程时copy出多个副本,add方法中使用了Arrays.copyOf()复制了一个副本,使得写操作也不会影响读操作。而读不需要加锁。
使用场景:读操作远多于写操作
21.volatile的作用
保证可见性和有序性。多线程环境下的单次操作(单次读和单次写)
22.Servlet是线程安全的吗
Servlet不是线程安全的
Servlet是单例多线程的,当多个线程访问同一个方法时,是不能保证共享变量的线程安全的。
@Controller,@Service默认Scope是SIngleton,单例的,多个请求使用一个。尽量不要定义变量。一定要定义变量的话,用ThreadLocal封装。
23.wait()和sleep()方法有什么不同
wait()会释放锁,而sleep()会一直持有锁。wait用于线程间通信,sleep通常用于暂停执行。
new runnable(ready,running).
24.一个线程运行时发生异常会怎么样
如果异常没有被捕获,将线程将停止运行。Thread.UncaughtExceptionHandler是用于处理未捕获异常的。
25.为什么wait,notify,notify方法是Object的
java提供的锁是对象级的,而不是线程级的,每个对象都有锁,通过线程获得。wait,notify,notifyall都是锁级别的操作。锁属于对象。
26.ThreadLocal变量
为每一个使用变量的线程提供独立的变量副本,每个线程都可以修改自己的独立的线程副本,但是并不会影响其他的线程副本,从而保证了线程安全。一个线程可以有多个ThreadLocal。threadLocals属性是一个ThreadLocalMap,key是ThreadLocal
具体操作是1.获取当前线程。2.获取当前线程的threadLocals变量。就是ThreadLocalMap,类似于HashMap。key就是ThreadLocal 3.如果threadLocals变量不为null,就可以在map中查找到本地变量的值。
每个线程的本地变量存放在自己的本地内存变量threadLocals中,如果当前线程一直不消亡,那么这些本地变量就会一直存在(所以可能会导致内存溢出).
ThreadLocal不支持继承。在父类中设置后,在子类中获取不到。InheritableThreadLocal可以做到这一点。
ThreadLocalMap中的Entry的key使用的是ThreadLocal对象的弱引用,其他地方没有对ThreadLoca依赖,ThreadLocalMap中的ThreadLocal对象就会被回收掉,但是对应的value不会被回收,这个时候Map中就可能存在key为null但是value不为null的项,这需要实际的时候使用完毕及时调用remove方法避免内存泄漏。
27.interrupt和interrupted的区别
interrupt方法用于中断线程。调用该方法的线程的状态为将被置为”中断”状态。
interrupted ,查询当前线程的中断状态,并且清除原状态。
28.为什么wait和notify方法要在syncjhronized同步块中
当一个线程需要调用对象的wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的notify()方法。当一个线程需要调用对象的notify()方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。所有的这些方法都需要线程持有对象的锁。
java API强制要求这样做,如果你不这么做,你的代码会抛出IllegalMonitorStateException异常。还有一个原因是为了避免wait和notify之间产生竞态条件。
29.如何检查一个线程是否有锁
Thread.holdsLock()
30.jvm参数控制线程堆栈大小
-Xss
31.yield方法
使当前线程由运行状态转为就绪状态
32.ConcurrentHashMap
JDK1.7:ReentrantLock+Segment+HashEntry,锁的粒度是基于Segment的,包含多个HashEntry。Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,也就是上面的提到的锁分离技术,而每一个Segment元素存储的是HashEntry数组+链表,这个和HashMap的数据存储结构一样。Segment大小默认是16.
put操作:对于ConcurrentHashMap的数据插入,这里要进行两次Hash去定位数据的存储位置。当执行put操作时,会进行第一次key的hash来定位Segment的位置,如果该Segment还没有初始化,即通过CAS操作进行赋值。然后进行第二次hash操作,找到相应的HashEntry的位置。在插入时会通过继承ReentrantLock的tryLock()方法尝试去获取锁。如果没有获得锁会自旋等待。
获取size()的两种方案
1、第一种方案他会使用不加锁的模式去尝试多次计算ConcurrentHashMap的size,最多三次,比较前后两次计算的结果,结果一致就认为当前没有元素加入,计算的结果是准确的
2、第二种方案是如果第一种方案不符合,他就会给每个Segment加上锁,然后计算ConcurrentHashMap的size返回。
table中的HashEntry叫做桶
JDK1.8:synchronized+CAS+Node+红黑树,锁的粒度是首节点。使用红黑树来优化链表,当Node节点数大于8时,就会转为TreeNode,链表转为红黑树。
取消segments字段,直接采用transient volatile HashEntry<K,V>[] table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率。
使用synchronized的原因:基于JVM的synchronized优化空间更大,使用内嵌的关键字比使用API更加自然。在大量的数据操作下,对于JVM的内存压力,基于API的ReentrantLock会开销更多的内存
put过程:如果没有初始化就先调用initTable()方法来进行初始化过程。
如果没有hash冲突就直接CAS插入
如果还在进行扩容操作就先进行扩容
如果存在hash冲突,就加锁来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入,
最后一个如果该链表的数量大于阈值8,就要先转换成黑红树的结构,break再一次进入循环(阿里面试官问题,默认的链表大小,超过了这个值就会转换为红黑树);
如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容
33.ThreadLocal解决SimpleDateFormate格式问题
SimpleDateFormate的创建要花费很大的代价
但是如果多个线程共用一个,就会出现线程安全问题。是因为其中的calendar字段,多线程时可以随意修改这个字段。
为每个线程创建一个SimpleDateFormate的副本
34.同步方法和同步块,哪个更好
同步块更好,这意味着同步块之外的代码可以异步执行。同步的范围越小越好。
35.Java Timer 类
可以用来执行周期任务或定时任务
timer.schedule(TimerTask,,delay,period);
36.Condition接口
lock.newCondition()。可以多个等待队列。即多个condition。并且响应中断,等待固定长时间,定时
await()
await(long time, TimeUnit unit) 当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
awaitUninterruptibly
signal()
signalALl()
37.AQS
AQS是JUC的核心
AQS:AbstractQuenedSynchronizer抽象队列式同步器。是除了java自带的synchronized关键字之外的锁机制。
采用的是模板方法模式:定义了一个操作的算法骨架,而将一些步骤延迟到子类中实现。
AQS的核心思想:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node),来实现锁的分配。
实现了AQS的锁有:
资源共享的两种方式
Exclusive:独占,只有一个线程能执行。ReentrantLock
Share:共享:多个线程可以同时执行Semaphore、CountDownLatch、ReadWriteLock
实现自定义同步器的时候只需要实现共享资源state的获取和释放方式。主要实现一下几个方法
isHeldExclusively()该线程是否正在独占资源。只有用到condition才需要去实现它
tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
ReentrantLock:state初始化为0,表示未锁定状态,A线程lock()时,会调用tryAcquire()独占锁并将state+1.之后其他线程再想tryAcquire的时候就会失败,直到A线程unlock()到state=0为止,其他线程才有机会获取该锁。A释放锁之前,自己也是可以重复获取此锁(state累加),这就是可重入的概念。
CountDownLatch:基于AQS的共享模式。任务分N个子线程去执行,state就初始化 为N,N个线程并行执行,每个线程执行完之后countDown()一次,state就会CAS减一。当N子线程全部执行完毕,state=0,会unpark()主调用线程,主调用线程就会从await()函数返回,继续之后的动作。
Semaphore:可以限制访问某些资源的线程数目,但是并没有同步。seqmaphore.aquire()获取许可证。seqmaphore.release()
CyclicBarrier : ReentrantLock 和 Condition 。一组线程相互等待,当所有线程都到达某个屏障点后再进行后续的操作。内部有一个计数器,每个线程在到达屏障点的时候都会调用await方法将自己阻塞,此时计数器会减1,当计数器减为0的时候所有因调用await方法而被阻塞的线程将被唤醒。在唤醒所有的线程之前,可以通过barrierCommand来执行自己的任务。
38.CyclicBarrier 和CountDownLatch的区别
CountDownLatch:让一些线程阻塞直到另一些线程完成一系列操作后才唤醒。它通过调用await方法让线程进入阻塞状态等待倒计时0时唤醒。它通过线程调用countDown方法让倒计时中的计数器减去1,当计数器为0时,会唤醒哪些因为调用了await而阻塞的线程。底层是使用AQS实现的。
CountDownLatch:让一组线程到达一个屏障时被阻塞,知道最后一个线程到达屏障,所有被屏障拦截的线程才会继续执行。它通过调用await方法让线程进入屏障。底层是通过ReentrantLock以及Condition中的await和signal实现。
区别
1)CountDownLatch是做减法,CyclicBarrier是做加法,Semaphor的临界资源可以反复使用
2)CountDownLatch不能重置计数,CycliBarrier提供的reset()方法可以重置计数,不过只能等到第一个计数结束。Semaphor可以重复使用