线程的三种实现:
- Runnable
- Thread
- Callable<>
线程的状态:
- NEW: 线程已经创建, 但是没有start()
- RUNNABLE: 启动线程
- WAITING: 线程等待状态, 调用wait()方法
- BLOCK: 线程阻塞状态, 一个cpu执行多个线程时出现
- TIME_WAITING: 定时等待状态, sleep(2000), waiting(...)
- TERMINAL: 线程终止状态
interupt和isInterupted, interupted方法
- interupt: 中断当前线程, 将中断标志由false设置为true
- isInteruputed: 获取当前线程的中断标志
- interupted: 返回当前线程的中断标志, 并将当前线程中断标志复位, 都设置为false, interrupted()方法测试的是当前线程是否被中断,当前线程!!!当前线程!!!
线程的安全性
- 可见性: 最典型的就是volitale
- 有序性: cpu在执行时, 为提高运行效率, 会对代码的顺序做出改变, 因此会造成安全性的问题, 而锁存在的意义就是保证锁内部的代码不被重新排序;
- 原子性: i++操作的问题, 可以使用原子操作类, 或者使用锁
线程的内存模型
原因: 主要就是存在主内存和线程自己缓存的问题, 线程每次取值会从自己本身的内存中获取值
解决的方式: 使用volitale, cas或者使用锁来解决
join()方法
- t.join()/t.join(1000): 等待t线程执行完毕再继续向下执行;
Sychronized锁原理
这里面有两个重要的概念: 对象头和monitor
对象头
- java虚拟机中, java的内存布局分为三个区域, 对象头, 数据实例对象填充, 而对象头就是实现Sychronized的关键, 因为她使用的锁的对象就存储在对象头中
- 而在java对象头中有一个mark word, 这是存储锁信息的区域, 锁的标志, GC分代, 线程ID和Hash码都在这个对象中;
Monitor; 可以理解为一个同步工具, 存在于每个对象中
锁名词的解释和Sychronized的锁级别
就是让不满足条件的线程不会立即挂起, 而是在执行一段时, 是一段无意义的循环, 这种方式虽然减少了线程切换的开销, 但是如果长时间做循环, 会浪费处理器的资源;
在大多数的情况下, 锁不存在竞争问题, 而是由同一个线程多次持有, 这时候当线程获得锁之后, 就会在锁的对象头中将自己线程id, 当之后这个线程再次获得到这个锁的时候, 不需要再进行CAS的竞争来进行加锁和解锁, 而是直接比对对象头中是否保存了自己的线程id, 如果有则直接获取, 如果失败, 在检查当前锁的标识是否是偏向锁, 如果不是偏向锁, 则用cas进行竞争, 是偏向锁, 则使用cas尽量将锁的标识修改为自己的;
当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁
重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。前面我们在讲Java对象头的时候,讲到了monitor这个对象,在hotspot虚拟机中,通过ObjectMonitor类来实现monitor。他的锁的获取过程的体现会简单很多, 重量级锁的实现实际上和AQS是类似的, 只不过他不是用java代码实现, 而是在虚拟机和操作系统中实现;
锁的使用场景和优缺点
加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。如果线程间存在锁竞争,会带来额外的锁撤销的消耗。适用于只有一个线程访问同步块场景。
竞争的线程不会阻塞,提高了程序的响应速度。如果始终得不到锁竞争的线程使用自旋会耗CPU。追求响应时间。同步块执行速度非常快。
线程竞争不使用自旋,不会消耗CPU。线程阻塞,响应时间缓慢。追求吞吐量。同步块执行速度较长。
synchronized的执行过程:
- 检测Mark Word里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁
- 如果不是,则使用CAS将当前线程的ID替换Mard Word,如果成功则表示当前线程获得偏向锁,置偏向标志位1
- 如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁。
- 当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁
- 如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
- 如果自旋成功则依然处于轻量级状态。
- 如果自旋失败,则升级为重量级锁。
wait和notify的原理
调用wait方法,首先会获取监视器锁,获得成功以后,会让当前线程进入等待状态进入等待队列并且释放锁;然后当其他线程调用notify或者notifyall以后,会选择从等待队列中唤醒任意一个线程,而执行完notify方法以后,并不会立马唤醒线程,原因是当前的线程仍然持有这把锁,处于等待状态的线程无法获得锁。必须要等到当前的线程执行完按monitorexit指令以后,也就是锁被释放以后,处于等待队列中的线程就可以开始竞争锁了
Lock锁
- 锁的优点, 相比Sychronized, 使用更加灵活, 能够显示获取和释放锁, 可以实现尝试获取锁功能, 也可以实现公平锁和非公平锁功能;
- 常用的实现ReentrantLock和ReentrantReadWriteLock
- 锁的实现是依赖于AQS实现的, 而AQS的关键就是四个
- 锁状态, 用户标志锁是否已经被占有
- 线程存储, 保存获取锁的线程
- 队列, 对于没有获取到锁的线程, 会被放到该队列中, 公平锁, 从队列中安顺序取获取锁, 公平锁和非公平锁的区别是: 公平锁会先去判断该所是否已经被持有, 如果被持有, 则放进队列, 没有则获取, 而非公平锁则会一开始就去竞争锁, 如果竞争失败则放入队列中;
- 基于cas实现的compareAndSet方法, 是一个在保证线程安全的前提下的乐观锁
condition的使用
- 他和Sychronized的wait和notify对应, 分别是await和singnal()
并发框架工具类
- countDownLatch: 类似于wait和notify, 可以控制线程同一执行到某个点时相互等待, 直到所有线程都执行完成, 在继续向下执行
- semaphore: 类似于停车场, 只允许有固定的线程进入
独占锁和共享锁
- 独占锁: 就是这个锁在被一个线程占有时, 其他线程不能获取锁
- 共享锁: 多个线程可以同时持有一把锁
线程池
Excutors包下的工具类
- newFixedThreadPool: 创建固定数量的线程, 但是对列的容量是无限的
- SinglerThreadExcutor: 只有一个线程的线程池
- cacheThreadPool: 无限线程数量的线程池
https://segmentfault.com/a/1190000022692938