并发编程的三个概念
原子性:一系列操作,要么独自执行完毕,要么完全不执行(体现不可分割)
可见性:当一个线程修改了对象的状态后,其他线程能看到状态的变化
有序性:即本线程内代码执行的顺序按照代码的先后顺序执行,(参见指令重排序)
synchronized原理
原子性: 通过该关键字所包含的临界区的排他性保证在任何一个时刻只有一个线程能够执行临界区中的代码,使得临界区中的代码代表一个原子操作。
可见性:保证内存的可见性。线程访问某个变量时,可能会读取CPU缓存区内的值,但每个CPU都有一块缓存区,可能会使一个CPU缓存区的值对其他缓存区不可见。该关键字就会保证值的可见性。
volatile原理
volatile变量提供可见性,但不保证原子性。要满足以下条件才应该使用:1.运算结果不依赖当前值或者只有一个线程参与修改(i++不行)2.变量不需要与其他状态变量共同参与不变约束。如在while(!asleep){…}, asleep变量为volatile,保证能通知循环的结束。
volatile强制线程从公共堆栈(内存)中读取值,而不是从私有工作堆栈(内存)
volatile可以禁止指令重排序优化
观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令。lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
特殊使用情况:在32位电脑上写64位的long型变量可以使用。
静态与普通方法加上synchronized的区别
synchronized普通方法:锁是当前对象
synchronized静态方法:锁是当前类的Class对象
两者并不互斥
加锁的情况与如何保证线程安全
保证线程安全:
不在线程之间共享状态变量(线程封闭)
将状态变量修改为不可变(只读共享)
在访问状态变量时使用同步(加锁保护)
线程安全共享,对象在内部进行同步(客户端就不需要自己同步了)
需要加锁的情况
线程安全的核心:对状态访问操作进行管理,特别是对共享的,可变的状态的访问
共享:状态变量可以由多个线程进行同步访问。
可变:意味着变量的值在其生命周期内可以发生改变。
线程封闭
栈封闭:只有方法内的局部变量,比如Serlvet的状态全在service()方法中。
ThreadLocal封闭:通常用于对可变的单实例变量或全局变量进行共享。为每一个线程创建一个全局变量的副本(详见ThreadLocal)
脏读,幻读,不可重复读与死锁
脏读:在读取实例变量,此值已经被其他线程修改过了
幻读:同一事务中,用同样的操作读取两次,得到的记录数不相同
死锁:两个线程互相等待对方释放锁
饥饿:优先级低的线程一直抢不到cpu资源
等待/通知机制
synchronized (obj) {
while(<condition does not hold>){
obj.wait()
}
}
synchronized (obj) {
<change conditon>
obj.notifyAll();
}
等待者要始终在while循环模式中,来调用wait()方法。
等待者:1.获取对象锁。2.判断条件满足,调用锁的wait()方法,进入WAITING状态并释放对象锁。3.等待被通知后,判断条件不满足,然后继续执行接下来的逻辑。
通知者:1.获取对象锁。2.改变等待者的条件。3.调用锁的notifyAll()方法,继续执行完毕剩下的逻辑,然后等待者才会继续。
一些原则:
有一个条件谓词,线程执行前必须先通过测试
在调用wait之前测试条件谓词,被通知后要再次测试
在一个循环中调用wait
要在持有锁的时候,调用wait(),notifyAll(),notify()方法
只有在1.所有等待线程的类型都相同。2.只能由一个线程来执行,此时才能调用notify()方法,其他情况都调用notifyAll()方法。
为什么要在一个循环中,调用wait()方法?
当某个地方要唤醒线程之前,会将即将要唤醒的线程之前的volatile变量设置为true,然后才会调用notifyAll()方法。
此时所有的线程都会唤醒,但还是在循环中,不满足条件的线程会重新调用wait()方法进入WAIT状态。
只有满足条件的线程才会跳出循环并往下执行。
join()和yield()
join()的本质是是让调用线程wait()在当前线程对象实例上
public class JoinMain{
public static void main(String...args){
public volatile static int i = 0;
Thread t1 = new Thread(() -> {
for(;i < 100000;i++);
});
t1.start();
t1.join();
System.out.println(i);
}
}
yield()会使让出cpu,但让出的时间不确定,可能马上又得到了。用于优先级较低的线程,害怕会占据大量的cpu资源,因此在适当的时间调用Thread.yield()
- *
多线程的其他不常用方法
sleep()与wait()的区别:sleep是线程让出cpu,有一个指定的时间。wait是一个已经得到对象锁,暂时让出对象锁,等待被notify()。
stop():弃用,不安全,直接中断线程的执行。比如A账户中取了500元,本应给B账户打500元,中断后就飞了。
suspend():弃用,会造成死锁。被suspend的对象会一直持有锁,其他对象也没法持有这个锁来调用resume()来恢复线程的运行。
乐观锁与悲观锁
悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。悲观锁的实现,往往依靠底层提供的锁机制;悲观锁会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。
乐观锁:假设不会发生并发冲突,每次不加锁而是假设没有冲突而去完成某项操作,只在提交操作时检查是否违反数据完整性。如果因为冲突失败就重试,直到成功为止。乐观锁大多是基于数据版本记录机制实现。为数据增加一个版本标识,比如在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
乐观锁的缺点是不能解决脏读的问题。在实际生产环境里边,如果并发量不大且不允许脏读,可以使用悲观锁解决并发问题;但如果系统的并发非常大的话,悲观锁定会带来非常大的性能问题,所以我们就要选择乐观锁定的方法.
锁机制存在以下问题:
在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
一个线程持有锁会导致其它所有需要此锁的线程挂起
如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。