Java教程

Java之Synchronized深入理解

本文主要是介绍Java之Synchronized深入理解,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

目录
  • 1 Synchronized
    • 1.1 引言
    • 1.2 概念理解
      • 1.2.1 不同锁对象
      • 1.2.2 对象锁和类锁概念区别
      • 1.2.3 同步概念
      • 1.2.4 Synchronized概念
    • 1.3 原理
      • 1.3.1 Synchronized实现原理
      • 1.3.2 Java对象头
    • 1.4 实际操作
      • 1.4.1 对象锁
        • 1.4.1.1 使用同一对象锁
        • 1.4.1.2 使用不同对象锁
        • 1.4.4.3 Synchronized块
      • 1.4.2 类锁

1 Synchronized

1.1 引言

在多线程并发编程中Synchronized一直是元老级角色,很多人都会称呼它为重量级锁,但是随着Java SE1.6Synchronized进行了各种优化之后,有些情况下它并不那么重了,为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁,以及锁的存储结构和升级过程

术语 英文 说明
CAS Compare and Swap 比较并设置。用于在硬件层面上提供原子性操作。在 Intel 处理器中,比较并交换通过指令cmpxchg实现。比较是否和给定的数值一致,如果一致则修改,不一致则不修改

1.2 概念理解

1.2.1 不同锁对象

Java中的每一个 对象 都可以作为
对于同步方法,锁是当前实例对象(this)
对于静态同步方法,锁是当前对象的Class对象,又因为Class的相关数据存储在永久带PermGenjdk1.8则是metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程
对于同步方法块,锁是Synchonized括号里配置的对象。当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁,synchronized是自动释放的

1.2.2 对象锁和类锁概念区别

java的对象锁和类锁:在锁的概念上基本上和内置锁是一致的,但是,两个锁实际是有很大的区别的,对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以 不同对象实例的对象锁是互不干扰的,但是每个类 只有一个类锁。但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的

1.2.3 同步概念

JVM规范规定JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。
代码块同步是使用monitorentermonitorexit指令实现,而方法同步是使用另外一种方式实现的,细节在JVM规范里并没有详细说明,但方法的同步同样可以使用这两个指令来实现。monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处, JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个 monitor 与之关联,并且一个monitor 被持有后,它将处于锁定状态。线程执行到 monitorenter 指令时,将会尝试获取对象所对应的 monitor 的所有权,即尝试获得对象的锁

1.2.4 Synchronized概念

synchronizedJava中的关键字,是一种同步锁。它修饰的对象有以下几种:

  1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
  2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
  3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
  4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象

注意synchronized关键字是不能继承的,也就是说,基类的方法synchronized f(){} 在继承类中并不自动是synchronized f(){},而是变成了f(){}。继承类需要显式的指定它的某个方法为synchronized方法

1.3 原理

1.3.1 Synchronized实现原理

Synchronized实现如下图所示;
在这里插入图片描述
它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。

  1. Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
  2. Entry ListContention List中那些有资格成为候选资源的线程被移动到Entry List中;
  3. Wait Set:那些调用wait方法被阻塞的线程被放置在这里;
  4. OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck
  5. Owner:当前已经获取到所有资源的线程被称为Owner
  6. !Owner:当前释放锁的线程。

JVM每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList会被大量的并发线程进行CAS访问,为了降低对尾部元素的竞争,JVM会将一部分线程移动到EntryList中作为候选竞争线程。Owner线程会在unlock时,将Contention List中的部分线程迁移到Entry List中,并指定Entry List中的某个线程为OnDeck线程(一般是最先进去的那个线程)。Owner线程并不直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeckOnDeck需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM中,也把这种选择行为称之为竞争切换

OnDeck线程获取到锁资源后会变为Owner线程,而没有得到锁资源的仍然停留在EntryList中。如果Owner线程被wait方法阻塞,则转移到WaitSet队列中,直到某个时刻通过notify或者notifyAll唤醒,会重新进去EntryList中。

处于ContentionListEntryListWaitSet中的线程都处于阻塞状态,该阻塞是由操作系统来完成的

Synchronized是非公平锁。 Synchronized在线程进入Contention List时,等待的线程会先尝试自旋获取锁,如果获取不到就进入Contention List,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占OnDeck线程的锁资源

