1. 什么是线程
线程是程序执行的最小单位,它被包含在进程中,是进程中的实际运作单位。
2. 线程和进程的区别
线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。CPU切换一个线程的花费比进程要小得多,同时创建一个线程的开销也比进程要小很多。
3. 并行和并发的区别
并行(Parallel):指两个或者多个事件在同一时刻发生,即同时做某些事情,可以互不干扰的同时做几件事。例如垃圾回收时,多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
并发(Concurrent):指两个或多个事件在同一时间间隔内发生,即交替做不同事的能力,多线程是并发的一种形式。例如垃圾回收时,用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。
简单的举例帮助我们理解:
"食堂打饭"很多人都经历过,放学后学生都冲向食堂,特别是12:00~12:30这个时间段人流量是最大的,这就叫作高并发。排队是解决并发的一种方法。"食堂准备多个窗口, 每个窗口学生都排队打饭"这就是并行了,这也是解决并发的一种方法。简而言之就是并发是多个事件在同一时间段执行,而并行是多个事件在同一时间点执行。
4. 守护线程是什么
守护线程又称为后台线程,它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件,是个服务线程。
正常创建的线程都是普通线程,或称为前台线程,守护线程与普通线程在使用上没有什么区别,但是他们有一个最主要的区别是在于进程的结束中。当一个进程中所有普通线程都结束时,那么进程就会结束。如果进程结束时还有守护线程在运行,那么这些守护线程就会被强制结束。守护线程拥有自动结束自己生命周期的特性。java中垃圾回收线程就是特殊的守候线程。
5. 创建线程有哪几种方式
1. 继承Thread类(真正意义上的线程类,Thread类实现了Runnable接口)。
2. 实现Runnable接口,重写run方法。
3. 实现Callable接口
具体参考博客:https://blog.csdn.net/duan196_118/article/details/103898131
6.说一下 runnable 和 callable 有什么区别
Runnable从JDK1.0开始就有了,Callable是在 JDK1.5增加的。Callable接口提供的call方法比run方法功能更强大,可以有返回值,支持泛型的返回值,可以声明抛出异常,而run()方法没有这些功能。
7. 线程有哪些状态
新建,就绪,运行,阻塞,死亡。说出各种状态的特征及其如何转换。
8. sleep() 和 wait() 有什么区别
1. 用法不同:sleep()时间到会自动恢复,wait()需要使用notify()/notifyAll()直接唤醒。
2. 类不同:sleep()是Thread的方法,wait()是Object的方法。
3. 释放锁:sleep()不释放锁,wait()释放锁。
9. notify()和 notifyAll()有什么区别
这是一个刁钻的问题,因为多线程可以等待单监控锁,Java API 的设计人员提供了一些方法当等待条件改变的时候通知它们,但是这些方法没有完全实现。notify()方法不能唤醒某个具体的线程,所以只有一个线程在等待的时候它才有用武之地。而notifyAll()唤醒所有线程并允许他们争夺锁确保了至少有一个线程能继续运行。
10. 线程的 run()和 start()有什么区别
1. start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。
2. run() 可以重复调用,而 start() 只能调用一次。
3. 第二次调用start() 必然会抛出运行时异常
11. Java中如何停止一个线程
Java提供了很丰富的API但没有为停止线程提供API。JDK 1.0本来有一些像stop(), suspend() 和 resume()的控制方法但是由于潜在的死锁威胁因此在后续的JDK版本中他们被弃用了,之后Java API的设计者就没有提供一个兼容且线程安全的方法来停止一个线程。当run() 或者 call() 方法执行完的时候线程会自动结束,如果要手动结束一个线程,你可以用volatile 布尔变量来退出run()方法的循环或者是取消任务来中断线程。
12. 为什么wait, notify 和 notifyAll这些方法不在thread类里面
这是个设计相关的问题,它考察的是面试者对现有系统和一些普遍存在但看起来不合理的事物的看法。回答这些问题的时候,你要说明为什么把这些方法放在 Object类里是有意义的,还有不把它放在Thread类里的原因。一个很明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁 就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。
13. 什么是线程池? 为什么要使用它
创建线程要花费昂贵的资源和时间,如果任务来了才创建线程那么响应时间会变长,而且一个进程能创建的线程数有限。为了避免这些问题,在程序启动的时 候就创建若干线程来响应处理,它们被称为线程池,里面的线程叫工作线程。从JDK1.5开始,Java API提供了Executor框架让你可以创建不同的线程池。比如单线程池,每次处理一个任务;数目固定的线程池或者是缓存线程池(一个适合很多生存期短 的任务的程序的可扩展线程池)。
14. 在 java 程序中怎么保证多线程的运行安全
1. 使用自动锁synchronized
2. 使用手动锁Lock
3. 使用安全类,比如java.util.Concurrent下的类
15. 什么是死锁?怎么防止死锁
当两个线程相互等待对方释放“锁”时就会发生死锁。出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。如果线程A持有锁L并且想获得锁M,线程C持有锁M并且想要获得锁L,那么这两个线程将永远等待下去,这就是简单的死锁形式。
死锁需要满族的四大条件如下:
产生死锁的主要原因有:
防止死锁:
1、让程序每次至多只能获得一个锁。当然,在多线程环境下,这种情况通常并不现实
2、设计时考虑清楚锁的顺序,尽量减少嵌在的加锁交互数量
3、既然死锁的产生是两个线程无限等待对方持有的锁,那么只要等待时间有个上限不就好了。当然synchronized不具备这个功能,但是我们可以使用Lock类中的tryLock方法去尝试获取锁,这个方法可以指定一个超时时限,在等待超过该时限之后变回返回一个失败信息
可以参考博客:https://blog.csdn.net/duan196_118/article/details/104653053
16. 怎么唤醒一个阻塞的线程
如果线程是因为调用了wait()、sleep()或者join()方法而导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它;如果线程遇到了IO阻塞,无能为力,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。
17. synchronized 和 volatile 的区别是什么
1. volatile 是变量修饰符;synchronized 是修饰类、方法、代码段。
2. volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。
3. volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
18. synchronized 和 Lock 有什么区别
1. synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。
2. synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己手动加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
3. 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
19. synchronized 和 ReentrantLock 区别是什么
1. ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;
2. ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
3. ReentrantLock 只适用于代码块锁,而 synchronized 可用于修饰方法、代码块等。
20. 怎么检测一个线程是否拥有锁
在java.lang.Thread中有一个方法叫holdsLock(),它返回true如果当且仅当当前线程拥有某个具体对象的锁。
21. Thread类中的yield方法有什么作用
yield方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield()的线程有可能在进入到暂停状态后马上又被执行。
22. 创建线程池有哪几种方式
1. newSingleThreadExecutor():它的特点在于工作线程数目被限制为 1,操作一个无界的工作队列,所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态,并且不允许使用者改动线程池实例,因此可以避免其改变线程数目;
2. newCachedThreadPool():它是一种用来处理大量短时间工作任务的线程池,具有几个鲜明特点:它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲置的时间超过 60 秒,则被终止并移出缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用 SynchronousQueue 作为工作队列;
3. newFixedThreadPool(int nThreads):重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有 nThreads 个工作线程是活动的。这意味着,如果任务数量超过了活动队列数目,将在工作队列中等待空闲线程出现;如果有工作线程退出,将会有新的工作线程被创建,以补足指定的数目 nThreads;
4. newSingleThreadScheduledExecutor():创建单线程池,返回 ScheduledExecutorService,可以进行定时或周期性的工作调度;
5. newScheduledThreadPool(int corePoolSize):和newSingleThreadScheduledExecutor()类似,创建的是个 ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程;
6. newWorkStealingPool(int parallelism):这是一个经常被人忽略的线程池,Java 8 才加入这个创建方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序;
7. ThreadPoolExecutor():是最原始的线程池创建,上面1-3创建方式都是对ThreadPoolExecutor的封装。
23.线程池都有哪些状态
线程池的5种状态:Running、ShutDown、Stop、Tidying、Terminated。
24. 为什么要使用线程池
避免频繁地创建和销毁线程,达到线程对象的重用。另外,使用线程池还可以根据项目灵活地控制并发的数目。
25. ConcurrentHashMap的并发度是什么
ConcurrentHashMap的并发度就是segment的大小,默认为16,这意味着最多同时可以有16条线程操作ConcurrentHashMap,这也是ConcurrentHashMap对Hashtable的最大优势,任何情况下,Hashtable能同时有两条线程获取Hashtable中的数据
26. JDK几引入并发包
JDK1.5
27.ThreadLocal 是什么?有哪些使用场景
ThreadLocal 是线程本地存储,在每个线程中都创建了一个 ThreadLocalMap 对象,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。
经典的使用场景是
为每个线程分配一个 JDBC 连接 Connection。这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A 线程关了 B线程正在使用的 Connection; 还有 Session 管理 等问题。
28. 用两个线程,一个输出字母,一个输出数字,交替输出
这道题显然有多种写法,但是如何写才能更高效优雅呢?
方法一:
public class ThreadDemo {
static Thread t1=null, t2=null;
public static void main(String[] args) {
char [] a1 = "1234567".toCharArray();
char [] a2 = "ABCDEFG".toCharArray();
t1 = new Thread(() ->{
for(char c:a1) {
System.out.println(c);
LockSupport.unpark(t2);
LockSupport.park();
}
});
t2 = new Thread(() ->{
for(char c:a2) {
LockSupport.park();
System.out.println(c);
LockSupport.unpark(t1);
}
});
t1.start();
t2.start();
}
}
方法一当两个数组的length不同时,就要做相应的判断了。
方法二:
public static void main(String[] args) {
final Object o = new Object();
char [] a1 = "1234567".toCharArray();
char [] a2 = "ABCDEFG".toCharArray();
new Thread(() ->{
synchronized(o) {//锁住某个对象
for(char c:a1) {
System.out.println(c);
try {
o.notify();
o.wait();//让出锁
}catch(InterruptedException e) {
e.printStackTrace();
}
}
o.notify();//必须的,否则无法停止程序。
}
}).start();
new Thread(() ->{
synchronized(o) {
for(char c:a2) {
System.out.println(c);
try {
o.notify();
o.wait();
}catch(InterruptedException e) {
e.printStackTrace();
}
}
o.notify();//必须的,否则无法停止程序。
}
}).start();
}
方法三:
public static void main(String[] args) {
char [] a1 = "1234567".toCharArray();
char [] a2 = "ABCDEFG".toCharArray();
ReentrantLock lock = new ReentrantLock();
//相当于lock锁下定义两个条件
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
new Thread(()-> {
try {
lock.lock();
for(char c:a1) {
System.out.println(c);
condition2.signal();//唤醒持有condition2锁的线程
condition1.await();//持有condition1锁的线程等待
}
condition2.signal();
}catch(Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}).start();
new Thread(()-> {
try {
lock.lock();
for(char c:a2) {
System.out.println(c);
condition1.signal();
condition2.await();
}
condition1.signal();
}catch(Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}).start();
}
29. Java当中有哪几种锁
自旋锁: 自旋锁在JDK1.6之后就默认开启了。基于之前的观察,共享数据的锁定状态只会持续很短的时间,为了这一小段时间而去挂起和恢复线程有点浪费,所以这里就做了一个处理,让后面请求锁的那个线程在稍等一会,但是不放弃处理器的执行时间,看看持有锁的线程能否快速释放。为了让线程等待,所以需要让线程执行一个忙循环也就是自旋操作。在jdk6之后,引入了自适应的自旋锁,也就是等待的时间不再固定了,而是由上一次在同一个锁上的自旋时间及锁的拥有者状态来决定。
偏向锁: 在JDK1.之后引入的一项锁优化,目的是消除数据在无竞争情况下的同步原语。进一步提升程序的运行性能。偏向锁就是偏心的偏,意思是这个锁会偏向第一个获得他的线程,如果接下来的执行过程中,改锁没有被其他线程获取,则持有偏向锁的线程将永远不需要再进行同步。偏向锁可以提高带有同步但无竞争的程序性能,也就是说他并不一定总是对程序运行有利,如果程序中大多数的锁都是被多个不同的线程访问,那偏向模式就是多余的,在具体问题具体分析的前提下,可以考虑是否使用偏向锁。
轻量级锁: 为了减少获得锁和释放锁所带来的性能消耗,引入了“偏向锁”和“轻量级锁”,所以在Java SE1.6里锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。
30. 如何在两个线程间共享数据
通过在线程之间共享对象就可以了,然后通过wait/notify/notifyAll、await/signal/signalAll进行唤起和等待,比方说阻塞队列BlockingQueue就是为线程之间共享数据而设计的。
31. 什么是线程池(thread pool)?
在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。
在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这就是“池化资源”技术产生的原因。线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。
Java 5+中的Executor接口定义一个执行线程的工具。它的子类型即线程池接口是ExecutorService。要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,因此在工具类Executors面提供了一些静态工厂方法,生成一些常用的线程池,如下所示:
newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。newSingleThreadExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。
32. 为什么要使用线程池
避免频繁地创建和销毁线程,达到线程对象的重用。另外,使用线程池还可以根据项目灵活地控制并发的数目。
33. java中用到的线程调度算法是什么
抢占式。一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。
34. Thread.sleep(0)的作用是什么
由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。
35. 什么是CAS
CAS,全称为Compare and Swap,即比较-替换。假设有三个操作数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,才会将内存值修改为B并返回true,否则什么都不做并返回false。当然CAS一定要volatile变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值,否则旧的预期值A对某条线程来说,永远是一个不会变的值A,只要某次CAS操作失败,永远都不可能成功
36. CyclicBarrier和CountDownLatch区别
这两个类非常类似,都在java.util.concurrent下,都可以用来表示代码运行到某个点上,二者的区别在于:
CyclicBarrier的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;CountDownLatch则不是,某线程运行到某个点上之后,只是给某个数值-1而已,该线程继续运行
CyclicBarrier只能唤起一个任务,CountDownLatch可以唤起多个任务
CyclicBarrier可重用,CountDownLatch不可重用,计数值为0该CountDownLatch就不可再用了。
37. java中的++操作符线程安全么?
不是线程安全的操作。它涉及到多个指令,如读取变量值,增加,然后存储回内存,这个过程可能会出现多个线程交差。
38. 你有哪些多线程开发良好的实践?
给线程命名
最小化同步范围
优先使用volatile
尽可能使用更高层次的并发工具而非wait和notify()来实现线程通信,如BlockingQueue,Semeaphore
优先使用并发容器而非同步容器.
考虑使用线程池
39. volatile类型变量提供什么保证?
volatile 主要有两方面的作用:1.避免指令重排2.可见性保证.例如,JVM 或者 JIT为了获得更好的性能会对语句重排序,但是 volatile 类型变量即使在没有同步块的情况下赋值也不会与其他语句重排序。volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。某些情况下,volatile 还能提供原子性,如读 64 位数据类型,像 long 和 double 都不是原子的(低32位和高32位),但 volatile 类型的 double 和 long 就是原子的。
40. 高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?
这是我在并发编程网上看到的一个问题,把这个问题放在最后一个,希望每个人都能看到并且思考一下,因为这个问题非常好、非常实际、非常专业。关于这个问题,个人看法是:
高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换
并发不高、任务执行时间长的业务要区分开看:
假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换
并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)。
业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。
41. 讲讲线程池的实现原理
首先要明确为什么要使用线程池,使用线程池会带来什么好处?
线程是稀缺资源,不能频繁的创建。应当将其放入一个池子中,可以给其他任务进行复用。
解耦作用,线程的创建于执行完全分开,方便维护。
42. 创建一个线程池
以一个使用较多的
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue
其中的 corePoolSize 为线程池的基本大小。
maximumPoolSize 为线程池最大线程大小。
keepAliveTime 和 unit 则是线程空闲后的存活时间。
workQueue 用于存放任务的阻塞队列。
handler 当队列和最大线程池都满了之后的饱和策略。
43. 合理配置线程池
线程池并不是配置越大越好,而是要根据任务的熟悉来进行划分:如果是 CPU 密集型任务应当分配较少的线程,比如 CPU 个数相当的大小。
如果是 IO 密集型任务,由于线程并不是一直在运行,所以可以尽可能的多配置线程,比如 CPU 个数 * 2 。
当是一个混合型任务,可以将其拆分为 CPU 密集型任务以及 IO 密集型任务,这样来分别配置。
和看到的小伙伴共勉之,欢迎留言交流,望不吝赐教。。。