为什么要并发编程
最次要还是压迫硬件(上图为我 cpu 的使用率)。当初硬件都是过剩的状态,不压迫干嘛,也不能天天指望程序员拿头发来优化算法啊。
并发编程带来的问题
安全性问题
都晓得并发编程能提高效率,然而这必定有代价的,会带来很多问题次要分为 3 大类。
原子性问题
- 原子性:一个或多个操作在 cpu 执行过程中不可分割。不可分割也就是中间状态对外不可见,所以只有保障对外不可见即可。
-
为什么会有这个问题?
cpu 会相似上图每隔肯定工夫铁环线程执行从而达到,多个程序同时在执行的感觉。也就是说你的程序执行到一半,就可能切换走了。java 是种高级语言,一行往往对应多条 cpu 指令,加上 cpu 指令冲排序,很可能后果线进去,然而过程还没实现,一旦被外界拿去用,就会呈现问题。比方Object o=new Object();
这一部操作对应了以下 3 步。- 申请内存,赋值默认值
- 成员变量初始化
- 赋值给对象援用
如果这个过程对外不可见,轻易怎么重拍都无所谓,然而如果 2 和 3 掉了个地位,他人用的时候发现没有初始化,很可能就会呈现问题。这还只是一行命令,就曾经呈现了危险,多行语句出问题的概率更大
- 如何解决?
-
加锁
可见性问题
- 可见性:一个线程对一个值的批改另外一个线程能够立马看见
- 为什么会有这个问题?
内存速度绝对于 cpu 而言慢很多,就呈现了 cpu 缓存,每个核都有本人的 cpu 缓存。如果多个 cpu 核解决同一个变量,解决实现之后没有及时刷回主存并告诉其余线程从新读取就会导致可见性问题。 - 如何解决?
- voliate
-
加锁
有序性问题
- 有序性:程序依照代码的先后顺序执行
-
为什么会有这个问题?
后面也说了高级语言一行往往对应多条 cpu 指令,编译器为了优化性能,有时候会改变程序中语句的先后顺序。指令集并行的重排序是对 CPU 的性能优化,从指令的执行角度来说一条指令能够分为多个步骤实现,如下:- 取指 IF
- 译码和取寄存器操作数 ID
- 执行或者无效地址计算 EX (ALU 逻辑计算单元)
- 存储器拜访 MEM
- 写回 WB (寄存器)
x 代表在这里进展了下,应为 R2 数据还没有筹备好,所以在这里期待了一会。上面咱们看另外一个状况
a=b+c
d=e-f
这会有很多进展,对这些指令略微重拍下就能够解决这些进展,进步 cpu 利用率
在不影响后果的前提下,只是做了指令重排,然而效率进步了。但也不能为了缩小进展进行排序升高乱排序,JMM 通过 happens-before 保障了可见性保障。- 程序程序规定:一个线程中的每个操作,happens-before 于该线程中的任意后续操作。
- 监视器锁规定:对一个锁的解锁,happens-before 于随后对这个锁的加锁。
- volatile 变量规定:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。
- 传递性:如果 A happens-before B,且 B happens-before C,那么 A happens-before C。
- start()规定:如果线程 A 执行操作 ThreadB.start()(启动线程 B),那么 A 线程的 ThreadB.start()操作 happens-before 于线程 B 中的任意操作。
- Join()规定:如果线程 A 执行操作 ThreadB.join()并胜利返回,那么线程 B 中的任意操作 happens-before 于线程 A 从 ThreadB.join()操作胜利返回。
- 程序中断规定:对线程 interrupted()办法的调用后行于被中断线程的代码检测到中断工夫的产生。
- 对象 finalize 规定:一个对象的初始化实现(构造函数执行完结)后行于产生它的 finalize()办法的开始。
- 如何解决?
- voliate
- 加锁
-
final
活跃性问题
死锁
一组相互竞争资源的线程因相互期待,导致“永恒”阻塞的景象。并发程序一旦死锁,个别没有特地好的办法,很多时候咱们只能重启利用。因而,解决死锁问题最好的方法还是躲避死锁。
- 互斥,共享资源 X 和 Y 只能被一个线程占用;
- 占有且期待,线程 T1 曾经获得共享资源 X,在期待共享资源 Y 的时候,不开释共享资源 X;
- 不可抢占,其余线程不能强行抢占线程 T1 占有的资源;
-
循环期待,线程 T1 期待线程 T2 占有的资源,线程 T2 期待线程 T1 占有的资源,就是循环期待。
活锁
有时线程尽管没有产生阻塞,但依然会存在执行不上来的状况,这就是所谓的“活锁”
饥饿
所谓“饥饿”指的是线程因无法访问所需资源而无奈执行上来的状况。优先级低的线程失去执行的机会很小,就可能产生线程“饥饿”;持有锁的线程,如果执行的工夫过长,也可能导致“饥饿”问题。
性能问题
不能适当的施展多线程性能的劣势,不能为了用多线程而用多线程,因为用锁会不可避免带来一点性能问题,很可能在某些状况下很可能执行工夫还不如多线程。咱们之所以应用多线程搞并发程序,为的就是晋升性能。
- 应用无锁优化
- 缩小锁的持有工夫
- 缩小锁的粒度
- 应用读写锁拆散锁来替换独占锁
- 锁拆散
- 锁粗化
线程生命周期
管程
java 多线程基本上都是基于 Monitor 实现的
管程博客这个博客写的不错倡议看这个
synchronized
synchronized 首先说下如何应用,作用于办法,作用于同步代码块。当写在静态方法中,锁的是 Class 对象,和同步代码块中写(xxx.class)成果统一,上面有个测试代码,感兴趣的能够测试下
public class SynchronizedDemo {public synchronized void test1() throws InterruptedException {System.out.println("test1");
Thread.sleep(10000);
System.out.println("test1 end");
test3();}
public static synchronized void test2() throws InterruptedException {System.out.println("test2");
Thread.sleep(10000);
System.out.println("test2 end");
}
public void test3() throws InterruptedException {synchronized (this){System.out.println("test3");
Thread.sleep(10000);
System.out.println("test3 end");
}
}
public void test4() throws InterruptedException {synchronized (SynchronizedDemo.class){System.out.println("test4");
Thread.sleep(10000);
System.out.println("test4 end");
test2();}
}
public static void main(String[] args) {SynchronizedDemo synchronizedDemo=new SynchronizedDemo();
Runnable r=new Runnable() {
@Override
public void run() {
try {// synchronizedDemo.test1();
synchronizedDemo.test2();} catch (InterruptedException e) {e.printStackTrace();
}
}
};
Runnable r1=new Runnable() {
@Override
public void run() {
try {// synchronizedDemo.test3();
synchronizedDemo.test4();} catch (InterruptedException e) {e.printStackTrace();
}
}
};
Thread t1=new Thread(r);
Thread t2=new Thread(r1);
// t1.start();
t2.start();}
}
synchronized 用的锁其实是存在 java 对象头中,jvm 中采纳 2 个字来存储对象头(如果对象是数组则会调配 3 个字,多进去的 1 个字记录的是数组长度),其次要构造是由 Mark Word 和 Class Metadata Address 组成,其构造阐明如下表:
虚拟机位数 | 头对象构造 | 阐明 |
---|---|---|
32/64bit | Mark Word | 存储对象的 hashCode、锁信息或分代年龄或 GC 标记等信息 |
32/64bit | Class Metadata | Address 类型指针指向对象的类元数据,JVM 通过这个指针确定该对象是哪个类的实例。 |
Mark Word 在不同的锁状态下存储的内容不同
当只有一个线程获取锁时,偏差锁的标识改成 1,线程 id,工夫戳。当有其余线程尝试获取锁的时候,会判断以后线程 id 和 markword 外面的线程 id 是否是统一,不统一,收缩为轻量级锁,而后自旋比拟,还没有拿到锁,就收缩为重量级锁,重量级锁的指针就指向了一个 monitor 对象,构造如下
ObjectMonitor() {
_header = NULL;
_count = 0; // 记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; // 处于 wait 状态的线程,会被退出到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; // 处于期待锁 block 状态的线程,会被退出到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
- _owner:指向持有 ObjectMonitor 对象的线程
- _WaitSet:寄存处于 wait 状态的线程队列
- _EntryList:寄存处于期待锁 block 状态的线程队列
- _recursions:锁的重入次数
- _count:用来记录该线程获取锁的次数
当多个线程同时拜访一段同步代码时,首先会进入_EntryList 队列中,当某个线程获取到对象的 monitor 后进入_Owner 区域并把 monitor 中的_owner 变量设置为以后线程,同时 monitor 中的计数器_count 加 1。即取得对象锁。
若持有 monitor 的线程调用 wait()办法,将开释以后持有的 monitor,_owner 变量复原为 null,_count 自减 1,同时该线程进入_WaitSet 汇合中期待被唤醒。若以后线程执行结束也将开释 monitor(锁)并复位变量的值,以便其余线程进入获取 monitor(锁)。如下图所示
volatile
该关键字确保了对一个变量的更新对其余线程可见。当一个变量被申明为 volatile 时候,线程写入时候不会把值缓存在寄存器或者或者在其余中央,当线程读取的时候会从主内存从新获取最新值,而不是应用以后线程的拷贝内存变量值。volatile 尽管提供了可见性保障,然而不能应用他来构建复合的原子性操作,也就是说当一个变量依赖其余变量或者更新变量值时候新值依赖以后老值时候不在实用。
如图线程 A 批改了 volatile 变量 b 的值,而后线程 B 读取了扭转量值,那么所有 A 线程在写入变量 b 值前可见的变量值,在 B 读取 volatile 变量 b 后对线程 B 都是可见的,图中线程 B 对 A 操作的变量 a,b 的值都可见的。volatile 的内存语义和 synchronized 有类似之处,具体说是说当线程写入了 volatile 变量值就等价于线程退出 synchronized 同步块(会把写入到本地内存的变量值同步到主内存),读取 volatile 变量值就相当于进入同步块(会先清空本地内存变量值,从主内存获取最新值)。转自
final
final 根底用法想必大家都曾经理解了,他在多线程中的比拟重要的 2 个点。
- 在构造函数中对 final 域写入,随后再把这个变量赋值给一个援用变量,这两个不能重排
- 读取对象的援用和读取这个 final 域,两个操作之间不能重排(大部分处理器是这样的,然而有少部分处理器抽风)
参考书籍
《实战 Java 高并发程序设计》