前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java-Synchronized

Java-Synchronized

原创
作者头像
笔头
修改2022-02-09 10:03:13
3750
修改2022-02-09 10:03:13
举报
文章被收录于专栏:Android记忆

在多线程环境中,对同一个数据进行操作时,很容易出现同步问题。为了解决该问题,java提供了Synchronized手段。

一、Synchronized如何用

Synchronized主要在这三处使用

1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象。

2. 修饰一个非静态方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象。

3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象,所有的静态同步方法用的也是同一把锁——类本身。

二、字节码角度看Synchronized原理

我们可以从字节码来看下Synchronized如何运行。

代码语言:javascript
复制
public class Run{

    public synchronized void m() {
        System.out.printf("M 干了一堆事");
    }

    public static synchronized void m1() {
        System.out.printf("M1 干了一堆事");
    }

    public void m2() {
        synchronized(this) {
            System.out.printf("M2 干了一堆事");
        }
    }

}

对应字节码如下

代码语言:javascript
复制
 public synchronized void m();
    descriptor: ()V
    flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #13                 // String M 干了一堆事
         5: iconst_0
         6: anewarray     #2                  // class java/lang/Object
         9: invokevirtual #15                 // Method java/io/PrintStream.printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
        12: pop
        13: return
      LineNumberTable:
        line 5: 0
        line 6: 13

  public static synchronized void m1();
    descriptor: ()V
    flags: (0x0029) ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=0, args_size=0
         0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #21                 // String M1 干了一堆事
         5: iconst_0
         6: anewarray     #2                  // class java/lang/Object
         9: invokevirtual #15                 // Method java/io/PrintStream.printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
        12: pop
        13: return
      LineNumberTable:
        line 9: 0
        line 10: 13

  public void m2();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=3, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorente
         4: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
         7: ldc           #23                 // String M2 干了一堆事
         9: iconst_0
        10: anewarray     #2                  // class java/lang/Object
        13: invokevirtual #15                 // Method java/io/PrintStream.printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
        16: pop
        17: aload_1
        18: monitorexit
        19: goto          27
        22: astore_2
        23: aload_1
        24: monitorexit
        25: aload_2
        26: athrow
        27: return

我们可以看出m方法中flags标记包含ACC_SYNCHRONIZED,m1方法中除了ACC_SYNCHRONIZED还包含ACC_STATIC,而m2中flags不包含ACC_SYNCHRONIZED。

synchronized修饰的方法在字节码中添加了一个ACC_SYNCHRONIZED的flags,而同步代码块则是在同步代码块前插入monitorenter,在同步代码块结束后插入monitorexit。

每一个对象,对象头信息中包含一个monitor,就是监视器,当线程获取monitor成功的时候,这个对象就被锁住了。

synchronized修饰的方法情况下:

当线程执行到某个方法时,JVM会去检查该方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了那线程会去获取这个对象所对应的monitor对象(每一个对象都有且仅有一个与之对应的monitor对象),获取成功后才执行方法体,方法执行完再释放monitor对象,在这一期间,任何其他线程都无法获得这个monitor对象。

同步代码块情况下:

monitorenter指令,

执行monitorenter就是为了尝试获取monitor的拥有权如果一个monitor的数值是0,那么线程直接进入monitor,并且将monitor置为1,并将对象的抢占该线程置为该线程。

如果该线程已经占用monitor,则直接进入,monitor数+1 (可重入锁)

如果该对象的抢占线程不是该线程,那么该线程就会被阻塞,直到monitor的数值变成0,再次抢占。

monitorexit指令,

执行monitorexit的线程必须是已经拥有该对象monitor的线程,执行monitorexit命令后,monitor的进入数会减1,当减为0时,其它线程就可以尝试获得monitor的所有权。

我们看到其中有两个monitorexit,这其实是和try/finally是一个道理,我们一般编程的时候,都要在finally里加一个解锁unlock,防止异常情况的发生,那么这里也是一样的,为了防止代码出现异常,最后在进行monitorexit一下。

三、相关问题

1.为什么说Synchronized是可重入锁?

可重入性 是 锁 的 一 个 基 本 要 求 , 是 为 了 解 决 自 己 锁 死 自 己 的 情 况,比如一个类中出现一个同步方法调用另一个同步方法,在调用method1方法中monitor会被占用,那再调用method2方法,在method2尝试获取锁的过程中,假如Synchronized不支持可重入性,那就一直获取不到锁,直到method1运行完毕,释放锁。这样就会导致自己阻塞自己。

2.为什么说Synchronized是非公平锁?

非公平主要表现在获取锁的行为上,并非是按照申请锁的时间前后给等待线程分配锁的,每当锁被释放后,任何一个线程都有机会竞争 到锁,这样做的目的是为了提高执行性能,缺点是可能会产生线程饥饿现象.

3.什么是锁消除和锁粗化?

