java 并发——内置锁 (3)

这说明是同一个锁进入了 2 次,即调用子类方法的子类对象。而这也正好符合多态的思想,调用 super.doSomething() 方法时,是子类对象调用父类方法。

三、同步关键字 volatile 原子性

原子是世界上的最小单位,具有不可分割性。 比如 a=0 这个操作不可分割,我们说这是一个原子操作。而 ++i 就不是一个原子操作。它包含了"读取-修改-写入"的操作。


同步代码块,可以视作是一个原子操作。Java从JDK 1.5开始提供了java.util.concurrent.atomic包,这个包中的原子操作类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式。比如:AtomicBoolean AtomicInteger AtomicLong 等原子操作类。具体可查阅JDK源码或者参考《ava并发编程的艺术》第7章。


下面说说复合操作与线程安全的问题。
然后说说复合操作与线程安全的问题。
我们知道,java 集合框架中的 Vector 类是线程安全的,查看该类的源码发现,很多关键方法都用了synchronized 加以修饰。但是实际使用时候,稍有不慎,你会发现,它可能并不是线程安全的。比如在某个类中拓展一下 Vector 的方法,往 vector 中添加一个元素时,先判断该元素是否存在,如果不存在才添加,该方法大概像下面这样:

public class CJUtil{ public void putElement(Vector<E> vector, E x){ boolean has = vector.contains(x); if(!has){ vector.add(x); } } }

上面这个代码肯定是线程不安全的,但是为什么呢?不是说好,Vector 类是线程安全的吗?上网搜了一下,居然发现关于 Vector 类是不是线程安全的存在争议,然后我看到有人说它不是线程安全的,给出的理由比如像上面这种先判断再添加,或者先判断再删除,是一种复合操作,然后认真地打开 JDK 的源码看了,发现 Vector 类中 contains 方法并没有用 synchronized 修饰,然后得出了结论,Vector不是线程安全的...


事实到底是怎样的呢?我们假设 Vector 类的 contains 也用 synchronized 关键字加锁同步了,此时有两个线程 tA 和 tB 同时访问这个方法,tA 调用到 contains 方法的时候,tB 阻塞, tA 执行完 contains 方法,返回 false 后,释放了锁,在 tA 执行 add 之前,tB 抢到了锁,执行了 contains 方法,tA 阻塞。对于同一个元素, tb 判断也不包含,后面, tA 和 tB 都向 Vector 添加了这个元素。经过分析,我们发现,对于上述复合操作线程不安全的原因,并非是其中单个操作没有加锁同步造成的。


那如何解决这个问题呢?可能马上会想到,给 putElement 方法加上 synchronized 同步。

public class CJUtil{ public synchronized void putElement(Vector<E> vector, E x){ boolean has = vector.contains(x); if(!has){ vector.add(x); } } }



这样整个方法视为一个原子操作,只有当 tA 执行完整个方法后,tB 才能进入,也就不存在上面说的问题了。其实,这只是假象。这种在加锁的方法,并不能保证线程安全。我们可以从两个方面来分析一下:

从上文我们知道,给方法加锁,锁对象,是调用该方法的对象。这和我们操作 Vector 方法的锁并不是同一个锁。我们虽然保证了只有一个线程能够进入到 putElement 方法去操作 vector,但是我们没法保证其它线程通过其它方法不去操作这个 vector 。

上一条中,只有一个线程能够进入到 putElement 方法,是不准确的,因为这个方法不是静态的,如果在两个线程中,分别用 CJUtil 的两个不同的实例对象,是可以同时进入到 putElement 方法的。


正确的做法应该是:

public class CJUtil{ public void putElement(Vector<E> vector, E x){ synchronized(vector){ boolean has = vector.contains(x); if(!has){ vector.add(x); } } } } 重排序

重排序通常是编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段。重排序分为两类:编译器重排序和运行期重排序,分别对应编译时和运行时环境。

不要假设指令执行的顺序,因为根本无法预知不同线程之间的指令会以何种顺序执行。

编译器重排序的典型就是通过调整指令顺序,在不改变程序语义的前提下,尽可能的减少寄存器的读取、存储次数,充分复用寄存器的存储值。

int a = 5;① int b = 10;② int c = a + 1;③ 假设用的同一个寄存器

这三条语句,如果按照顺序一致性,执行顺序为①②③寄存器要被读写三次;但为了降低重复读写的开销,编译器会交换第二和第三的位置,即执行顺序为①③②

可见性

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zyxwzj.html