时间仓促,水平有限,如有错误之处,欢迎指出,有时间我会在优化这篇文章,最近加班,工作偏多,趁着雾霾中午休息时间比较多,随笔一写。
- 为什么需要synchronized:
相信大家一定有答案,笔者依然还是要罗嗦下,首先我们通过下面这段代码,通过模拟售票方式模拟线程不安全的情况,
public class ThreadExample extends Thread { static final ConcurrentLinkedDeque<Integer> queue = new ConcurrentLinkedDeque<>(); private static final int MAX = 500; private static int init = 0; /** * 模拟售票大厅 */ @Override public void run() { while (init < MAX) { int i = init++; System.out.println(Thread.currentThread().getName() + " add--> " + i); queue.add(i); } } public static void main(String[] args) { IntStream.range(0, 100).forEach(i -> { ThreadExample threadExample = new ThreadExample(); threadExample.start(); }); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } //我们期望他的大小为500 System.out.println(queue.size()); } }
在这段代码中我们期望我们的queue的大小为500,可是笔者几次执行过后得到的结果却与期望的值有一些小出入,几次得到结果为502或者501,当然也有500
再多线程环境下,多个线程对共享资源进行操作,如果这个共享资源出现脏数据,比如说我们期望queue的大小为500,但是却得到502或者501,我们把这次的操作叫它线程不安全的操作,所以大家肯定想到了一个java关键字synchronized,笔者加上了synchronized之后,每次得到的结果都是我们期望的500,有就是说这次操作时线程安全的。
- 再说说synchronized的用法:
同步代码块和同步方法,
//同步方法
public synchronized void test3() {
try {
System.out.println("this is the public_lock SyncTest.class and thread name is " + Thread.currentThread().getName());
TimeUnit.SECONDS.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//同步代码块
public void test4() {
synchronized (this) {
try {
System.out.println("this is the public_lock this and thread name is " + Thread.currentThread().getName());
TimeUnit.SECONDS.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 使用synchronized的注意事项:
什么情况下synchronized会失效?
1.当锁引用的值为null时,我们可以看下以下代码:
private static Object NUll = null;
public static void test5() {
synchronized (NUll) {
System.out.println("this is the public_lock reffence and thread name is " + Thread.currentThread().getName());
shotSleep(30);
}
}
public static void test6() {
synchronized (NUll) {
System.out.println("this is the public_lock reffence and thread name is " + Thread.currentThread().getName());
shotSleep(30);
}
}
接下来是输出结果
我们可以看到同步代码块中的锁是不可以为空的。
2.当锁的重复,不同的锁想同步相同的方法:请看一下代码:
甚至连run方法都没有执行。不过没关系我们换一种方式
public class RunnableExample implements Runnable {
private final Object MUTEX = new Object();
@Override
public void run() {
synchronized (MUTEX) {
System.out.println(Thread.currentThread().getName() + " ==============");
shotSleep(30);
}
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
RunnableExample runnableExample = new RunnableExample();
new Thread(runnableExample).start();
}
// IntStream.range(0, 10).forEach(i -> {
// System.out.println("...........");
// new Thread(RunnableExample::new).start();
// });
}
可以看到输出结果立马打印到了控制台上,说明这些锁只锁到了自己,我们可以通过jstack命令看到,各个线程之间根本没有同步,都是自己执行自己
- 那么什么时候会死锁呢?看以下代码
- public class DeadLock {
private Object READ = new Object();
private Object WRITE = new Object();
public void read() {
synchronized (READ) {
System.out.println(Thread.currentThread().getName() + " read");
synchronized (WRITE) {
System.out.println(Thread.currentThread().getName() + " write in read");
}
}
}
public void write() {
synchronized (WRITE) {
System.out.println(Thread.currentThread().getName() + " write");
synchronized (READ) {
System.out.println(Thread.currentThread().getName() + " read in write ");
}
}
}
public static void main(String[] args) {
DeadLock deadLock = new DeadLock();
IntStream.range(0, 10).forEach(i -> {
new Thread(()->{
deadLock.write();
}).start();
new Thread(()->{
deadLock.read();
}).start();
});
}
}
我们通过jstack命令可以看到已经死锁
- 接下来我们介绍This Monitor 和class Monitor 也就是this 锁和class锁,
我们先来看下什么是Monitor,在使用synchronized关键字时,jvm会为我们提供一对指令,分别是monitor entry 和monitor exit,而这两个指令需要绑定一个monitor,当其中一个线程获取到monitor entry时,其他线程只能等待,知道执行完monitor exit,其它线程才开始重新竞争新的执行权限,当执行同步方法时,实际上monitor就是this这个对象本身,和执行同步代码块中的synchronized(this)一样,当使用同步方法时和使用同步代码块synchronized(this)可以起到同步作用,为了验证我们的结论可以看执行下以下代码:
public synchronized void test3() { try { System.out.println("this is the public_lock SyncTest.class and thread name is " + Thread.currentThread().getName()); TimeUnit.SECONDS.sleep(30); } catch (InterruptedException e) { e.printStackTrace(); } } 同步代码块使用this,可以同步 public void test4() { synchronized (this) { try { System.out.println("this is the public_lock this and thread name is " + Thread.currentThread().getName()); TimeUnit.SECONDS.sleep(30); } catch (InterruptedException e) { e.printStackTrace(); } } }
通过jstack命令可以看到,除了线程获取到cpu执行权,其他线程都在等待中,并且等待的都是同一个monitor
所以一个类中两个同步方法,当执行其中的一个同步方法时,另一个同步方法只能等待,直到执行完成。那么请大家思考this和XXX.Class 会相互等待,互相排斥吗?
带着这个疑问,我们执行以下代码:
public void test7() { synchronized (this) { System.out.println("test7 and thread name is " + Thread.currentThread().getName()); shotSleep(30); } } public void test8() { synchronized (SyncTest.class) { System.out.println("test8 and thread name is " + Thread.currentThread().getName()); shotSleep(30); } }
执行完这段代码后,通过jstack命令可以看到,线程所绑定的monitor分别属于locked <0x00000000d6628790>和lock <0x00000000d6625c30>,所以这两种monitor,不会互斥,也不会相互等待
所以这就是我们常说的对象和this的区别,他们所属于不同的monitor,但是还有个神奇的地方在于,我们把上面同步方法和同步代码块(this)的代码将同步方法改为static会有什么情况变化呢,各位小伙伴们不妨试一试,码字不易,
接下来我们说一说,jvm对synchronized的优化,在很久之前,大概是在1.4或者1.5之前synchronized关键字效率堪忧,但是经过几次优化之后,效率已经大大提升,那么jvm对synchronized提升了那些方面呢?他们分别是
自旋锁
锁消除
锁粗化
轻量级锁
偏向锁
Java锁使用的锁其实是在对象头的中锁标识,下面是锁在对象头中的结构
以及锁的状态
自旋锁:
所谓自旋就是说,线程频繁的从阻塞到唤醒这段时间,可能会非常耗时,那么自旋锁就会让线程等待一段时间,执行一段无意义的代码(通常是无意义的循环)
锁消除:就是指有的时候为了保证数据的完整性,我们通常要对这部分代码执行同步控制,但是在某些情况下jvm检测到不存在竞争条件,jvm会将这段同步代码中消除锁
锁粗化:
有些时候需要让同步代码的作用范围要尽可能小点,但是有些情况我们需要对多个小部位的加锁替换成整体加锁
轻量级锁:
引入轻量级锁的主要目的是在多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁,其步骤如下:
获取锁
判断当前对象是否处于无锁状态(hashcode、0、01),若是,则JVM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方把这份拷贝加了一个Displaced前缀,即Displaced Mark Word);否则执行步骤(3);
JVM利用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指正,如果成功表示竞争到锁,则将锁标志位变成00(表示此对象处于轻量级锁状态),执行同步操作;如果失败则执行步骤(3);
判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀为重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态;
偏向锁
引入偏向锁主要目的是:为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径。上面提到了轻量级锁的加锁解锁操作是需要依赖多次CAS原子指令的
重量级锁:
指没有优化过的同步方法或者同步代码块
参考资料
Java高并发详解 汪文君著
以及网上一些资料
但是如果把this,换成对象锁还会同步吗?我们可以看到很多线程已经在执行,在等待执行的线程也没有等待同一把锁,说明同步代码块中使用对象锁(SyncTest.class),并没有起到同步的作用,
但是如果换成静态方法和对象锁比较呢:我们可以看到这个时候同步代码块已经和同步方法互斥了,