多线程内容梳理

前言

本文针对多线程的内容进行梳理和总结,对之前的文章做以扩充,且持续更新

什么是进程和线程?区别是什么?

  • 进程是系统进行资源分配和调用的基本单位,通俗的讲就是内存中运行的程序,每个进程都有自己独立的一块内存空间,一个进程可以有多个线程
  • 线程是进程中的一个控制单元,负责当前进程中程序的执行,一个进程至少有一个线程,多个线程可以共享数据。
  • 区别:
    • (内存分配) 同一进程的线程共享地址空间和资源,而进程之间的地址空间和资源是相互独立的。
    • (影响关系) 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但一个线程崩溃整个程序会死掉,所以多进程比多进程健壮。

创建线程的方式

  1. 继承Thread类
  2. 实现Runnable接口
  3. 实现Callable接口
  4. 使用 Executors 工具类创建线程池

run()和start()的区别?为何不直接调用run()

首先start()用于启动线程,只能调用一次,而run()用于执行线程的运行时代码,run()也称为线程体,可重复调用,相当于调用普通函数。

其次调用start()方法,会启动一个线程并使线程进入就绪状态,当分配到时间片就可以开始运行了。

什么是上下文切换?

当前任务在执行完CPU时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。

什么是多线程?

指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务。

  • 好处: 可以提高CPU利用率
  • 劣势: 线程之间对共享资源的访问会互相影响,必须解决资源竞争问题。

并发编程三要素?Java中如何保证多线程运行安全?

  1. 原子性:指一个或多个操作要么全部执行成功要么全部失败
  2. 可见性:一个线程对共享变量的修改,另一个线程能立刻看到
  3. 有序性:程序执行的顺序按照代码先后顺序执行

出现线程安全问题的原因:

  • 线程切换带来的原子性问题
  • 缓存导致的可见性问题
  • 编译优化带来的有序性问题

解决:

  • Atomic开头的原子类、synchronized、Lock可解决原子性问题。
  • synchronized、volatile、Lock可解决可见性问题
  • Happens-Before规则可以解决有序性问题

守护线程和用户线程的区别

  • 用户线程:运行在前台,执行具体的任务,如程序的主线程(main函数)、垃圾回收等
  • 守护线程:运行在后台,为其他前台线程服务,一旦所有用户线程都结束运行,守护线程也会结束。

setDaemon(true)必须在start()方法前执行,否则会抛出异常。

线程死锁

指两个或两个以上的线程在执行过程中,由于资源竞争而造成的一种阻塞现象。

例如线程1锁住了A,然后尝试对B加锁,而线程2已经锁住了B,尝试对A加锁,这时候就发生了死锁

死锁的产生条件

  • 互斥使用:即当一个资源被一个线程使用时,别的线程不能使用
  • 不可抢占资源:资源请求者不能强制从资源占有者手中夺取资源
  • 占有且等待:即当资源请求者请求其他资源的同时保持对原有资源的占有
  • 循环等待:线程一、二彼此等待对方线程所占有的资源

如何避免死锁

强制线程按照指定的顺序获取锁,另外预防死锁可以采取破坏循环等待条件,对各进程请求资源顺序做一个规定,避免相互等待。

线程生命周期即五种线程状态

  1. 初始状态(new):新建了一个对象
  2. 就绪状态(runnable):调用对象的start()方法,等待被调用
  3. 运行状态(running):可运行状态的线程获得了cpu时间片,执行程序
  4. 阻塞状态(block):线程由于某种原因停止,直到进入就绪状态才有机会再次被调用到运行状态。
  5. 死亡状态(dead):线程run()、main()方法执行结束,或异常退出run()方法。该线程结束生命周期,死亡的线程不可再次复生。

什么是线程同步和线程互斥?

线程同步:当一个线程对共享数据进行操作时,在没有完成相关操作前,不允许其他线程打断它,否则会破坏数据的完整性。

线程互斥:一种特殊的线程同步,当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其他线程必须等待

实现线程同步方法

  • 同步代码方法
  • 同步代码块
  • 使用特殊变量域volatile实现线程同步
  • 使用重用锁实现线程同步

volatile关键字为域变量的访问提供了一种免锁机制

说出线程同步与线程调度的相关方法

  1. wait() : 使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁
  2. sleep() : 使一个正在运行的线程处于睡眠状态,不会释放锁
  3. notify() : 唤醒一个处于等待状态的线程
  4. notifyAll() : 唤醒所有处于等待状态的线程

Thread 类的 yield 方法有什么作用?

使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。

如何保证多线程的运行安全

  1. 使用安全类(java.util.concurrent下的类)、原子类
  2. 自动锁synchronized
  3. 手动锁Lock

手动锁代码示例

