java并发编程之synchronized关键字

本文详细介绍了Java中synchronized关键字的作用、实现原理及使用方式,包括代码块同步、方法同步以及锁的优化。通过示例代码展示了synchronized如何保证线程安全,探讨了偏向锁、轻量级锁的引入原因及其升级过程,分析了不同锁的优缺点和适用场景。
摘要由CSDN通过智能技术生成

在java多线程并发编程中,为了保证共享数据在同一时刻只能被一个线程使用,为了达到这一目的,需要一个叫做 “同步” 的实现思想,在共享数据里保存一个锁,没有线程访问时,锁是空的。当有第一个线程访问时,在锁里保存这个线程的标识并允许这个线程访问共享数据。在当前线程释放共享数据之前,如果有其他线程想访问共享数据,就要等待锁释放。

所以会经常使用到synchronized关键字来实现同步操作,synchronized主要有三个作用: 

  1. 确保线程互斥的访问同步代码
  2. 保证共享变量的修改的可见性
  3. 有效解决指令重排序问题

1.synchronized的实现原理

基于JVM规范中可知,JVM通过进入和退出monitor对象来实现方法同步和代码同步,代码块的同步是通过monitorenter指令和monitorexit指令来实现的,方法同步是通过。

1.1代码块同步

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

  1. 如果monitor的进入数为0,则该线程进入monitor,将进入数设置为1,该线程即为monitor的所有者。
  2. 如果线程已经占有该monitor,只是重新进入monitor,进入数加1。
  3. 如果其他线程已经占有了monitor,则该线程进入阻塞状态,知道monitor的进入数为0,再重新尝试获取monitor的所有权。
 
monitorexit:执行monitorexit的线程必须是monitor的所有者,monitorexit指令执行时,进入数减1,如果减1后进入数为0,则线程退出monitor,不再是monitor的所有者。其他被阻塞的线程可以尝试获取monitor的所有权。
 

1.2方法同步

同步方法是通过在方法的常量池中设置ACC_SYNCHRONIZED标志来实现的,当线程执行有ACC_SYNCHRONIZED标志的方法,需要先获取monitor锁,然后开始执行方法,方法执行之后释放monitor锁,方法不管是正常return还是抛出异常都会释放对应的monitor锁。在这期间,如果其他线程来请求执行方法,会因为无法获得monitor锁而被阻塞。

2.synchronized的使用

当一个线程试图访问同步代码块时,它首先必须获得锁,退出或抛出异常时必须释放锁。利用synchronized实现同步的基础:java中的每一个对象都可以作为锁,具体表现为以下三种形式:

  1. 修饰普通方法,锁的是当前实例对象。
  2. 修饰静态方法,锁的是当前类的Class对象。
  3. 修饰代码块,锁的是synchronized括号里的对象。

2.1代码示例

1.没有使用synchronized的情况,例如以下代码:

public class SynchronizedTest {
    public void method1(){
        System.out.println("Method 1 start");
        try {
            System.out.println("Method 1 execute");
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 1 end");
    }

    public void method2(){
        System.out.println("Method 2 start");
        try {
            System.out.println("Method 2 execute");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 2 end");
    }

    public static void main(String[] args) {
        final SynchronizedTest test = new SynchronizedTest();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method1();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method2();
            }
        }).start();
    }
}

执行结果如下:

Method 1 start
Method 1 execute
Method 2 start
Method 2 execute
Method 2 end
Method 1 end

线程1和线程2同时进入执行状态,但是线程2执行速度比线程1快,所以线程2先执行完。

2.使用synchronized修饰普通方法,例如以下代码:

public class SynchronizedTest {

    public synchronized void method1(){
        System.out.println("Method 1 start");
        try {
            System.out.println("Method 1 execute");
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 1 end");
    }

    public synchronized void method2(){
        System.out.println("Method 2 start");
        try {
            System.out.println("Method 2 execute");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 2 end");
    }

    public static void main(String[] args) {
        final SynchronizedTest test = new SynchronizedTest();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method1();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method2();
            }
        }).start();
    }
}

执行结果如下:

Method 1 start
Method 1 execute
Method 1 end
Method 2 start
Method 2 execute
Method 2 end

使用sychronized修饰普通方法,锁的是当前实例对象,线程1和线程2运行的都是同一个实例对象的方法,当线程1执行时,会优先获取锁,线程2需要等待线程1的method1方法执行完,才能获取锁开始执行method2方法。

3.使用sychronized修饰静态方法,例如以下代码:

public class SynchronizedTest {

    public static synchronized void method1(){
        System.out.println("Method 1 start");
        try {
            System.out.println("Method 1 execute");
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 1 end");
    }

    public static synchronized void method2(){
        System.out.println("Method 2 start");
        try {
            System.out.println("Method 2 execute");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 2 end");
    }

    public static void main(String[] args) {
        final SynchronizedTest test = new SynchronizedTest();
        final SynchronizedTest test2 = new SynchronizedTest();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method1();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test2.method2();
            }
        }).start();
    }
}

执行结果如下:

Method 1 start
Method 1 execute
Method 1 end
Method 2 start
Method 2 execute
Method 2 end

