可见性,原子性,有序性
提高并发量策略:
【尽量不使用锁】
- 类里面不共享变量
- 单个线程保有自己的数据——ThreadLocal
- 消息队列【生产者-消费者模式】(若是消费者可以批量执行,效率更高)
- StampedLock【乐观锁】
- 原子类【CAS+自旋】
- ReentrantReadWriteLock【读写锁】
- 线程池
- 锁
线程安全:若这个数据在线程间共享且读写不互斥,就是线程不安全
特别注意:线程安全的容器,方法是线程安全的,但是方法的组合不是原子性的,多线程情况下还是会因为线程的执行顺序导致结果的不同,即存在竞态条件 【给对容器的所有操作加锁即可】
回调函数,要注意是主线程还是新建一个线程来执行任务。如果是主线程,最好新建一个单线程的线程池来异步执行任务
可变对象不能加锁
Integer,Long等包装类不适合加锁,因为他们用到享元模式,这会导致看上去私有的锁其实锁的是一个对象
volatile
volatile int x = 0 告诉编译器这个变量不能使用CPU缓存,要在内存中读写(实际上是在CPU缓存中改变值之后,强制刷新到内存)
Happens-Before 规则
(意思是:前面操作的结果对后面可见)
1.程序的顺序性规则(在一段代码里,几条代码顺序执行,后面的代码能看到前面代码执行的结果)
2.volatile 变量规则(如果对一个volatile 变量进行了写操作,后面的读操作可以看见写操作的结果)(前提是Java1.5及以上)
3.传递性(a对b可见 , b对c可见 则 a对c可见)
4.管程中锁的规则(锁的解锁对后面锁的加锁可见,就是某个线程在解锁前的操作,对另一个线程拿到锁之后可见)
5.线程start()规则(如果主线程start子线程,主线程的操作对子线程可见)
6.线程join()规则(如果主线程join子线程,子线程的操作对主线程可见)
使用final好处:值不会变,编译器可以使劲优化
synchronized
默认对代码块进行加锁解锁操作
加在静态方法上,意思是锁这个类的class对象
加在普通方法上,意思是锁this,锁类的对象
或者加在代码块上,括号里面加要锁的东西【不能用自己的锁锁别人的资源】
用两把锁锁不同的资源【下面的锁括号里面不能是 this.balance和this.password,因为这两个东西是可变的,不能锁可变对象】
class Account {
// 锁:保护账户余额
private final Object balLock
= new Object();
// 账户余额
private Integer balance;
// 锁:保护账户密码
private final Object pwLock
= new Object();
// 账户密码
private String password;
// 取款
void withdraw(Integer amt) {
synchronized(balLock) {
if (this.balance > amt){
this.balance -= amt;
}
}
}
// 查看余额
Integer getBalance() {
synchronized(balLock) {
return balance;
}
}
// 更改密码
void updatePassword(String pw){
synchronized(pwLock) {
this.password = pw;
}
}
// 查看密码
String getPassword() {
synchronized(pwLock) {
return password;
}
}
}
预防死锁:
1.互斥(不能避免)
2.占有且等待(方法:一次性获取全部资源)
3.不可抢占(用Lock)
4.循环等待(把资源排序,按序号从小到大取)
class Account {
private int id;
private int balance;
// 转账
void transfer(Account target, int amt){
Account left = this ①
Account right = target; ②
if (this.id > target.id) { ③
left = target; ④
right = this; ⑤
} ⑥
// 锁定序号小的账户
synchronized(left){
// 锁定序号大的账户
synchronized(right){
if (this.balance > amt){
this.balance -= amt;
target.balance += amt;
}
}
}
}
}
写并发程序要关注3大方面问题:
1.安全性(数据竞争:多个线程共享数据,且这个数据会被至少一个线程修改,会导致bug【加锁就可】)(竞态条件:程序的结果依赖于程序执行的顺序,比如两个线程同时改一个数据,数据不变,先后改数据,数据改变【加锁即可】)
2.活跃性(活锁:比如两个线程获取一个资源,一直互相谦让【等待随机时间再尝试获取资源】)(饥饿:因为优先级低,或者一直拿不到资源,线程一直不能被执行【使用公平锁按先后顺序获取资源】)
3.性能性(使用无锁的算法和数据结构 线程本地存储 写入时复制 乐观锁,减少锁的持有时间 读写锁 分段锁)
用锁的最佳实践
1.永远只在更新对象的成员变量时用锁
2.永远只在访问可变的成员变量时用锁
3.永远不要在调用其他对象的方法时用锁
4.减少锁的持有时间
5.减少锁的粒度