JAVA并发编程实战-线程安全学习记录

思维导图

在这里插入图片描述

1 线程安全性

当多个线程访问一个类,不用考虑这个线程所处环境,并且不需要调用方额外的代码做出协调,这个类仍然可以保持正确性,则称这个类为线程安全。

线程安全类如java中Vector许多涉及改变状态的方法都使用了内置锁synchronized如下:
在这里插入图片描述

Vector作为线程安全类,封装了必要的同步,因此外部使用的客户不需要自己提供额外操作。

无状态对象是永远线程安全的
在这里插入图片描述
如图所示的代码,由于不包含状态也不包含对其它类的域,特定的计算状态会存储在线程栈中的局部变量表中,而虚拟机栈是线程私有的,不会影响其它线程。

2 原子性

编写如下的测试2-1demo:

public class AutomicClass {
    /**
     * 计数值
     */
    private long count;

    public long getCount() {
        return count;
    }

    /**
     * 服务方法
     */
    public void service() {
        // do something
        System.out.println("do something");
        // cala count++;
        ++count;
    }
}

如果多个线程访问service,那么count将会发生改变,但是类似++count是原子性的吗?

分析++count,其实它并不是原子性的,它包括三个状态:
  1. 读取操作:内存变量count->cpu寄存器。
  2. 计算操作:cpu运算器中加法器获取寄存器count,计算+1.
  3. 写回操作:cpu将计算结果写入指定的内存地址位置。

从上述分析可知count++是一个“读-改-写”的过程。每个状态依赖之前的状态,所以该操作不是原子性的。

2.1 竞争条件

上述2-1demo之所以会出现线程不安全,是由于存在竞争条件,也就是要想结果正确,只能依赖于正确的时序。

这里的竞争条件是“检查再运行”,也就是使用一个潜在的过期值,作为决定下一步操作的依据。

惰性初始化的竞争条件
以懒加载单例模式为例2-2demo:

public class LazyDemo {
    //单例实例
    private static LazyDemo single = null;
    //构造私有
    private LazyDemo(){};
    //懒加载创建实例
    public LazyDemo getSingle() {
        if (single == null) {
            return new LazyDemo();
        }
        return single;
    }
}

比如说A、B线程同时执行getSingle,执行判断single==null可能存在A和B都判断single为null导致两个线程获取两个不同的LazyDemo对象,而我们希望总是返回相同的实例。

2.2 原子操作

假设有操作A和B,如果从执行A的线程看,执行B操作的线程,要么B全部执行,要么一点没执行,这样A和B互为原子操作。

一个原子操作是指:该操作对于所有操作,都应该满足上述的描述。

3 锁

为了保护状态的一致性,应该在单一的原子操作中更新互相关联的状态变量。

3.1 内部锁

Java内部提供了强制原子性的内置锁机制:synchronized块。

synchronized包括锁对象的引用(监视器)和锁保护的代码块。
每个java对象都可以作为内部锁或者监视器锁。
synchronized可以用在:内部代码块、实例方法和静态方法:
  1. 内部代码块:也就是方法内部加锁,可以使用任意对象作为锁对象:
  public void demo() { synchronized (object) { getSingle(); } }
  2. 实例方法:也就是方法上加锁,锁对象是当前对象实例:
  public synchronized void demo() { getSingle(); }
  3. 静态方法:也就是类方法static加锁:锁对象是Class对象本身:
  public static synchronized void demo() {
//测试代码
single = new LazyDemo();
}
以字节码的形式看:对于方法加锁会添加flags:ACC_SYNCHRONIZED;对于内部代码块会使用指令: monitorenter和monitorexit

synchronized是互斥锁:不能实现多个线程的共同持有。

3.2 重进入

当一个线程获取了内部锁,再次获取所持有的锁,请求会成果也就是可重入的。

考虑如下代码2-3demo:

/**
 * 演示锁的可重入——如果不是可重入,则子类调用super.doSomething会导致死锁。
 * 执行结果:
 *  inside son doSomething
 * 子类this:com.example.concurrent.threadsafeTwo.SychronizedRepeatableTest@49476842
 * 子类super:com.example.concurrent.threadsafeTwo.SychronizedRepeatableTest@49476842
 * inside parent doSomething
 * 父类:com.example.concurrent.threadsafeTwo.SychronizedRepeatableTest@49476842
 *
 * 可见获取子类和父类的的锁其实都是同一个对象,所以可以演示可重入。
 */
public class SychronizedRepeatableTest extends Parent {
    @Override
    public synchronized void doSomething() {
        System.out.println("inside son doSomething");
        System.out.println("子类this:"+ this);
        System.out.println("子类super:"+ super.toString());
        super.doSomething();
    }

    public static void main(String[] args) {
        new SychronizedRepeatableTest().doSomething();
    }
}

class Parent {
    public synchronized void doSomething() {
        System.out.println("inside parent doSomething");
        System.out.println("父类:"+ this);
    }
}

运行结果:
在这里插入图片描述
可见此时可以成功运行,未发生死锁,也就是内部锁可重用。

上述代码子类覆写父类方法,并在代码中使用super调用父类加锁方法,其实此时的this和super都代表子类对象,所以进入父类方法也是获取的同一个锁。

4 用锁保护状态

锁可以使线程串行的访问它保护的代码路径。所以对于复合操作,需要在完整的运行期间占有锁。

一种常见的锁规则:在对象内部封装所以可变的状态,通过对象的内部锁来同步任何可导致状态变化的代码路径,这样就不需要在外部进行额外操作,许多线程安全类就是这个模式如Vector和StringBuffer等。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

LamaxiyaFc

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值