学好java并发编程,可以将并发抽象成以下三个问题:分发,同步,互斥

分发:

Java SDK 并发包里的 Executor、Fork/Join、Future 本质上都是一种分工方法

同步:

一个线程执行完了一个任务,如何通知执行后续任务的线程开工,线程之间相互协作,而解决协作的核心技术就是管程

互斥:

互斥用于解决线程安全问题,保障同一时间只允许有一个线程访问共享变量,实现互斥的核心技术就是锁

java 下载 并发编程实战 java并发编程教程_java 下载 并发编程实战

线程带来的原子性,可见性,有序性问题

可见性问题: (缓存导致)

在CPU多核场景下,两个线程同时操作内存中的值,会将内存值加载到cpu的缓存中,进行计算,最后再写回内存,两个cpu的缓存是不可见,导致最终的结果与预期是存在差距的

java 下载 并发编程实战 java并发编程教程_java_02

原子性问题 (使用互斥锁解决,也就是加锁解决)

count+1的指令可以被拆分为以下三个步骤

1、count加载进cpu寄存器

2、count+1

3、count写回内存

cpu保证的原子性是指令级别的,而不是高级操作符(count+1 可以)

java 下载 并发编程实战 java并发编程教程_java 下载 并发编程实战_03

有序性问题 (编译优化导致 使用volatile禁止指令重排以及遵循happen-before原则)

java 下载 并发编程实战 java并发编程教程_互斥_04

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 耗时)