努力经营当下 直至未来明朗
答:① 进程包含线程;
② 线程比进程更轻量,创建更快、销毁也更快;
③ 同一个进程的多个线程之间共用一份内存和文件资源,而进程和进程之间则是独立的文件和内存资源;线程共用资源就省去了线程分配资源的过程
④ 进程是资源分配的基本单位,线程是调度执行的基本单位
① 继承Thread类,重写run方法,run方法是新线程的入口
② 实现Runnable接口,重写run
③ 使用匿名内部类,实现创建Thread子类的方式
④ 使用匿名内部类,实现 实现Runnable接口的方式
⑤ 使用Lambda表达式(lambda本质上是一个“匿名函数”)
(其实lambda表达式一般用于一个方法的实现上,该方法可以作为参数传入)
⑥ 使用线程池
⑦ 使用Callable(中间类FutureTask进行辅助,是获取结果的凭证)
答:① 直接调用run并没有创建线程,只是在原来的线程中运行的代码,只是相当于调用方法
② 调用start则是创建了线程,在新线程中执行代码(和原来的线程是并发执行的(并发+并行))
① NEW:Thread 对象创建出来了,但是内核的PCB还没有创建,也就是说:还没有真正创建线程。
② TERMINATED:内核的PCB销毁了,但是Thread 对象还在。
③ RUNNABLE:就绪状态(正在CPU上运行+在就绪队列中排队)
④ TIMED_WAITING:按照一定的时间进行阻塞。 调用sleep、join这类带时间的都是TIMED_WAITING
⑤ WAITING:特殊的阻塞状态,调用wait等
⑥ BLOCKED:等待锁的时候进入的状态
(①②都是Alive状态)
① 抢占式执行:多个线程调度执行过程可以视为是“全随机”的(也不能理解成纯随机的,但是确实在应用层程序上是没有规律的)(所以:在写代码的时候,就需要考虑到在任意一种调度的情况下都是能够运行出正确结果的)
(内核实现的,我们无能为力)
② 多个线程修改同一个变量:
【String是不可变对象(也就是不能修改String的内容,这并不是说用final修饰,而是把set系列方法给藏起来了),设计成不可变的好处之一就是“线程安全”】
(有时候可以通过调整代码来规避线程安全问题,但是普适性不高)
③ 修改操作不是原子的:
CPU执行指令都是以“ 一个指令”为单位进行执行,一个指令就相当于CPU上的“最小单位”了,不会说该条指令还没执行完就把线程调度走了。
(eg. count++就是三条指令
而像是有的修改操作如int的赋值就是单个CPU指令,安全一些)
注:解决线程安全问题最常见的方法就是从这里入手:把多个操作通过特殊手段打包成一个原子操作 (一个线程是否安全的判定是复杂的)
④ 内存可见性问题:JVM的代码优化(逻辑等价条件下提高效率)引入的bug
⑤ 指令重排序
(以上并不是线程不安全的全部原因)
1)线程安全,不是加了锁就一定安全的;而是通过加锁让并发修改同一个变量变为串行修改同一个变量,此时才是安全的。 而不正确的加锁方式并不一定能够解决线程安全问题。
2) 所以:是不能保证线程安全的。一个线程加锁并不会涉及锁竞争,也就不会阻塞等待,也就不会由并发修改同一变量变为串行修改同一变量,故是不安全的。
1) synchronized不仅可以修饰方法,还可以修饰代码块。所以可以将要加锁的代码放到一个代码块中。
2) 在synchronized修饰代码块时,()括号中的内容是我们所要针对的加锁对象,成为“锁对象”。
3) 在使用锁的时候,一定要明确 当前是针对哪个对象加锁。这很重要,会直接影响后面锁操作是否会触发阻塞。我们关心的是 是否存在(同一个)锁对象,是否存在锁竞争。
1)不安全线程:
ArrayList、LinkedList、HashMap、TreeMap、HashSet、TreeSet、StringBuilder
2)安全线程:
Vector (不推荐使用) :相当于线程安全的ArrayList
HashTable (不推荐使用):相当于线程安全的HashMap
ConcurrentHashMap
StringBuffer:StringBuffer 的核心方法都带有synchronized
3)虽然没有加锁, 但是不涉及 “修改”, 仍然是线程安全的:String
① volatile是会禁止编译器优化的,避免直接读取了CPU寄存器中缓存的数据,而是每次都重新读取内存。
② 站在JMM的角度来看待volatile:
正常程序执行过程中会把主内存(也就是所说的内存)的数据先加载到工作内存(也就是所说的CPU寄存器,不是真的内存)中,然后再进行计算处理; 编译器优化可能会导致不是每次都真的读取主内存,而是直接取从工作内存中缓存的数据,这就可能会导致内存可见性问题; volatile起到的效果 就是保证每次读取内存都是真的从主内存中重新读取
答: ① wait 需要搭配 synchronized 使用没有synchronized就会抛异常; sleep 不需要。
② wait 是 Object 的方法, sleep 是 Thread 的静态方法.
其实理论上 wait 和 sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间。
唯一的相同点就是都可以让线程放弃执行一段时间。
1)wait和notify用来调配线程线程执行的顺序
2)wait是Object方法,而Object是java所有类的祖宗,因此可以使用任意类的实例来调用wait方法; wait可能会抛出InterruptedException异常,这个异常就是被interrupt方法唤醒的。
3)当线程执行到wait的时候就会发生阻塞,直到另一个线程调用notify才会把这个wait唤醒,然后继续往下走。
3)wait操作 在内部本质上是做三件事:
①释放当前锁;
②进行等待通知;
③满足一定条件的时候(也就是别的线程调用notify)被唤醒,然后重新获取锁
所以等待通知的前提是释放锁,而释放锁的前提是先加锁
wait的第一步操作是释放锁,保证其他线程能够正常往下执行
wai和加锁操作密不可分。(也就是synchronized之后才可以wait)
wait这里可以记住类似举例:ATM取钱
4)notify也是要包含在synchronized里面的。
线程1没有释放锁的话,线程2也就无法调用notify(因为锁在阻塞等待);线程1调用wait,在wait中就释放锁了,这个时候虽然线程1代码阻塞在synchronized里面,但是此时的锁还是在释放状态,线程2 就可以拿到锁。
其他线程也是需要上锁才能调用notify,调用了notify就会唤醒wait,wait就会尝试重新加锁,但是wait加锁可能需要阻塞一会儿,直到notify所在的线程释放锁完成后wait才加锁成功。
5)要保证:加锁的对象和调用wait的对象是同一个对象;另外同时还要保证:调用wait的对象和调用notify的对象也是同一对象!!
6)多个线程都在wait时,notify是随机唤醒一个线程; notifyAll则是全都唤醒,但是即使是唤醒了所有的wait,这些wait也是需要重新竞争锁的,而重新竞争锁的过程仍然是串行的,所以这个其实并不是很常用。
7)wait, notify, notifyAll 都是 Object 类的方法。
8)wait 要搭配 synchronized 来使用, 脱离 synchronized 使用 wait 会直接抛出异常。
9)方法notify()【即:唤醒等待】也要在同步方法或同步块【即:使用synchronized修饰】中调用。
答:懒汉模式线程不安全,饿汉模式线程安全。
① 考虑某个模式是否线程安全,本质上是在考虑多个线程同时调用getInstance的时候是否会有问题。
② 饿汉模式获取实例getInstance的操作只是单纯的“读数据”,不涉及到修改,因为饿汉模式在类加载的时候就已经创建好实例对象了。
③ 懒汉模式获取实例getInstance的操作既涉及到读,又涉及到修改,此时线程就是不安全的。
(单例模式:某个类有且只有一个实例)
答:办法就是:禁止指令重排序。那么如何禁止呢?就是使用volatile关键字,既能保证内存可见性(读、修改线程并发,但是其实细想是不会存在的:因为每个线程有各自工作的一套CPU寄存器,有各自的上下文),又能禁止指令重排序(避免得到不完全对象,内存数据无效)。
① 加锁 / 解锁是一件开销比较高的事情, 而懒汉模式的线程不安全只是发生在首次创建实例的时候。
因此后续使用的时候, 不必再进行加锁了。
② 外层的 if 就是判定下看当前是否已经把 instance 实例创建出来了。
③ 同时为了避免 “内存可见性” 导致读取的 instance 出现偏差, 于是补充上 volatile 。
④ 当多线程首次调用 getInstance, 大家可能都发现 instance 为 null, 于是又继续往下执行来竞争锁, 其中竞争成功的线程, 再完成创建实例的操作。
⑤ 当这个实例创建完了之后, 其他竞争到锁的线程就被里层 if 挡住了, 也就不会继续创建其他实例。
⑥ 这样会降低操作开销。
队列:先进先出;然而,并不是所有的队列都是“先进先出”,“先进先出”针对的是普通的队列,复杂队列就不一定“先进先出”。
如:非“先进先出”:
① 优先级队列PriorityQueue
② 消息队列(在队列元素中引入一个“类型”,此时的“类型”是指业务上的类型):入队列的时候没啥,但是出队列的时候会指定某个类型的元素先出。
① 使用阻塞队列,有利于代码 “解耦合”
耦合:两个模块之间的关联关系,关系越紧密则耦合性越高。
【如:两个服务器直接通信则关联性强,互相影响;
但是如果在两个服务器之间加上一个阻塞队列就有效降低了两个服务器的关联性,耦合性降低,并且这样的话增加/删除服务器也较为方便】
也就是说: 生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,此时的阻塞队列就类似于“缓冲区”
② 削峰填谷:(举例:三峡水库)
如果按照没有 生产者消费者模型的写法,外面流量过来的压力就会直接压在每个服务器上,如果某个服务器抗压能力不太行就容易挂。
为什么一个服务器同一时刻收到很多请求就挂了?
理由:服务器每处理一个请求都是需要消耗一定的硬件资源,这些硬件资源包括但不限于CPU、内存、硬盘、宽带等,同一时刻请求越多则消耗的资源越多;而一台主机的硬件资源是有限的,一旦某个硬件资源耗尽了,此时机器也就挂了。
【而所谓的分布式系统,本质上就是加入了更多的硬件资源】
如果使用阻塞队列,当流量骤增的时候,生产者和阻塞队列就承受了压力,而其余消费者还是按照原来的节奏来消费数据,即对消费者的冲击就不大。
(队列有三个基本操作:入队列、出队列、取 队首元素)
schedule(TimerTask task, long delay)
。① MyTimer 类提供的核心方法为 schedule, 用于注册一个任务, 并指定这个任务多长时间后执行。
② MyTask 类用于描述一个任务, 里面包含一个 Runnable 对象和一个 time(毫秒时间戳),也就是schedule的俩参数。
这个对象需要放到 优先队列 中, 因此需要实现 Comparable 接口。(时间短的先执行)
③ MyTimer 实例中, 通过 PriorityBlockingQueue 来组织若干个 MyTask 对象,通过 schedule 来往队列中插入一个个 MyTask 对象。(优先队列线程不安全,阻塞队列线程安全)
④ MyTimer 类中存在一个扫描线程, 一直不停的扫描队首元素, 看看是否能执行这个任务。(所谓 “能执行” 指的是该任务设定的时间已经到达了)
⑤ 引入一个对象, 借助该对象的 wait / notify 来解决 while (true) 的忙等问题(也就是不停扫描,浪费CPU)
修改 扫描线程 的 run 方法, 引入 wait, 等待一定的时间;
修改 MyTimer 的 schedule 方法, 每次有新任务到来的时候唤醒一下 扫描 线程. (因为新插入的任务可能是需要马上执行的)。
答: 创建线程还是会申请点资源的,但是这个资源已经很少了,速度已经很快了,暂可以忽略不计。
1)【原因】创建线程是要在操作系统内核中完成的,涉及到用户态到内核态之间的切换操作!这个操作是存在一定的开销的。(Ps:加锁也涉及到 用户态到内核态之间的切换)
2)一般来说,纯用户态速度更快,即:使用线程池是纯用户态操作,要比创建线程(要经历内核态)速度更快
Executors.newCachedThreadPool()
来创建线程池,使用ExecutorService来接收答: 生产者消费者模型。
① 先搞一个阻塞队列,每个被提交的任务都放到阻塞队列中;搞M个线程来取队列元素,如果队列空则M个线程就进行阻塞等待;但是如果队列不为空,每个线程都取一个任务,执行任务完成后再来取下一个…直到队列空,线程继续阻塞。
② 不能平均分:因为每个线程执行时间都是不一样的
③ 不用结束,因为无法判定啥时候会有新的线程过来;如果非要结束,那就单独写一个shutdown方法强制中断interrupt所有的工作线程
标准库里提供的ThreadPoolExecutor其实是更复杂一些的,尤其是构造方法,可以支持很多参数,可以支持很多选项,让我们创建出不同风格的线程池
构造方法【常见面试题!!】
1) 查看ThreadPoolExecutor里的构造方法:java.util.concurrent(并发) -> ThreadPoolExecutor (线程池)
2) 此处只分析最后一个构造方法:
① corePoolSize:核心线程数
② maximumPoolSize:最大线程数
(任务数量是不太确定的,有时候任务多了,核心线程处理不过来,此时就需要更多的线程来帮助一起处理任务;当任务处理完之后,这些除了核心线程外的线程在一定时间的空闲之后就可以销毁了;但是核心线程即使空闲也不会销毁。
灵活调配这两个数值,可以做到既能够处理任务巅峰,又能够在空闲的时候节省资源。)
③ keepAliveTime:运行的额外线程空闲的最大时间,也就是空闲上限。
④ unit:时间的单位
⑤ workQueue:手动给线程池传入一个任务队列。其实在线程池中是有自己的队列的(如果不自己手动传入就会在线程池内部自己创建),但是有时候代码的业务逻辑中本身就有一个队列来保存这里的任务,此时如果把自己队列中的任务再拷贝到线程池内部就是画蛇添足了,直接就让线程池消费业务逻辑中已有的队列即可!
⑥ threadFactory:描述了线程是如何创建的。工厂对象就负责创建线程,程序员可以手动指定线程的创建策略。
⑦ RejectedExecutionHandler handler:【重点!常考!】线程池的拒绝策略。线程池的任务队列已经满了(工作线程忙不过来了),如果又添加了别的新任务,那该怎么办呢?
——这个拒绝策略对于实现“高并发”服务器也是非常有意义的。
以下就是标准库中提供的拒绝策略:
① AbortPolicy:中断策略,直接抛异常 handler(回调,处理方法)
② CallerRunsPolicy:调用者来执行,而不是被调用者来执行(按理来说是被调用者执行);如果调用者也不执行就丢弃该任务
③ DiscardOldestPolicy:丢弃最老的未处理请求
④ DiscardPolicy:直接丢弃最新的任务
(实际开发中,需要根据请求来决定使用哪种策略)
【面试官考察你对于ThreadPoolExecutor的理解,其实主要就是在考察拒绝策略】
—— 网上大部分说法是错误的。只要你具体说出一个数字都是错误的! 因为我们在这里是不可以确定出具体的个数的。
理由:① 主机的CPU的配置不确定;
② 程序的执行特点(也就是:代码里具体都干了啥?是CPU密集型的任务还是IO密集型的任务)也是不确定的。
执行特点:也就是代码里具体都干了啥?是CPU密集型的任务(做了大量的算术运算和逻辑运算)还是IO密集型的任务(做了大量的读写网卡/读写硬盘)
有些程序代码里既需要进行很多的CPU密集型任务,又需要很多的IO任务,则此时是很难量化该进程的两种任务的比例的。
1)如果任务100%是CPU密集型的话,线程说明最多也就是N,更大的话其实已经没意义了,因为此时CPU已经被占满了
2)如果线程只有10%是CPU密集型,其余90%都是在操作IO(不使用CPU),那么此时线程数目设置成10N也是没关系的,因此此时所有线程中只有10%是在使用CPU的。
工作中实际的处理方案就是进行验证,也就是针对进程做性能测试:分别给线程池设置成不同的数目,如0.5N,N,1.5N,2N…都试试,分别记录每种情况下该线程的一些核心性能指标和系统负载情况,最终选择一个你认为比较合适的配置。
【其实面试官考察的关键是如何设置线程池数目的方法(实验+压测)】
① 线程池执行流程:
当任务来了之后,线程池的执行流程是:先判断当前线程数是否大于核心线程数?如果结果为 false,则新建线程并执行任务;如果结果为 true,则判断任务队列是否已满?如果结果为 false,则把任务添加到任务队列中等待线程执行,否则则判断当前线程数量是否超过最大线程数?如果结果为 false,则新建线程执行此任务,否则将执行线程池的拒绝策略
② 拒绝策略:
当任务过多且线程池的任务队列已满时,此时就会执行线程池的拒绝策略,线程池的拒绝策略默认有以下 4 种:
AbortPolicy:中止策略,线程池会抛出异常并中止执行此任务;
CallerRunsPolicy:把任务交给添加此任务的(main)线程来执行;
DiscardPolicy:忽略此任务,忽略最新的一个任务;
DiscardOldestPolicy:忽略最早的任务,最先加入队列的任务。
默认的拒绝策略为 AbortPolicy 中止策略。
答: ① 悲观锁认为多个线程访问同一个共享变量冲突的概率较大, 会在每次访问共享变量之前都去真正加锁。
② 乐观锁认为多个线程访问同一个共享变量冲突的概率不大, 并不会真的加锁, 而是直接尝试访问数据。 在访问的同时识别当前的数据是否出现访问冲突。
③ 悲观锁的实现就是先加锁(比如借助操作系统提供的 mutex), 获取到锁再操作数据, 获取不到锁就等待。
④ 乐观锁的实现可以引入一个版本号,借助版本号识别出当前的数据访问是否冲突。
答: ① 读写锁就是把读操作和写操作分别进行加锁.
② 读锁和读锁之间不互斥.
③ 写锁和写锁之间互斥.
④ 写锁和读锁之间互斥.
⑤ 读写锁最主要用在 “频繁读, 不频繁写” 的场景中.
答: ① 自旋锁即:如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止。 第一次获取锁失败, 第二次的尝试会在极短的时间内到来; 一旦锁被其他线程释放, 就能第一时间获取到锁。
② 相比于挂起等待锁,
优点: 没有放弃 CPU 资源, 一旦锁被释放就能第一时间获取到锁, 更高效。在锁持有时间比较短的场景下非常有用。
缺点: 如果锁的持有时间较长, 就会浪费 CPU 资源。
答: ① 是可重入锁.
② 可重入锁指的就是连续两次加锁不会导致死锁.(同一个线程针对同一把锁)
③ 实现的方式是在锁中记录该锁持有的线程身份, 以及一个计数器(记录加锁次数)。如果发现当前加锁的线程就是持有锁的线程, 则直接计数自增。
如果当前场景中锁竞争不激烈,则是以轻量级锁状态来进行工作(轻量级锁是通过自旋来实现的,可以第一时间拿到锁);
如果当前场景中锁竞争激烈,则是以重量级锁状态来进行工作的(重量级锁通过挂起等待来实现,可能拿到锁每那么及时,但是节省了CPU的开销)
答: 偏向锁不是真的加锁, 而只是在锁的对象头中记录一个标记(记录该锁所属的线程)。 如果没有其他线程参与竞争锁, 那么就不会真正执行加锁操作, 从而降低程序开销。 一旦真的涉及到其他的线程竞争, 再取消偏向锁状态, 进入轻量级锁状态。
答: 参考【synchronized原理】所有内容:特点+加锁过程+优化手段。
答: ① Callable 是一个 interface 。相当于把线程封装了一个 “返回值”, 方便程序员借助多线程的方式计算结果。
② Callable 和 Runnable 相对, 都是描述一个 “任务”。Callable 描述的是带有返回值的任务, Runnable 描述的是不带返回值的任务。
③ Callable 通常需要搭配 FutureTask 来使用, FutureTask 用来保存 Callable 的返回结果。 因为Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定。 FutureTask 就可以负责这个等待结果出来的工作。
① newFixedThreadPool: 创建固定线程数的线程池
② newCachedThreadPool: 创建线程数目动态增长的线程池.
③ newSingleThreadExecutor: 创建只包含单个线程的线程池.
④ newScheduledThreadPool: 设定延迟时间后执行命令,或者定期执行命令。 是进阶版的 Timer.
Executors 本质上是 ThreadPoolExecutor
类的封装
1.Callable声明一个带返回值的任务,需要搭配FutureTask
2.ReentrantLock容易遗漏解锁操作,可以实现公平锁,还可以实现tryLock,还可以搭配Condition来唤醒指定的等待线程
3.原子类:基于CAS实现的,能够比较高效的完成线程安全的自增自减
4.线程池
5.信号量Semaphore:这是广义的锁,相当于计数器,描述了可用资源的个数。
P操作:申请资源,计数器-1; V操作:释放资源,计数器+1。
如果计数器的值被减成0时,继续P操作,则会产生阻塞。
答:synchronized, ReentrantLock, Semaphore 等都可以用于线程同步。
答:以 juc 的 ReentrantLock 为例,
① synchronized 使用时不需要手动释放锁,ReentrantLock 使用时需要手动释放, 使用起来更灵活。
② synchronized 在申请锁失败时, 会死等。 ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃。
③ synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个true 开启公平锁模式。
④ synchronized 是通过 Object 的 wait / notify 实现等待-唤醒, 每次唤醒的是一个随机等待的线程。 ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程。
答: 参考:CAS的应用:原子类
主要会用于类似于count++的形式
(主要看伪代码那儿:比较相同并赋值)
答:① 信号量, 用来表示 “可用资源的个数”, 本质上就是一个计数器。
② 使用信号量可以实现 “共享锁”, 比如某个资源允许 3 个线程同时使用, 那么就可以使用 P 操作作为加锁, V 操作作为解锁, 前三个线程的 P 操作都能顺利返回, 后续线程再进行 P 操作就会阻塞等待, 直到前面的线程执行了 V 操作。
不一定,我们主要担心的是读的结果是一个修改了一半的数据。
但是ConcurrentHashMap在设计的时候慎重考虑了这一点,在读的时候能够保证读到的是囫囵个的数据,也就是说要么是旧版本、要么是新版本,不可能是写到一半的数据。
另外,读操作中也广泛使用了volatile关键字来保证读到的数据是及时的。
答:① HashMap线程不安全,HashTable、ConcurrentHashMap 线程安全。
② HashTable、ConcurrentHashMap虽然都是线程安全,但是有很多差别:锁粒度的控制(一把、很多锁),ConcurrentHashMap写操作加锁、读操作不加锁,ConcurrentHashMap利用了CAS特性,ConcurrentHashMap扩容优化:化整为零。
③ 旧版本(jdk1.8之前不包含1.8)的ConcurrentHashMap的实现是分段锁,而新版本(jdk1.8开始)的ConcurrentHashMap是每个链表分一个锁。 【分段锁:好几个链表共用同一把锁。 但是分段锁的锁冲突概率要比每个链表加一把锁更高,代码实现也更复杂】
④ HashMap的 key 允许为null(HashMap是无序的!!TreeMap是有序的),HashTable、ConcurrentHashMap的key不能为null。
答: 读操作没有加锁。目的是为了进一步降低锁冲突的概率, 为了保证读到刚修改的数据, 搭配了volatile 关键字。
答:① 这个是 Java1.7 中采取的技术, Java1.8 中已经不再使用了。
② 简单的说就是把若干个哈希桶分成一个"段" (Segment), 针对每个段分别加锁。
③ 目的也是为了降低锁竞争的概率。 当两个线程访问的数据恰好在同一个段上的时候, 才触发锁竞争。
答: ① 取消了分段锁, 直接给每个哈希桶(每个链表)分配了一个锁(就是以每个链表的头结点对象作为锁对象)。
② 将原来 数组 + 链表 的实现方式改进成 数组 + 链表 / 红黑树 的方式. 当链表较长的时候(大于等于8 个元素)就转换成红黑树。
① 互斥使用:锁A被线程1占用,线程2就用不了 (打破不了,锁的基本特性)
② 不可抢占:锁A被线程1占用,线程2就不能把锁A给抢过来,除非线程1释放锁(打破不了,锁的基本特性)
③ 请求和保持:有多把锁,线程1拿到锁A之后,不想释放锁A,还想请求再拿到一个锁B(取决于代码:获取锁B的时候是否释放锁A,有可能打破,但是不普适。主要看需求场景是否允许这么写)
④ 循环等待:线程1等待线程2释放锁,线程2释放锁得等待线程3释放锁,线程3释放锁得等待线程1释放锁 (有把握打破:约定好加锁顺序就可以打破循环等待)
答:volatile 能够保证内存可见性, 强制从主内存中读取数据。 此时如果有其他线程修改被 volatile 修饰的变量, 可以第一时间读取到最新的值。
答:① JVM 把内存分成了这几个区域:方法区, 堆区, 栈区, 程序计数器。
② 其中堆区这个内存区域是多个线程之间共享的。 只要把某个数据放到堆内存中, 就可以让多个线程都能访问到。
答:1)创建线程池主要有两种方式:
① 通过 Executors 工厂类创建, 创建方式比较简单, 但是定制能力有限.
② 通过 ThreadPoolExecutor 创建, 创建方式比较复杂, 但是定制能力强.
2)LinkedBlockingQueue 表示线程池的任务队列, 用户通过 submit / execute 向这个任务队列中添加任务, 再由线程池中的工作线程来执行任务。
答:① NEW: 安排了工作, 还未开始行动。 新创建的线程, 还没有调用 start 方法时处在这个状态.
② RUNNABLE: 可工作的。 又可以分成正在工作中和即将开始工作。 调用 start 方法之后, 并正在CPU 上运行/在即将准备运行 的状态。
③ BLOCKED: 使用 synchronized 的时候, 如果锁被其他线程占用, 就会阻塞等待, 从而进入该状态.
④ WAITING: 调用 wait 方法会进入该状态.
⑤ TIMED_WAITING: 调用 sleep 方法或者 wait(超时时间) 会进入该状态.
⑥ TERMINATED: 工作完成了。 当线程 run 方法执行完毕后, 会处于这个状态.(线程销毁,对象还在)
答:① 使用 synchronized / ReentrantLock 加锁
② 使用 AtomInteger 原子操作
答: Servlet 本身是工作在多线程环境下。
如果在 Servlet 中创建了某个成员变量, 此时如果有多个请求到达服务器, 服务器就会多线程进行操作, 是可能出现线程不安全的情况的。
答:① Thread 类描述了一个线程,Runnable 描述了一个任务。
② 在创建线程的时候需要指定线程完成的任务, 可以直接重写 Thread 的 run 方法, 也可以使用Runnable 来描述这个任务。
答: ① 第一次调用 start 可以成功调用.
② 后续再调用 start 会抛出java.lang.IllegalThreadStateException 异常。
答:① synchronized 加在非静态方法上, 相当于针对当前对象加锁。
② 如果这两个方法属于同一个实例:
线程1 能够获取到锁, 并执行方法. 线程2 会阻塞等待, 直到线程1 执行完毕释放锁, 线程2 获取到锁之后才能执行方法内容。
③ 如果这两个方法属于不同实例:
两者能并发执行, 互不干扰。
答: ① 进程是包含线程的, 每个进程至少有一个线程存在,即主线程。
② 进程和进程之间不共享内存空间, 同一个进程的线程之间共享同一个内存空间。
③ 进程是系统分配资源的最小单位,线程是系统调度的最小单位。