Java多线程之Synchronized深入理解

1 Synchronized

1.1 引言

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

synchronized关键字可保证三大性:原子性,有序性,可见性,volatile只能保证有序性,可见性,不能保证原子性。

术语 英文 说明
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 同步概念monitorenter&monitorexit

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

monitorenter&monitorexit讲解:

  • monitorenter
    每一个对象都有一个monitor,一个monitor只能被一个线程拥有。当一个线程执行到monitorenter指令时会尝试获取相应对象的monitor,获取规则如下:
    • 如果monitor的进入数为0,则该线程可以进入monitor,并将monitor进入数设置为1,该线程即为monitor的拥有者。
    • 如果当前线程已经拥有该monitor,只是重新进入,则进入monitor的进入数加1,所以synchronized关键字实现的锁是可重入的锁。
    • 如果monitor已被其他线程拥有,则当前线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor
  • monitorexit
    只有拥有相应对象的monitor的线程才能执行monitorexit指令。每执行一次该指令monitor进入数减1,当进入数为0时当前线程释放monitor,此时其他阻塞的线程将可以尝试获取该monitor

1.2.4 Synchronized概念

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

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

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

1.2.5 Synchronized阻塞影响

java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在用户态核心态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。

如果线程状态切换是一个高频操作时,这将会消耗很多CPU处理时间;
如果对于那些需要同步的简单的代码块,获取锁挂起操作消耗的时间比用户代码执行的时间还要长,这种同步策略显然非常糟糕的。
synchronized会导致争用不到锁的线程进入阻塞状态,所以说它是java语言中一个重量级的同步操纵,被称为重量级锁

1.2.6 为什么Synchronized是重量级锁

由于Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的 Mutex Lock 来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此, 这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为“重量级锁”

线程上下文切换概念
巧妙地利用了时间片轮转的方式, CPU给每个任务都服务一定的时间,然后把当前任务的状态保存下来,在加载下一任务的状态后,继续服务下一任务, 任务的状态保存及再加载, 这段过程就叫做上下文切换。时间片轮转的方式使多个任务在同一颗 CPU 上执行变成了可能
在这里插入图片描述
引起线程上下文切换的原因

  • 当前执行任务的时间片用完之后,系统 CPU 正常调度下一个任务;
  • 当前执行任务碰到 IO 阻塞,调度器将此任务挂起,继续下一任务;
  • 多个任务抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续下一任务;
  • 用户代码挂起当前任务,让出 CPU 时间;
  • 硬件中断;

1.2.7 可重入锁概念

如果锁具备可重入性,则称作为可重入锁synchronized是可重入锁,可重入性实际上表明了锁的分配机制:基于线程分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2
看下面这段代码就明白了:

class MyClass {
    public synchronized void method1() {
        method2();
    }
     
    public synchronized void method2() {}
}

上述代码中的两个方法method1method2都用synchronized修饰了,假如某一时刻,线程A执行到了method1,此时线程A获取了这个对象的锁,而由于method2也是synchronized方法,假如synchronized不具备可重入性,那么此时线程A需要重新申请锁。但是这就会造成一个问题,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁。而由于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对象头和各种锁基础理解

对于实例对象synchronized锁,锁的就是类实例对象头中的markword关键字
对于 synchronized 用在静态方法或静态代码块上时,实际上锁定的是类的 Class 对象。每个 Java 类在 JVM 中都有一个唯一的 Class 对象,这个对象代表了类的元数据。

  • Class 对象:在 JVM 中,每个类都有一个 Class 对象,这个对象在类加载时创建,并且代表了类的元数据(如类的结构、方法、字段等)。
  • 锁定 Class 对象:当在静态方法或静态同步块上使用 synchronized 时,JVM 实际上是锁定了这个 Class 对象。换句话说,所有对该类的静态同步方法或静态同步块的访问都会受到该 Class 对象锁的控制。
  • 全局锁:由于 Class 对象是类级别的,所以所有实例共享同一个 Class 对象锁。这确保了在同一时间内只有一个线程可以执行该类的任何静态同步方法或同步代码块。

1.3.3 Synchronized锁的操作原理

经过多次demo验证后才知道Synchronized判断能不能锁住是看的地址,同一个地址是会锁住,不同的地址就锁不住

1.3.3.1 获取对象地址

