Java线程
标签(空格分隔): 面试 线程/进程
1. sleep join yield有什么区别
多线程的五种状态: 新建状态, 就绪状态, 运行状态, 阻塞状态, 死亡状态.
-
新建状态: 当new 一个线程的时候, 程序还没有运行其中的````run```代码.
-
就绪状态: 一个新创建的线程并不会自动开始, 要想执行线程, 必须调用其start方法., 之后线程处于就绪状态, 位于可运行线程池中, 等待被线程调度选中,
-
运行状态: 就绪状态的线程获得了CPU的时间片, 执行程序代码.
-
阻塞: 阻塞是因为线程由于某种原因放弃了CPU的使用权, 让出了时间片, 暂停运行, 直到线程再次进入可运行状态, 才有机会再次获得CPU时间片.
-
等待阻塞: 运行的线程执行wait, JVM会将其放入等待队列当中.
-
同步阻塞: 运行的线程在获取对象的同步锁的时候, 若该同步锁被其他线程占用, 则JVM会将其放入锁池当中.
-
其他阻塞:运行的线程执行
Thread.sleep
, 或者join
方法,或者是翻出了I/O
请求,置为阻塞状态. 当sleep状态超时,join等待线程种植 -
死亡: 线程run, main方法执行结束, 或者异常原因推出了退出了run方法.
1.1 sleep
sleep
方法需要制定等待的时间, 他可以让当前正在执行的线程在制定的时间内暂停, 进入阻塞状态, 该方法既可以让其他同优先级或者高优先级的线程得到执行的机会,也可以让低优先级的线程得到执行的机会. 但是sleep
不会释放锁的标志, 如果有synchronize
同步代码块, 其他线程依然不能获得锁,不能访问共享数据.
1.2 wait
wait()
方法需要和notify()
以及notifyAll
两个方法一起介绍, 这三个方法用于协调多个线程对共享数据的存取, 所以必须要在synchronize
语句块内使用, 也就是说, 调用wait()
, notify()
和notifyAll
的任务调用在这些方法前必须拥有对象的锁. 注意, 他们都是Object
类的方法,而不是Thread
类的方法.
wait
和sleep
方法的不同之处在于, wait
方法会释放对象的"锁标志". 当调用某一对象的wait
方法后,会使当前线程暂停执行, 并将当前线程放入对象等待池中, 直到调用了notify
方法后, 将对象从等待池中移除任意一个线程并放入锁标志等待池中, 只有锁标志等待池中的线程可以获得锁标志, 他们随时准备争夺锁的拥有权, 当调用了某个对象的notifyAll
方法, 会将对象等待池中的所有线程都移动到该对象的锁标志等待池.
此外, wait
, notify
,以及notifyAll
只能在synchronize
内使用, 但是如果使用的是ReenTrantLock
实现同步, 该如何达到这三个方法的效果呢? 解决方式就是使用ReenTrantLock.newCondition
获取一个Condition
类对象, 然后Condition
的await
,signal
以及signalAll
分别对应上面的三个方法.
1.3 yield
yield
方法和sleep
方法类似, 也不会释放"锁标志", 区别在于,她没有参数, 即yield
方法被执行之后, 当前线程进入就绪
状态.所以执行yield
的线程有可能在进入到 这种可执行状态之后马上又被执行, 另外yield
方法只能使同优先级或者更高优先级的线程得到执行机会, 这也和sleep
不同
1.4 join
join
方法会使当前线程等待调用join
方法的线程结束之后才会继续执行.
2. 创建线程的方式以及实现
2.1 继承Thread类创建线程
- 定义
Thread
的子类, 并重写该类的run
方法, 该run
方法的方法体就代表了线程要完成的任务, 因此把run
成为执行体. - 实例化
Thread
子类的实例, 创建线程对象. - 调用线程对象的
start
方法来启动该线程.
2.2 通过Runnable接口创建线程类
- 定义
Runnable
接口的实现类, 并重写接口的run
方法,该run
方法的方法体同样是该线程的执行体. - 创建
Runnable
实现类的实例, 并依此实例作为Thread
的target
来创建Thread
对象, 该Thread
才是真正的线程对象 . - 调用上述真正的线程对象的
start
方法去启动线程.
2.3 通过Callable和Future创建线程
- 创建
Callable
接口的实现类, 并实现call
方法, 该call
方法将作为线程执行体, 并且有返回值. - 创建
Callable
实现类的实例, 使用FutureTask
类来包装Callable
对象, 该FutureTask
对象封装了该Callable
对象的call
方法的返回值. - 调用
FutureTask
对象作为Thread
对象的target
创建并启动新线程.
2.4 采用Runnable Callable接口的方式创建多线程时
- 优势: 线程类只是实现了
Runnable
接口或Callable
接口,还可以继承其他类. 在这种方式下,多个线程可以共享一个target
对象, 所以非常适合多个相同线程来处理同一份资源的情况, 从而可以将CPU,代码, 数据分开,形成清晰的模型, 较好的体现了面向对象的思想.
劣势: 编码稍微复杂.
2.5 使用继承Thread类的方法创建多线程
- 优势: 编写简单
- 劣势: 无法继承其他父类.
3 周边
3.1 CountDownLatch
CountDownLatch
内部维护了一个整数n(n>=0)
, 在当前线程初始化CountDownLatch
的时候指定其值. 当前线程调用CountDownLatch
的await
方法阻塞当前线程, 等待其他调用CountDownLatch
对象的CountDown
方法的线程执行完毕, 其他线程调用该CountDownLatch
的CountDown
方法会将n-1
, 知道所有线程执行完毕, 当前线程则回复运行.
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CountDownLatchDemo {
private static final JackMa demo = new JackMa();
public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newFixedThreadPool(100);
for (int i=0; i<10; i++){
exec.submit(demo);
}
// 等待检查
JackMa.getLatch().await();
// 发射火箭
System.out.println("Fire!");
// 关闭线程池
exec.shutdown();
}
}
class JackMa implements Runnable{
static CountDownLatch getLatch() {
return latch;
}
private static final CountDownLatch latch = new CountDownLatch(10);
@Override
public void run() {
// 模拟检查任务
try {
Thread.sleep(new Random().nextInt(10) * 1000);
System.out.println("check complete");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//计数减一
//放在finally避免任务执行过程出现异常,导致countDown()不能被执行
latch.countDown();
}
}
}
3.2 线程池
在
Executors
类里面提供了一些静态工厂, 生成一些常用的线程池.
newFixThreadPool
: 创建固定大小的线程池, 线程池的大小一旦达到最大值,就会保持不变, 如果某个线程因为执行异常而结束, 那么线程池会补充一个新的线程.newCachedThreadPool
: 创建一个可以缓存的线程池, 如果线程池的大小超过了处理任务所需的线程, 那么就会回收部分空闲的线程(60S 不执行任务), 当任务数量增加的时候, 此线程池又可以只能的添加新线程来处理任务, 此线程池不会对线程池大小做限制, 线程池的大小依赖于操作系统和JVM能创建的最大线程数的大小.newSingleThreadExecutor
: 创建一个单线程的线程池, 这个线程池只有一个线程在工作, 也就是相当于单线程串行执行所有任务, 如果这个唯一的线程因为异常而结束的时候, 那么会有一个新的线程去替代它, 此线程池保证所有任务的执行顺序按照任务的提交顺序执行.newScheduledThreadPool
: 创建一个大小无限的线程池, 此线程池支持定时以及周期性执行任务的需求.newSingleThreadScheduledExecutor
: 创建一个单线程的线程池, 此线程池支持定时以及周期性执行任务的需求.
4. 锁
4.1 volatile实现原理
在JVM底层volatile是采用"内存屏障"来实现的.
缓存一致性协议(MESI协议) 它确保每个缓存中使用的共享变量的副本是一致的.其核心思想如下: 当某个CPU在写数据的时候, 如果发现操作的变量是共享变量, 则会通知其他的CPU告知该变量的缓存是无效的, 因此其他CPU在读取该变量时, 发现其无效会从主存中加载数据. CPU的临时寄存器, 会把一些使用频率比较高的数据放到寄存器中, 以减少读数据方面的瓶颈.
指令重拍: 编译器或者CPU对操作指令进行重排序, 在一些特定的情况下,指令重排可能会给代码造成一些不可预料的后果.
-
在计算机执行指令的顺序在经过程序编译器编译之后形成指令序列, 一般而言, 这个指令序列是会输出确定的结果, 以确保每一次的执行都有确定的结果. 但是一般情况下, CPU和编译器为了提升程序执行的效率,会按照一定的规则允许进行指令优化, 在某些情况下这个指令优化会带来一些执行的逻辑问题. 主要原因在于代码逻辑之间存在一定的先后顺序, 在并发执行的情况下, 会发生二义性, 即按照不同的执行顺序得到不同的执行结果.
-
数据依赖性: 不同的程序指令之间的顺序是不允许进行交互的, 即可以称这些程序指令之间存在数据依赖性.
名称 | 代码示例 | 说明 |
---|---|---|
写读 | a=1;b=a; |
写一个变量之后, 再读这个位置 |
读写 | a=b;b=1; |
读一个变量之后, 再写这个变量 |
可以发现这里的每一组指令之中都有写操作, 这个写操作的位置是不允许变化的, 否则会带来不一样的执行结果.
编译器将不会对存在数据依赖性的程序指令进行重排, 这里的依赖性仅仅指单线程情况下的数据依赖性; 多线程并发情况下, 此规则将失效.
4.2 synchronize实现原理
同步代码块是使用
monitorenter
和monitorexit
指令实现的, 同步方法(在这看不出来需要看JVM底层实现) 依靠的是方法修饰符上的ACC_SYNCHRONIZED
.
4.3 synchronized 与 lock 的区别
synchronized
和lock
的用法区别synchronized(隐式锁)
:在需要同步的对象中加入此控制,synchronized
可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。lock
(显示锁):需要显示指定起始位置和终止位置。一般使用ReentrantLock
类做为锁,多个线程中必须要使用一个ReentrantLock
类做为对象才能保证锁的生效。且在加锁和解锁处需要通过lock()
和unlock()
显示指出。所以一般会在finally
块中写unlock()
以防死锁。