提起关键字synchronized,想必大家都不陌生,或许大家在代码中很少用到这个关键字,毕竟单例模式已经封装,多线程并发问题的话大家一般使用分布式锁,因为关于资源的抢占都是针对数据库或者其他共有资源的,而且现在的部署都是分布式的,所以在平时代码中synchronized的作用显得少了许多。
不过,大家还是会经常看到这个字段,比如,我们知道ArrayList是线程不安全的,那Vector为什么是线程安全的,哦,原来是加了synchronized锁,防止并发增删,hashMap是线程不安全的,并发而且发生hash碰撞的时候,几乎等同于操作一个linkedList,新增的时候也有可能出问题,那ConcurrentHashMap为什么是线程安全的,奥,原来是通过CAS操作和使用synchronized锁槽,细化颗粒度锁,同时避免了HashMap可能会出现的问题。
好了,我们来一起看下synchronized这个关键字吧。
synchronized一般来说,就是锁方法,静态方法以及代码块,还有些地方说会锁类,其实总的来说就两种,类级锁和方法级别的锁。我们来看下代码:
1、锁同步代码块和锁对象。
public class SyncThread implements Runnable {
private Integer number = 0;
public void method() throws Exception{
synchronized (this) {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+"-synchronized锁同步代码块:"+number++);
Thread.sleep(10);
}
}
}
@Override
public void run() {
try {
method();
} catch (Exception e) {
}
}
}
我们来运行下
public static void main(String[] args) {
SyncThread syncThread = new SyncThread();
Thread thread1 = new Thread(syncThread, "SyncThread1");
Thread thread2 = new Thread(syncThread, "SyncThread2");
thread1.start();
thread2.start();
}
我们创建了一个SyncThread 对象,然后通过SyncThread 创建了两个线程,然后启动,也就是说两个线程都会去操作同一个number,当然先抢到的会加锁,然后执行完毕,下一个线程继续执行,那么结果就是:
SyncThread1-synchronized锁同步代码块:0
SyncThread1-synchronized锁同步代码块:1
SyncThread1-synchronized锁同步代码块:2
SyncThread1-synchronized锁同步代码块:3
SyncThread1-synchronized锁同步代码块:4
SyncThread2-synchronized锁同步代码块:5
SyncThread2-synchronized锁同步代码块:6
SyncThread2-synchronized锁同步代码块:7
SyncThread2-synchronized锁同步代码块:8
SyncThread2-synchronized锁同步代码块:9
有些伙伴会诧异,怎么不是线程一线程二交替执行累加啊,他们操作的是同一个实现runnable的方法,相当于进入方法之后在synchronized这里就会有一个线程阻塞,哪来的交替执行?
当然希望交替执行的话也可以,创建两个SyncThread就是了。
Thread thread1 = new Thread(new SyncThread(), "SyncThread1");
Thread thread2 = new Thread(new SyncThread(), "SyncThread2");
thread1.start();
thread2.start();
一运行,铛铛。。
SyncThread1-synchronized锁同步代码块:0
SyncThread2-synchronized锁同步代码块:0
SyncThread2-synchronized锁同步代码块:1
SyncThread1-synchronized锁同步代码块:1
SyncThread2-synchronized锁同步代码块:2
SyncThread1-synchronized锁同步代码块:2
SyncThread2-synchronized锁同步代码块:3
SyncThread1-synchronized锁同步代码块:3
SyncThread2-synchronized锁同步代码块:4
SyncThread1-synchronized锁同步代码块:4
发现线程一和线程二各干各的,本来就是两个类么,加锁锁的又是方法,变量也是普通的变量,两个对象并没有竞争关系啊,注意,是对象。那怎么让它们有竞争关系呢?那就是吧锁升级到类锁,最简单的就是number加上static,这就相当于和对象绑定了,所有的类都是对这一个number操作。当然这个时候,我们还继续只是做代码块的话,肯定会有并发问产生的,锁肯定要上升到类锁或者说是静态对象的锁,我们看下代码:
private static Integer number = 0;
public void method() throws Exception{
synchronized (number) {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+"-synchronized锁同步代码块:"+number++);
Thread.sleep(10);
}
}
}
然后在运行两个对象的那种测试方法:
SyncThread1-synchronized锁同步代码块:0
SyncThread2-synchronized锁同步代码块:1
SyncThread2-synchronized锁同步代码块:2
SyncThread1-synchronized锁同步代码块:3
SyncThread2-synchronized锁同步代码块:4
SyncThread1-synchronized锁同步代码块:5
SyncThread2-synchronized锁同步代码块:6
SyncThread2-synchronized锁同步代码块:7
SyncThread1-synchronized锁同步代码块:8
SyncThread1-synchronized锁同步代码块:9
Process finished with exit code 0
如果还是synchronized (this)的话,会有并发问题,大家可以自己试下。
2、锁方法
public synchronized void method() throws Exception {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "-synchronized锁同步代码块:" + number++);
Thread.sleep(10);
}
}
我们看下运行结果:
SyncThread1-synchronized锁同步代码块:0
SyncThread2-synchronized锁同步代码块:1
SyncThread2-synchronized锁同步代码块:2
SyncThread1-synchronized锁同步代码块:2
SyncThread1-synchronized锁同步代码块:3
SyncThread2-synchronized锁同步代码块:4
SyncThread1-synchronized锁同步代码块:5
SyncThread2-synchronized锁同步代码块:6
SyncThread1-synchronized锁同步代码块:7
SyncThread2-synchronized锁同步代码块:8
Process finished with exit code 0
这个时候可以看到,锁方法和锁同步代码块其实是一个级别的,都是对方法中的循环进行了加锁,共同抢占资源number。
我们看下静态方法呢?
public static synchronized void method() throws Exception {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "-synchronized锁同步代码块:" + number++);
Thread.sleep(10);
}
}
静态方法明显是类级别的锁,大家可以看下运行结果:
SyncThread1-synchronized锁同步代码块:0
SyncThread1-synchronized锁同步代码块:1
SyncThread1-synchronized锁同步代码块:2
SyncThread1-synchronized锁同步代码块:3
SyncThread1-synchronized锁同步代码块:4
SyncThread2-synchronized锁同步代码块:5
SyncThread2-synchronized锁同步代码块:6
SyncThread2-synchronized锁同步代码块:7
SyncThread2-synchronized锁同步代码块:8
SyncThread2-synchronized锁同步代码块:9
Process finished with exit code 0
一旦线程一占有了资源,那么这个线程会一直等待这个方法运行完毕,然后才会释放资源,它锁的其实就是这个类。
当然,这样写也是一个效果。
public synchronized void method() throws Exception {
synchronized (SyncThread.class) {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "-synchronized锁同步代码块:" + number++);
Thread.sleep(10);
}
}
}
好了,synchronized锁我们已经大致看明白了,那么synchronized是怎么实现锁机制的呢?
我们都知道,对象被创建在堆中。并且对象在内存中的存储布局方式可以分为3块区域:对象头、实例数据、对齐填充。其中,对象头中就包含锁的信息。下面就是对象头自身运行的数据。
存储内容 | 标志位 | 状态 |
对象的哈希码、对象的分代年龄 | 01 | 未锁定 |
指向锁记录的指针 | 00 | 轻量级锁定 |
指向重量级锁的指针 | 10 | 重量级锁定 |
空 | 11 | GC标记 |
偏向线程ID,偏向时间戳,分代年龄 | 01 | 可偏向 |
当然,还有一部分是类型指针,JVM通过这个指针来确定这个对象是哪个类的实例。
synchronized锁其实就是一个乐观+悲观混合的锁,先进行乐观锁处理,失败的话就使用悲观锁,悲观锁,也就是重量级锁,是通过monitor(监视器)来实现。synchronized的对象锁,其指针指向的是一个monitor对象的地址,每个对象都会有一个monitor,其中monitor可以和对象一起创建、销毁,也可以在线程试图获取锁的时候自动生成。monitor是由C++实现的,这个有兴趣的可以去探索下,我这里就不多说了。
我们将上面锁定同步代码块的那个方法编译后查看(javap).我们可以看到monitorenter和monitorexit,其实就是锁定和释放锁。在执行monitorenter指令时,首先要尝试获取对象锁,也就是monitor对象,如果这个对象没有被锁定,或者当前线程已经拥有了这个对象的锁,那么我们就把锁的数值加1(所以synchronized是可重入的),当然,释放的时候monitor就会减1。
好了,synchronized就讲到这里,那里有不对的,大家可以在评论中指出,不胜感激!