在java中万物皆是对象,有对象就有地址,Object中获取地址方法是默认调用toString方法

 public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }
public native int hashCode();

但是本次测试获取地址用的是System.identityHashCode()方法,该方法是native方法,和hashCode()类似,在Object中查看地址都要转为16进制(Integer.toHexString方法):

  • hashCode()未被重写时,System.identityHashCode()hashCode()返回值相同,都是调用底层native方法
  • hashCode()被重写,则System.identityHashCode()hashCode()返回值不同。hashCode()返回重写结果,System.identityHashCode()返回底层生成的hashcode
  • 获取方式可以采用String.class.hashCode()方式或者System.identityHashCode(String.class)

1.3.3.2 Demo示例

public class Sync2Demo extends Thread{
    public Sync2Demo( ) {    }
    public Sync2Demo(String name){ super(name);}
    private int i=2;
    private String s1 = "123";
    private String s2 = new String("123");
    private Integer i1= 123;
    private Integer i2= new Integer(123);
    @SneakyThrows
    @Override
    public void run() {
        String s1 = "123";//顺序
        String s2 = new String();//并发
        Integer i1= 123;//小于128 顺序  大于或等于128并发
        Integer i2= new Integer(123);//并发
        while (true) {
            System.out.println(Thread.currentThread().getName() +"进入方法前===synchronized中地址==="+Integer.toHexString(System.identityHashCode(String.class)));
            synchronized (String.class){
                System.out.println(Thread.currentThread().getName() +"进入方法后===synchronized中地址==="+Integer.toHexString(System.identityHashCode(String.class)));
                if (i>0){
                    Thread.sleep(500);
                    System.out.println(Thread.currentThread().getName() + " in block");
                    Thread.sleep(500);
                    System.out.println(Thread.currentThread().getName()+" out block ,i = "+i);
                    i--;
                }else{
                    break;
                }
            }
        }
    }
    public static void main(String[] args) {
        new Sync2Demo("Thread-1").start();
        new Sync2Demo("Thread-2").start();
    }
}

1.3.3.3 Demo结果分析

1.3.3.3.1 synchronized (其他类.class)

synchronized(String.class) 加载后只加载一次所以在内存中地址一样,就会控制并发
在这里插入图片描述

1.3.3.3.2 synchronized (本类.class)

synchronized(Sync2Demo.class)加载后只加载一次所以在内存中地址一样,也会控制并发
在这里插入图片描述

1.3.3.3.3 synchronized (全局变量String-常量池)

synchronized(s1) 此处s1是全局变量但是s1="123",是在字符串常量池中,地址还是一样不变,因此还是会阻塞操作
在这里插入图片描述

1.3.3.3.4 synchronized (全局变量String-new String)

String s2 = new String("123");synchronized(s2)
此处s2是全局变量但是s2=new String("123"),地址是在堆内存中,每次加载时地址是不一样的,所以锁不住对象,会并发
在这里插入图片描述

1.3.3.3.5 synchronized (全局变量Integer < 128)

Integer i1= 123; synchronized(i1),此处i1是全局变量但是i1=123<128,Integer会自动缓存[-128,127]之间的数据到缓存池中,超过了就在堆中new对象,因此每次加载时地址是一样的,可以锁住对象,阻塞并发
在这里插入图片描述

1.3.3.3.6 synchronized (全局变量Integer >= 128)

Integer i1= 128; synchronized(i1)
此处i1是全局变量但是i1=128,由于Integer会自动缓存[-128,127]之间的数据到缓存池中,超过了就在堆中new对象,因此此处每次加载时地址是不一样的,锁不住对象,会并发

在这里插入图片描述

1.3.3.3.7 synchronized (全局变量Integer-new Integer)

Integer i2= new Integer(123); synchronized(i2),
此处i2是全局变量是new出来的堆中对象,这种情况和new String 一样,每次加载时地址不一样,就锁不住对象,会并发
在这里插入图片描述

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.1.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方法时,它们的执行顺序也是顺序的,也就是说一个线程先去执行方法,执行完毕后另一个线程才开始

1.4.3 死锁

线程发生死锁可能性很小,即使看似可能发生死锁的代码,在运行时发生死锁的可能性也是小之又小。
发生死锁的原因一般是两个对象的锁相互等待造成的

使用Synchronized实现死锁操作如下:

