深入理解系列之JAVA多线程(2)——synchronized同步原理

多线程中为了解决线程安全问题,一个重要的手段就是同步!所谓同步其实就是使得原本各个线程交叉执行(异步),变成排队执行(同步)。同步策略使得不同线程操作共享数据遵循“先来后到“,从而避免某个线程没有处理完数据就被另一线程抢占操作出现数据被覆盖或者脏读的情况。其中同步最常用的手段就是synchronized关键字!

1、synchronized有哪些主要用法?有什么区别?

synchronized主要有两种用法,一个是同步方法,另一个是同步代码块!
同步方法在普通方法上加上synchronized关键字,默认持有锁为该实例对象!则如果A线程执行该方法,则B线程必须等到A线程执行完之后才能执行,但是需要注意的有两点:
···1、A、B两个线程执行该方法所引用的实例对象必须是同一个。如果创建了两个实例instanceA、instanceB,而A线程通过instanceA.method调用方法,B线程通过instanceB.method调用方法,则不能实现同步,因为两个线程持有不同的实例锁。
···2、同一实例中的方法如果都加了synchronized,那么默认调用该实例下的所有方法都会同步进行(排队进行),因为该实例下的方法都持有this锁
···3、对于实例下不加synchronized的其他普通方法,线程调用不会同步进行!这就会出现安全问题,这是因为同步方法A的代码只能阻止另一持有this锁的同步方法B在A执行的时候不受打扰,却不能阻止非同步方法在A执行过程中执行!其实道理很简单,但是叙述看起来复杂一点,所以这里举个例子:

public class SynchronizedTest {
  public static void main(String[] args) {
    Service service = new Service();;
    Thread thread1 = new Thread(new Task1(service));
    Thread thread2 = new Thread(new Task2(service));
    thread1.start();
    thread2.start();
  }
}


class Service {
  private int count = 0;

  public synchronized void add() throws InterruptedException {
    while (count < 10) {
      System.out.println("count=" + count + ",add完之后变成:" + (count + 1));
      Thread.sleep(3000);
      count++;
      System.out.println("count=" + count);
    }
  }

  public int getCount() {
    System.out.println("获取count:" + count);
    return count;
  }
}

class Task1 implements Runnable {
  private Service service;

  public Task1(Service service) {
    this.service = service;
  }

  @Override
  public void run() {
    try {
      service.add();
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}

class Task2 implements Runnable {
  private Service service;

  public Task2(Service service) {
    this.service = service;
  }

  @Override
  public void run() {
    service.getCount();
  }
}

例子虽然有点长,但是实际很简单,有一定基础的可以很快看明白:一个线程执行该实例的同步方法进行自加操作;另一个线程执行实例下的非同步方法进行取用操作;因为对自加操作加了同步方法,所以我们本来的目的是利用synchronized的原子性特点等待该函数完全执行完成取用,即结果为100!但是实际结果如下:

count=0,add完之后变成:1
获取count:0
count=1
count=1,add完之后变成:2
count=2
count=2,add完之后变成:3

也就是说,出现了脏读!在数据操作过程中就被另一个线程取用。解决的方法很简单就是在getCount方法上加锁。但是我们从这个例子得出一个什么容易忽视的结论呢?我们常说synchronized具有原子性,但是实际上这个原子性是相对的:即加锁的方法原子性是相对于另一持有该锁的线程来说的,却对不持有该锁的线程无效!

讲完同步方法之后,实际基本上synchronized的用法也就清晰了,还需要注意的一点如果同步方法时静态方法则他的锁默认是该字节码对象。这个不必赘述。接下来就是同步代码块了!因为使用机制其实是一样的,我们就需要谈论一点:**为什么还需要同步代码块?**两个原因:
···1、同步代码块可以指定持有的锁,不必是默认this这就解决了上述问题中同一实例下的方法如果都是同步方法的话必须同步(顺序)执行带来的效率问题。因为有些方法可能不必等待所有的同步方法执行完毕就可以执行。
···2、同步代码块可以更精细的控制要同步的代码范围!一个方法,为了方法中某一段操作可以同步就得为所有的代码块加锁,但是有了同步代码块仅仅需要为其中几行加锁就行了!要知道同步代码的内容越多,就得强制实现顺序执行,然后顺序执行的效率是很低的!(假设一个方法100行代码,只有其中一句i++需要同步,其他操作不需要,那么如果使用同步方法的方式则必须等待100行代码执行完成才能执行其他同步方法,有了同步代码块我仅仅需要执行i++这一行的同步就行了)
所以综上,同步代码块的出现是为了提高同步的效率问题!

2、synchronized的实现原理是什么?

synchronized关键字使得持有同样锁的另一个线程必须排队执行,那么这个在底层是怎么实现的呢?谈到底层我们不得不再次祭出JVM原理!
synchronized关键字在代码编译成字节码后会形成两个字节码指令:monitorenter和monitorexit(注:实际通过javap -c测试中并没有看到这个字节码指令,不知道该如何获得,还求大佬指教)这两个字节码都需要reference类型的参数来指明要锁定的和解锁的对象。如果指明那就是这个对象的reference,如果没有指明则根据是否是静态方法来决定是实例对象还是类对象!有三点需要注意的:
···1、存在一个锁计数器,当拥有该锁则锁的计数器加1,当释放该锁,锁的计数器减1;
···2、由于synchronized是可重入锁,即同一线程下执行该同步代码块(同步方法)可以在该方法内继续调用其他持有该锁的同步代码块或者方法,而不会出现互斥同步!所以锁的计数器可以一直加1,在释放的时候直到释放为0才代表真的把锁完全释放
···3、同步会使得其他线程由运行态变成阻塞状态,从而等待锁释放在执行实现“顺序运行”的效果,但是我么说过java的线程实现方式是映射到操作系统上的原生线程上的,所以阻塞或者唤醒一个线程都需要操作系统来帮忙完成,这就需要从用户态切换到内核态,但是状态转换相对来说会花费很长的时间,比如简单的setter或者getter操作可能切换的时长要大于方法执行的时长。这也是采用同步导致效率低下的另一重要原因(另一原因就是顺序执行带来的等待消耗)。

注:已经解决为什么字节码指令没有监视器monitorenter和monitorexit,原因如下:

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

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

经测试,的确是这样:

public synchronized void print();
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #3                  // String XXX
       5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #5                  // class SynchronizedTest
       2: dup
       3: astore_1
       4: monitorenter
       5: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       8: ldc           #6                  // String YYY
      10: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      13: aload_1
      14: monitorexit
      15: goto          23
      18: astore_2
      19: aload_1
      20: monitorexit
      21: aload_2
      22: athrow
      23: new           #7                  // class SynccompileTest
      26: dup
      27: invokespecial #8                  // Method "<init>":()V
      30: invokevirtual #9                  // Method print:()V
      33: return
    Exception table:
       from    to  target type
           5    15    18   any
          18    21    18   any
}

最近手残,搞了个公众号,主要闲暇时间随便聊一些程序圈的一些事,也会分享一些技术面试的资料,感兴趣的可以关注一波。关注后,后台发送 面试指南,可以获取2021最新JAVA面试总结,基本看完后,JAVA八股文这些应该不在话下了。

在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值