1.3.2 Java对象头

点此了解Java对象头和各种锁基础理解

1.4 实际操作

同步机制可以使用synchronized关键字实现。
synchronized关键字修饰一个方法的时候,该方法叫做同步方法。
synchronized方法执行完或发生异常时,会自动释放锁

1.4.1 对象锁

1.4.1.1 使用同一对象锁

同一个object使用synchronized会有以下几种情况:

  1. 两个方法都没有synchronized修饰,调用时都可进入:方法A和方法B都没有加synchronized关键字时,调用方法A的时候可进入方法B;
  2. 一个方法有synchronized修饰,另一个方法没有,调用时都可进入:方法A加synchronized关键字而方法B没有加时,调用方法A的时候可以进入方法B;
  3. 两个方法都加了synchronized修饰,一个方法执行完才能执行另一个:方法A和方法B都加了synchronized关键字时,调用方法A之后,必须等A执行完成才能进入方法B;
    当一个对象中有2个方法同时用synchronized修饰,那么当线程一在访问方法1时,其他线程是否可以访问方法二
    答案:由于对象的内置锁(监视器锁)是唯一的,所以当线程一在访问对象的方法1时,持有了该对象的内置锁,那么在线程一释放该内置锁之前,其他线程是无法获取该对象内置锁,所以其他线程无法访问方法二
  4. 两个方法都加了synchronized修饰,其中一个方法加了wait()方法,调用时都可进入:方法A和方法B都加了synchronized关键字时,且方法A加了wait()方法时,调用方法A的时候可以进入方法B;
  5. 一个添加了synchronized修饰,一个添加了static修饰,调用时都可进入:方法A加了synchronized关键字,而方法B为static静态方法时,调用方法A的时候可进入方法B;
  6. 两个方法都是静态方法且还加了synchronized修饰,一个方法执行完才能执行另一个:方法A和方法B都是static静态方法,且都加了synchronized关键字,则调用方法A之后,需要等A执行完成才能进入方法B;
  7. 两个方法都是静态方法且还加了synchronized修饰,分别在不同线程调用不同的方法,还是需要一个方法执行完才能执行另一个:方法A和方法B都是static静态方法,且都加了synchronized关键字,创建不同的线程分别调用A和B,需要等A执行完成才能执行B(因为static方法是单实例的,A持有的是Class锁,Class锁可以对类的所有对象实例起作用)
  8. 同一个object中多个方法都加了synchronized关键字的时候,其中调用任意方法之后需等该方法执行完成才能调用其他方法,即同步的阻塞的
    对于object中使用synchronized(this)同步代码块的场景也是如此,synchronized锁定的都是当前对象

下面例子中Worker worker = new Worker();就是使用同一对象的例子

package cn.jzh.test.thread;

