今天想复习一下多线程的内容,可就目前工作中,用的还是很少,找了一下之前资料,无意之间就翻出了2年前的笔记,稍作更新之后便贴在这里,后续也想就多线程开一个专题,这里就作为前奏吧
一、常见的问题:
-
interupted和isInterupted的区别
- interupted会清除中断的状态,而后者不会
-
如果一个线程既传入了一个Runnable对象也实现了Thread子类,会调用哪个的方法,为什么?
- 会调用子类的实现,因为子类已经重写了run方法,传进去的target就不会执行
-
Callable和TaskFuture既能抛异常,也可以有返回值
- TaskFuture实现了Runnable和Future接口,Runnable提供任务封装,Future负责获取返回值、取消等操作;Future的get方法会阻塞;
-
Timer类是一个使用Thread实现的定时器
- TimerTask是一个实现了Runnable接口的定时器任务类,定时器任务建议学习Quartz框架
-
线程池
- 分类
- FixedThreadPool
- CachedThreadPool
- SingleThreadPool
- ScheduledThreadPool
- 线程池中的shutdown和shutdownNow方法
- shutdown方法会等待被调用之前的代码任务结束后关闭线程
- shutdownNow方法会立即结束线程不会等待任务执行结束
- 分类
-
synchronized
- 代码层面:synchronized代码块会持有一个内置锁,实例方法的话这个锁是当前对象this,静态方法的话是当前类的字节码对象
- 字节码层面:sychronized代码块被两个指令夹击,一个是开始的时候的monitorenter,另外一个是代码块结束的monitorexit
-
Java6的时候提出了几个锁机制
- 偏向锁:当一个线程重复访问一个同步代码块的时候,会设置对象头中的偏向锁,只有别的线程访问同一个同步代码块的时候,这个锁就升级为一个轻量级锁
- 对象头(两个字节的数组):1-Mark Word 2-Class Metadata Address 3-Array Length
- 对象头的Mark Work中会记录:1-线程id 2-Epoch 3-对象的分代年龄信息 4-锁标志位(锁类型)
- 轻量级锁:当一个线程要执行同步代码块,那么就会在自己线程的栈中分配一块空间用来记录对象头中的所信息,完成后便会指向对象头的所信息,接着就开始执行代码块,这个时候另外第二个线程也要执行同一个同步代码块,这个时候第二个线程也会在自己线程的栈中存放对象头中的锁信息,但是发现这个锁已经被占用,那么就会自旋等待一定的时间,如果在这个时间内能获取到这个锁,那么就执行同步代码块,否则这个锁就升级为一个重量级锁轻量级锁中,第二个线程还是能够进入同步代码块的,但是执行前还是要获取到锁;
- 重量级锁
- 偏向锁:当一个线程重复访问一个同步代码块的时候,会设置对象头中的偏向锁,只有别的线程访问同一个同步代码块的时候,这个锁就升级为一个轻量级锁
-
添加了volatile关键字之后,字节码指令中就会多一个lock指令
- 两个作用:
- 强制将当前处理器的缓存行(CPU缓存的最小单位)写回系统的内存
- 强制该写回会使得其他CPU的缓存中的缓存数据失效
- 注意
- 大量的使用volatile会使得系统的性能降低,因为CPU的缓存在很大程度上就没有意义了也会降低代码的优化,如指令重排,synchronized保证操作的原子性,volatile保证数据的可见性;
- 两个作用:
-
线程安全性问题的前提条件?
- 多线程环境
- 多线程同时竞争同一个共享资源
- 对共享资源的操作是非原子性的操作
-
保证线程安全的方式
- synchronized
- volatile
- java.util.concurrent.atomic中的原子类,所有的Atomic大头的类都是用来处理自增和自减的以及CAS操作的,
- 分类:
- 原子操作基本类型:AtomicBoolean、AtomicInteger、AtomicLong
- 原子操作数组:ActomicIntegerArray、ActomicLongArray、AtomicRefernceArray
- 原子操作自定义类型(类):AtomicReference,AtomicMarkableReference
- 原子操作对象中的字段:ActomicIntegerFieldUpdater、ActomicLongFieldUpdater
- 其他:DoubleAccumulator、LongAccumulator、DoubleAdder、LongAdder、Stripe64
- 实现:自旋的CAS
- 分类:
- java.util.concurrent.locks.Lock接口1.5开始支持
-
AbstractQueuedSynchronizer(AQS)
这个类帮我们实现了一个FIFO等待队列的阻塞锁和相关同步器(信号量,事件,等等)的框架, 注意的是这个类不是一个抽象类, 只是意义上的抽象
内部帮我们维护了一个状态state和一个FIFO队列, 我们可以通过继承这个类实现一个内部类工具,在这个内部类中选择性的实现独占模式或者共享模式的方法
这些方法内部就可以调用AQS提供的getState/setState/compareAndSetState方法来管理状态,队列是自动帮我们管理的 -
ReentrantLock
- 这个类实现了一个可重入锁, 实现了Lock接口
- 可重入: 可以在一个上锁之后释放锁之前调用了另外一个加了同一个锁的方法, 即称这个锁是一个可重入的
- 可重入实现:当有线程进入lock方法的时候先判断是否已经上锁, 如果上锁, 再判断上锁的线程是否是当前线程, 如果是则允许进入,锁的计数加1
- 公平性:自己维护一个队列, 为每一个队列元素上不同的锁, 唤醒的时候使用具体的锁来唤醒, 即可实现公平性
- 在ReentrantLock中的AQS的int类型的状态state表示锁的重入次数
- 可重入: 可以在一个上锁之后释放锁之前调用了另外一个加了同一个锁的方法, 即称这个锁是一个可重入的
- 这个类实现了一个可重入锁, 实现了Lock接口
-
ReadWriteLock
- 这个接口定义了一个比Lock的粒度更细的锁,把锁细化成读锁(共享锁)和写锁(排它锁),只有读锁之间不互斥,其他均互斥,很大程度上解决了系统的性能问题,因为系统中一般都是读远远大于写的操作
- 实现:实现类ReentrantReadWriteLock,因为把锁细粒度化,而AQS内部只有一个状态, 所以采用状态State的低位保存写锁的重入次数,高位保存读锁的可重入次数
- 这个接口定义了一个比Lock的粒度更细的锁,把锁细化成读锁(共享锁)和写锁(排它锁),只有读锁之间不互斥,其他均互斥,很大程度上解决了系统的性能问题,因为系统中一般都是读远远大于写的操作
-
降级锁:在写锁没有释放前,加上读锁,以防止其他的写线程抢占到资源,过程称为锁的降级
-
二、多线程间的通信
-
使用Object的wait和notify
- 这两个方法的调用必须放在synchronized修饰的代码块或者方法中
- 这两个方法只能由当前同步代码块或者同步方法的锁对象来调用
- wait方法执行的时候会释放获取到的锁
- notify方法会随机唤醒一个wait状态的线程,notifyAll方法会唤醒所有的wait状态的线程
- notify方法调用后,所在代码块执行完毕后才会释放当前的锁,其他的叫醒的线程才会开始抢占资源
-
Condition,用来替代第一种方式,其中wait对应Condition中的await,nofify对应signal
- Condition更加灵活可控,如让几个线程按照顺序执行
*实现:使用AQS中的Node类构建了一个单向链表,当进行await的时候,就向这个队列的尾部插入一个节点,如果是唤醒就头头部移除一个节点移除的这个节点会放到同步队列中,每一个Condition都是一个等待队列
- Condition更加灵活可控,如让几个线程按照顺序执行
-
join
- 在一个线程t1的任务中启动线程t2,并且调用t2的join方法,那么t1线程就会被wait,直到t2线程执行完成,t1线程才会继续执行
-
工具类
- CountDownLatch:可以让指定数量的线程处于wait状态,直到指定数量的线程都调用了CountDownLatch的CountDown方法后,CountDownLatch的await就停止,代码继续向下执行依赖于AQS实现,是一个共享锁,因为只要计数不为0,那么所有的线程都可以进
- CyclicBarrier:和CountDownLatch很像,但是用法稍有不同,初始化的时候指定参与的线程的数量C和全部线程完成后的动作A,当在C个线程中barrier对象都调用了await方法后,就会触发A动作;需要注意的是,这个barrier是可以复用的,参与者数量每达到一次,那么就会触发一次等待,参与者任务都完成后就会触发A动作
- Semaphore:所谓的信号量,就是允许同时有几个线程执行,通过一个信号量控制,acquire可以获取一个许可,release释放一个线程
初始化的时候可以指定同时最多有多少许可,这个许可即信号量的大小 - Exchanger:两个线程之间数据的交换,可以尝试通过Exchanger来实现消费者生产者问题
- resume发生在suspend之前,这个线程就永远也不会执行了
- unpark发生在park之前则不会出现这个情况,因为LockSupport使用信号量的原理来实现的
- park只能挂起无许可的情况,有许可的时候,park是无效的;如果park期间发生中断,则立即返回,内部处理了异常不会抛出
网友评论