Java同步synchronized

  • Post author:
  • Post category:java


一. synchronized是什么?

synchronized是Java的一个关键字,synchronized依赖于JVM具体实现。

1.1 synchronized的作用

synchronized也称为同步锁。synchronized是一种独占锁。


synchronized的两大功能:


  • 内存可见性

  • 操作原子性

什么是内存可见性?



可见性

是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

可参考《

Java内存分级和指令重排序》


。volatile也可实现内存可见性。


什么是操作原子性?

所谓


原子操作

是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何上下文切换

(context switch: 换到另一个线程)。

原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱

(禁止重排序)

,也不可以被切割而只执行其中的一部分。


将整个操作视作一个整体是原子性的核心特征。

1.2 synchronized什么时候释放锁?

出现异常或代码块执行结束,自动释放锁。

因为异常导致线程结束,同步锁释放,引起等待线程执行了,

发生程序乱入

,出现意想不到的效果。

/**
 * 测试synchronized出现异常释放锁
 *
 * @author zhouronghua
 * @time 2021/6/28 5:03 下午
 */
public class TestSyncException {
    /**
     * 计算器
     */
    private int count = 0;

    /**
     * 测试synchronized异常
     *
     * @author zhouronghua
     * @time 2021/6/28 5:18 下午
     */
    private synchronized void test() {
        System.out.println(Thread.currentThread().getName() + " test start...");
        count++;
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (count == 5) {
            // 产生一个Exception
            System.out.println(Thread.currentThread().getName() + "测试异常");
            int i = 5 / 0;
        }
        System.out.println(Thread.currentThread().getName() + " test end");
    }

    public static void main(String[] args) {
        TestSyncException testSyncThread = new TestSyncException();
        for (int i = 0; i < 10; i++) {
            new Thread(testSyncThread::test, "thread " + (i + 1)).start();
        }
    }
}

当count=5发生异常,当前线程结束,释放锁。后续等待队列的线程, 检测到同步锁释放,

开始执行。

thread 1 test start...
thread 1 test end
thread 10 test start...
thread 10 test end
thread 9 test start...
thread 9 test end
thread 8 test start...
thread 8 test end
thread 7 test start...
thread 7测试异常
thread 6 test start...
Exception in thread "thread 7" java.lang.ArithmeticException: / by zero
	at com.joe.helloapp.thread.TestSyncException.test(TestSyncException.java:32)
	at java.lang.Thread.run(Thread.java:748)
thread 6 test end
thread 5 test start...
thread 5 test end
thread 4 test start...
thread 4 test end
thread 3 test start...
thread 3 test end
thread 2 test start...
thread 2 test end

1.3 synchronized实现

字节码层级,synchronized是通过monitorenter和monitorexit指令实现的.

例如单例模式获取单例对象,

其中同步代码块,对应的字节码:

    MONITORENTER
   L0
    LINENUMBER 18 L0
    GETSTATIC com/joe/helloapp/Sington.sInstance : Lcom/joe/helloapp/Sington;
    IFNONNULL L7
   L8
    LINENUMBER 20 L8
    NEW com/joe/helloapp/Sington
    DUP
    INVOKESPECIAL com/joe/helloapp/Sington.<init> ()V
    PUTSTATIC com/joe/helloapp/Sington.sInstance : Lcom/joe/helloapp/Sington;
   L7
    LINENUMBER 22 L7
   FRAME APPEND [java/lang/Object]
    ALOAD 0
    MONITOREXIT

根据虚拟机规范的要求,在执行monitorenter指令时,首先要去尝试获取对象的锁,如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1;相应地,在执行monitorexit指令时会将锁计数器减1,当计数器被减到0时,锁就释放了。如果获取对象锁失败了,那当前线程就要阻塞等待,直到对象锁被另一个线程释放为止。


synchronized锁定的是对象

,而不是代码块。

1.4 synchronized是可重入锁


可重入锁是指同一线程可以多次获得同一个锁。


若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入(reentrant或re-entrant)的

。即当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果。与多线程并发执行的线程安全不同,

可重入强调对单个线程执行时重新进入同一个子程序仍然是安全的

同一线程在调用自己类中其他synchronized方法/块或调用父类的synchronized方法/块都不会阻碍该线程的执行,就是说

同一线程对同一个对象锁是可重入的

验证

package com.joe.helloapp.thread;

/**
 * 测试synchronized可重入锁
 *
 * @author zhouronghua
 * @time 2021/6/28 11:27 上午
 */
public class TestSyncReentrantLock {

    /**
     * 线程任务
     * 说明:此处synchronized,锁的对象是this对象
     *
     * @author zhouronghua
     * @time 2021/6/25 9:05 上午
     */
    public synchronized void fun1() {
        System.out.println(Thread.currentThread().getName() + " fun1 start...");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        /**
         * 调用synchronized方法。因为synchronized是可重入锁,
         * 同一线程再次访问synchronized方法,可重入,执行fun2。
         */
        fun2();
        System.out.println(Thread.currentThread().getName() + " fun1 end");
    }

    /**
     * 线程任务2
     *
     * @author zhouronghua
     * @time 2021/6/25 9:05 上午
     */
    public synchronized void fun2() {
        System.out.println(Thread.currentThread().getName() + " fun2 start...");
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " fun2 end");
    }

    /**
     * main入口
     *
     * @param args 参数列表
     * @author zhouronghua
     * @time 2021/6/28 11:28 上午
     */
    public static void main(String[] args) {
        TestSyncReentrantLock testSyncReentrantLock = new TestSyncReentrantLock();
        new Thread(testSyncReentrantLock::fun1, "thread 1").start();
    }
}