public class DeadLockDemo {
    public static void main(String[] args) {
        DeadlockRisk dead = new DeadlockRisk();
        MyThread t1 = new MyThread(dead, 1, 2);
        MyThread t2 = new MyThread(dead, 3, 4);
        MyThread t3 = new MyThread(dead, 5, 6);
        MyThread t4 = new MyThread(dead, 7, 8);

        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }

    static class MyThread extends Thread {
        private DeadlockRisk dead;
        private int a, b;


        MyThread(DeadlockRisk dead, int a, int b) {
            this.dead = dead;
            this.a = a;
            this.b = b;
        }

        @Override
        public void run() {
            dead.read();
            dead.write(a, b);
        }
    }

    static class DeadlockRisk {
        private static class Resource {
            public int value;
        }

        private Resource resourceA = new Resource();
        private Resource resourceB = new Resource();

        public int read() {
            synchronized (resourceA) {
                System.out.println("read():" + Thread.currentThread().getName() + "获取了resourceA的锁!");
                synchronized (resourceB) {
                    System.out.println("read():" + Thread.currentThread().getName() + "获取了resourceB的锁!");
                    return resourceB.value + resourceA.value;
                }
            }
        }

        public void write(int a, int b) {
            synchronized (resourceB) {
                System.out.println("write():" + Thread.currentThread().getName() + "获取了resourceB的锁!");
                synchronized (resourceA) {
                    System.out.println("write():" + Thread.currentThread().getName() + "获取了resourceA的锁!");
                    resourceA.value = a;
                    resourceB.value = b;
                }
            }
        }
    }
}

运行结果
read():Thread-0获取了resourceA的锁!
read():Thread-0获取了resourceB的锁!
write():Thread-0获取了resourceB的锁!
read():Thread-3获取了resourceA的锁!

此时线程就因竞争锁而陷入死锁了

1.5 和其他锁比较

1.5.1 CAS锁和Synchronized比较

假如cas可以保证操作的线程安全吗,为什么还要用Synchronized
原因:CAS也是适用一些场合的,比如资源竞争小时,是非常适用的,不用进行内核态和用户态之间的线程上下文切换,同时自旋概率也会大大减少,提升性能,但资源竞争激烈时(比如大量线程对同一资源进行写和读操作)并不适用,自旋概率会大大增加,浪费CPU资源,降低性能,就很不划算

1.5.2 和ReentrantLock 的区别

  1. ReentrantLock 显示的获得、释放锁, synchronized 隐式获得释放锁
  2. ReentrantLock 可响应中断、可轮回, synchronized 是不可以响应中断的,为处理锁的不可用性提供了更高的灵活性
  3. ReentrantLockAPI 级别的, synchronized 是 JVM 级别的
  4. ReentrantLock可以实现公平锁
  5. ReentrantLock 通过 Condition 可以绑定多个条件
  6. 底层实现不一样, synchronized 是同步阻塞,使用的是悲观并发策略, lock 是同步非阻塞,采用的是乐观并发策略
  7. Lock 是一个接口,而 synchronizedJava 中的关键字, synchronized 是内置的语言实现。
  8. synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁。
  9. Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断。
  10. 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
  11. Lock 可以提高多个线程进行读操作的效率,既就是实现读写锁等

1.6 线程同步

  1. 线程同步的目的是为了保护多个线程访问一个资源时对资源的破坏
  2. 线程同步方法是通过锁来实现,每个对象都有切仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的其他同步方法。
  3. 对于静态同步方法,锁是针对这个类的,锁对象是该类的Class对象。静态和非静态方法的锁互不干预。一个线程获得锁,当在一个同步方法中访问另外对象上的同步方法时,会获取这两个对象锁。
  4. 对于同步,要时刻清醒在哪个对象上同步,这是关键。
  5. 编写线程安全的类,需要时刻注意对多个线程竞争访问资源的逻辑和安全做出正确的判断,对原子操作做出分析,并保证原子操作期间别的线程无法访问竞争资源。
  6. 当多个线程等待一个对象锁时,没有获取到锁的线程将发生阻塞。
  7. 死锁是线程间相互等待锁锁造成的,在实际中发生的概率非常的小。真写个死锁程序,不一定好使,但是,一旦程序发生死锁,程序将死掉。
posted @ 2021-12-19 20:48  上善若泪  阅读(540)  评论(0编辑  收藏  举报