synchronized的分析与使用

synchronized的分析与使用


  • 同步 机制: synchronized是Java同步机制的一种实现,即互斥锁机制,它所获得的锁叫做互斥锁
  • 互斥锁: 指的是每个对象的锁一次只能分配给一个线程,同一 时间只能由一个线程占用
  • 作用: synchronized用于保证同一时刻只能由一个线程进入到临界区,同时保证共享变量的可见性、原子性和有序性
  • 使用: 当一个线程试图访问同步代码方法(块)时,它首先必须得到锁,退出或抛出异常时必须释放锁

synchronized关键字的使用:

  1. 普通同步方法,锁是当前实例对象
  2. 静态同步方法,锁是当前类的class对象
  3. 同步方法块,锁是括号里面的对象
代码1:
/**
 * 一个变量类类
 * @author yang
 */
public class NewNum {
    /**
     * 一个静态变量
     */
    static int num = 0;

    public static void main(String[] args) {
        for(int j = 0; j < 1000; j++) {
            SetNumThread setNumThread = new SetNumThread();
            Thread timeOne = new Thread(setNumThread);
            timeOne.start();
        }
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(NewNum.num);
    }
}

class SetNumThread implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            NewNum.num++;                //1
            System.out.println(NewNum.num);
        }
    }
}

以上代码块的理想输出结果应当是10000,但是实际上却是每一次输出都不一样。造成这种结果,正是由于线程对共享变量的操作是不安全的。应当对共享变量num加锁。将1处代码变为:

synchronized (SetNumThread.class){
    NewNum.num++;
}

synchroized 的实现原理

同步代码块是使用monitorentermonitorexit指令实现的,同步方法(在这看不出来需要看JVM底层实现)依靠的是方法修饰符上的ACC_SYNCHRONIZED实现。

同步代码块:monitorenter指令插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置,JVM需要保证每一个monitorenter都有一个monitorexit与之相对应。任何对象都有一个monitor与之相关联,当且一个monitor被持有之后,他将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁;

同步方法:synchronized方法则会被翻译成普通的方法调用和返回指令如:invokevirtual、areturn指令,在VM字节码层面并没有任何特别的指令来实现被synchronized修饰的方法,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Klass做为锁对象。

锁优化

jdk1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

锁的实现机制与java对象头息息相关,锁的所有信息,都记录在java的对象头中。用2字(32位JVM中1字=32bit=4baye)存储对象头,如果是数组类型使用3字存储(还需存储数组长度)。对象头中记录了hash值、GC年龄、锁的状态、线程拥有者、类元数据的指针。

锁

偏向锁:

在实际应用运行过程中发现,“锁总是同一个线程持有,很少发生竞争”,也就是说锁总是被第一个占用他的线程拥有,这个线程就是锁的偏向线程。

 那么只需要在锁第一次被拥有的时候,记录下偏向线程ID。这样偏向线程就一直持有着锁,直到竞争发生才释放锁。以后每次同步,检查锁的偏向线程ID与当前线程ID是否一致,如果一致直接进入同步,退出同步也,无需每次加锁解锁都去CAS更新对象头,如果不一致意味着发生了竞争,锁已经不是总是偏向于同一个线程了,这时候需要锁膨胀为轻量级锁,才能保证线程间公平竞争锁。

轻量级锁:

轻量锁与偏向锁不同的是: 1. 轻量级锁每次退出同步块都需要释放锁,而偏向锁是在竞争发生时才释放锁 2. 每次进入退出同步块都需要CAS更新对象头 3. 争夺轻量级锁失败时,自旋尝试抢占锁

自旋锁:

所谓自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。怎么等待呢?执行一段无意义的循环即可(自旋)。

重量级锁:

当竞争线程尝试占用轻量级锁失败多次之后,轻量级锁就会膨胀为重量级锁,重量级线程指针指向竞争线程,竞争线程也会阻塞,等待轻量级线程释放锁后唤醒他。

Synchronized的可重入性

  • 重入锁: 当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功
  • 实现: 一个线程得到一个对象锁后再次请求该对象锁,是允许的,每重入一次,monitor进入次数+1

Synchronized与String锁

  • 隐患: 由于在JVM中具有String常量池缓存的功能,因此 相同字面量是同一个锁!!!
  • 注意: 严重不推荐将String作为锁对象,而应该改用其他非缓存对象
  • 提示: 对字面量有疑问的话请先回顾一下String的基础,这里不加以解释

参考文章:

https://blog.csdn.net/noble510520/article/details/78834224

https://blog.csdn.net/noble510520/article/details/78834224

http://ju.outofmemory.cn/entry/349840

posted @ 2018-09-14 17:18  一把水果刀  阅读(149)  评论(0编辑  收藏  举报