从程序执行结果可以看出,和示例2程序的输出结果一样。使用synchronized修饰静态方法,锁的是类的Class对象,就是对类的同步,因为静态方法是属于类的方法,而不是对象的方法。即使线程1和线程2的属于不同的实例对象,但是它们都属于SynchronizedTest这个类的实例对象,所以线程2也必须等待线程1执行完method1方法后线程2才能执行method2方法,不能并发执行。

4.使用synchronized修饰代码块,代码如下:

public class SynchronizedTest {

    Object lockObject = new Object();

    public void method1(){
        System.out.println("Method 1 start");
        try {
            synchronized (lockObject){
                System.out.println("Method 1 execute");
                Thread.sleep(3000);
            }

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 1 end");
    }

    public void method2(){
        System.out.println("Method 2 start");
        try {
            synchronized (lockObject){
                System.out.println("Method 2 execute");
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 2 end");
    }

    public static void main(String[] args) {
        final SynchronizedTest test = new SynchronizedTest();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method1();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method2();
            }
        }).start();
    }
}

执行结果如下:

Method 1 start
Method 1 execute
Method 2 start
Method 1 end
Method 2 execute
Method 2 end

使用synchronized修饰代码块,锁的是synchronized括号里的对象。所以线程2必须要等线程1中method1方法的同步代码块执行完,线程2才能继续执行。

 

3. 锁的优化

Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。在java SE 1.6中,锁一共有4中状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁之后,不能再降级成偏向锁。这种锁升级不能降级的策略,目的是为了提高获得锁和释放锁的效率。

3.1 偏向锁

1.为什么引入偏向锁?

Hotspot作者经过研究发现,大多数时候锁不仅不存在竞争,而且总是由同一个线程多次获得锁。所以为了降低线程获得锁的代价而引入了偏向锁。

2.偏向锁的升级和撤销

当线程1访问代码块并获取锁对象时,会在对象头和栈帧中的记录锁对象的线程ID,因为偏向锁不会主动释放锁,以后线程1再次获取锁的时候,只需要比较当前线程的线程ID和对象头中的线程ID是否一致,如果一致,还是线程1获取锁对象,无需使用CAS来加锁、解锁。如果不一致(其他线程,如:线程2要竞争锁对象,因为偏向锁不会主动释放锁,因此对象头中存储的还是线程1的线程ID),那么需要查看对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,其他线程(如线程2)可以竞争,将其设置为偏向锁。如果存活,那么查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁。如果线程1不再使用该锁对象,那么锁对象状态置为无锁状态,重新偏向新的线程。

3.关闭偏向锁

偏向锁在java 6和java 7中是默认启用的,但是它在程序启动几秒钟之后才激活,如果不想有这个延迟,那么可以使用-XX:BiasedLockingStartUpDelay=0,来关闭延迟。如果确定程序所有的锁通常情况下都处于竞争状态,可以通过:-XX:-UseBiasedLocking = false,关闭偏向锁,那么程序默认会进入轻量级锁状态。

3.2 轻量级锁

1.为什么要引入轻量级锁?

轻量级锁考虑的是竞争锁对象的线程不多且线程持有锁对象的时间也不长的情景,因为阻塞线程需要CPU从用户态转为内核态,代价较大。如果刚刚阻塞不久这个锁就别释放了,那就有点得不偿失了,因此干脆不阻塞这个线程,而让它自旋着等待锁释放。

2.轻量级锁什么时候升级为重量级锁?

线程1获取轻量级锁时会先把锁对象的对象头中的Mark Word复制到线程1的栈帧中创建用于存储锁记录的空间(称为:DisplacedMarkWord),然后尝试用CAS把对象头中的内容替换为线程1存储的锁记录(DisplacedMarkWord)的地址。如果在线程1复制对象头的同时(在线程1CAS之前),线程2也准备获取锁对象,复制了对象头到线程2的锁记录空间中,但是在线程2尝试用CAS替换的时候,发现线程1已经把对象头替换成功了,线程2的CAS失败,那么线程2就会尝试使用自旋来等待线程1释放锁。自旋需要消耗CPU资源,因此自旋的次数是有限制的,如果自旋次数到了线程1还没有释放锁,或者线程1还在执行,线程2还在自旋等待,这时又有一个线程3来竞争这个锁对象,那这个时候轻量级锁就会升级为重量级锁。重量级锁会把除了拥有锁对象的线程都阻塞住,防止CPU空转。

注意:为了避免无用的自旋,轻量级锁一旦膨胀为重量级锁就不会再降级为轻量级锁了,偏向锁升级为轻量级锁之后也不能再降级为偏向锁。但是偏向锁可以被置为无锁状态。

3.3 锁的优缺点和对比

锁的优缺点和对比
优     点缺     点适用场景
偏向锁加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级别的差距如果线程间存在锁竞争,会带来额外的锁撤销的消耗使用与只有一个线程访问同步块的场景
轻量级锁竞争的线程不会阻塞,提高了程序的响应速度如果始终得不到锁竞争的线程,使用自旋等待锁,会消耗CPU资源追求响应时间,同步块执行速度非常快
重量级锁线程竞争不使用自旋,不会消耗CPU资源线程阻塞,响应时间缓慢追求吞吐量,同步块执行速度较长

 

参考资料:java并发编程的艺术

 

 

 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值