Java并发基础 - synchronized篇

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。


在Java并发编程中,synchronizedvolatile是两个非常重要的关键字,它们可以用来控制并发中的互斥性与可见性,本文我们先来看看在并发环境下,synchronized应该如何使用,以及它能够如何保证互斥性与可见性。

在正式开始之前,我们首先来看一下互斥性和可见性的概念:

  • 互斥性:即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程中的协调机制,这样在同一时间只有一个线程对需同步的代码块(复合操作)进行访问。互斥性我们也往往称为操作的原子性。
  • 可见性:必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作从而引起不一致。

我们知道synchronized关键字是用来控制线程同步的,在多线程的环境下,使用synchronized能够控制代码不被多个线程同时执行,来看看它的具体使用。

1、同步非静态方法

被修饰的方法称为同步方法,这时的锁是当前类的实例对象。

a、多个线程访问相同对象的相同synchronized方法:

public class SynchronizedDemo1 {
    public synchronized  void access() {
        try {
            System.out.println(Thread.currentThread().getName()+" start");
            TimeUnit.SECONDS.sleep(2);
            System.out.println(Thread.currentThread().getName()+" end");
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        SynchronizedDemo1 demo01=new SynchronizedDemo1();
        for(int i=0;i<5;i++){
            new Thread(demo01::access).start();
        }
    }
}
复制代码

运行结果:

可以看出,当多个线程对同一个对象的同步方法进行操作时,只有一个线程能够抢到锁。在一个线程获取了该对象的锁后,其他的线程无法获取该对象的锁,需要等待线程先把这个锁释放掉才能访问同步方法。

b、 多个线程访问相同对象的不同synchronized方法:

public class SynchronizedDemo2 {
    public synchronized void access1() {
        try {
            System.out.println(Thread.currentThread().getName()+" in access1 start");
            TimeUnit.SECONDS.sleep(5);
            System.out.println(Thread.currentThread().getName()+" in access1 end");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public synchronized void access2() {
        try {
            System.out.println(Thread.currentThread().getName()+" in access1 start");
            TimeUnit.SECONDS.sleep(5);
            System.out.println(Thread.currentThread().getName()+" in access1 end");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        SynchronizedDemo2 test = new SynchronizedDemo2();
        new Thread(test::access1).start();
        new Thread(test::access2).start();
    }
}
复制代码

运行结果:

由此可以确认,当线程访问synchronized修饰的任意方法时,如果当前对象被其他线程加锁,都需要等待其他线程先把当前的对象锁释放掉。

c、 多个不同对象的线程访问synchronized方法:

public class SynchronizedDemo3 {
    public synchronized void access1() {
        try {
            System.out.println(Thread.currentThread().getName()+" start");
            TimeUnit.SECONDS.sleep(5);
            System.out.println(Thread.currentThread().getName()+" end");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        final SynchronizedDemo3 test1 = new SynchronizedDemo3();
        final SynchronizedDemo3 test2 = new SynchronizedDemo3();
        new Thread(test1::access1).start();
        new Thread(test2::access1).start();
    }
}
复制代码

运行结果:

可以看出两个线程同时开始执行,这时因为两个线程属于不同的对象,而锁住的是类产生的实例对象,两个线程就获得了不同的锁。因此,不同对象产生的线程可以同时访问synchronized方法。

2、同步静态方法

静态方法是属于类的而不属于对象的 ,所以同样的, synchronized修饰的静态方法锁定的是这个类的class对象 。

public class SynchronizedDemo4 {
    public synchronized static void access() {
        try {
            System.out.println(Thread.currentThread().getName()+"  start");
            TimeUnit.SECONDS.sleep(2);
            System.out.println(Thread.currentThread().getName()+"  end");
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        for(int i=0;i<5;i++){
            new Thread(SynchronizedDemo4::access).start();
        }
    }
}
复制代码

运行结果:

分析可知,当synchronized修饰静态方法时,线程之间也发生了互斥,当一个线程访问同步方法时,其他线程必须等待。因为当synchronized修饰静态方法时,锁是class对象,而不是类的实例对象。

3、同步代码块

被修饰的代码块称为同步代码块,其作用的范围是大括号括起来的代码,这时锁是括号中的对象。

那么为什么要使用同步代码块呢?在方法比较长,而需要同步的代码只有一小部分时,如果对整段方法进行同步操作,可能会造成等待时间过长。这时我们可以使用同步代码块对需要同步的代码进行包围,而无需对整个方法进行同步。

根据锁的对象不同,又可以分为以下两类:

a、以对象作为锁:

使用实例对象作为锁,即线程需要进入被synchronized的代码块时,必须持有该对象锁,而后来的线程则必须等待该对象的释放。

//以this为例
public void accessResources() {
    synchronized (this) {
        try {
            TimeUnit.SECONDS.sleep(2);
            System.out.println(Thread.currentThread().getName() + "  is running");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
复制代码

此处,因为this指的是当前对象,所以不能用在static方法上。

b、使用类的class对象作为锁:

public void accessResources() {
    synchronized (SynchroDemo5.class) {
        try {
            TimeUnit.SECONDS.sleep(2);
            System.out.println(Thread.currentThread().getName() + "  is running");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

public static void main(String[] args) {
    final SynchroDemo5 demo5 = new SynchroDemo5();
    for (int i = 0; i < 5; i++) {
        new Thread(demo5::accessResources).start();
    }
}
复制代码

此时,有该class对象的所有的对象都共同使用这一个锁。

在当没有明确的对象作为锁时,只是想让一段代码同步时,则可以创建一个特殊的对象来充当锁,例如创建一个Object对象。

private final Object MUTEX =new Object();
public void methodName(){
   Synchronized(MUTEX ){
     //TODO
   }
}
复制代码

看完了实现,那么synchronized底层的实现原理是怎样的呢?我们分同步代码块与同步方法来。

原理

反编译使用同步代码块的类生成的class文件:

图片

这里使用了monitorentermonitorexit对进入同步代码进行了控制。

monitorenter :

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

  • 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有
  • 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1
  • 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

monitorexit:

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

反编译使用同步方法的类生成的class文件:

图片

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

好了,这篇文章就先到这里,下一篇我们再看看volatile

最后

如果觉得对您有所帮助,小伙伴们可以点赞、转发一下,非常感谢

公众号码农参上,加个好友,做个点赞之交啊

关于更多synchronized原理,可以参考这篇文章:再谈synchronized锁升级

猜你喜欢

转载自juejin.im/post/7018467447858561055

相关文章