1
2
3
4
5
6
7
8
9
10
Lock lock=new ReentrantLock();
lock.lock();
try{
System.out.println("获得锁");
}catch(Exception e){

}finally{
System.out.println("释放锁");
lock.unlock();
}

说一说线程优先级

每个线程都是有优先级的,一般高优先级线程运行具有优先权,,但并不能保证,这依赖于线程调度,1代表最低优先级,10代表最高

双重校验实现单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton{
private volatile static Singleton uniqueInstance;
private Singleton(){}
public static Singleton getUniqueInstance(){
if(uniqueInstance==null){
synchronized(Singleton.class){
if(uniqueInstance==null){
uniqueInstance=new Singleton();
}
}
}
return uniqueInsatnce;
}
}

synchronized和Lock有什么区别?

  • synchronized是Java关键字,Lock是Java类
  • synchronized可以给类、方法、代码块加锁,Lock只能给代码块加锁
  • synchronized不需要手动释放锁,发生异常会自动释放锁、lock需手动释放、否则会造成死锁。

乐观锁与悲观锁

  • 乐观锁:对线程安全问题持乐观状态,认为竞争不总会发生,因此不需要持有锁,适用于多读的应用类型,这样可以提高吞吐量
  • 悲观锁:持悲观状态,每次操作数据都会持有一个独占的锁,例如synchronized

interrupted 和 isInterrupted 方法的区别?

  • interrupt:用于中断线程
  • interrupted: 静态方法,查看当前中断信号是true还是false并清除中断信号
  • isInterrupted:查看当前中断信号是true还是false

什么是线程池?

线程池(ThreadPool)就是用来存储线程的容器,即事先创建若干个可执行的线程放入一个池(容器中),需要的时候从池中获取线程,从而不用自行创建,使用完不需要销毁而是放回池中,从而减少创建和销毁线程对象的开销,提高响应速度,通过调整参数增强系统的可控性

线程池的执行原理

提交一个任务到线程池中,线程池的处理流程如下:

  1. 判断线程池里核心线程是否都在执行任务,若不是(核心线程空闲或还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则判断工作队列是否已满
  2. 如果工作队列没有满,则将新提交的任务存储在工作队列里。如果满了,则判断线程池里的线程是否都处于工作状态。
  3. 如果没有,则会创建一个新的工作线程来执行任务,如果已经满了则交给饱和策略来处理这个任务。

自旋锁

互斥同步进入阻塞状态的开销都很大,应该尽量避免,许多应用共享数据的锁定状态只会持续很短的一段时间,自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态。

自旋锁虽能避免进入阻塞状态从而减少开销,但它占用CPU,所以只适合共享数据锁定状态很短的场景

锁消除

是指对于被检测出不可能存在竞争的共享数据的锁进行消除

锁消除主要是通过逃逸分析来支持

synchronized

synchronized底层原理

synchronized是由一对monitor enter和monitor exit指令来实现同步的,JDK6之前monitor的实现是依靠操作系统内部的互斥锁来实现的,此时的同步操作性能很低。

JDK1.6提供了三种monitor的实现方式,分别是偏向锁、轻量级锁和重量级锁,会有一个锁升级的过程

用在静态方法和非静态方法上的区别

修饰静态方法是类锁

修饰非静态方法是对象锁

synchronized中锁的状态

  • 无锁状态
  • 偏向锁状态
  • 轻量级锁状态
  • 重量级锁状态

四种状态会随着竞争的情况逐渐升级。且不可逆,是JVM在使用synchronized时为提高锁的获取与释放而做的优化。

锁升级

偏向锁 -> 轻量级锁 ->重量级锁

锁升级的目的是为了减少锁带来的性能消耗

升级过程:

在锁对象的对象头里有个thread id字段,默认为空,当第一次有线程访问时,则将该threadid设置为当前的线程id,我们称为让其获取偏向锁,当线程执行结束,则重新将threadid设置为空

之后,如果线程再次进入的时候,会先判断threadid与该线程的id是否一致,如果一致,则可获取该对象,如果不一致,则发生锁升级,从偏向锁升级为轻量级锁

轻量级锁的工作模式是通过自旋循环的方式来获取锁,看对方线程是否已经释放了锁,如果执行一定次数后,还是没有获取到锁,则发生锁升级,从轻量级锁升级为重量级锁

ReenTrantLock与synchronized区别

ReenTrantLock就是可重入锁(统一线程每进入一次,锁计数器自增1,锁计数器为0时才能释放锁),是基于JDK实现的,而synchronized是基于JVM实现的,在synchronized没优化时,其性能很差,但自从引用了偏向锁、轻量级锁后,两者性能就差不多了。ReenTrantLock可以指定是公平锁还是非公平锁,而synchronized只能是非公平锁,所谓公平锁是指先等待的线程先获得锁。

请我喝杯咖啡吧~

支付宝
微信