竞态条件
当某一个计算的值依赖于多个线程交替执行时序时,就会发生竞态条件。换句话说,就是正确的结果取决于运气。
复合操作
i++是一个复合操作,因为这其中包括了读取-修改-写入
处理器如何进行原子操作
1.锁住总线来保证原子性
所谓总线锁就是使用处理器提供一个Lock#信号,当一个处理器在总线上输出这个信号时。其他处理器的请求将会被阻塞,那么该处理器就可以独占内存
2.锁住缓存来保证完整性
在同一时刻,我们只需要保证对某一个内存地址的操作是原子性即可,但是总线锁把CPU和整个内存的通信都锁住了。
有两种情况不会使用缓存锁
1.处理器不支持锁住缓存
2.操作的数据不能存储在处理器内部
可重入锁
当一个线程请求获得请他线程的锁时,就会发生阻塞。然而,由于锁是可重入的,因此一个线程如果试图获得一个获得一个已经由他自己获得的锁时,这个请求就会成功,这就是锁的内置锁的重入。
重入的一种实现方法时,当一个锁被一个线程持有时,JVM就记下锁的持有者,并获取计数器,置为1.每次重入一次,计数器加一,释放一次减一。当计数器为0时,线程释放锁。
volatile
在Java中,如果一个字段被声明为了volatile,那么Java内存模型将确保所有的线程看到的这个变量都是一致的。还有,这个变量的读/写都将具有原子性
实现原理:被volatile修饰的字段,会比其他的变量多出一条lock前缀指令,前缀指令在多核处理器中会引发两件事情
1) 将当前处理器缓存行的数据写回系统内存
2) 这个写回内存的操作将会使其他CPU里缓存了该内存地址的数据无效。当处理器想要对该数据进行处理使用时,会重新从系统内存中把该数据读取到处理器缓存中去。
synchronized
同步锁。在Java SE 1.6以后为了减少获得锁和释放锁带来的性能消耗,引入了偏向锁和轻量级锁。
Java中任何一个对象都可以作为锁,具体表现为一下三种形式。
1) 对于普通的方法,锁住的是当前的对象
2) 对于被static修饰的静态方法,锁住的是该对象的Class对象
3) 对于同步代码块,锁住的则是括号中的对象
JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。代码块同步是使用monitorenter和monitorexit指令来实现的,JVM必须保证每个monitorenter都有一个对应的monitorexit配对。JVM在同步代码开始的地方插入monitorenter,在异常和同步代码结束的地方插入monitorexit。任何一个对象都有一个monitor与之关联,当且一个monitor对象呗持有之后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象的monitor的所有权,即尝试获得对象的锁。
在Java对象头中存储着三种内存,分别是Mark Word,Class Medadata Address,Array length
Mark Word :存储对象的hashCode,锁信息,以及分代年龄
Class Medadata Address:存储指向对象类型的地址的指针
Array length:存储数组的长度(如果当前对象是数组的话)
在Java中锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态,锁会随着竞争情况逐渐升级。只能升级,不能降级。
偏向锁
偏向锁在进入和退出同步代码块时不需要进行CAS操作来加锁或者解锁,只需要测试一下当前对象头的Mark Word中是否存储着当前线程的偏向锁。如果测试成功。表示已经获取了锁。如果测试失败,则需要再次测试一下Mark Word中的偏向锁标志位是否设置为1(表示当前是偏向锁),如果没有设置,就采用CAS竞争锁。如果没有设置,则尝试使用CAS将对象头中的偏向锁设置为当前线程
偏向锁的撤销:偏向锁采用了一种需要竞争才会释放锁的机制。当其他线程竞争锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销需要在全局安全点执行(这个时间点没有字节码在执行)。它会首先暂停拥有偏向锁的线程,然后去检查偏向锁的线程是否还活着,如果死亡,则将锁的对象头设置为无锁状态。如果线程任然活着,那么拥有偏向锁的栈想会被执行,遍历对象的锁记录。栈中的锁记录和对象中的Mark Word要么重新偏向其他线程,要么标记为无锁状态或者标记对象不适合偏向锁。
上面的意思是,先暂停持有偏向锁的线程,尝试直接切换。如果不成功,就继续运行,并且标记对象不适合偏向锁,锁膨胀(锁升级)。
轻量级锁
线程首先在栈帧中创建用于存储锁对象对象头中的锁记录的空间,并且将Mark Word中的记录复制到所记录中去。然后使用CAS尝试将对象头中的Mark Word替换为指向锁记录的指针。如果成功,则表示当前线程获得了锁,如果失败则表示处于竞争状态,会采用CAS自旋来获取锁。
轻量级锁在解锁时,会尝试把对象头中的指针替换为Mark Word,如果成功,表示已经释放了锁。如果失败,则表示当前处于竞争条件下。锁就会膨胀为重量级锁
注意,轻量级锁会一直保持,唤醒总是发生在轻量级锁解锁时。因为加锁的时候CAS操作已经成功,而CAS失败的线程会立即锁膨胀,并且阻塞等待唤醒。
重量级锁
重量级锁在线程竞争是不会使用自旋,不会消耗CPU。缺点是线程阻塞,响应时间缓慢。使用的场景在是追求吞吐量和同步代码中执行速度的时候。
Java线程的中断
中断可以理解为一个标志位属性,它表示其他线程是否对该线程进行了中断操作。线程通过检查自身是否被中断来执行响应,线程通过isInterupt()来判断是否被中断。也可以调用静态方法Thread.interupted()来对当前线程的中断标志位进行复位。如果现场处于终结状态,那么即时该线程被中断过,也会返回false。
线程间的通信
通过volatile和synchronized关键字来保证线程之间的共享数据。
通过等待/唤醒机制来通信
notify() nitifyAll() wait() wait(long) 等待超时的时间 毫秒 wait(long, int) 精确到纳秒
生产-消费者模式 一个线程修改了一个对象的值,而另外一个线程感知到了变化,然后进行相应的操作。
Thread.join()
当线程A执行了thread.join()的方法之后表示,当线程A等待thread线程终止之后才从join方法返回。
Lock
Lock与sychronized相比,特性是
1 可以尝试非阻塞式的获取锁 tryLock()
2 能够中断式的获取锁,获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常会被抛出,同时会释放锁。 lockInterruptibly();
3 能够超时获取锁 tryLock(long time, TimiUnit unit);