目录
1.进程与线程的区别
- 进程是资源分配的最小单位,线程是CPU调度的最小单位
- 进程是抢占处理机的调度单位;线程属于某个进程,共享其资源
- 线程只由堆栈寄存器、程序计数器和TCB组成
- 线程不能看做独立应用,而进程可看做独立应用
- 进程有独立的地址空间,相互不影响,线程只是进程的不用执行路径
- 线程没有独立的地址空间,多进程的程序比多线程程序健壮
- 进程的切换比线程的切换开销大
2.Java进程和线程的关系
- Java对操作系统提供的功能进行封装,包括进程和线程
- 运行一个程序会产生一个进程,进程包含至少一个线程
- 每个进程对应一个JVM实例,多个线程共享JVM里的堆
- Java采用单线程编程模型,程序会自动创建主线程
- 主线程可以创建子线程,原则上要后于子线程完成执行
3.Thread中的start和run方法的区别
- 调用start()方法会创建一个新的子线程启动
- run()方法只是Thread的一个普通方法的调用
4.如何实现处理线程的返回值
实现的方式主要有三种
- 主线程等待法
- 使用Thread类的join()阻塞当前线程以等待子线程处理完毕
- 通过Callable接口实现:通过Future Task 或 线程池获取
线程的状态
-
新建(New):创建后尚未启动的线程的状态
-
运行(Runnable):包含Running和Ready
-
无限期的等待(Waiting):不会被分配CPU执行时间,需要显式被唤醒
没有设置timeout参数的Object.wait()方法
没有设置timeout参数的Thread.join()方法
LockSupport.park()方法 -
限期等待(Timed Waiting):在一定时间后由系统自动唤醒
Thread.sleep()方法
设置了timeout参数的Object.wait()方法
设置了timeout参数的Thread.join()方法
LockSupport.parkNanos()方法
LockSupport.parkUntil()方法 -
阻塞(Blocked):等待获取排它锁
-
结束(Terminated):已终止线程的状态,线程已经结束执行
5.sleep和wait的差别
基本差别
- sleep是Thread类的方法,wait是Object类中定义的方法
- sleep()方法可以在任何地方使用
- wait()方法只能再synchronized方法或synchronized块中使用
最主要的本质区别
- Thread.sleep只会让出CPU,不会导致锁行为的改变
- Object.wait不仅让出CPU,还会释放已经占有的同步资源锁
6.notify和notifyAll的差别
两个概念
- 锁池EntryList
- 等待池WaitSet
锁池
假设线程A已经拥有了某个对象(不是类)的锁,而其他线程B、C想要调用这个对象的某个synchronized方法(或者块),由于B、C线程在进入对象的synchronized方法(或者块)之前必须先获得该对象锁的拥有权,而恰巧该对象的锁目前正被线程A所占用,此时B、C线程就会被阻塞,进入一个地方去等待锁的释放,这个地方便是对象的锁池
等待池
假设线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁,同时线程A就会进入到该对象的等待池中,进入到等待池中的线程不会去竞争该对象的锁
notify 和 notifyAll 的区别
- notifyAll会让所有处于等待池的线程全部进入锁池去竞争锁的机会
- notify只会随机选取一个处于等待池中的线程进入锁池去竞争获取锁的机会
yield
当调用Thread.yield()函数时,会给线程调度器一个当前线程愿意让出CPU使用的暗示,但是线程调度器可能会忽略这个暗示
如何中断线程
- 调用interrupt(),通知线程应该中断了
- 如果线程处于被阻塞状态,那么线程将立即退出被阻塞状态,并抛出一个InterruptException异常
- 如果线程处于正常活动状态,那么会将线程的中断标志设置为true。将设置中断标志的线程将继续正常运行,不受影响
7.synchronized
线程安全问题的主要诱因
- 存在共享数据(也称临界资源)
- 存在多条线程共同操作这些共享数据
- 解决问题的根本方法:同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再对共享数据进行操作
互斥锁的特性
- 互斥性:即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程的协调机制,这样在同一时间只有一个线程对需要同步的代码块(复合操作)进行访问。互斥锁也称为操作的原子性
- 可见性:必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程可能是在本地缓存的某个副本上继续操作,从而引起不一致
- synchronized 锁的不是代码,锁的是对象
根据获取的锁的分类:获取对象锁和获取类锁
- 获取对象锁的两种用法
1.同步代码块(synchronized(this),synchronized(类实例对象)),锁是小括号()中的实例对象
2.同步非静态(synchronized method),锁是当前对象的实例对象
自旋锁
- 许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得
- 通过让线程执行忙循环等待锁的释放,不让出CPU
- 缺点:若锁被其他线程长时间占用,会带来许多性能上的开销
自适应自旋锁
- 自适应测次数不再固定
- 由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定
synchronized的四种状态
- 无锁、偏向锁、轻量级锁、重量级锁
- 锁膨胀方向:无锁 > 偏向锁 > 轻量级锁 > 重量级锁
偏向锁:减少同一线程获取锁的代价
- 大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得
- 如果一个线程获得了锁,那么锁就进入偏向锁,此时Mark Word的结构也变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查Mark Word的锁标记为偏向锁以及当前线程Id等于Mark Word的ThreadID即可,这样就省去了大量有关锁申请的操作
轻量级锁
- 轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁
- 适应的场景:线程交替执行同步块
- 若存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁
8.ReentrantLock(再入锁)
与synchronized的区别
- 位于java.util.concurrent.locks包
- 和CountDownLatch、FutureTask、Semaphore一样基于AQS实现
- 能够实现比synchronized更细粒度的控制,比如控制fairness
- 调用lock()之后,必须调用unlock()释放锁
- 性能未必比synchronized高,并且也是可重入的
ReentrantLock公平性的设置
- ReentrantLock fairLock = new ReentrantLock(true);
- 参数为true时。倾向于将锁赋予等待时间最久的线程
- 公平锁:获取锁的顺序按先后调用lock方法的顺序(慎用)
- 非公平锁:抢占的顺序不一定,看运气
- synchronized是非公平锁
ReentrantLock将锁对象化
- 判断是否有线程,或者某个特定线程,在排队等候获取锁
- 带超时的获取锁的尝试
- 感知有没有成功获取锁
总结
- synchronized是关键字,ReentrantLock是类
- ReentrantLock可以对获取锁的等待时间进行设置,避免死锁
- ReentrantLock可以获取各种锁的信息
- ReentrantLock可以灵活地实现多路通知
- 机制:sync操作Mark Word,lock 调用Unsafe类的park()方法
9.volatile和synchronized的区别
- 可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
- volatile本质是在告诉JVM当前变量在寄存器(工作内存)中的值是不确定的,需要从主内存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住直到该线程完成变量操作为止
- volatile仅能使用在变量级别;synchronized则可以使用在变量,方法和类级别
- volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量修改的可见性和原子性
- volatile不会造成线程阻塞;synchronized可能会造成线程阻塞
- volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
9.CAS(Compare and Swap)
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则返回V
一种高效实现线程安全性的方法
- 支持原子更新操作,适用于计数器,序列发生器等场景
- 属于乐观锁机制,号称 lock-free
- CAS操作失败时由开发者决定是继续尝试,还是执行别的操作
缺点
- 若循环时间长,则开销很大
- 只能保证一个共享变量的原子操作
- ABA问题 解决:AtomicStampedReference