java并发编程-jvm解决java并发问题的方法

目录
•1 java解决并发问题的方式
•2 volatile关键字
•2.1 volatile的作用
•2.2 volatile的应用场景
•3 happen-before规则的简单介绍
•4 synchronized关键字
•4.1 如何保护多个资源
•4.1.1 保护多个不关联的共享资源
•4.1.2 保护具有关联关系的共享资源
•4.2 死锁
•4.3 线程通信
•4.3.1 等待通知机制
•4.3.2 java使用synchronized的同步等待机制
•4.3.3 关于notify()与 notifyAll()
•3 java解决并发问题的其他方式

1 java解决并发问题的方式
上一章我们了解了并发编程遇到的问题,其中提到了安全性问题,线程安全是并发程序最基本的保障,那么java是如何解决线程安全问题的呢?具体点说就是volatile、synchronized与happens-before规则。这一章,我们就了解下java的volatile、synchronized与happens-before规则

2 volatile关键字
2.1 volatile的作用
volatile关键字是一种能解决java并发中原子性,可见性,有序性问题的方案。它能够防止辨析器指令重排序,禁用cpu使用高速缓存,保障单次读写操作的原子性。下面是三个使用volatile的例子解决上述问题

•防止指令重牌

public class Singleton {
    public static volatile Singleton singleton;
    /**
    * 构造函数私有,禁止外部实例化
    */
    private Singleton() {};
    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

上述的例子是经典的双重校验单例,singleton使用volatile修饰防止指令重拍使singleton还没有初始化的引用暴露

•可见性保障

class VolatileExample {
int x = 0;
volatile boolean v = false;
public void writer() {
    x = 42;
    v = true;
}
public void reader() {
    if (v == true) {
    // 这里 x 会是多少呢?
    }
}
}

假设一个线程读一个线程写,那么对v 加上volatile 可以保障writer()x = 42;v = true;对reader()的读可见,因为happen-before规则中的顺序一致性。

•保证原子性:单次读/写
long变量在32位机器上的运行问题,long变量是64位,32位操作系统读long变量需要两次load操作,volatile可以保障这种原子性。但是对于i++这种累加操作,操作本身即有读又有写,就不能保障其原子性了

2.2 volatile的应用场景
1.状态标志
2.一次性安全发布 (对象的空指针异常问题)
例如:

public class BackgroundFloobleLoader {
    public volatile Flooble theFlooble;

    public void initInBackground() {
        // do lots of stuff
        theFlooble = new Flooble();  // this is the only write to theFlooble
    }
}

public class SomeOtherClass {
    public void doWork() {
        while (true) { 
            // do some stuff...
            // use the Flooble, but only if it is ready
            if (floobleLoader.theFlooble != null) 
                doSomething(floobleLoader.theFlooble);
        }
    }
}

1.双重校验

3 happen-before规则的简单介绍
happen-before就是说前面一个操作的结果对后续操作是可见的,也就是说Happens-Before 约束了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定遵Happens-Before 规则。一般java程序员需要了解的六条规则:

1.程序的顺序性规则:前面的操作 Happens-Before 于后续的任意操作
2.volatile 变量规则:对一个 volatile 变量的写操作, Happens-Before 于后续对这个 volatile 变量的读操作
3.传递性 这条规则是指如果 A Happens-Before B,且 B Happens-Before C,那么 A Happens-Before C 这三条规则就解释了上面可见性保障的例子中为什么可见的问题

传递性示意图
markdown图片转码好麻烦自行脑补
4.管程中锁的规则:
对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。
5.线程 start() 规则:
主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作
6.线程 join() 规则:
在线程 A 中,调用线程 B 的 join() 并成功返回,那么线程 B 中的任意操作 Happens-Before 于该 join() 操作的返回

4 synchronized关键字
jvm基于管程模型 英文称 Monitor(mesa) 的实现互斥锁,主要用来线程切换导致的操作不原子性问题,实现对共享数据的互斥访问。
在使用锁的过程中,我们应该关注几个点:受保护的资源是谁,锁和受保护的而资源什么关系(N:1)不可以用多把锁锁定一个共享资源 例如

class SafeCalc {
static long value = 0L;
synchronized long get() {
    return value;
}
synchronized static void addOne() {
    value += 1;
}
}  

同时使用了 this 和 SafeCalc.class 两个对象锁定value,会产生同时进入共享区。

4.1 如何保护多个资源
4.1.1 保护多个不关联的共享资源
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;
    }
}
}
用不同的锁锁定不同的资源,这样就可以细化锁粒度。

4.1.2 保护具有关联关系的共享资源
请看下面这个例子

class Account {
private int balance;
// 转账
synchronized void transfer(
    Account target, int amt){
    if (this.balance > amt) {
    this.balance -= amt;
    target.balance += amt;
    }
} 
}

在上面的程序中,转账操作中用到了两个共享资源(this,target)但是却上了this对象锁,jvm的监monitor上锁的原理是把当前线程的id写入monitor对象的对象头,当前的monitor是this这个示例,对target没有影响,其他线程还是可以进入target对象。改正的方法:增加锁的力度,是两个对象都被锁住 方法1:使用一把公共锁锁定所有的转账操作(锁定的共享资源是所有的Account用户)

