前言
在bilibili上看了一部分周阳老师的课另外呢也参考了一些博客,教主做一些小总结同时也作为了解多线程的开端,如有不当还望指正
看多线程与并发这一部分其实挺老火的,似乎许多东西都要要求了解原理可事实上想要弄清楚却又要涉及到底层的东西,即使能把视频看个大概也仅仅停留在表面。换言之,操作系统、编译原理、JVM这些东西终究是必不可少的。总之,长路漫漫唯~作伴
Java Memory Model(JAVA内存模型)初识
JAVA在堆中为引用类型(对象)分配内存,在栈中为基本类型分配内存,在方法区保存类信息和static变量
方法区只是逻辑上的概念,实际上实现于堆的永久代中。JAVA8去除了永久代,取而代之的是元空间——一块与堆不相连的本地内存,并将字符串常量池、符号引用、类的静态变量移至堆中,其余的数据作为元数据存储在元空间中
JAVA程序运行时至少有两条线程main线程和GC线程(实际上在启动时并不止),每条线程都有自己的工作空间。调用方法时以栈帧的形式压栈,栈帧中保存了
- 局部变量表
- 操作数栈
- 动态链接
- 返回地址
多线程
线程的生命周期和状态
NEW(初始)
新创建了一个线程对象,但还没有调用
start()方法
RUNNABLE(可运行)
该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)就绪状态的线程在获得CPU时间片后变为运行中状态(running)
BLOCKED(阻塞)
表示线程阻塞于锁
WAITING(等待)
进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)
TIMED_WAITING(超时等待)
该状态不同于WAITING,它可以在指定的时间内自行返回
TERMINATED(终止)
表示该线程已经执行完毕
线程常用方法
Thread类静态方法 |
|
|---|---|
publc static native void yield() |
暂停当前正在执行的线程对象,并执行其他线程 |
public static void sleep(long millisec) |
让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响 |
public static native Thread currentThread() |
返回对当前正在执行的线程对象的引用 |
Thread类实例方法 |
|
|---|---|
public synchronized void start() |
使该线程开始执行;JAVA虚拟机调用该线程的run()方法 |
public final void setDaemon(boolean on) |
将该线程标记为守护线程或用户线程 |
public final void join() |
等待该线程终止的时间最长为 millis 毫秒 |
public void interrupt() |
中断线程 |
public final native boolean isAlive() |
测试线程是否处于活跃状态 |
Object类实例方法 |
|
|---|---|
public final void wait() |
使当前线程等待,直到另一个线程调用notify()或notifyAll()。该方法要在同步方法或者同步代码块中才使用的 |
public final native void notify() |
唤醒正在此对象的监视器上等待的单个线程。该方法要在同步方法或者同步代码块中才使用的 |
public final native void notifyAll() |
唤醒此对象的监视器上正在等待的所有线程。该方法要在同步方法或者同步代码块中才使用的 |
线程通信
线程之间通信(线程操作资源类)至少有三个步骤
- 将共享变量从主内存复制到自己的工作空间
- 修改工作空间中的共享变量的副本
- 将共享变量的副本刷新到主内存
线程安全
多线程访问共享资源时必须进行同步控制才能保证线程安全。通常并发需要满足多线程的三个特性
原子性
操作中途不应被其他线程干扰,对参与不变性约束的变量的操作要么都成功,要么都不成功
可见性
某个线程修改了共享变量,其状态能够立即被其他线程所知晓
有序性
保证串行语意,避免指令重排
线程安全的实现方法
互斥同步
使用synchronized关键字
使用ReentrantLock类
互斥同步最主要的问题就是线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步
互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,那就肯定会出现问题
非阻塞同步
Compare And Swap(比较并交换)
乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。硬件支持的原子性操作最典型的是CAS
java.util.concurrent包下的原子类
JUC并发包下的
compareAndSet()等方法都使用了Unsafe类的CAS操作
无同步方案
栈封闭
线程本地存储
可重入代码
锁机制
按照锁的性质划分
公平锁/非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁
非公平锁是指多个线程呼叫偶去锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁
可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁
独享锁/共享锁
独享锁时指该锁一次只能被一个线程锁持有
共享锁时指该锁可被多个线程所持有
互斥锁/读写锁
独享锁/共享锁是一种广义的说法,互斥锁/读写锁是具体的实现
乐观锁/悲观锁
乐观锁则认为对于同一个数据的并发操作,是不会发生修改的
在更新数据的时候,会采用尝试更新,不断重新的方式更新数据
乐观的认为,不加锁的并发操作是没有事情的
悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改
因此对于同一个数据的并发操作,悲观锁采取加锁的形式
悲观的认为,不加锁的并发操作一定会出问题
按照设计理念划分
分段锁
分段锁其实是一种锁的设计,并不是具体的一种锁,对于
ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作
偏向锁/轻量级锁/重量级锁
这三种锁是指锁的状态,并且是针对
SynchronizedJava 5通过引入锁升级的机制来实现高效
Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的
偏向锁时指一段同步代码一直被一个线程锁访问,那么该线程会自动的获取锁。降低获取锁的代价
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自选,但自选不会一直持续下去,当自旋一定次数的时候还没获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其它申请的线程进入阻塞,性能降低
自旋锁
在JAVA中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式区尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU
直观分类
这张图片是在一篇博客里看到的,教主觉得总结的非常nice,基本上涵盖了好几节的视频,然后呢记录下来。原博客地址java中的各种锁详细介绍
常用的锁
| 按照锁的性质 | 按照设计理念 | |
|---|---|---|
Synchronized |
独享锁 非公平锁 |
JDK5之后引入了锁升级机制 |
ReentrantLock |
独享锁 互斥锁 可重入锁 默认非公平锁 |
|
ReadWriteLock |
读锁是共享锁,写锁是独享锁 读写锁 |
Synchonized关键字和Lock接口的区别
-
Lock是接口,基于JDK实现。而Synchonized是关键字,基于JVM层面实现 -
synchonized会自动释放锁,而Lock必须手动释放锁 -
Lock可以让等待锁的线程响应中断,而synchronized不会,线程会一直等待下去 - 通过
Lock可以知道线程有没有拿到锁,而synchronized不能 -
Lock能提高多个线程读操作的效率 -
synchronized能锁住类、方法和代码块,而Lock是块范围内的
volatile关键字
- 保证内存可见性
- 不保证原子性
- 禁止指令重排序