【Java】Java并发学习笔记

Java并发学习笔记



Volatile

volatile关键字修饰的变量,在赋值后,会执行一条“lock addl $0x0,(%esp)”操作,该操作是一个空操作,但是作用相当于一个内存屏障。
内存屏障:JVM重新对指令进行排序的时候,处于内存屏障后面的指令不会被排到内存屏障之前。
lock前缀的作用,使得本CPU的Cache写入内存,并且会引起其他CPU或者别的内核无效化其Cache,(就是说其他CPU再次使用变量的时候需要从主存中重新read,load),于是对volatile变量的修改对其他CPU立即可见。

规则

  1. read load use必须连续一起出现,保证了每次使用volatile变量的时候都是从主存中获取最新的值,也保证了可以看到其他线程对变量进行修改后的值。
  2. assign store write必须连续一起出现,保证了每次对变量的修改都会立刻同步回主存,也保证了其他线程可以看到自己对变量所做的修改。
  3. volatile修饰的变量不会被指令重排序优化,保证了代码执行顺序与程序的执行顺序相同。

long 和 double 型变量的非原子性协定

Java内存模型中要求lock、unlock、read、load、use、assign、store、write这8个操作都具有原子性,但是对于64位的数据类型(long 和 double),在模型中定义了一条比较宽松的规定:
允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为2个32位的操作来进行,即允许虚拟机不保证64位数据类型的load、store、read和write的原子性。
所以如果有多个线程共享一个没有声明为volatile的long或double类型的变量,同时对他们进行读取和修改操作,那么某些线程可能读取到一个既非原值,也不是其他线程修改过的值,可能只是“半个”。
不过上述情况比较罕见,在商用虚拟机中不会出现,Java内存模型允许虚拟机选择把这些操作实现为具有原子性的操作,而且“强烈建议”这么做。
在实际开发中,各平台下的商用虚拟机几乎都选择把64位数据的读写操作作为原子操作来对待,所以我们写代码的时候一般不需要把使用到的long和double专门申明为volatile。

先行发生原则

  1. 程序次序规则:在一个线程内按照程序的控制流程顺序(不等同于代码顺序),书写在前面的操作先行发生于书写在后面的操作。
  2. 管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。
  3. volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的都操作。
  4. 线程启动规则:Thread对象的start()方法先行与此线程的每一个动作。
  5. 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测,可以通过Thread.join()方法结束,Thread.isAlive()的返回值等手段检测线程已经终止执行。
  6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()检测是否有中断发生。
  7. 对象终结规则:一个对象的初始化(构造函数执行结束)先行于他的finalize()方法的开始。
  8. 传递性:如果操作A先行于操作B,B先行于操作C,那么A先行于C。

Java语言中的线程安全

按照线程安全的“安全程度”由强到弱排序,可以将Java语言中各种操作共享的数据分为5类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。

  1. 不可变
    Java1.5之后,不可变对象一定是线程安全的。final、String、Integer等等。
  2. 绝对线程安全
    “不管运行时环境如何,调用者都不需要任何额外的同步措施。” Java API中标注自己是线程安全的类大多都达不到绝对的线程安全。有时候达到绝对线程安全的代价是不切实际的。
  3. 相对线程安全
    就是我们通常意义上的线程安全,他需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要额外的保障措施,对于特殊的调用可能需要额外的同步手段来保证调用的正确性。
    例如Vector、HashTable、Collections的synchronizedCollection()方法包装的集合等。
  4. 线程兼容
    指对象本身不是线程安全的,但是可以通过在调用端正确的使用同步手段来保证对象在并发环境中可以安全的使用,我们平常说一个类不是线程安全的,绝大多数时候指的是这种情况。
    例如ArrayList、HashMap等。
  5. 线程对立
    无论调用端采取何种同步措施都无发在多线程中并发使用的代码。
    例如Thread的suspend()和resume()方法(已废弃)。

线程安全的实现方法

互斥同步

互斥同步是一种常见的并发正确性保障手段。
同步:指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个(或者一些,使用信号量的时候)线程使用。
互斥:实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。
二者关系:互斥是因,同步是果;互斥是方法,同步是目的。

在Java中最基本的互斥手段就是synchronized关键字,synchronized关键字经过编译之后,会在同步块的前后分别形成monitorenter和monitorexit(底层lock unlock)两个字节码指令,
这两个字节码指令都需要一个reference类型的参数指明要锁定和解锁的对象,如果指明了对象参数,那就是这个对象的reference;如果没有指明,那就根据synchronized修饰的是实例方法还是类方法,去取对应得对象实例或Class对象来作为锁对象。

