Java Concurrency 基础知识
1.线程安全
a.无状态的类是线程安全的
b.所有状态都具有原子性的类是线程安全的
原子性:即对该类的操作是不被打断的,即使在多线程的环境下
如果一个类只有单个状态,推荐使用jdk中的java.util.concurrent.atomic包AtomicBoolean
AtomicInteger
AtomicIntegerArray
AtomicIntegerFieldUpdater
AtomicLong
AtomicLongArray
AtomicLongFieldUpdater
AtomicMarkableReference
AtomicReference
AtomicReferenceArray
AtomicReferenceFieldUpdater
AtomicStampedReference
这些类作为状态都具有原子性
2.Synchronized
a.如果一个方法用Synchronized关键字修饰,则调用该方法所用的锁,为调用方法的对象的固有锁(如反射),如果同时带有Static关键字修饰,则使用的锁为该方法所在的类的固有锁。
固有锁:每个对象都隐性地带有一个锁,该锁为重入锁(重入锁将在后文介绍),用于synchronized关键字
3.重入锁
关 于重入锁的解释用书中英文的解释会更准确:But because intrinsic locks are reentrant, if a thread tries to acquire a lock that it already holds, the request succeeds.
可能看完上面的还是有点难理解
举一个书中的例子:
看以下代码
假设不是可重入锁,那会出现一个什么现象?
调用super.doSomething()时发现该方法被锁住了,然后等待,但等待的锁自己正持有着,也就变成了死锁
重入锁的实现:
重 入锁是通过一个count和记录owner--一个持有该锁的Thread来实现的。当count值为0,认为这个锁当前没被任何线程持有。当 一个线程持有了该锁了,jvm就会把owner置为该线程,并把count置为1,如果同样锁线程再次请求该锁,那jvm就会把count+1,如果线程 退出同步块,则count-1,当count为0时,锁就释放了。
4.线程状态的锁操作
对于每一个涉及到多个变量的不变量,所有涉及到的变量都应该用同一个锁来进行同步
怎么理解呢?
看例子:
在 vector中,每个方法都是用synchronized修饰的,但在上面的使用中--如果两个线程先后进入该语句块,再轮流执行 vector.contain(element),显然还是会添加两次同样的element到vector中,所以还是不能保证vector的原子性
5.程序中的代码是可能被打乱的
如果没有进行同步,编译器,进程,和运行环境将有可能会打乱运行的顺序。
如文中所举例子:
将有可能出现这样的状况:输出结果为0.
这是因为执行的顺序被打乱了
6.64位数据的读或写是非原子性的
一般的数据类型,如int,执行
int a = 1;
int b = a;
这些操作都是原子操作,但是像long,double就不是了
jvm是允许把64位数据的读和写分开两次操作的,每次操作32位。
因此,有些项目,我们对某些数据的实时性不会太在意,就不会定义该变量为原子变量,但如果是long或者double类型,就要小心了。很有可能某用户在读取的时候读到的是前一个数据的前32位,和后一个数据的后32位
7.volatile关键字
当 一个变量用volatile声明了,编译器会放一个notice到该变量的共享区,然后当执行的时候,就不会对这个变量在程序中的执行顺序进行 修改。并且volatile不会缓存在寄存器中,也不会缓存在别的进程中,因此,读volatile修饰的变量时总能返回一个最新更新的值。
使用锁能同时保证变量的可见性和原子性,但volatile只能保证其可见性。
什么时候不适合使用volatile?
举一个经典的例子:i++操作,在并发环境下volatile是不能保证其正确性的
什么时候可以用volatile?
* 该变量的值不依赖该变量当前的值,像i++,或者你能保证该变量的更新只在单线程的环境下出现
* 该变量不和其他的变量一起包含在一个不变量中,如上文的vector
* 由于其他的原因,该变量不需要使用锁