每个java对象都隐含有一把锁,这里称为java内置锁,使用synchronized关键字相当于获取了一把内置锁,对临界区进行排他性保护.
synchronized的使用
1:一把锁只能被一个线程获取,没有获得锁的线程只能等待.
2:每个实例都有自己的一把锁(this),不同实例互不影响.Class实例和synchronized 修饰的static方法用的是同一把锁.
3:synchronized修饰的方法不管正常执行完毕还是抛出了异常都会释放锁.
synchronized同步方法
public class SynchronizedTest01 implements Runnable {
static SynchronizedTest01 lock = new SynchronizedTest01();
@Override
public void run() {
synchronizedMethod();
}
private synchronized void synchronizedMethod() {
System.out.println("线程" + Thread.currentThread().getName()
+ "抢到锁了");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程" + Thread.currentThread().getName()
+ "结束了");
}
public static void main(String[] args) {
Thread thread1 = new Thread(lock,"线程-1");
Thread thread2 = new Thread(lock,"线程-2");
thread1.start();
thread2.start();
}
}
从运行结果可以看出来,线程一和线程二是有序执行方法内的代码.
synchronized同步块
public class SynchronizedTest02 implements Runnable {
private static int sum1 = 0;
private static int sum2 = 0;
private static SynchronizedTest02 lock1 = new SynchronizedTest02();
private static SynchronizedTest02 lock2 = new SynchronizedTest02();
private void synchronizedMethod() {
synchronized (this) {
System.out.println("线程" + Thread.currentThread().getName()
+ "进入了lock0");
sum1 = ++sum1;
System.out.println(sum1);
}
synchronized (lock1) {
System.out.println("线程" + Thread.currentThread().getName()
+ "进入了lock1");
sum1 = ++sum1;
System.out.println(sum1);
}
synchronized (lock2) {
System.out.println("线程" + Thread.currentThread().getName()
+ "进入了lock2");
sum2 = ++sum2;
System.out.println(sum2);
}
}
@Override
public void run() {
synchronizedMethod();
}
public static void main(String[] args) {
Thread thread1 = new Thread(lock1,"线程-1");
Thread thread2 = new Thread(lock2,"线程-2");
thread1.start();
thread2.start();
}
}
synchronized修饰静态的同步方法
public class SynchronizedTest03 implements Runnable{
private static SynchronizedTest03 lock1 = new SynchronizedTest03();
private static SynchronizedTest03 lock2 = new SynchronizedTest03();
@Override
public void run() {
synchronizedMethod();
}
private synchronized static void synchronizedMethod() {
System.out.println("线程" + Thread.currentThread().getName()
+ "抢到锁了");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程" + Thread.currentThread().getName()
+ "结束了");
}
public static void main(String[] args) {
Thread thread1 = new Thread(lock1,"线程-1");
Thread thread2 = new Thread(lock2,"线程-2");
thread1.start();
thread2.start();
}
}
从这个例子可以看出来,虽然不是同一个实例,但是被synchronized修饰的static方法抢的是Class实例的锁,只能有一个线程拥有锁,粒度也是比较大的.
synchronized以Class对象为锁
public class SynchronizedTest03 implements Runnable {
private static SynchronizedTest03 lock1 = new SynchronizedTest03();
private static SynchronizedTest03 lock2 = new SynchronizedTest03();
@Override
public void run() {
synchronizedMethod2();
}
private static void synchronizedMethod2() {
synchronized (SynchronizedTest03.class) {
System.out.println("线程" + Thread.currentThread().getName()
+ "抢到锁了");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程" + Thread.currentThread().getName()
+ "结束了");
}
}
public static void main(String[] args) {
Thread thread1 = new Thread(lock1, "线程-1");
Thread thread2 = new Thread(lock2, "线程-2");
thread1.start();
thread2.start();
}
}
这个例子以Class对象为锁,只能有一个线程可以获取锁,粒度也是比较大的.
JVM内置锁的分类 无锁 偏向锁 轻量级锁 重量级锁
在JDK1.6之前,所有的java内置锁都是重量级锁.重量级锁会造成CPU在用户态和核心态的切换,对性能的消耗比较高.为了减少获取锁和释放锁带来的消耗.在1.6引入了偏向锁和轻量级锁.所以总共有四种状态,无锁 偏向锁 轻量级锁 和重量级锁.
1:无锁状态
java对象刚刚创建时还没有任何线程来竞争,说明该对象处于无锁状态(无线程竞争),偏向锁的标志位为0,锁状态为01.
2:偏向锁状态
偏向锁是指一段同步代码一直被一个线程访问,那么该线程会自动获取锁,降低每次获取锁的代价.如果内置锁处于一个偏向状态,当有一个线程来竞争这个锁的时候,先用偏向锁,表示内置锁偏向这个线程,这个线程来执行同步块代码的时候不需要进行任何检查和判断.所以偏向锁在没有线程竞争的时候效率很高.
3:轻量级锁状态
当有两个线程来竞争锁的时候,不在是偏向了,锁会升级为轻量级锁,两个线程公平竞争,哪个线程先抢到锁,锁对象的Mark Word就会指向哪个线程的栈帧中的锁记录.另一个线程就会不断通过自旋来尝试获取锁而不是进行阻塞.通过避免内核态和用户态的切换提高性能.
4:重量级锁状态
重量级锁会让其他申请线程进入阻塞状态,性能降低,重量级锁也叫同步锁,这个锁对象的Mark Word会再次发生变化,会指向一个监视器对象,该监视器对象用集合的形式来登记管理和安排排队线程.
锁状态的变化过程
锁膨胀方向:无锁→偏向锁→轻量级锁→重量级锁(此过程是不可逆的).
通过变化过程理解锁优化
无锁→偏向锁
通过经验总结可以得到,大多数情况下代码块都是同一个线程来执行,所以引入了偏向锁,同一个线程来获取锁时,只需要第一次需要进行抢锁操作,后面再来执行的时候,不需要再次进行判断,大大提高了获取锁的效率.JVM启动的时候,默认五秒后才会开启偏向锁.(后续会整理例子.)
偏向锁→轻量级锁
当有第二个线程来抢锁的时候,就会从无锁升级为偏向锁.但是共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和回复阻塞线程并不值得,所以引入了自旋和自适应自旋.
自旋锁早在JDK1.4 中就引入了,只是当时默认时关闭的。在JDK 1.6后默认为开启状态。自旋锁本质上与阻塞并不相同,如果锁占用的时间非常的短,那么自旋锁的性能会非常的好,相反,其会带来更多的性能开销(因为在线程自旋时,始终会占用CPU的时间片,如果锁占用的时间太长,那么自旋的线程会白白消耗掉CPU资源)。因此自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数仍然没有成功获取到锁,就应该使用传统的方式去挂起线程了,在JDK定义中,自旋锁默认的自旋次数为10次,用户可以使用参数-XX:PreBlockSpin
来更改。
那么通过上面的理解,又有了新问题,如果自旋刚结束,就释放了锁,就有点得不偿失.所以又引入了自适应自旋,会根据当前线程上一次是否获取过锁,来适当的增加自旋次数,或者适当的减少自旋次数.
锁消除
锁消除是指虚拟机即时编译在运行时,对一些代码要求同步,但是检测到不可能存在数据共享的竞争,就会消除锁.提高性能.锁消除的主要判定依据来源于逃逸分析的数据支持。意思就是:JVM会判断再一段程序中的同步明显不会逃逸出去从而被其他线程访问到,那JVM就把它们当作栈上数据对待,认为这些数据是线程独有的,不需要加同步。此时就会进行锁消除。
锁粗化
进行加锁的时候,我们都会尽量控制锁的粒度尽可能小.这样是为了使得需要同步的操作数量尽可能变小。在存在锁同步竞争中,也可以使得等待锁的线程尽早的拿到锁. 大部分上述情况是完美正确的,但是如果存在连串的一系列操作都对同一个对象反复加锁和解锁,甚至加锁操作时出现在循环体中的,那即使没有线程竞争,频繁的进行互斥同步操作也会导致不必要的性能操作。所以会进行锁的粗化,简单理解就是多次加锁,释放锁转换成一次.用于提高性能.