1、什么是线程和进程?
进程:在操作系统中能够独立运行,并且作为资源分配的基本单位。它表示运行中的程序。系统运行一个程序就是一个进程从创建、运行到消亡的过程。
线程:是一个比进程更小的执行单位,能够完成进程中的一个功能,也被称为轻量级进程。一个进程在其执行的过程中可以产生多个线程。
【注】线程与进程不同的是:同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多。
为什么程序计数器、虚拟机栈和本地方法栈是线程私有的呢?为什么堆和方法区是线程共享的呢?
程序计数器为什么是私有的?
程序计数器主要有下面两个作用:
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
(需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。)
所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。
- 虚拟机栈和本地方法栈为什么是私有的?
- 虚拟机栈: 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
- 本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。
堆和方法区是所有线程共享的资源,其中:
- 堆是进程中最大的一块内存,主要用于存放新创建的对象 (所有对象都在这里分配内存)。
- 方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
2、什么是上下文切换?
即使单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程是同时执行的。(时间片一般是几十毫秒)
CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到加载的过程就是一次上下文切换。上下文切换会影响多线程的执行速度。
3、并发与并行?
并发指的是多个任务交替进行,并行则是指真正意义上的“同时进行”。
实际上,如果系统内只有一个CPU,使用多线程时,在真实系统环境下不能并行,只能通过切换时间片的方式交替进行,从而并发执行任务。真正的并行只能出现在拥有多个CPU的系统中。
4、线程的生命周期和状态?(重要!)
Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态:
初始状态、运行状态、阻塞状态、等待状态、超时等待状态、终止状态
线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换:
- 线程创建之后它将处于 初始状态(NEW),调用
start()
方法后开始运行,线程这时候处于 可运行状态(READY)。 - 可运行状态的线程获得了 CPU 时间片后就处于 运行状态(RUNNING)。
- 当线程执行
wait()
方法之后,线程进入 等待状态(WAITING),进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态【notify()】。 而 超时等待状态(TIME_WAITING)相当于在等待状态的基础上增加了超时限制,【sleep(long millis)/
wait(long millis)】,
当超时时间到达后 Java 线程将会返回到运行状态。 - 当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到阻塞状态(BLOCKED)。
- 线程在执行 Runnable 的
run()
方法之后将会进入到 终止状态(TERMINATED)。
5、什么是线程死锁?如何避免死锁?
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
假如线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
避免死锁的几个常见方法:
- 避免一个线程同时获取多个锁
- 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
- 尝试使用定时锁,使用 lock.tryLock(timeout) 来代替使用内部锁机制。
- 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
6、sleep() 方法和 wait() 方法区别和共同点?/notify()方法和notifyAll()方法区别(重要!)
相同点:
两者都可以暂停线程的执行,都会让线程进入等待状态。
不同点:
- sleep()方法没有释放锁,而 wait()方法释放了锁。
- sleep()方法属于Thread类的静态方法,作用于当前线程;而wait()方法是Object类的实例方法,作用于对象本身。
- 执行sleep()方法后,可以通过超时或者调用interrupt()方法唤醒休眠中的线程;执行wait()方法后,通过调用notify()或notifyAll()方法唤醒等待线程
-
notify()方法:唤醒处于等待状态的线程,这里应该注意是,如果有多个线程处于等待的状态,那么调用一次notify()方法后,具体释放的是哪一个线程是不确定的,执行的过程是java虚拟机来实现的。
notifyAll方法 唤醒所有等待的线程
7、为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
new 一个 Thread,线程进入初始状态;调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结: 调用 start 方法可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。
8、多线程开发带来的问题与解决方法?(重要)
使用多线程主要会带来以下几个问题:
(一)线程安全问题
线程安全问题指的是在某一线程从开始访问到结束访问某一数据期间,该数据被其他的线程所修改,那么对于当前线程而言,该线程就发生了线程安全问题,表现形式为数据的缺失,数据不一致等。
线程安全问题发生的条件:
1)多线程环境下,即存在包括自己在内存在有多个线程。
2)多线程环境下存在共享资源,且多线程操作该共享资源。
3)多个线程必须对该共享资源有非原子性操作。
线程安全问题的解决思路:
1)尽量不使用共享变量,将不必要的共享变量变成局部变量来使用。
2)使用synchronized关键字同步代码块,或者使用jdk包中提供的Lock为操作进行加锁。
3)使用ThreadLocal为每一个线程建立一个变量的副本,各个线程间独立操作,互不影响。
(二)性能问题
线程的生命周期开销是非常大的,一个线程的创建到销毁都会占用大量的内存。同时如果不合理的创建了多个线程,cup的处理器数量小于了线程数量,那么将会有很多的线程被闲置,闲置的线程将会占用大量的内存,为垃圾回收带来很大压力,同时cup在分配线程时还会消耗其性能。
解决思路:
利用线程池,模拟一个池,预先创建有限合理个数的线程放入池中,当需要执行任务时从池中取出空闲的先去执行任务,执行完成后将线程归还到池中,这样就减少了线程的频繁创建和销毁,节省内存开销和减小了垃圾回收的压力。同时因为任务到来时本身线程已经存在,减少了创建线程时间,提高了执行效率,而且合理的创建线程池数量还会使各个线程都处于忙碌状态,提高任务执行效率,线程池还提供了拒绝策略,当任务数量到达某一临界区时,线程池将拒绝任务的进入,保持现有任务的顺利执行,减少池的压力。
(三)活跃性问题
1)死锁,假如线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。多个线程环形占用资源也是一样的会产生死锁问题。
解决方法:
- 避免一个线程同时获取多个锁
- 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
- 尝试使用定时锁,使用 lock.tryLock(timeout) 来代替使用内部锁机制。
想要避免死锁,可以使用无锁函数(cas)或者使用重入锁(ReentrantLock),通过重入锁使线程中断或限时等待可以有效的规避死锁问题。
2)饥饿,饥饿指的是某一线程或多个线程因为某些原因一直获取不到资源,导致程序一直无法执行。如某一线程优先级太低导致一直分配不到资源,或者是某一线程一直占着某种资源不放,导致该线程无法执行等。
解决方法:
与死锁相比,饥饿现象还是有可能在一段时间之后恢复执行的。可以设置合适的线程优先级来尽量避免饥饿的产生。
3)活锁,活锁体现了一种谦让的美德,每个线程都想把资源让给对方,但是由于机器“智商”不够,可能会产生一直将资源让来让去,导致资源在两个线程间跳动而无法使某一线程真正的到资源并执行,这就是活锁的问题。
(四)阻塞
阻塞是用来形容多线程的问题,几个线程之间共享临界区资源,那么当一个线程占用了临界区资源后,所有需要使用该资源的线程都需要进入该临界区等待,等待会导致线程挂起,一直不能工作,这种情况就是阻塞,如果某一线程一直都不释放资源,将会导致其他所有等待在这个临界区的线程都不能工作。当我们使用synchronized或重入锁时,我们得到的就是阻塞线程,如论是synchronized或者重入锁,都会在试图执行代码前,得到临界区的锁,如果得不到锁,线程将会被挂起等待,知道其他线程执行完成并释放锁且拿到锁为止。
解决方法:
可以通过减少锁持有时间,读写锁分离,减小锁的粒度,锁分离,锁粗化等方式来优化锁的性能。
临界区:
临界区是用来表示一种公共的资源(共享数据),它可以被多个线程使用,但是在每次只能有一个线程能够使用它,当临界区资源正在被一个线程使用时,其他的线程就只能等待当前线程执行完之后才能使用该临界区资源。
比如办公室办公室里有一支笔,它一次只能被一个人使用,假如它正在被甲使用时,其他想要使用这支笔的人只能等甲使用完这支笔之后才能允许另一个人去使用。这就是临界区的概念。
参考 https://www.cnblogs.com/Eternally-dream/p/9678314.html
9、 synchronized 关键字
synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
synchronized关键字最主要的三种使用方式:修饰实例方法:、修饰静态方法、修饰代码块。
- 对于普通同步方法,锁是当前实例对象。
- 对于静态同步方法,锁是当前类的Class对象。
- 对于同步代码块,锁是synchronized括号里配置的对象。
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。
synchronized在JVM里是怎么实现的?
synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因)
synchronized用的锁是存在哪里的?
synchronized用到的锁是存在Java对象头里的。
10、说说 JDK1.6 之后的synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗
JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
关于这几种优化的详细信息可以查看这篇文章:https://gitee.com/SnailClimb/JavaGuide/blob/master/docs/java/Multithread/synchronized.md
11、synchronized和 Lock 的区别?(重要)
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁(tryLock()方法:如果获取锁成功,则返回true),而synchronized却无法办到。
5)Lock可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
参考https://blog.csdn.net/qq_38200548/article/details/82943222
12、synchronized和ReentrantLock(重入锁) 的区别?
- 两者都是可重进入锁,就是能够支持一个线程对资源的重复加锁。sychnronized关键字隐式的支持重进入,比如一个sychnronized修饰的递归方法,在方法执行时,执行线程在获取了锁之后仍能连续多次地获取该锁。ReentrantLock虽然没能像sychnronized关键字一样隐式的重进入,但是在调用lock()方法时,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞。
- 线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于0时表示锁已经成功被释放。
- synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API。ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成)
- ReentrantLock 比 synchronized 增加了一些高级功能,主要有3点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)
- ReentrantLock提供了一种能够中断等待锁的线程的机制,也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。通过lock.lockInterruptibly()来实现这个机制。
- ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。(公平锁就是先等待的线程先获得锁)
- synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制。ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。用ReentrantLock类结合Condition实例可以实现“选择性通知” 。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程
13、volatile关键字
保证共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
把变量声明为volatile,这就指示 JVM每次使用它都到主存中进行读取。
14、synchronized 关键字和 volatile 关键字的区别
- volatile关键字是线程同步的轻量级实现,所以volatile性能比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。
- 多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞。、
- volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。
- volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。
15、使用线程池的好处?
- 降低资源消耗。通过重复利用已创建的线程,降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。
16、说一说几种常见的线程池及适用场景?(重要)
可以创建(Executors.newXXX)3种类型的ThreadPoolExecutor:FixedThreadPool、SingleThreadExecutor、CachedThreadPool。
- FixedThreadPool:可重用固定线程数的线程池。(适用于负载比较重的服务器)
- FixedThreadPool使用无界队列LinkedBlockingQueue作为线程池的工作队列
- 该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
- SingleThreadExecutor:只会创建一个线程执行任务。(适用于需要保证顺序执行各个任务;并且在任意时间点,没有多线程活动的场景。)
- SingleThreadExecutorl也使用无界队列LinkedBlockingQueue作为工作队列
- 若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
- CachedThreadPool:是一个会根据需要调整线程数量的线程池。(大小无界,适用于执行很多的短期异步任务的小程序,或负载较轻的服务器)
- CachedThreadPool使用没有容量的SynchronousQueue作为线程池的工作队列,但CachedThreadPool的maximumPool是无界的。
- 线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
- ScheduledThreadPool:继承自ThreadPoolExecutor。它主要用来在给定的延迟之后运行任务,或者定期执行任务。使用DelayQueue作为任务队列。
16、线程池都有哪几种工作队列?(重要)
- ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序。
- LinkedBlockingQueue:是一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
- SynchronousQueue:是一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于Linked-BlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
- PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
17、创建线程的几种方式?(重要)
有4种方式:继承Thread类、实现Runnable接口、实现Callable接口、使用Executor框架来创建线程池。
(1)通过继承Thread类创建线程
public class MyThread extends Thread {//继承Thread类
//重写run方法
public void run(){
}
}
----------------------------------------------------------------------------------
public class Main {
public static void main(String[] args){
new MyThread().start(); //创建并启动线程
}
}
(2)通过实现Runnable接口来创建线程
public class MyThread2 implements Runnable {//实现Runnable接口
//重写run方法
public void run(){
}
}
------------------------------------------------------------------------------------------
public class Main {
public static void main(String[] args){
//创建并启动线程
MyThread2 myThread=new MyThread2();
Thread thread=new Thread(myThread);
thread().start();
//或者 new Thread(new MyThread2()).start();
}
}
不管是继承Thread还是实现Runnable接口,多线程代码都是通过运行Thread的start()方法来运行的。
(3)实现Callable接口来创建线程
与实现Runnable接口类似,和Runnable接口不同的是,Callable接口提供了一个call() 方法作为线程执行体,call()方法比run()方法功能要强大:call()方法可以有返回值、call()方法可以声明抛出异常。
public class Main {
public static void main(String[] args){
MyThread3 th=new MyThread3();
//使用Lambda表达式创建Callable对象
//使用FutureTask类来包装Callable对象
FutureTask<Integer> future=new FutureTask<Integer>(
(Callable<Integer>)()->{
return 5;
}
);
new Thread(task,"有返回值的线程").start();//实质上还是以Callable对象来创建并启动线程
try{
System.out.println("子线程的返回值:"+future.get());//get()方法会阻塞,直到子线程执行结束才返回
}catch(Exception e){
ex.printStackTrace();
}
}
}
(4)使用Executor框架来创建线程池
Executors.newXXXX: newFixedThreadPool(int )、newSingleThreadExecutor、newCachedThreadPool、newScheduledThreadPool(int)
通过Executors的以上四个静态工厂方法获得 ExecutorService实例,而后可以执行Runnable任务或Callable任务。
- Executor执行Runnable任务:
通过Executors的以上四个静态工厂方法获得 ExecutorService实例,而后调用该实例的execute(Runnable command)方法即可。一旦Runnable任务传递到execute()方法,该方法便会自动在一个线程上。
public class TestCachedThreadPool{ public static void main(String[] args){ ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < 5; i++){ executorService.execute(new TestRunnable()); System.out.println("************* a" + i + " *************"); } executorService.shutdown(); } } class TestRunnable implements Runnable{
//重写run方法 public void run(){ System.out.println(Thread.currentThread().getName() + "线程被调用了。"); }
- Executor执行Callable任务:
当将一个Callable的对象传递给ExecutorService的submit方法,则该call方法自动在一个线程上执行,并且会返回执行结果Future对象。同样,将Runnable的对象传递给ExecutorService的submit方法,则该run方法自动在一个线程上执行,并且会返回执行结果Future对象,但是在该Future对象上调用get方法,将返回null。
public class CallableDemo{ public static void main(String[] args){ ExecutorService executorService = Executors.newCachedThreadPool(); List<Future<String>> resultList = new ArrayList<Future<String>>(); //创建10个任务并执行 for (int i = 0; i < 10; i++){ //使用ExecutorService执行Callable类型的任务,并将结果保存在future变量中 Future<String> future = executorService.submit(new TaskWithResult(i)); //将任务执行结果存储到List中 resultList.add(future); } //遍历任务的结果 for (Future<String> fs : resultList){ try{ while(!fs.isDone);//Future返回如果没有完成,则一直循环等待,直到Future返回完成 System.out.println(fs.get()); //打印各个线程(任务)执行的结果 }catch(InterruptedException e){ e.printStackTrace(); }catch(ExecutionException e){ e.printStackTrace(); }finally{ //启动一次顺序关闭,执行以前提交的任务,但不接受新任务 executorService.shutdown(); } } } } class TaskWithResult implements Callable<String>{ private int id; public TaskWithResult(int id){ this.id = id; } // 重写call()方法 public String call() throws Exception { System.out.println("call()方法被自动调用!!! " + Thread.currentThread().getName()); //该返回结果将被Future的get方法得到 return "call()方法被自动调用,任务返回的结果是:" + id + " " + Thread.currentThread().getName(); } }
实现Runnable接口和Callable接口的区别?
Runnable接口或Callable接口实现类都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor执行。两者的区别在于 Runnable 接口不会返回结果但是 Callable 接口可以返回结果。
执行execute()方法和submit()方法的区别是什么呢?
1) execute()
方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
2) submit()
方法用于提交需要返回值的任务。线程池会返回一个Future类型的对象,通过这个Future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)
方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
18.线程池参数?
①corePoolSize:线程池的基本大小,当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。说白了就是,即便是线程池里没有任何任务,也会有corePoolSize个线程在候着等任务。
②maximumPoolSize:最大线程数,不管你提交多少任务,线程池里最多工作线程数就是maximumPoolSize。
③keepAliveTime:线程的存活时间。当线程池里的线程数大于corePoolSize时,如果等了keepAliveTime时长还没有任务可执行,则线程退出。
⑤unit:这个用来指定keepAliveTime的单位,比如秒:TimeUnit.SECONDS。
⑥workQueue:用于保存等待执行任务的阻塞队列,提交的任务将会被放到这个队列里。
⑦threadFactory:线程工厂,用来创建线程。主要是为了给线程起名字,默认工厂的线程名字:pool-1-thread-3。
⑧handler:拒绝策略,即当线程和队列都已经满了的时候,应该采取什么样的策略来处理新提交的任务。默认策略是AbortPolicy(抛出异常),其他的策略还有:CallerRunsPolicy(只用调用者所在线程来运行任务)、DiscardOldestPolicy(丢弃队列里最近的一个任务,并执行当前任务)、DiscardPolicy(不处理,丢弃掉)
19.线程池执行流程?
任务被提交到线程池,会先判断当前线程数量是否小于corePoolSize,如果小于则创建线程来执行提交的任务,否则将任务放入workQueue队列,如果workQueue满了,则判断当前线程数量是否小于maximumPoolSize,如果小于则创建线程执行任务,否则就会调用handler,以表示线程池拒绝接收任务。
20.Java并发
1,原子性
由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。
2,可见性:
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
当然通过synchronized和Lock也能够保证可见性。
3,有序性
很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
21.happens-before原则
1)程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
2)锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作
3)volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
4)传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
22.synchronized的修饰方法和修饰代码块区别
同步方法直接在方法上加synchronized实现加锁,同步代码块则在方法内部加锁,很明显,同步方法锁的范围比较大,而同步代码块范围要小点,一般同步的范围越大,性能就越差,一般需要加锁进行同步的时候,肯定是范围越小越好,这样性能更好
23.concurrent包有啥内容
通常所说的concurrent包基本有3个package组成
java.util.concurrent:提供大部分关于并发的接口和类,如BlockingQueue,Callable,ConcurrentHashMap,ExecutorService, Semaphore等
java.util.concurrent.atomic:提供所有原子操作的类, 如AtomicInteger, AtomicLong等;
java.util.concurrent.locks:提供锁相关的类, 如Lock, ReentrantLock, ReadWriteLock, Condition等;
concurrent包的优点:
1. 首先,功能非常丰富,诸如线程池(ThreadPoolExecutor),CountDownLatch等并发编程中需要的类已经有现成的实现,不需要自己去实现一套; 毕竟jdk1.4对多线程编程的主要支持几乎就只有Thread, Runnable,synchronized等
concurrent包里面的一些操作是基于硬件级别的CAS(compare and swap),就是在cpu级别提供了原子操作,简单的说就可以提供无阻塞、无锁定的算法; 而现代cpu大部分都是支持这样的算法的;
Java并发包(java.util.concurrent及其子包)提供的并发工具类
- 比synchronized更加高级的各种同步结构,如:Semaphore,CyclicBarrier, CountDownLatch
- 各种线程安全的容器(主要有四类:Queue,List,Set,Map),如:ConcurrentHashMap,ConcurrentSkipListMap,CopyOnWriteArrayList
- 各种并发队列的实现,如各种BlockingQueue的实现(ArrayBlockingQueue, LinkedBlockingQueue, SynchorousQueue, PriorityBlockingQueue,DelayQueue,LinkedTranferQueue)等。
- Executor框架与线程池
24.ThreadLocal
ThreadLocal提供了线程的局部变量,每个线程都可以通过set()和get()来对这个局部变量进行操作,但不会和其他线程的局部变量进行冲突,实现了线程的数据隔离~。
ThreadLocal在每个线程中对该变量会创建一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。
ThreadLocal和Synchronized都是为了解决多线程中相同变量的访问冲突问题,不同的点是
- Synchronized是通过线程等待,牺牲时间来解决访问冲突
- ThreadLocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突,并且相比于Synchronized,ThreadLocal具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值。
25.读写锁
ReentrantReadWriteLock:读写各用一把锁;对于读多写少场景效率高。
读写锁表示两个锁,一个是读操作相关的锁,称为共享锁;另一个是写操作相关的锁,称为排他锁。我把这两个操作理解为三句话:
1、读和读之间不互斥,因为读操作不会有线程安全问题
2、写和写之间互斥,避免一个写操作影响另外一个写操作,引发线程安全问题
3、读和写之间互斥,避免读操作的时候写操作修改了内容,引发线程安全问题
总结起来就是,多个Thread可以同时进行读取操作,但是同一时刻只允许一个Thread进行写入操作。
26.阻塞队列
1.前言:
在新增的Concurrent包中,BlockingQueue很好的解决了多线程中,如何高效安全“传输”数据的问题。通过这些高效并且线程安全的队列类,为我们快速搭建高质量的多线程程序带来极大的便利。本文详细介绍了BlockingQueue家庭中的所有成员,包括他们各自的功能以及常见使用场景。
2.认识BlockingQueue
阻塞队列,顾名思义,首先它是一个队列,而一个队列在数据结构中所起的作用大致如下图所示:
从上图我们可以很清楚看到,通过一个共享的队列,可以使得数据由队列的一端输入,从另外一端输出;
常用的队列主要有以下两种:(当然通过不同的实现方式,还可以延伸出很多不同类型的队列,DelayQueue就是其中的一种)
先进先出(FIFO):先插入的队列的元素也最先出队列,类似于排队的功能。从某种程度上来说这种队列也体现了一种公平性。
后进先出(LIFO):后插入队列的元素最先出队列,这种队列优先处理最近发生的事件。
多线程环境中,通过队列可以很容易实现数据共享,比如经典的“生产者”和“消费者”模型中,通过队列可以很便利地实现两者之间的数据共享。假设我们有若干生产者线程,另外又有若干个消费者线程。如果生产者线程需要把准备好的数据共享给消费者线程,利用队列的方式来传递数据,就可以很方便地解决他们之间的数据共享问题。但如果生产者和消费者在某个时间段内,万一发生数据处理速度不匹配的情况呢?理想情况下,如果生产者产出数据的速度大于消费者消费的速度,并且当生产出来的数据累积到一定程度的时候,那么生产者必须暂停等待一下(阻塞生产者线程),以便等待消费者线程把累积的数据处理完毕,反之亦然。然而,在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。好在此时,强大的concurrent包横空出世了,而他也给我们带来了强大的BlockingQueue。(在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒)
1. ArrayBlockingQueue
基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。
ArrayBlockingQueue在生产者放入数据和消费者获取数据,都是共用同一个锁对象,由此也意味着两者无法真正并行运行,这点尤其不同于LinkedBlockingQueue;按照实现原理来分析,LinkedBlockingQueue完全可以采用分离锁,从而实现生产者和消费者操作的完全并行运行。Doug Lea之所以没这样去做,也许是因为ArrayBlockingQueue的数据写入和获取操作已经足够轻巧,以至于引入独立的锁机制,除了给代码带来额外的复杂性外,其在性能上完全占不到任何便宜。 ArrayBlockingQueue和LinkedBlockingQueue间还有一个明显的不同之处在于,前者在插入或删除元素时不会产生或销毁任何额外的对象实例,而后者则会生成一个额外的Node对象。这在长时间内需要高效并发地处理大批量数据的系统中,其对于GC的影响还是存在一定的区别。而在创建ArrayBlockingQueue时,我们还可以控制对象的内部锁是否采用公平锁,默认采用非公平锁。
2.LinkedBlockingQueue
基于链表的阻塞队列,同ArrayListBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。而LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
作为开发者,我们需要注意的是,如果构造一个LinkedBlockingQueue对象,而没有指定其容量大小,LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。
27.CAS操作
CAS操作包括三个操作数–内存位置V,预期原值A,新值B。
分为三个步骤:
读取内存中的值
将读取的值和预期的值进行比较
如果比较的结果符合预期,则写入新值;如果不符合,则什么都不做
原子操作就是靠CAS算法保证的,那***一个CPU下会不会同时两个线程同时比较且同时替换***呢?不会。
因为CAS是一种系统原语:
原语由若干条指令组成的,用于完成一定功能的一个过程。
原语的执行必须是连续的,在执行过程中不允许被中断。
CAS存在的问题:
1. ABA问题:
CAS会检查值有没有发生变化,没有则更新,但如果一个值A,变成了B,又变成了A,CAS检查时会发现它没有变化,但实际是变化了。
解决:使用版本号,每次变量更新把版本号加1,A-B-A 就会变成1A-2B-3A。
JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全相等,则以原子的方式将该引用和该标志的值设置为给定的更新值。
2.循环时间长,开销大:
自旋CAS如果长时间不成功,会不断尝试,长时间占用CPU,带来非常大的执行开销。
解决:如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
3.只能保证一个共享变量的原子操作:
当对一个共享变量执行操作时,可以使用循环CAS来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性了。这个时候就要用到锁了。
或者把多个共享变量合并成一个共享变量来操作,比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
28.ConcurrentHashMap、HashMap、HashTable
HashTable
- 底层数组+链表实现,无论key还是value都不能为null,线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMap做了相关优化
- 初始size为11,扩容:newsize = olesize*2+1
- 计算index的方法:index = (hash & 0x7FFFFFFF) % tab.length
HashMap
- 底层数组+链表实现,可以存储null键和null值,线程不安全
- 初始size为16,扩容:newsize = oldsize*2,size一定为2的n次幂
- 扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入
- 插入元素后才判断该不该扩容,有可能无效扩容(插入后如果扩容,如果没有再次插入,就会产生无效扩容)
- 当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀
- 计算index方法:index = hash & (tab.length – 1)
ConcurrentHashMap
- 底层采用分段的数组+链表实现,线程安全
- 通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)
- Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术
- 有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁
- 扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容
锁分段技术:首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
ConcurrentHashMap提供了与Hashtable和SynchronizedMap不同的锁机制。Hashtable中采用的锁机制是一次锁住整个hash表,从而在同一时刻只能由一个线程对其进行操作;而ConcurrentHashMap中则是一次锁住一个桶。
ConcurrentHashMap默认将hash表分为16个桶,诸如get、put、remove等常用操作只锁住当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有16个写线程执行,并发性能的提升是显而易见的。
29.Java终止线程的方式
在 Java 中有以下 3 种方法可以终止正在运行的线程:
- 使用退出标志,使线程正常退出,也就是当 run() 方法完成后线程中止。
- 使用 stop() 方法强行终止线程,但是不推荐使用这个方法,该方法已被弃用。
- 使用 interrupt 方法中断线程。
现在我们知道了使用 stop() 方式停止线程是非常不安全的方式,那么我们应该使用什么方法来停止线程呢?答案就是使用 interrupt() 方法来中断线程。
需要明确的一点的是:interrupt() 方法并不像在 for 循环语句中使用 break 语句那样干脆,马上就停止循环。调用 interrupt() 方法仅仅是在当前线程中打一个停止的标记,并不是真的停止线程。