关键字:synchronized详解

429 阅读6分钟

这是我参与8月更文挑战的第13天,活动详情查看:8月更文挑战

往期推荐

一、Synchronized详解

synchronized是Java中的一个关键字,在多线程共同操作共享资源的情况下,可以保证在同一时刻只有一个线程可以对共享资源进行操作,从而实现共享资源的线程安全。

二、Synchronized的特性

  1. 原子性。synchronized可以确保多线程下对共享资源的互斥访问,被synchronized作用的代码可以实现原子性。
  2. 可见性。synchronized保证对共享资源的修改能够及时被看见。在Java内存模型中,对一个共享变量操作后进行释放锁即进行unlock操作前,必须将修改同步到主内存中。如果对一个共享资源进行加锁即lock操作之前,必须将工作内存中共享变量的值清空(因为每一个线程获取的共享变量都是主存中共享变量的一个副本,如果不进行清空,就会发生数据不一致,即当前线程中的共享变量与主存中的共享变量不一致),在使用此共享变量时,就需要从主存中重新加载此共享变量以获得该共享变量最新的值。
  3. 有序性。synchronized可以有效解决重排序问题,即一个unlock解锁操作必定先行发生于后面线程对同一个锁的lock操作,这样就会保证主内存值的共享变量永远是最新的。

三、Synchronized的使用

在应用Sychronized关键字时需要把握如下注意点:

  • 一把锁只能同时被一个线程获取,没有获得锁的线程只能等待;
  • 每个实例都对应有自己的一把锁(this),不同实例之间互不影响;例外:锁对象是*.class以及synchronized修饰的是static方法的时候,所有对象公用同一把锁;
  • synchronized修饰的方法,无论方法正常执行完毕还是抛出异常,都会释放锁。

对象锁

包括方法锁(默认锁对象为this,当前实例对象)和同步代码块锁(自己指定锁对象锁) 155.png

代码块形式:手动指定锁定对象,也可是是this,也可以是自定义的锁

public class SynchronizedObjectLock implements Runnable {
    static SynchronizedObjectLock instence = new SynchronizedObjectLock();
    // 创建2把锁
    Object block1 = new Object();
    Object block2 = new Object();

