参考书籍:《深入理解java虚拟机》
- 0 jvm线程模型

- 1 synchronized是什么?有什么用?
synchronized是java中的关键字,表示对资源进行同步。作用是防止多个线程对共享资源同时操作时导致数据不一致。
- 2 线程同步问题产生的根本原因是jvm内存模型
处理器速度和内存读取速度相差太大,于是在处理器和内存中加入了高速缓存,这样一来多处理器就会造成内存数据同步的问题,内存数据同步和jvm线程数据同步类似。

在jvm中可共享的数据存放于堆和方法区,虚拟机栈在执行方法的时候要把共享数据读入到栈帧的局部变量表中,也就是共享数据的拷贝,这样会造成每个虚拟机栈中都有一份共享数据的拷贝,而且可能会导致数据不一致。
堆对应主内存,线程的虚拟机栈栈帧的局部变量表对应高速缓存这只是逻辑上的对应,并不是一个层次的划分。

主内存和工作内存之间的交互,jvm定义了4对8个原子性操作:
lock、unlock:作用于主内存的变量,将变量标识为线程独占的状态;解锁。unlock操作之前必须把变量同步会主内存中,也就是最新store、write操作
read、load:主内存变量传递到线程工作内存;将read到的值放到工作内存的变量副本。
use、assign:将变量传递给执行引擎;把执行引擎返回的值赋值给工作内存的变量副本;
store、write:将工作内存的变量副本传递到主内存;将store获得的值保存到主内存的变量。
插一段volatile的东西:
volatile的设计:(1)read到write的操作必须是一连串的动作,保证了取最新的值和立马刷新到主内存 (2)禁止指令重排序
禁止重排序的原理:动作C之前必然发生动作A、B,动作D之后必然发生动作E、F,如果C优先于D则整个执行顺序是A-B-C-D-E-F。我认为volatile的设计本质在于:如果一个变量马上被使用但是没有被load过那样是禁止的。由于jdk1.6后对synchronized进行优化,几乎没有volatile的用处了。
happen-before原则:A操作先于B操作,那么B操作能够观察到A操作产生的影响,这些影响包括变量的改变等等。
- 3 synchronized的设计思路
2.1 synchronized汇编代码:通过javap命令看下synchronized的汇编代码,synchronized会对应生成一对monitorenter和monitorexit(看到有两个monitorexit,一个是正常退出,一个是异常退出)
package indi.wangx.java.thread;
public class SynchronizedTest {
public static int a = 1;
public void testSynchronized() {
synchronized(this) {
a++;
}
}
}

2.2 jvm是如何实现monitorener和monitorexit的呢?
jvm通过8个原子性操作中的lock和unlock进行操作。
java.util.concurrency中的ReentrantLock在synchronized上做了3方面的优化:
(1)等待可中断 (2)公平锁:先排队的先获得锁 (3)锁定多个Condition对象
3 实现线程安全的方法
(1)互斥同步:例如使用synchronized关键字
(2)非阻塞同步:先进行操作共享数据,如没有其他线程使用共享数据,直接操作;如果有其他线程使用共享数据进行其他不长措施(比如不停地重试直到成功为止)
非阻塞同步靠硬件指令完成,有一个实例可以看下,就是原子类AutomicInteger

这段代码的核心在于不停的compareAndSet,把一个比自己大1的值设置给自己,直到能够设置成功的时候返回。
4 锁优化
(1)锁自旋:大部分线程持有锁的时间很短,于是在线程同步互斥时不切换到等待状态,切换到等待状态需要转到内核态,这非常耗费时间,所以就让线程进行一段自旋操作。而且jdk1.6之后引入了自适应的自旋锁,自旋时间由钱一次同一个锁的自旋时间和锁的拥有者状态决定。
(2)编译器进行锁消除:编译器把无用的锁进行消除
(3)锁粗化和锁细化:加锁的范围太小导致重复加锁,加锁的范围太大导致不应该加锁的地方也加了锁
(4)偏向锁:在无竞争下消除整个同步,只在第一次使用CAS操作获取锁,并在Mark Word中存储线程ID,持有偏向锁的线程每次进入这个锁相关的同步块,虚拟机不进行任何同步操作。
(5)轻量级锁:本质上跟锁自旋是一个道理,采用CAS操操作避免了将其他线程变为等待状态。对于绝大部分的锁,整个同步周期中并没有竞争。如果有2个以上的线程轻量级锁会转变为重量级锁,Mark Word的标志位变为10。
hotspot对象头内存布局:
1 hotspot对象头分为2部分:
(1)对象自身运行时数据:HashCode、GC分代年龄、锁标记等,这部分称为Mark Word
(2)存放指向方法区对象类型数据的指针


轻量级锁加锁的过程如下图:



jvm中的锁分为四种状态:无锁、偏向锁、轻量级锁、重量级锁,有这几种锁的原因是避免线程的阻塞和唤醒,线程的阻塞和唤醒需要切换到内核态,非常耗时
偏向锁的执行过程:本质上就是有锁但无竞争。看偏向标志,如果是"01"则表示当前为偏向模式,用CAS操作线程ID记录到Mark Word中。如果有二个线程尝试获取锁的时候,撤销偏向锁。
轻量级锁的执行过程:本质上是只有2个线程的轻度竞争,虚拟机栈栈帧建立一个Lock Record的空间,存放Mark Word的拷贝,CAS操作尝试将Mark Word更新为指向指向线程虚拟机栈栈帧的Lock Record。
如果两条以上的线程竞争一个锁就转变为重量级锁,意味着其它线程需要进行wait。
以上是jvm锁内存变化,下面这种图是对象持有的ObjectMonitor

5 死锁
package indi.wangx.java.thread;
import org.junit.Test;
public class DeadLockTest {
public Object a = new Object();
public Object b = new Object();
@Test
public void testDeadLock() {
new Thread(new LockA()).start();
new Thread(new LockB()).start();
}
public class LockA implements Runnable{
@Override
public void run() {
synchronized(a) {
System.out.println("LockA get a");
try {
Thread.sleep(1000*1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(b) {
System.out.println("LockA get b");
}
}
}
}
public class LockB implements Runnable{
@Override
public void run() {
synchronized(b) {
System.out.println("LockB get b");
try {
Thread.sleep(1000*1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(a) {
System.out.println("LockB get a");
}
}
}
}
}
用jstack和jConsloe命令可以看到造成死锁的线程。
如何避免死锁?
1 如果要获取多个锁的时候,考虑获取锁的顺序,保证不会出现循环等待
2 超时放弃,用Lock接口提供的tryLock
如何确保多个线程顺序执行?
1使用join接口
2使用单个线程池
4397

被折叠的 条评论
为什么被折叠?



