美文网首页程序员面经集
Java并发编程之内置锁(synchronized)

Java并发编程之内置锁(synchronized)

作者: JAVA架构师的圈子 | 来源:发表于2021-03-29 21:59 被阅读0次

    synchronized在JDK5.0的早期版本中是重量级锁,效率很低,但从JDK6.0开始,JDK在关键字synchronized上做了大量的优化,如偏向锁、轻量级锁等,使它的效率有了很大的提升。

    synchronized的作用是实现线程间的同步,当多个线程都需要访问共享代码区域时,对共享代码区域进行加锁,使得每一次只能有一个线程访问共享代码区域,从而保证线程间的安全性。

    因为没有显式的加锁和解锁过程,所以称之为隐式锁,也叫作内置锁、监视器锁。

    如下实例,在没有使用synchronized的情况下,多个线程访问共享代码区域时,可能会出现与预想中不同的结果。

    public class Apple implements Runnable {
     private int appleCount = 5;
     
     @Override
     public void run() {
      eatApple();
     }
     
     public void eatApple(){
      appleCount--;
      System.out.println(Thread.currentThread().getName() + "吃了一个苹果,还剩" + appleCount + "个苹果");
     }
     
     public static void main(String[] args) {
      Apple apple = new Apple();
      Thread t1 = new Thread(apple, "小强");
      Thread t2 = new Thread(apple, "小明");
      Thread t3 = new Thread(apple, "小花");
      Thread t4 = new Thread(apple, "小红");
      Thread t5 = new Thread(apple, "小黑");
      t1.start();
      t2.start();
      t3.start();
      t4.start();
      t5.start();
     }
    }
    

    可能会输出如下结果:

    小强吃了一个苹果,还剩3个苹果
    小黑吃了一个苹果,还剩3个苹果
    小明吃了一个苹果,还剩2个苹果
    小花吃了一个苹果,还剩1个苹果
    小红吃了一个苹果,还剩0个苹果

    输出结果异常的原因是eatApple方法里操作不是原子的,如当A线程完成appleCount的赋值,还没有输出,B线程获取到appleCount的最新值,并完成赋值操作,然后A和B同时输出。(A,B线程分别对应小黑、小强)

    如果改下eatApple方法如下,还会不会有线程安全问题呢?

    public void eatApple(){
        System.out.println(Thread.currentThread().getName() + "吃了一个苹果,还剩" + --appleCount + "个苹果");
    }
    //加入Java开发交流君样:756584822一起吹水聊天
    

    还是会有的,因为--appleCount不是原子操作,--appleCount可以用另外一种写法表示:appleCount = appleCount - 1,还是有可能会出现以上的异常输出结果。

    synchronized的使用
    synchronized分为同步方法和同步代码块两种用法,当每个线程访问同步方法或同步代码块区域时,首先需要获得对象的锁,抢到锁的线程可以继续执行,抢不到锁的线程则阻塞,等待抢到锁的线程执行完成后释放锁。

    1.同步代码块

    锁的对象是object:

    public class Apple implements Runnable {
     private int appleCount = 5;
     private Object object = new Object();
     
     @Override
     public void run() {
      eatApple();
     }
     
     public void eatApple(){
        //同步代码块,此时锁的对象是object
      synchronized (object) {
       appleCount--;
       System.out.println(Thread.currentThread().getName() + "吃了一个苹果,还剩" + appleCount + "个苹果");
      }
     }
     
      //...省略main方法
    }
    

    2.同步方法,修饰普通方法

    锁的对象是当前类的实例对象:

    public class Apple implements Runnable {
     private int appleCount = 5;
     
     @Override
     public void run() {
      eatApple();
     }
     
     public synchronized void eatApple() {
      appleCount--;
      System.out.println(Thread.currentThread().getName() + "吃了一个苹果,还剩" + appleCount + "个苹果");
     }
     
     //...省略main方法
    }
    

    等价于以下同步代码块的写法:

    public void eatApple() {
        synchronized (this) {
            appleCount--;
            System.out.println(Thread.currentThread().getName() + "吃了一个苹果,还剩" + appleCount + "个苹果");
        }
    }
    

    3.同步方法,修饰静态方法

    锁的对象是当前类的class对象:

    public class Apple implements Runnable {
     private static int appleCount = 5;
     
     @Override
     public void run() {
      eatApple();
     }
     
     public synchronized static void eatApple() {
      appleCount--;
      System.out.println(Thread.currentThread().getName() + "吃了一个苹果,还剩" + appleCount + "个苹果");
     }
     
     //...省略main方法
    }
    

    等价于以下同步代码块的写法:

    public static void eatApple() {
        synchronized (Apple.class) {
            appleCount--;
            System.out.println(Thread.currentThread().getName() + "吃了一个苹果,还剩" + appleCount + "个苹果");
        }
    }
    

    4.同步方法和同步代码块的区别

    a.同步方法锁的对象是当前类的实例对象或者当前类的class对象,而同步代码块锁的对象可以是任意对象。

    b.同步方法是使用synchronized修饰方法,而同步代码块是使用synchronized修饰共享代码区域。同步代码块相对于同步方法来说粒度更细,锁的区域更小,一般锁范围越小效率就越高。如下情况显然同步代码块更适用:

    public static void eatApple() {
        //不需要同步的耗时操作1
        //...
        synchronized (Apple.class) {
            appleCount--;
            System.out.println(Thread.currentThread().getName() + "吃了一个苹果,还剩" + appleCount + "个苹果");
        }
        //不需要同步的耗时操作2
        //...
    }
    

    内置锁的可重入性

    内置锁的可重入性是指当某个线程试图获取一个它已经持有的锁时,它总是可以获取成功。如下:

    public static void eatApple() {
        synchronized (Apple.class) {
            synchronized (Apple.class) {
                synchronized (Apple.class) {
                    appleCount--;
                    System.out.println(Thread.currentThread().getName() + "吃了一个苹果,还剩" + appleCount + "个苹果");
                }
            }
        }
    }
    

    如果锁不是可重入的,那么假如某线程持有了该锁,然后又需要等待持有该锁的线程释放锁,这不就造成死锁了吗?

    synchronized可以被继承吗?

    synchronized不可以被继承,如果子类中重写后的方法需要实现同步,则需要手动添加synchronized关键字。

    public class AppleParent {
     public synchronized void eatApple(){
     
     }
    }
     
    public class Apple extends AppleParent implements Runnable {
     private int appleCount = 5;
     
     @Override
     public void run() {
      eatApple();
     }
     
     @Override
     public void eatApple() {
      appleCount--;
      System.out.println(Thread.currentThread().getName() + "吃了一个苹果,还剩" + appleCount + "个苹果");
     }
     
     //...省略main方法
    }
    

    基于内置锁的等待和唤醒
    基于内置锁的等待和唤醒是使用Object类中的wait()和notify()或notifyAll()来实现的。这些方法的调用前提是已经持有对应的锁,所以只能在同步方法或者同步代码块里调用。如果在没有获取到对应锁的情况下调用则会抛出IllegalMonitorStateException异常。下面介绍下相关的几个方法:

    wait():使当前线程无限期地等待,直到另一个线程调用notify()或notifyAll()。

    wait(long timeout):指定一个超时时间,超时时间过后线程将会被自动唤醒。线程也可以在超时时间之前被notify()或notifyAll()唤醒。注意,wait(0)等同于调用wait()。

    wait(long timeout, int nanos):类似于wait(long timeout),主要区别是wait(long timeout, int nanos)提供了更高的精度。

    notify():随机唤醒一个在相同锁对象上等待的线程。

    notifyAll():唤醒所有在相同锁对象上等待的线程。

    一个简单的等待唤醒实例:

    public class Apple {
     //苹果数量
     private int appleCount = 0;
     
    //加入Java开发交流君样:756584822一起吹水聊天
     /**
      * 买苹果
      */
     public synchronized void getApple() {
      try {
       while (appleCount != 0) {
        wait();
       }
      } catch (InterruptedException ex) {
       ex.printStackTrace();
      }
     
      System.out.println(Thread.currentThread().getName() + "买了5个苹果");
      appleCount = 5;
      notify();
     }
    //加入Java开发交流君样:756584822一起吹水聊天
     
     /**
      * 吃苹果
      */
     public synchronized void eatApple() {
      try {
       while (appleCount == 0) {
        wait();
       }
      } catch (InterruptedException ex) {
       ex.printStackTrace();
      }
     
      System.out.println(Thread.currentThread().getName() + "吃了1个苹果");
      appleCount--;
      notify();
     }
    }
    
    /**
     * 生产者,买苹果
     */
    public class Producer extends Thread{
     private Apple apple;
     
     public Producer(Apple apple, String name){
      super(name);
      this.apple = apple;
     }
     
     @Override
     public void run(){
      while (true)
      apple.getApple();
     }
    }
     
    //加入Java开发交流君样:756584822一起吹水聊天
    /**
     * 消费者,吃苹果
     */
    public class Consumer extends Thread{
     private Apple apple;
     
     public Consumer(Apple apple, String name){
      super(name);
      this.apple = apple;
     }
     
     @Override
     public void run(){
      while (true)
      apple.eatApple();
     }
    }
    
    public class Demo {
     public static void main(String[] args) {
      Apple apple = new Apple();
      Producer producer = new Producer(apple,"小明");
      Consumer consumer = new Consumer(apple, "小红");
      producer.start();
      consumer.start();
     }
    }
    //加入Java开发交流君样:756584822一起吹水聊天
    

    输出结果:

    小明买了5个苹果
    小红吃了1个苹果
    小红吃了1个苹果
    小红吃了1个苹果
    小红吃了1个苹果
    小红吃了1个苹果
    小明买了5个苹果
    小红吃了1个苹果
    ......
    ]


    image

    最新2020整理收集的一些高频面试题(都整理成文档),有很多干货,包含mysql,netty,spring,线程,spring cloud、jvm、源码、算法等详细讲解,也有详细的学习规划图,面试题整理等,需要获取这些内容的朋友请加Q君样:756584822

    相关文章

      网友评论

        本文标题:Java并发编程之内置锁(synchronized)

        本文链接:https://www.haomeiwen.com/subject/pkyihltx.html