学好java并发编程,可以将并发抽象成以下三个问题:分发,同步,互斥
分发:
Java SDK 并发包里的 Executor、Fork/Join、Future 本质上都是一种分工方法
同步:
一个线程执行完了一个任务,如何通知执行后续任务的线程开工,线程之间相互协作,而解决协作的核心技术就是管程
互斥:
互斥用于解决线程安全问题,保障同一时间只允许有一个线程访问共享变量,实现互斥的核心技术就是锁
线程带来的原子性,可见性,有序性问题
可见性问题: (缓存导致)
在CPU多核场景下,两个线程同时操作内存中的值,会将内存值加载到cpu的缓存中,进行计算,最后再写回内存,两个cpu的缓存是不可见,导致最终的结果与预期是存在差距的
原子性问题 (使用互斥锁解决,也就是加锁解决)
count+1的指令可以被拆分为以下三个步骤
1、count加载进cpu寄存器
2、count+1
3、count写回内存
cpu保证的原子性是指令级别的,而不是高级操作符(count+1 可以)
有序性问题 (编译优化导致 使用volatile禁止指令重排以及遵循happen-before原则)
Demo实例
当我们需要操作不相关的资源,而不是使用单个锁
//如果使用Bank对象作为锁,会导致所有操作都是串行,性能效率差 //用不同的锁对受保护资源进行精细化管理,能够提升性能。这种锁叫细粒度锁。
public class Bank {
private int balance;
private String password;
//钱锁
private final Object balanceLock = new Object();
//密码锁
private final Object passwordLock = new Object();
private void saveMoney() {
synchronized (balanceLock) {
balance = +1;
}
}
private int getMoney() {
synchronized (balanceLock) {
return balance;
}
}
private void updatePass(String newPss) {
synchronized (passwordLock) {
password = newPss;
}
}
}
使用一把锁保护多种资源 (性能较差,所有对Account的操作都是串行的)
public class Account {
private int balance;
//Account在java加载过程中就已经被创建了,是所有account实例对象所共享的,
// 在jvm中是唯一的,因此可以作为唯一共享的锁
private void transfer(Account target, int mount) {
synchronized (Account.class) {
target.balance += mount;
this.balance -= mount;
}
}
}
优化 (但是会存在死锁问题)
死锁原因
账户A调用transfer,执行synchronized (target),此时账户A获取A的锁,等待B锁释放
账户B也调用了transfer,执行synchronized (target),此时账户B获取B的锁,等待A锁释放
此时账户A,B陷入无限等待的死循环,出现了死锁
private void transfer(Account target, int mount) {
synchronized (this) {
synchronized (target) {
this.balance = balance - mount;
target.balance += mount;
}
}
}
死锁产生的条件:
互斥,共享资源 X 和 Y 只能被一个线程占用;
占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;
不可抢占,其他线程不能强行抢占线程 T1 占有的资源;
循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。
破坏死锁:
1、破坏占有且等待
以账户的例子来说,当线程进行转账,需要同时获取A,B两个锁,才能进行操作,此时需要一个管理者来分配和释放资源
class Allocator {
private List<Object> als =
new ArrayList<>();
// 一次性申请所有资源
synchronized boolean apply(
Object from, Object to) {
if (als.contains(from) ||
als.contains(to)) {
return false;
} else {
als.add(from);
als.add(to);
}
return true;
}
// 归还资源
synchronized void free(
Object from, Object to) {
als.remove(from);
als.remove(to);
}
}
class Account{
private Allocator actr;
private int balance;
private void transfer2(Account target, int amt) {
// 一次性申请转出账户和转入账户,直到成功
while (!actr.apply(this, target)) {
try {
// 锁定转出账户
synchronized (this) {
// 锁定转入账户
synchronized (target) {
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}
} finally {
actr.free(this, target);
}
}
}
}
2、破坏循环等待条件
获取锁的顺序按锁的ID从小到大排序
class Account{
private int balance
private int id;
private void transfer3(Account target, int amt) {
//按锁的ID从小到大排,先获取的锁一定是最小的锁
Account front = this;
Account latter = target;
if(front.id>latter.id){
front = target;
latter = this;
}
synchronized (front){
synchronized (latter){
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}
}
}
创建多少线程是最合适的?
进行多线程开发,无疑是为了提高吞吐与降低延迟,设置最合适的线程,就是如果最大化提供内存和cpu的利用率
从两个场景出发: (可以通过Visual VM查看线程执行情况)
CPU密集型任务(即计算性任务),也非绝对,因为有些cpu核数本就比较少,也有任务开到几倍以上的核数(这块我也是很迷茫)
线程的数量 = CPU核数(lscpu查看cpu核数),一般在工程上可以设置为线程数=CPU核数+1
IO密集型任务
最佳线程数 =1 +(I/O 耗时 / CPU 耗时)