1)进入fun1,synchronized对this加锁,对应的线程为thread1;

2)fun1中调用synchronized方法fun2,需要对synchronized对this加锁,此时fun1中已经对this加锁了,且此时访问的线程为thread1,是同一线程,满足可重入;

3)fun2执行完成,回到fun1继续执行

thread 1 fun1 start...
thread 1 fun2 start...
thread 1 fun2 end
thread 1 fun1 end

二. synchronized使用

1. synchronized修饰方法

synchronized修饰方法相当于对整个方法synchronized(this)处理,相当于this对象加锁。

测试多线程访问synchronized方法

/**
 * 测试线程同步
 *
 * @author zhouronghua
 * @time 2021/6/25 8:57 上午
 */
public class TestSyncThread {

    /**
     * 线程任务
     * 说明:此处synchronized,锁的对象是this对象
     *
     * @author zhouronghua
     * @time 2021/6/25 9:05 上午
     */
    public synchronized void fun1() {
        System.out.println(Thread.currentThread().getName() + " fun1 start...");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " fun1 end");
    }


    /**
     * 测试多线程访问同一个方法
     *
     * @author zhouronghua
     * @time 2021/6/25 6:15 下午
     */
    private static void testSyncLockMethod() {
        TestSyncThread testSyncThread = new TestSyncThread();
        for (int i = 0; i < 10; i++) {
            new Thread(testSyncThread::fun1, "thread " + (i + 1)).start();
        }
    }

    public static void main(String[] args) {
        testSyncLockMethod();
    }
}

1)线程1执行fun1;

2)线程2执行fun1,检测到已经加锁,进入任务队列等待;线程3执行fun1,检测已经加锁,如在等待队列队首;线程4~线程10也如此。

3)线程1执行完,释放锁,开始执行任务对列表中的等待任务线程10;

线程10执行完成,执行等待任务线程9;。。。

thread 1 fun1 start...
thread 1 fun1 end
thread 10 fun1 start...
thread 10 fun1 end
thread 9 fun1 start...
thread 9 fun1 end
thread 8 fun1 start...
thread 8 fun1 end
thread 7 fun1 start...
thread 7 fun1 end
thread 6 fun1 start...
thread 6 fun1 end
thread 5 fun1 start...
thread 5 fun1 end
thread 4 fun1 start...
thread 4 fun1 end
thread 3 fun1 start...
thread 3 fun1 end
thread 2 fun1 start...
thread 2 fun1 end

2. synchronized修饰静态方法

synchronized修饰方法相当于对整个方法synchronized(T.class)处理,相当于类的class对象加锁。

因为静态方法是锁定的class对象,

/**
 * 测试synchronized静态方法
 * 说明: synchronized静态方法相当于对使用到TestSyncStaticThread.class对象同步
 *
 * @author zhouronghua
 * @time 2021/6/28 2:08 下午
 */
public class TestSyncStaticThread implements Runnable {
   /**
    * 测试多线程访问同一个synchronized静态方法
    *
    * @author zhouronghua
    * @time 2021/6/28 2:05 下午
    */
   private static void testStaticSyncLockMethod() {
      for (int i = 0; i < 10; i++) {
         new Thread(new TestSyncStaticThread(), "thread " + (i + 1)).start();
      }
   }

    @Override
    public void run() {
        fun2();
    }

   /**
    * 线程任务2
    *
    * @author zhouronghua
    * @time 2021/6/25 9:05 上午
    */
   public synchronized static void fun2() {
         System.out.println(Thread.currentThread().getName() + " fun2 start...");
         try {
            Thread.sleep(500);
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
         System.out.println(Thread.currentThread().getName() + " fun2 end");
   }

   public static void main(String[] args) {
      testStaticSyncLockMethod();
   }
}

虽然线程使用的是不同的TestSyncStaticThread对象,静态同步方法锁定的是TestSyncStaticThread.class对象,而TestSyncStaticThread.class对象是唯一的。因此这些线程需要同步执行。

thread 1 fun2 start...
thread 1 fun2 end
thread 10 fun2 start...
thread 10 fun2 end
thread 9 fun2 start...
thread 9 fun2 end
thread 8 fun2 start...
thread 8 fun2 end
thread 7 fun2 start...
thread 7 fun2 end
thread 6 fun2 start...
thread 6 fun2 end
thread 5 fun2 start...
thread 5 fun2 end
thread 4 fun2 start...
thread 4 fun2 end
thread 3 fun2 start...
thread 3 fun2 end
thread 2 fun2 start...
thread 2 fun2 end

3. synchronized锁定代码块

对代码块部分进行加锁处理。

三. synchronized锁定对象


synchronized选取的锁定对象,不能是String常量,Long和Integer。

四. synchronized锁升级过程

偏向锁–》自旋锁–》重量级锁

什么时候偏向使用自旋锁

自旋锁:通过CAS处理,占用CPU时间。线程数比较少的时候,操作时间端。

重量级锁:通过等待队列。线程比较多的时候,操作消耗时间长,使用重量级锁。

参考文献:

1.

https://zhuanlan.zhihu.com/p/29866981



版权声明:本文为joedan0104原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。