文章目录
1.并发编程的三要素:(也是带来线程安全所在)
原子性:原子是不可再分割的最小单元,原子性是指一个或多个操作要么全部执行成功,要么全部执行失败。
可见性:一个线程对共享变量的修改,另一个线程能看到(synchronized,volatile)
有序性:程序的执行顺序按照代码的先后顺序
线程安全的问题原因有:
- 线程切换带来的原子性问题
- 缓存导致的可见性问题
- 编译优化带来的有序性问题
解决方案:
- JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题
- synchronized、volatile、LOCK,可以解决可见性问题
- Happens-Before 规则可以解决有序性问题
2.线程与进程的区别
根本区别:进程是操作系统资源分配的基本单元,而线程是处理器任务调度的和执行的基本单位。
内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的。
线程分类
分为用户线程与守护线程
用户(User)线程:运行在前台,执行具体任务,如程序的主线程,连接网络的子线程都是用户线程。必须执行完才会结束。例如,main主线程。
守护(Daemon)线程:运行在后台,为其它前台线程服务,也可以说守护线程是JVM非守护线程的”佣人“,一旦所有用户线程都执行结束,守护线程会随着JVM一起结束运行。例如,垃圾回收线程。
3.形成死锁的四个必要条件
- 互斥条件:线程(进程)对所分配的资源具有排它性,即一个资源只能被一个进程占用,直到该进程被释放。
- 请求与保持条件:一个进程(线程)因请求被占有资源而发生堵塞时,对已获取的资源保持不放。
- 不剥夺条件:线程(进程)已获取的资源在未使用完之前不能被其他线程强行剥夺,只有等自己使用完才释放资源。
- 循环等待条件:当发生死锁时,所等待的线程(进程)必定形成一个环路,死循环造成永久堵塞。
4.如何避免死锁
只需破坏形参死锁的四个必要条件之一即可。
- 破坏互斥条件:无法破坏,我们的🔒本身就是来个线程(进程)来产生互斥
- 破坏请求与保持条件:一次申请所有资源
- 破坏不剥夺条件:占有部分资源的线程尝试申请其它资源,如果申请不到,可以主动释放它占有的资源。
- 破坏循环等待条件:按序来申请资源。
5.什么是上下文的切换
当前任务执行完,CPU时间片切换到另一个任务之前会保存自己的状态,以便下次再切换会这个任务时可以继续执行下去,任务从保存到再加载执行就是一次上下文切换。
6.创建线程的四种方式
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
- Executors工具类创建线程池
补充:Callable接口call方法有返回值,是个泛型,和Futrue和FutureTask配合用来获取异步执行结果。Callable用于产生结果,Future用于接收结果。需要调用FutureTask.get()得到,此方法会堵塞主线程继续往下执行,如果不调用就不会堵塞。Runable接口run方法只能抛出运行时的异常,且无法捕获处理;Callable接口call方法允许抛出异常,可以获取异常信息。
7.start()
start方法使线程进入就绪,会执行线程前的相应准备工作
8.阻塞
- 等待阻塞:运行的线程执行wait()方法,JVM会有该线程放入等待队列
- 同步阻塞:运行的线程在获取对象的同步锁时,若该同步被别的线程占用,则JVM会把该线程放入锁池中
- 其他阻塞:运行的线程执行sleep或join方法,或者发出来I/O请求时,JVM会把该线程置为阻塞状态。当sleep状态超时,join等待线程终止或者超时,或者I/O处理完毕时,线程重新进入可运行状态。
9.线程调度算法
- 分时调度就是让所有的线程轮流获得CPU的使用权,并且平均分配到各个线程占有CPU的时间片。
- 抢占式调度:Java虚拟机采用抢占式调度模型,是指优先让线程池中优先级高的线程首先占用CPU,如果线程池中优先级相同,那么随机选择一个线程,使其占有CPU,处于这个状态的CPU会一直运行,优先级高的分的CPU的时间片相对会多一点。
10.sleep()和wait()有什么区别
两者都可以使线程进入等待状态
- 类不同:sleep()是Thread下的静态方法,wait()是Object类下的方法
- 是否释放锁:sleep()不释放锁,wait()释放锁
- 用处不同:wait()常用于线程间的通信,sleep()常用于暂停执行。
- 用法不同:wait()用完后,线程不会自动执行,必须调用notify()或notifyAll()方法才能执行,sleep()方法调用后,线程经过过一定时间会自动苏醒,wait(参数)也可以传参数使其苏醒。它们苏醒后还有所区别,因为wait()会释放锁,所以苏醒后没有获取到锁就进入堵塞状态,获取到锁就进入就绪状态,而sleep苏醒后之间进入就绪状态,但是如果cpu不空闲,则进入的是就绪状态的堵塞队列中。
11.Thread的sleep和Thread的yield
- yield作用:让出CPU的使用权,使当前线程从运行状态进入就绪状态,等待CPU的下次调度。
- Thread类的sleep()和yield()方法将在当前正在运行的线程上工作,所以其它处于等待状态的线程调用它们是没有意义的,所以设置为静态最合适。
- 线程调用sleep()方法进入堵塞状态,醒来后因为(没有释放锁)后直接进入了就绪状态,运行yield后也没有释放锁,于是进入了就绪状态。
- sleep()方法使用时需要处理InterruptException异常,而yield没有。
- sleep()执行后进入堵塞状态(计时等待),醒来后进入就绪状态(可能是堵塞队列),而yield是直接进入就绪状态。
12.保证多线程安全的方式
- 使用安全类,比如 java.util.concurrent 下的类,使用原子类AtomicInteger
- 使用自动锁,synchronized锁
- Lock lock = new ReentrantLock(),使用手动锁lock .lock(),lock.unlock()方法
13.锁
- CAS:Compare And Swap (Compare And Exchange) / 自旋 / 自旋锁 / 无锁 ,解决办法(版本号 AtomicStampedReference),基础类型简单值不需要版本号
- Unsafe:AtomicInteger中用到了Unsafe类,最终实现是:
lock cmpxchg 指令
14.锁升级过程
无锁 - 偏向锁 - 轻量级锁 (自旋锁,自适应自旋)- 重量级锁
- 如果有线程上锁,上偏向锁,指的就是,把markword的线程ID改为自己线程ID的过程
- 如果有线程竞争,撤销偏向锁,升级轻量级锁。线程在自己的线程栈生成LockRecord ,用CAS操作将markword设置为指向自己这个线程的LR的指针,设置成功者得到锁
- 如果竞争加剧,有线程超过10次自旋, 或者自旋线程数超过CPU核数的一半, 1.6之后,加入自适应自旋 Adapative Self Spinning , JVM自己控制
升级重量级锁:-> 向操作系统申请资源,linux mutex , CPU从3级-0级系统调用,线程挂起,进入等待队列,等待操作系统的调度,然后再映射回用户空间
15.锁消除 lock eliminate
public void add(String str1,String str2){
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}
我们都知道 StringBuffer 是线程安全的,因为它的关键方法都是被 synchronized 修饰过的,但我们看上面这段代码,我们会发现,sb 这个引用只会在 add 方法中使用,不可能被其它线程引用(因为是局部变量,栈私有),因此 sb 是不可能共享的资源,JVM 会自动消除 StringBuffer 对象内部的锁。
16.锁粗化 lock coarsening
public String test(String str){
int i = 0;
StringBuffer sb = new StringBuffer():
while(i < 100){
sb.append(str);
i++;
}
return sb.toString():
}
JVM 会检测到这样一连串的操作都对同一个对象加锁(while 循环内 100 次执行 append,没有锁粗化的就要进行 100 次加锁/解锁),此时 JVM 就会将加锁的范围粗化到这一连串的操作的外部(比如 while 虚幻体外),使得这一连串操作只需要加一次锁即可。
17.volatile的用途
- 线程可见性
- 防止指令重排序
18.volatile如何解决指令重排序
- volatile i
- ACC_VOLATILE
- JVM的内存屏障
- hotspot实现
JSR内存屏障