四、多线程并发学习---线程安全问题

**

线程安全问题:

如下是一个简单的序列生成器,在单线程情况下调用完全没有问题。

public class Sequence {

	private int value;

	public int getNext() {
		return value++;
	}

	public static void main(String[] args) {
		Sequence s = new Sequence();
		for (int i = 0; i < 100; i++) {
			System.out.println(s.getNext());
		}
}

接下来使用使用3个线程调用:

public class Sequence {

	private int value;

	public int getNext() {
		return value++;
	}

	public static void main(String[] args) {

		Sequence s = new Sequence();
		new Thread(new Runnable() {
			@Override
			public void run() {
				for (int i = 0; i < 50; i++) {
					System.out.println(Thread.currentThread().getName()+",value="+s.getNext());
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		}).start();
		
		new Thread(new Runnable() {
			@Override
			public void run() {
				for (int i = 0; i < 50; i++) {
					System.out.println(Thread.currentThread().getName()+",value="+s.getNext());
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		}).start();
		
		new Thread(new Runnable() {
			@Override
			public void run() {
				for (int i = 0; i < 50; i++) {
					System.out.println(Thread.currentThread().getName()+",value="+s.getNext());
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		}).start();
	}
}
输出结果:
Thread-0,value=0
Thread-1,value=1
Thread-2,value=2
Thread-0,value=3
Thread-2,value=4
Thread-1,value=4
Thread-2,value=6
Thread-0,value=6
Thread-1,value=5
Thread-0,value=7
Thread-1,value=8
Thread-2,value=9
Thread-2,value=10
Thread-1,value=10
Thread-0,value=10
Thread-1,value=11
Thread-2,value=12
Thread-0,value=13
Thread-1,value=14
Thread-2,value=16
Thread-0,value=15
Thread-1,value=17
Thread-0,value=18
Thread-2,value=18
Thread-1,value=19
Thread-2,value=19
Thread-0,value=19
Thread-0,value=21
Thread-1,value=22
。。。。。。。。。

从输出结果可以看出原本单线程环境下正常输出的程序,在多线程情况下出现了错误的输出,这里就出现了线程安全问题。原因是出在value++这里。
分析:
1、private int value定义的value是全局变量,位于主内存中。而getNext()方法中的value是位于栈内存中的变量,是每个线程私有的,它的值是从主内存load过来的,线程私有的变量修改后需要刷新到主内存。
2、通过javap反编译后,我们可以看到value++其实是分两步的的,第一步执行value+1,第二步把上一步的结果再赋值给value。
四、多线程并发学习---线程安全问题
3、我们假设Thread-0先执行,从主内存加载value到Thread-0的线程栈,这时value=0,执行value +1 ,这时cpu执行权被Thread-1抢走了,Thread-0的程序计数器记录下执行的行号。Thread-1也从主内存加载value=0到自己的线程栈中,执行value+1,这时cpu执行权被Thread-2抢走了,Thread-1的程序计数器记录下执行的行号。Thread-2也执行上述步骤,加载value=0到线程栈中,执行value+1, 把执行后的结果赋值给value,执行return返回,打印输出1,sleep。这时Thread-0和Thread-1又分别获得cpu执行权,恢复到之前的断点执行执行,分别打印value的值1和1,于是就出现在上述重复打印的线程安全问题。

四、多线程并发学习---线程安全问题
出现线程安全问题的原因:
1、多线程共享一个资源
2、对资源进行非原子性操作

解决方法:
在getNext()方法上加synchronized关键字即可。
这时加入Thread-0先执行了getNext()方法,由于加上锁,其余线程无法进入,当Thread-0执行完毕后将value修改后的值刷新到了主内存,释放锁,后续线程再进入getNext()后从主内存中加载的value就是最新的value了,就不会出现重复问题。

synchronized锁的锁对象
synchronized锁用于修改方法或者代码块,进行加锁,被修改的方法或代码块一旦被某个线程持有,在该线程释放锁之前,别的线程无法获取锁进入方法中。
1、synchronized修饰实例方法,锁对象是当前类的实例对象
2、synchronized修饰静态方法,锁对象是当前类的字节码对象
3、synchronized修饰实例方法中的代码块时,锁对象是任意对象。
4、synchronized修饰静态方法中的代码块时,锁对象是当前类的字节码对象。

synchronized锁的原理:
首先我们加锁getNext(),采用同步代码块方式。

	public int getNext() {
		int v;
		synchronized (this) {
			v = value++;
		}
		return v;
	}

反编译后查看getNext()方法,可以看到monitorenter和monitorexit,这里就是对应着synchronized的加锁和释放锁。
四、多线程并发学习---线程安全问题
关于这两条指令的作用,接参考JVM规范中描述:

monitorenter :
Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
• If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
• If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
• If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.

这段话的大概意思为:

每个对象有一个监视器锁(monitor)(存在于对象头信息mark word中)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。

2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1(锁重入)。

3、如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

monitorexit: 
The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

这段话的大概意思为:

执行monitorexit的线程必须是objectref所对应的monitor的所有者(释放锁和加锁要是同一个线程)。

指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

以上是同步代码块,再看下同步方法的synchronized的反编译结果:

public synchronized int getNext() {
		return value++;
	}

四、多线程并发学习---线程安全问题

从反编译的结果来看,方法的同步并没有通过指令monitorenter和monitorexit来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。

**

其他锁:

锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。

**
对象头:
任何对象都可以作为锁,锁信息存在于对象的什么中呢?
锁信息存在于对象头的mark word中。对象头信息包括:
Mark word
Class Matadata Address # 对象类型地址
Array Length #数组长度,当对象为数组时

*Mark word 中包含有线程id、Epoch、对象分代年龄、是否是偏向锁、锁标志位。

高性能锁;
synchronized是一种重量级锁(一个线程持有锁后,其余线程都要等待释放锁),效率比较低,jdk1.6后续引入几个性能高的锁:偏向锁、轻量级锁,重量级锁。

偏向锁:锁的获得和释放会浪费大量资源。很多情况下竞争锁不是有多个线程,而是由单个线程竞争,如果还是像多线程一样的加锁、释放,会效率很低。这时我们使用偏向锁,mark word中记录下线程id,锁标志位为是偏向锁。线程第一次进入时需要获取偏向锁,执行结束后,不会释放锁。下是否个线程进入时,会检查锁的对象头信息:是否是偏向锁,线程id是否和上一个加锁的线程相同。如果是偏向锁,且请求的线程id和锁对象头中记录相同,则可以直接进入。否则,有其他线程来竞争锁时,将释放之前的偏向锁,重新获得锁。这样就可以大大提高效率。(适用于只有一个线程访问同步代码块情况,其他情况反而会降低效率)。

轻量级锁
如果说偏向锁是只允许一个线程获得锁,那么轻量级锁就是允许多个线程获得锁,但是只允许他们顺序拿锁,不允许出现竞争,也就是拿锁失败的情况,轻量级锁的步骤如下:
1)线程1在执行同步代码块之前,JVM会先在当前线程的栈帧中创建一个空间用来存储锁记录,然后再把对象头中的Mark Word复制到该锁记录中,然后线程尝试使用CAS将对象头中的Mark Word 替换为指向锁记录的指针。如果成功,则获得锁,进入步骤3)。如果失败执行步骤2)
2)线程自旋,自旋成功则获得锁,进入步骤3。自旋失败,则膨胀成为重量级锁,线程阻塞进入步骤3)
3)锁的持有线程执行同步代码,执行完CAS替换Mark Word成功释放锁,如果CAS成功则流程结束,CAS失败执行步骤4)。