    @Override
    public void run() {
        // 这个代码块使用的是第一把锁,当他释放后,后面的代码块由于使用的是第二把锁,因此可以马上执行
        synchronized (block1) {
            System.out.println("block1锁,我是线程" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("block1锁,"+Thread.currentThread().getName() + "结束");
        }

        synchronized (block2) {
            System.out.println("block2锁,我是线程" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("block2锁,"+Thread.currentThread().getName() + "结束");
        }
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(instence);
        Thread t2 = new Thread(instence);
        t1.start();
        t2.start();
    }
}

输出结果:

block1锁,我是线程Thread-0
block1锁,Thread-0结束
block2锁,我是线程Thread-0  // 可以看到当第一个线程在执行完第一段同步代码块之后,第二个同步代码块可以马上得到执行,因为他们使用的锁不是同一把
block1锁,我是线程Thread-1
block2锁,Thread-0结束
block1锁,Thread-1结束
block2锁,我是线程Thread-1
block2锁,Thread-1结束

方法锁形式:synchronized修饰普通方法,锁对象默认为this

public class SynchronizedObjectLock implements Runnable {
    static SynchronizedObjectLock instence = new SynchronizedObjectLock();

    @Override
    public void run() {
        method();
    }

    public synchronized void method() {
        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 t1 = new Thread(instence);
        Thread t2 = new Thread(instence);
        t1.start();
        t2.start();
    }
}

类锁

包括方法锁(默认锁对象为this,当前实例对象)和同步代码块锁(自己指定锁对象)

synchronize修饰静态方法(类的class对象)

public class SynchronizedObjectLock implements Runnable {
    static SynchronizedObjectLock instence1 = new SynchronizedObjectLock();
    static SynchronizedObjectLock instence2 = new SynchronizedObjectLock();

    @Override
    public void run() {
        method();
    }

    // synchronized用在静态方法上,默认的锁就是当前所在的Class类,所以无论是哪个线程访问它,需要的锁都只有一把
    public static synchronized void method() {
        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 t1 = new Thread(instence1);
        Thread t2 = new Thread(instence2);
        t1.start();
        t2.start();
    }
}

输出结果:

我是线程Thread-0
Thread-0结束
我是线程Thread-1
Thread-1结束

synchronized修改实例方法

/**
 * synchronized修饰实例方法,当线程拿到锁,其他线程无法拿到该对象的锁,那么其他线程就无法访问该对象的其他同步方法
 * 但是可以访问该对象的其他非synchronized方法
 * 锁住的是类的实例对象
 */
public class synchronizedDemo1 implements Runnable  {
    //模拟一个共享数据
    private static int total=0;

    //同步方法,每个线程获取到锁之后,执行5次累加操作
    public synchronized void increase(){
        for (int i = 1; i < 6; i++) {
            System.out.println(Thread.currentThread().getName()+"执行累加操作..."+"第"+i+"次累加");
            try {
                total=total+1;
                Thread.sleep(2000);

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    //实例对象的另一个同步方法
    public synchronized void declare(){
        System.out.println(Thread.currentThread().getName()+"执行total-1");
        total--;
        System.out.println(Thread.currentThread().getName()+"执行total-1完成");
    }

    //普通实例方法
    public void simpleMethod(){
        System.out.println(Thread.currentThread().getName()+ "  ----实例对象的普通方法---");
    }


    @Override
    public void run() {
        //线程执行体
        System.out.println(Thread.currentThread().getName()+"准备执行累加,还没获取到锁");
        //执行普通方法
        simpleMethod();
        //调用同步方法执行累加操作
        increase();
        //执行完increase同步方法后,会释放掉锁,然后线程1和线程2会再一次进行锁的竞争,谁先竞争得到锁,谁就先执行declare同步方法
        System.out.println(Thread.currentThread().getName()+"完成累加操作");
        //调用实例对象的另一个同步方法
        System.out.println(Thread.currentThread().getName()+"准备执行total-1");
        declare();
    }

    public static void main(String[] args) throws InterruptedException {
        synchronizedDemo1 syn = new synchronizedDemo1();
        Thread thread1 = new Thread(syn,"线程1");
        Thread thread2 = new Thread(syn,"线程2");
        thread1.start();
        thread2.start();

    }
}

输出结果:

线程1准备执行累加,还没获取到锁
线程2准备执行累加,还没获取到锁
线程2  ----实例对象的普通方法---
线程2执行累加操作...第1次累加     //线程2通过与线程1的竞争率先拿到了锁,进入increase同步方法
线程2执行累加操作...第2次累加
线程1  ----实例对象的普通方法---  //从这里可看出,在线程2访问同步方法时,线程1是可以访问非同步方法的,但是不可以访问另外一个同步方法
线程2执行累加操作...第3次累加
线程2执行累加操作...第4次累加
线程2执行累加操作...第5次累加
线程2完成累加操作         //线程2执行累加后会释放掉锁
线程2准备执行total-1

线程1执行累加操作...第1次累加    //然后线程1拿到锁后进入increase同步方法执行累加
线程1执行累加操作...第2次累加
线程1执行累加操作...第3次累加
线程1执行累加操作...第4次累加
线程1执行累加操作...第5次累加
线程1完成累加操作    //线程1完成累加操作也会释放掉锁,然后线程1和线程2会再进行一次锁竞争
线程1准备执行total-1  
线程2执行total-1     //线程2通过竞争率先拿到锁进入declear方法执行total-1操作
线程2执行total-1完成
线程1执行total-1
线程1执行total-1完成

四、Synchronized实现原理

加锁和释放锁

synchronized同步是通过monitorentermonitorexit等指令实现的,会让对象在执行,使其锁计数器加1或者减1。

monitorenter指令:每一个对象在同一时间只与一个monitor(锁)相关联,而一个monitor在同一时间只能被一个线程获得,一个对象在尝试获得与这个对象相关联的Monitor锁的所有权的时候,会发生如下3种情况之一:

  • monitor计数器为0,意味着目前还没有被获得,那这个线程就会立刻获得然后把锁计数器+1,一旦+1,别的线程再想获取,就需要等待
  • 如果这个monitor已经拿到了这个锁的所有权,又重入了这把锁,那锁计数器就会累加,变成2,并且随着重入的次数,会一直累加
  • 若其他线程已经持有了对象监视器,则当前线程进入阻塞状态,直到对象监视器的进入数为0,重新尝试获取monitor的所有权。

monitorexit指令:释放对于monitor的所有权,释放过程很简单,就是讲monitor的计数器减1,如果减完以后,计数器不是0,则代表刚才是重入进来的,当前线程还继续持有这把锁的所有权,如果计数器变成0,则代表当前线程不再拥有该monitor的所有权,即释放锁。

对象、对象监视器、同步队列以及执行线程状态之间的关系:

166.png 该图可以看出,任意线程对Object的访问,首先要获得Object的监视器,如果获取失败,该线程就进入同步状态,线程状态变为BLOCKED,当Object的监视器占有者释放后,在同步队列中得线程就会有机会重新获取该监视器。

可重入原理:加锁次数计数器

从上图中就可以看出来,执行静态同步方法的时候就只有一条monitorexit指令,并没有monitorenter获取锁的指令。这就是锁的重入性,即在同一锁程中,线程不需要再次获取同一把锁。

Synchronized先天具有重入性。每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一。

保证可见性的原理:内存模型和happens-before规则

Synchronized的happens-before规则,即监视器锁规则:对同一个监视器的解锁,happens-before于对该监视器的加锁。

public class MonitorDemo {
    private int a = 0;

    public synchronized void writer() {     // 1
        a++;                                // 2
    }                                       // 3

    public synchronized void reader() {    // 4
        int i = a;                         // 5
    }                                      // 6
}

happens-before关系如图所示:

31.png

在图中每一个箭头连接的两个节点就代表之间的happens-before关系,黑色的是通过程序顺序规则推导出来,红色的为监视器锁规则推导而出:线程A释放锁happens-before线程B加锁,蓝色的则是通过程序顺序规则和监视器锁规则推测出来happens-befor关系,通过传递性规则进一步推导的happens-before关系。

总结

  • synchronized同步语句块的实现使⽤的是monitorenter和monitorexit指令,其中monitorenter指令指向同步代码块的开始位置, monitorexit指令则指明同步代码块的结束位置。

  • synchronized修饰的⽅法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是ACC_SYNCHRONIZED 标识,该标识指明了该⽅法是⼀个同步⽅法。

    不过两者的本质都是对对象监视器 monitor 的获取。

使用Synchronized有哪些要注意的?

  • 锁对象不能为空,因为锁的信息都保存在对象头里;
  • 作用域不宜过大,影响程序执行的速度,控制范围过大,编写代码也容易出错;
  • 避免死锁;
  • 在能选择的情况下,既不要用Lock也不要用synchronized关键字,用java.util.concurrent包中的各种各样的类,如果不用该包下的类,在满足业务的情况下,可以使用synchronized关键字,因为代码量少,避免出错。

参考文章

《深入理解Java虚拟机》

《Java并发编程的艺术》

彻底理解synchronized

Synchronized 详解