java多线程学习心得

431 阅读6分钟

java多线程学习心得

在java中,对象默认是单例,所以在单线程环境下的代码可能在多线程的环境下会出现各种各样的问题。这是由于java的内存模型(JMM)所决定的。

JMM

JMM(Java Memory Model)是java的内存模型,在JMM中,屏蔽了各种硬件差异,保证了java在各种平台下都能实现对内存的一致访问。 JAVA内存模型

在java内存模型中,原始数据存放于主内存中,当线程需要操作原始数据时,就会将原始数据copy一份到工作内存中,在线程对本地数据进行了操作后,再用本地数据覆盖主内存中的原始数据。这样的模型在进行单线程操作时不会有问题,但是如果出现了一个以上的线程操作同一个主内存中的数据,那就会出现多线程环境下的数据不一致问题。

例如:

      主内存中有一个变量a=1;
      线程1取出变量a(1),执行操作a=a+1;
      线程2从主内存中取出变量a(1),执行操作a=a+1;
      线程2执行完毕,用自己工作内存中的a=2覆盖了主内存中的a(1);
      线程1执行完毕,用自己工作内存中的a=2覆盖了主内存中的a(2);
      最终的结果就是两个线程都执行了对a+1的操作,但是最终的结果却是a=2;

如何实现多线程环境下内存数据可见性问题

java的线程之间是隔离的,线程1无法读取线程2的数据,线程只能读取工作内存中的数据。我们期望的是当一个线程对工作内存数据进行了操作之后,立即将数据刷新回主内存,同时,通知所有拿到了内存数据的其他工作内存,将工作内存中的数据修改为内存中最新的数据,这样就能实现在相互隔离的线程中,一个共享变量的修改会对其他的线程可见。

volatile关键字

多线程环境下,要想实现线程安全,需要实现三种条件:可见性、有序性、原子性。

可见性保证

在java的编译器中,会对被volatile修饰的关键字设置内存屏障,当工作内存中的数据副本发生变动之后,会添加一个内存屏障,只有当其他工作内存中的变量副本随之变动之后,才能执行后续的操作。(保证缓存一致性)

禁止指令重排序(有序性)

指令重排序:java编译器可能会对单线程环境下不影响最终结果的代码顺序进行调整。

编译器当遇到了被volatile修饰的变量时,会禁用指令重排序。

不保证原子性

对于非原子操作,要当寄存器中的多个操作指令执行完毕之后,才会将数据刷新回工作内存,然后刷新回主内存。 被volatile修饰的变量是不保证原子性的,例如自增操作符++

    volatile int i=1;
    i++;

这里执行了四步操作:

1. 加载i的数据进工作内存
2. 执行++操作(在寄存器中)
3. 将操作后的数据刷新回工作内存
4. 将工作内存的数据刷新回主内存

现在假设:当线程1的寄存器执行++操作时,线程2开始执行,从主内存中拿出i=1,然后经过自增运算后i=2,刷新回主内存,由于内存屏障,线程1的工作内存中的i变成2,然后线程1的寄存器自增之后也将2刷新回工作内存,工作内存又将2刷新回主存。 这就导致两个线程都执行了++操作,但是最后结果却等于2。


如何解决多线程环境下的原子性问题

多线程环境下的原子性有两种方式实现:

1. 使用synchronized实现原子性
2. lock锁
2. 使用原子包装类实现原子性

sychrinized

这个关键字可以用来修饰代码块、属性以及方法,其作用是为这些属性或者操作添加一个互斥锁,使复杂操作变为原子性操作。

lock锁

lock锁实现原子性的方式和sychrinized的方式相同,都是为代码加锁。

原子包装类(Atomic)以及ABA问题

原子包装类中包含了一个常量对象Unsafe,这个类就是实现原子性的核心。

        // initialize Unsafe machinery here, since we need to call Class.class instance method
        // and have to avoid calling it in the static initializer of the Class class...
        private static final Unsafe unsafe = Unsafe.getUnsafe();

Unsafe类当中,存在着compareAndSwap的方法(CAS),在这些方法中,通过自旋锁的方式来不断的读取最新的内存值进行操作,操作完后与期望值比较,如果两者一致,则操作成功,如果不一致,则再从内存中重新取值进行操作。

但是由于CAS只关注内存的当前值,所以如果内存中的值一开始为A,线程1拿到内存值,开始执行操作,然后线程2将内存值改成了B,线程3又将内存值改成了A,这时线程1执行操作完之后开始比较,期望值是A,然后发现内存值也是A,就执行了替换操作。如果我们需要当线程1操作的时候要拿到别的线程没有操作过的内存数据,这样就会出现误替换。也就是常说的ABA问题。

如何解决ABA问题

ABA问题的解决方案就是对原有的内存数据除了保存数据之外再加上一个版本号,当CAS时,必须满足内存数据与期望数据一致,并且版本号与拿到数据时的版本号一致时,才能操作成功,否则就一直自旋。 借助的类:AtomicStampedReference:原子标记引用

public class AtomicStampedReference<V> {

    private static class Pair<T> {
        final T reference;
        final int stamp;
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }
    ***
}

在这个Pair的静态内部类中,保存了对象的原始引用和stamp,这个stamp就相当于版本号。