锁消除。是Java虚拟机在JIT编译期间,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过锁消除,可以节省毫无意义的请求锁时间。比如在方法中创建局部变量StringBuffer,进行append操作,append操作用了synchronized,是线程安全的。但是这个时候不会存在线程安全问题,这个时候加锁无意义且耗时。

这个时候我们可以通过编译器将其优化,将锁消除,同时必须开启逃逸分析:

-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks

其中+DoEscapeAnalysis表示开启逃逸分析,+EliminateLocks表示锁消除。

锁粗化。原则上 ,同步块的作用范围要尽量小,避免不必要耗时操作在作用域中。但是如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁地进行互斥同步操作也会 导致不必要的性能损耗,这种情况,可以考虑扩大锁作用域。

4.为什么说Synchronized 是一个悲观锁,乐观锁的实现原理又是什么?

Synchronized是一个悲观锁,因为它的并发策略是悲观的,不管是否会产生竞争,任何的数据操作都必须要加锁、用户态核心态转换 、维护锁计数器和检查是否有被阻塞的线程需要被唤醒等操作 。

乐观锁的核心算法是CAS(比较&替换),它涉及到三个操作数 :内 存 值、预期值、新值。当且仅当预期值和内存值相等时才将 内存值修改为新值 。

java中的Atomic包下的一系列类就是使用了乐观锁机制。

首先检查某块内存的值是否跟之前我读取时的一样,如不一样,则期间此内存值已经被别的线程更改过,舍弃本次操作,否则说 明期间没有其他线程对此内存值操作,可以把新值设置给此块内存 。

但是这可能会出现ABA问题,举个例子,你看到桌子上有100块钱,然后你去干其他事了,回来之后看到桌子上依然是100块钱,你就认为这100块没人动过,其实在你走的那段时间,别人已经拿走了100块,后来又还回来了。这就是ABA问题。这么看好像没毛病。

我们再举个例子。

现有一个用单向链表实现的堆栈,栈顶为A,这时线程T1已经知道A.next为B,然后希望用CAS将栈顶替换为B:

在T1执行上面这条指令之前,线程T2介入,将A、B出栈,再pushD、C、A,此时堆栈结构如下图,而对象B此时处于游离状态:

此时轮到线程T1执行CAS操作,检测发现栈顶仍为A,所以CAS成功,栈顶变为B,但实际上B.next为null,所以此时的情况变为:

其中堆栈中只有B一个元素,C和D组成的链表不再存在于堆栈中,平白无故就把C、D丢掉了。

解决方案:

乐观锁的实现中通常都会用版本戳version来对记录或对象标记,避免并发操作带来的问题。在Java中,AtomicStampedReference<E>也实现了这个作用,它通过包装[E,Integer]的元组来对对象标记版本戳stamp,从而避免ABA问题,例如下面的代码分别用AtomicInteger和AtomicStampedReference来对初始值为100的原子整型变量进行更新,AtomicInteger会成功执行CAS操作,而加上版本戳的AtomicStampedReference对于ABA问题会执行CAS失败:

我们可以来个代码看看。

代码语言:javascript
复制
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;

public class Run {
    private static AtomicInteger atomicInt = new AtomicInteger(100);
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                atomicInt.compareAndSet(100, 101);
                atomicInt.compareAndSet(101, 100);
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                boolean c3 = atomicInt.compareAndSet(100, 101);
                System.out.println(c3);        //true
            }
        });

        thread1.start();
        thread2.start();
    }

}

打印true,这个代码就出现ABA问题,执行thread2前,atomicInt已经被修改过又复原,在执行thread2时,该线程认为没有变化,实际是有变化的。

我们再用AtomicStampedReference来看看

代码语言:javascript
复制

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;

public class ABA {
    
    private static AtomicStampedReference<Integer> atomicStampedRef = 
            new AtomicStampedReference<Integer>(100, 0);
    
    public static void main(String[] args) throws InterruptedException {
        Thread refT1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                atomicStampedRef.compareAndSet(100, 101, 
                        atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1);
                atomicStampedRef.compareAndSet(101, 100, 
                        atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1);
            }
        });
        
        Thread refT2 = new Thread(new Runnable() {
            @Override
            public void run() {
                int stamp = atomicStampedRef.getStamp();
                System.out.println("before sleep : stamp = " + stamp);    // stamp = 0
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("after sleep : stamp = " + atomicStampedRef.getStamp());//stamp = 1
                boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp+1);
                System.out.println(c3);        //false
            }
        });
        
        refT1.start();
        refT2.start();
    }

}

打印false,解决了ABA问题。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、Synchronized如何用
  • 二、字节码角度看Synchronized原理
  • 三、相关问题
    • 1.为什么说Synchronized是可重入锁?
      • 2.为什么说Synchronized是非公平锁?
        • 3.什么是锁消除和锁粗化?
          • 4.为什么说Synchronized 是一个悲观锁,乐观锁的实现原理又是什么?
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档