在执行monitorenter指令时,首先要尝试获取对象的锁。如果这个对象没有被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,
在执行monitorexit指令时,会将锁的计数器减1,当计数器为0,锁被释放。
如果获取锁失败,当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。

synchronized同步块对同一个线程是可重入的,不会出现把自己锁死的情况,同步块在已进入的线程执行结束之前,会阻塞后面其他线程的进入。
因为Java的线程是映射到操作系统的原生线程之上的,阻塞和唤醒线程都需要操作系统的协助,从用户态转换到内核态,需要消耗处理器时间,对于简单的同步块可能状态转换的时间比用户代码执行的时间还要长,所以synchronized是Java语言中的一个重量级操作。

除了synchronized之外,还有java.util.concurrent包中的重入锁(ReentrantLock)来实现同步,与synchronized一样,具备相同的线程重入特性。
ReentrantLock表现为API层面上的互斥锁(lock()和unlock()方法配合try/finally语句块完成),synchronized表现为原生语法层面的互斥锁。
相比较synchronized,ReentrantLock增加了一些高级功能,主要有以下3项:等待可中断、可实现公平锁,以及锁可以绑定多个条件。
- 等待可中断是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断性对处理执行时间非常长的同步块很有帮助。
- 公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁则不保证这一点,在锁释放的时候,任何一个等待锁的线程都有机会获得锁。synchronized中的锁是非公平的,ReentrantLock默认也是非公平的,可以通过带布尔值得构造函数要求使用公平锁。
- 锁可以绑定多个条件是指ReentrantLock对象可以同时绑定多个Condition对象,而在synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁,而ReentrantLock不需要这么做,只需要多次调用newCondition()方法即可。

在JDK 1.5 中多线程环境下synchronized的吞吐量下降的很严重,而ReentrantLock则能基本保持在同一个比较稳定的水平上。
在JDK 1.6 中针对锁进行了优化,synchronized和ReentrantLock的性能基本持平。虚拟机在未来性能的改进中肯定也会偏向原生的synchronized,所以提倡在synchronized能实现需求的情况下,优先考虑synchronized来进行同步。

互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此也被称为阻塞同步。

非阻塞同步

基于冲突检测的乐观并发策略。就是先进行操作,如果没有其他线程争用共享数据则操作成功,如果有争用,产生了冲突,那就采取其他的补偿措施,(常见的补偿措施就是不断的重试,直到成功为止)。这种乐观的并发策略的许多实现都不需要把线程挂起,因此被称为非阻塞同步。

CAS:比较并交换。JDK 1.5 中sun.misc.Unsafe类中的compareAndSwapInt()和compareAndSwapLong()等几个方法包装提供。但只有启动类加载器加载的Class才能访问。

锁的优化

自旋锁

在很多应用中,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。在多个处理器的机器中,能让两个或两个以上的线程并行执行,我们就可以让后面请求锁的那个线程“稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁,为了让线程等待,只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。

自旋等待不能代替阻塞,自旋本身虽然避免了线程切换的开销,但是占用处理器的时间,因此如果锁被占用的时间很短,自旋等待的效果就很好,反之,自旋等待只会白白消耗处理器资源。
JDK1.6 引入自适应自旋锁。自旋的时间由上一次在同一个锁上的自旋时间以及锁的拥有者的状态决定。

锁消除

即时编译器进行没有必要的锁的消除。
public String concatString(String s1,String s2,String s3){
return s1+s2+s3;
}

JDK 1.5 之前会转换成StringBuffer对象的连续append()操作,
JDK 1.5 之后会转换为StringBuilder对象的连续append()操作。

上述代码经过javac编译器可能会变成下面这个样子:
public String concatString(String s1,String s2,String s3){
StringBuffer sb=new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
在每个StringBuffer.append()中都有一个同步块,锁就是sb对象。虚拟机会观察变量sb,发现他的动态作用域被限制在concatString()方法的内部,也就是说sb的引用永远也不会“逃逸”到concatString()方法之外,其他线程都无法访问到他。因此虽然这里有锁,但是可以被安全地消除掉,在即时编译之后,这段代码就会忽略掉所有同步直接执行了。

锁粗化

即是把锁的范围扩大,比如上面那个concatString的例子,虚拟机可能把锁同步的范围拓展到第一个append()操作之前直到第3个append()操作之后,这样就只用加锁一次。

轻量级锁

JDK 1.6z中添加的新型锁机制。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值