public class Worker {
    public synchronized void executeA(String name) {
        for (int i = 0; i < 10; i++) {  
            System.out.println(name + "-executeA-" + i);  
            try {  
                Thread.sleep(500);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }
        }
    }
    public synchronized void executeB(String name) {
        for (int i = 0; i < 10; i++) {  
            System.out.println(name + "-executeB-" + i);  
            try {  
                Thread.sleep(500);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
   private class SynchronizerWorkerA extends Thread {
        public void run() {  
            executeA("thread1");
        }  
    }  
    private class SynchronizerWorkerB extends Thread {
        public void run() {  
            executeB("thread2");  
        }  
    }  
              
    public static void main(String args[]) {
        Worker worker = new Worker();
        worker.new SynchronizerWorkerA().start();
        worker.new SynchronizerWorkerB().start();     
    }
}

执行结果永远是执行完一个线程的输出再执行另一个线程的。  
说明:
如果一个对象有多个synchronized方法,某一时刻某个线程已经进入到了某个synchronized方法,那么在该方法没有执行完毕前,其他线程是无法访问该对象的任何synchronized方法的。
synchronized关键字修饰一个方法的时候,该方法叫做同步方法
Java中的每个对象都有一个锁(lock),或者叫做监视器(monitor),当一个线程访问某个对象的synchronized方法时,将该对象上锁,其他任何线程都无法再去访问该对象的synchronized方法了(这里是指所有的同步方法,而不仅仅是同一个方法),直到之前的那个线程执行方法完毕后(或者是抛出了异常),才将该对象的锁释放掉,其他线程才有可能再去访问该对象的synchronized方法。
注意: 这时候是给对象上锁,如果是不同的对象,则各个对象之间没有限制关系。
尝试在代码中构造第二个线程对象时传入一个新的对象,则两个线程的执行之间没有什么制约关系

1.4.1.2 使用不同对象锁

使用不同object时,如果是对象锁,那么由于不同对象实例的对象锁是互不干扰的,多线程是并行执行,且不会按顺序执行了,如果是类锁,那么还会按顺序执行

下面例子中不再使用Worker worker = new Worker();,而是使用new Worker()每次使用新对象,不同对象的线程执行结果就没有什么影响

package cn.jzh.test.thread;

public class Worker {
    public synchronized void executeA(String name) {
        for (int i = 0; i < 10; i++) {  
            System.out.println(name + "-executeA-" + i);  
            try {  
                Thread.sleep(500);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }
        }
    }
    public synchronized void executeB(String name) {
        for (int i = 0; i < 10; i++) {  
            System.out.println(name + "-executeB-" + i);  
            try {  
                Thread.sleep(500);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
   private class SynchronizerWorkerA extends Thread {
        public void run() {  
            executeA("thread1");
        }  
    }  
    private class SynchronizerWorkerB extends Thread {
        public void run() {  
            executeB("thread2");  
        }  
    }              
    public static void main(String args[]) {
        //Worker worker = new Worker();
        new Worker().new SynchronizerWorkerA().start();
        new Worker().new SynchronizerWorkerB().start();     
    }
}

1.4.4.3 Synchronized块

Synchronized块块锁和方法锁一样,都是使得两个线程的执行顺序进行,而不是并发进行,当一个线程执行时,将object对象锁住,另一个线程就不能执行对应的块。
synchronized方法实际上等同于用一个synchronized块包住方法中的所有语句,然后在synchronized块的括号中传入this关键字。当然,如果是静态方法,需要锁定的则是class对象
  
可能一个方法中只有几行代码会涉及到线程同步问题,所以synchronized块synchronized方法更加细粒度地控制了多个线程的访问,只有synchronized块中的内容不能同时被多个线程所访问,方法中的其他语句仍然可以同时被多个线程所访问(包括synchronized块之前的和之后的)。
注意:被synchronized保护的数据应该是私有的。

synchronized方法是一种粗粒度的并发控制,某一时刻,只能有一个线程执行该synchronized方法;
synchronized块则是一种细粒度的并发控制,只会将块中的代码同步,位于方法内、synchronized块之外的其他代码是可以被多个线程同时访问到的

1.4.2 类锁

如果是静态方法的情况,即便是向两个线程传入不同的对象,这两个线程仍然是互相制约的,必须 先执行完一个,再执行下一个,如下使用两个static修饰

package cn.jzh.test.thread;

public class Worker {
    public static synchronized void executeA(String name) {
        for (int i = 0; i < 10; i++) {  
            System.out.println(name + "-executeA-" + i);  
            try {  
                Thread.sleep(500);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }
        }
    }
    public static synchronized void executeB(String name) {
        for (int i = 0; i < 10; i++) {  
            System.out.println(name + "-executeB-" + i);  
            try {  
                Thread.sleep(500);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
   private class SynchronizerWorkerA extends Thread {
        public void run() {  
            executeA("thread1");
        }  
    }  
    private class SynchronizerWorkerB extends Thread {
        public void run() {  
            executeB("thread2");  
        }  
    }              
    public static void main(String args[]) {
        //Worker worker = new Worker();
        new Worker().new SynchronizerWorkerA().start();
        new Worker().new SynchronizerWorkerB().start();     
    }
}

结论:
如果某个synchronized方法是static的,那么当线程访问该方法时,它锁的并不是synchronized方法所在的对象,而是synchronized方法所在的类所对应的Class对象Java中,无论一个类有多少个对象,这些对象会对应唯一一个Class对象,因此当线程分别访问同一个类的两个对象的两个staticsynchronized方法时,它们的执行顺序也是顺序的,也就是说一个线程先去执行方法,执行完毕后另一个线程才开始

这篇关于Java之Synchronized深入理解的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!