java多线程与并发
线程状态及转换
新建New
- 线程创建后未启动
可运行Runnable
- 可能正在运行,也可能正在等待CPU时间片
包含了操作系统线程状态中的running和ready
阻塞Blocking
- 等待获取一个排它锁,如果其线程释放了锁就会结束此状态
无限期等待Waiting
-
等待其他线程显式的唤醒,否则不会被分配CPU时间片
-
进入方法
- 调用了没有设置Timeout的wai()方法
调用了没有设置Timeout的join()方法
- 调用了没有设置Timeout的wai()方法
-
退出方法
- 其他线程调用notify()/notifyAll()
等待调用了join方法的线程执行完毕
- 其他线程调用notify()/notifyAll()
限期等待Timed Waiting
-
无需等待其他线程唤醒,在一定时间之后会被系统自动唤醒
-
进入方法
- 调用了sleep()方法
调用了设置Timeout的wai()方法
调用了设置Timeout的join()方法
- 调用了sleep()方法
-
退出方法
- 时间结束
时间结束/其他线程调用notify()/notifyAll()
时间结束/等待调用了join方法的线程执行完毕
- 时间结束
死亡Terminated
- 可以是线程结束任务之后自己结束或者产生了异常而结束
线程实现的方式
继承Thread类
实现Runnable接口
实现Callable接口
三者的区别
- 最好采用实现接口方法开启多线程,类只能单继承,而接口可以多个实现
Callable接口可以有返回值,返回值通过Futuretask封装
Daemon
守护线程是程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分
当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程
synchronized
同步一个代码块,只作用于同一个对象,如果调用两个对象上的同步代码块,就不会进行同步
同步一个方法,和同步代码块一样,作用同一个对象
同步一个类,作用整个类,也就是两个线程调用同一个类的不同对象上的这种同步语句,也会进行同步
同步一个静态方法,作用于整个类
对象锁
-
方法锁
- synchronized修饰普通方法,锁对象默认为this
-
同步代码块锁
- 手动指定锁定对象,可以是this也可以是自定义的锁
类锁
- synchronized修饰静态方法
- 指定锁对象为class对象
线程锁
锁的类型
-
无锁
-
偏向锁
- 在大多数实际环境下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获取,那么在同一个线程反复获取释放锁中,其中并没有锁的竞争,那么多次获取释放锁带来了不必要的性能开销和上下文切换
- 在对synchronizied进行优化,引入偏向锁,当一个线程访问同步块并获取锁时,会在对象头和栈帧的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需要简单测试一下对象头存储着当前线程的偏向锁,如果成功则表示线程已经获取到了锁
- 优点
加锁和解锁不需要CAS操作,没有额外的性能消耗,和执行非同步方法相比仅存在纳秒级的差距 - 缺点
如果线程间存在锁竞争,会带来额外的锁撤销的开销 - 使用场景
适用于只有一个线程访问同步块的场景
-
轻量级锁
-
优点
- 竞争的锁不会阻塞,提高了响应速度
-
缺点
- 如果线程始终得不到锁竞争的线程,使用自旋会消耗CPU性能
-
使用场景
- 追求响应时间,同步块执行速度非常快
-
-
重量级锁
-
优点
- 线程竞争不适用自旋,不会消耗CPU
-
缺点
- 线程阻塞,响应时间缓慢,在多线程下,频繁的获取释放锁,会带来巨大的性能消耗
-
使用场景
- 追求吞吐量,同步块执行速度较长
-
锁的膨胀方式
- 无锁->偏向锁->轻量级锁->重量级锁(此过程不可逆)
自旋锁和自适应自旋锁
-
自旋锁
- 在锁还没有优化的时候,多线程竞争锁时,当一个线程获取锁时,会阻塞所有正在竞争的线程,极大影响性能,在挂起线程和恢复线程的操作都需要转入内核态中完成
- 大多数情况下,共享数据的锁的锁定状态只会持续很短的一段时间,为了这段时间去挂起和回复阻塞线程并不值得,自旋锁就是让没有获得锁的线程在门外等待即自旋,等待持有锁的线程是否很快就释放锁
- 自旋锁默认的自旋次数是10次,超过限定的次数就会将线程挂起
-
自适应自旋锁
- 自适应锁使锁的自旋时间由同一个锁上的自旋时间及锁的拥有者的状态来决定
- 如果同一个锁对象上,自旋等待刚刚成功获取过锁,并且持有锁的线程正在运行,那么jvm就会认为该锁自旋获取到锁的可能性很大,会自动增加等待时间,例如增加到100次自旋循环
- 如果对于某个锁,自旋很少成功获取锁,那么以后要获取整个锁时将可能省略自旋过程,避免处理器浪费
锁消除
- jvm会判断一段程序中的同步明显不会逃逸出去从而被其他线程访问到,认为这些数据是线程独有的,不需要加同步,就会把原先的锁消除掉
锁粗化
- 如果存在连串的一系列操作都对同一个对象反复加锁和解锁,甚至加锁操作时出现在循环体中的,即使没有线程竞争,频繁的进行互斥同步操作也会导致性能降低
- 如stringbuilder连续的调用append()操作,发现这一连串操作都是对同一个对象加锁,那么会将加锁同步的范围粗化到整个一系列操作的外部,使一连串的append()操作只需要加锁一次就好了