class Account {
private Object lock;
private int balance;
private Account();
// 创建 Account 时传入同一个 lock 对象
public Account(Object lock) {
    this.lock = lock;
} 
// 转账
void transfer(Account target, int amt){
    // 此处检查所有对象共享的锁
    synchronized(lock) {
    if (this.balance > amt) {
        this.balance -= amt;
        target.balance += amt;
    }
    }
}
} 

上述的例子会锁定所有转账操作,a->b,c-d也被互斥访问,实际上他们没有互斥关系。锁的力度过大影响性能。用时还需要保障所有Account都需要传入同一个对象锁,实际使用过工程中可能难以保障,也可以实用类锁,可以解决这个问题。方法2:

class Account {
private int balance;
// 转账
void transfer(Account target, int amt){
    // 锁定转出账户
    synchronized(this) {              
    // 锁定转入账户
    synchronized(target) {           
        if (this.balance > amt) {
        this.balance -= amt;
        target.balance += amt;
        }
    }
    }
} 
}

这样就不会锁定所有的Account对象,只会锁定需要使用的Account对象。

4.2 死锁
4.2.2 方法二中保护共享资源才去的锁方式,虽然增大了锁的力度,但是引入了新的问题-可能会导致死锁 例如 1线程:a给b转账,2线程:b也要给a转账,1线程:占有b的锁,等待a的锁,2线程:占有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 {
// actr 应该为单例
private Allocator actr;
private int balance;
// 转账
void transfer(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)
    }
} 
}  

•破坏不可抢占条件 synchroniczed做不到,但是java提供的juc可以实心此功能,后续介绍。
•破坏虚循环等待条件 -按序申请资源(例如哲学家进餐算法)

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;
        }
    }
    }
} 
}

4.3 线程通信
4.3.1 等待通知机制
什么是等待通知机制,等待通知机制是一种集体协作必然会发生的结果,关于等待通知,我们可以采取不同的策略-同步异步 阻塞非阻塞 简单解释下:

•同步非阻塞 假设 1线程与2线程是同步关系(此同步非彼同步,我的理解两个同步站的角度不同,一个站在事件的角度上的线程同步,一个站在线程的角度事件同步):线程1请求线程2 线程1等待线程2的结果,采取线程1轮询线程2的方式。(线程1 不阻塞,线程2不关注)
•同步阻塞
线程1等待线程2的结果,采取 线程1直接阻塞 ,等着线程2的被动唤醒。(线程1阻塞,线程2不关注)
•异步阻塞 线程1等待线程2的结果,采取 线程1发起请求后,不阻塞自己继续完成自己的操作,线程2 等待执行完毕后返回线程1,线程1再处理(线程1 不阻塞,线程2阻塞)
•异步非阻塞 线程1等待线程2的结果,采取 线程1发起请求后,不阻塞自己继续完成自己的操作,线程2 也不同步等待,而是等待线程1请求的结果完成,才响应线程1的请求(线程1 不阻塞,线程2不阻塞)
这里我认为 发起请求方如果选用异步策略,则他一定不阻塞。

4.3.2 java使用synchronized的同步等待机制

4.2.2中方法采用了同步非阻塞的方式(和上面的解释有点出入,上面的假设是线程1线程2是同步关系),这里的场景是互斥访问共享资源,多个执行线程之间是互斥关系,但是由于互斥访问,对共享资源会串行化,他们也会引起同步;上述代码中有:

//转账操作的方法中
while(!actr.apply(this, target))  
//公共锁分配中心的代码中
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;
}
他会一直访问是否具备条件(虽然不是像线程2询问)。

采用同步阻塞的方式,节约cpu资源

class Allocator {
private List<Object> als;
// 一次性申请所有资源
synchronized void apply(
    Object from, Object to){
    // 经典写法
    while(als.contains(from) ||
        als.contains(to)){
    try{
        wait();
    }catch(Exception e){
    }   
    } 
    als.add(from);
    als.add(to);  
}
// 归还资源
synchronized void free(
    Object from, Object to){
    als.remove(from);
    als.remove(to);
    notifyAll();
}
}

4.3.3 关于notify()与 notifyAll()
notify() 是会随机地通知等待队列中的一个线程,而 notifyAll() 会通知等待队列中的所有线程,一般情况下,如果所有线程执行的操作都是用一种,那么可以使用notify(),但是如果不一样,使用notify()可能会导致一些风险:例如

假设我们有资源 A、B、C、D,线程 1 申请到了 AB,线程 2 申请到了 CD,此时线程 3 申请 AB,会进入等待队列(AB 分配给线程 1,线程 3 要求的条件不满足),线程 4 申请 CD 也会进入等待队列。我们再假设之后线程 1 归还了资源 AB,如果使用 notify() 来通知等待队列中的线程,有可能被通知的是线程 4,但线程 4 申请的是 CD,所以此时线程 4 还是会继续等待,而真正该唤醒的线程 3 就再也没有机会被唤醒了。

3 java解决并发问题的其他方式
上面只介绍了使用jvm层面提供的同步方式,还有sdk层面提供的juc后续进行继续分析

参考文献
1.《并发编程实战-极客时间》
2.《并发编程实战-Brian Goetz等》

转载:https://mp.weixin.qq.com/s/maBB7lhI0Q8ycdHQM7rSKQ

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值