07_线程中的锁

线程中的锁

5.1 乐观锁与悲观锁

乐观锁与悲观锁是一种广义上的概念,是从不同角度来看待线程同步操作。这两种锁在 Java 语言以及数据库中都有对应的具体实现。

乐观锁:在并发操作时,乐观锁认为不会有其他线程来修改当前自己待修改的数据,因此乐观锁不会对数据加锁,只是在修改数据的时候,判断一下当前的数据有没有被别的线程修改。如果数据没有被修改,当前线程就会更新数据;如果数据已经被别的线程修改,则根据不同的实现方式执行不同的操作(重试或报错)。乐观锁在 java 中是通过无锁编程实现的,最常用的算法是 CAS,Java 原子类中的自增操作就是通过 CAS 实现的。

悲观锁:在并发操作时,悲观锁认为一定会有其他线程来修改当前自己待修改的数据,因此,在获取数据的时候,会先对数据上锁,确保数据不会被修改。在 Java 中,Synchronized 关键字以及 Lock 的实现类都是悲观锁。

在实际生成中:

  1. 悲观锁适合写操作居多的场景,先对数据加锁,可以保证写操作时数据的正确性
  2. 乐观锁适合读操作居多的场景,不加锁能够提升读操作的性能。

5.2 自旋锁与适应性自旋锁

在计算机系统中,阻塞或唤醒一个 Java 线程需要操作系统切换 CPU 状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块逻辑非常简单,执行代码的时间小于切换 CPU 状态的时间,这种切换是不划算的。在许多场景中,同步资源的锁时间很短,这就不需要让线程进入挂起状态,而是让线程稍微等待一下。

在多线程环境下,如果当前同步资源被其他线程占用,让当前线程自旋(稍等一下),如果在自旋完成后前面锁定同步资源的线程已经释放了锁,当前线程就不必进入阻塞状态而是直接获取同步资源,从而避免线程切换的开销。这就是自旋锁。

自旋锁也有缺点,不能代替阻塞。虽然自旋避免了线程之间切换带来的开销,但是自旋会消耗 CPU 的时间。如果同步资源的锁占用时间很短,则自旋的效果很多,反则反之。所以,自旋需要一定的限度,如果自旋超过了一定的次数,仍然没有成功获取锁,就应当挂起当前线程。自旋的次数默认为 10 次,可以通过参数 -XX:PreBlockSpin 来更改。自旋锁的实现原理是 CAS,在原子类中的自增就是通过一个 do…while 循环来实现自旋,如果修改不成功,进入循环,直到修改成功。

自旋锁是在 JDK1.4 中引入的,使用参数 -XX:+UseSpining 来开启。在 JDK1.6 中,变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。

在适应性自旋锁中,自旋的次数不再固定,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很可能成功的,因此,自旋的时间可以更长。如果对于某个锁,自旋很少成功,那再以后尝试获取这个锁时,很可能线程直接进入阻塞状态,避免浪费处理器资源。

5.3 无锁、偏向锁、轻量级锁、重量级锁

无锁、偏向锁、轻量级锁和重量级锁是锁的四种状态。

Java 对象头:在 Hotspot 虚拟机的对象头中,主要包含两部分数据:Mark Work(标记字段)、Class Pointer(类型指针)

  1. Mark Word:默认存储对象的 HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以 Mark Word 被设计成一个非固定的数据结构以便在极小的空间内存储更多的数据。它会根据对象的状态复用自己的存储空间,在运行期间 Mark Word 中存储的数据会随着锁标志位变化为变化。

  2. Class Pointer:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的示例。

Monitor:

​ Monitor 可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个 Java 对象就有一把看不见的锁,称为内部锁或者 Monitor 锁。Monitor 是线程是有的数据结构,每一个线程都有一个可用 monitor record 列表,同时还有一个全局的可用列表。每一个被锁住的对象都会个一个 Monitor 关联,同时 Monitor 中以一个 Owner 字段存放拥有该锁的线程的唯一标识,表示该所被这个线程占用。

Synchronized 是悲观锁的实现,它能够实现线程同,在操作资源前,会给同步资源加锁,这把锁就在对象头中。Synchronized 通过 Monitor 来实现线程同步,Monitor 是依赖于底层的操作系统中的 Mutex Lock(互斥锁)来实现线程同步的,这种依赖于操作系统 Mutex Lock 实现的锁,被称之为重量级锁。在 JDK 6 之前,Synchronized 效率低,是因为阻塞或唤醒一个线程,需要操作系统切换 CPU 状态来完成,而切换 CPU 状态的消耗比执行用户代码的时间更长。为了减少获得锁与释放锁带来的性能消耗,在 JDK6 中引入了“偏向锁”与“轻量级锁”。

在 Java 中,一种有四种锁状态,级别从低到高分别是:无锁、偏向锁、轻量级锁和重量级锁。锁状态只能升级,不能降级。四种锁状态对应的 Mark Word 内容分别是:

image-20200803232629560

  1. 无锁:

    无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。

    无锁的特点就是修改操作在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。上面我们介绍的 CAS 原理及应用即是无锁的实现。无锁无法全面代替有锁,但无锁在某些场合下的性能是非常高的。

  2. 偏向锁

    偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。

    在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁。其目标就是在只有一个线程执行同步代码块时能够提高性能。

    当一个线程访问同步代码块并获取锁时,会在 Mark Word 里存储锁偏向的线程 ID。在线程进入和退出同步块时不再通过 CAS 操作来加锁和解锁,而是检测 Mark Word 里是否存储着指向当前线程的偏向锁。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次 CAS 原子指令,而偏向锁只需要在置换 ThreadID 的时候依赖一次 CAS 原子指令即可。

    偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态。撤销偏向锁后恢复到无锁(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

    偏向锁在 JDK 6 及以后的 JVM 里是默认启用的。可以通过 JVM 参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。

  3. 轻量级锁

    是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。

    在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的 Mark Word 的拷贝,然后拷贝对象头中的 Mark Word 复制到锁记录中。

    拷贝成功后,虚拟机将使用 CAS 操作尝试将对象的 Mark Word 更新为指向 Lock Record 的指针,并将 Lock Record 里的 owner 指针指向对象的 Mark Word。

    如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象 Mark Word 的锁标志位设置为“00”,表示此对象处于轻量级锁定状态。

    如果轻量级锁的更新操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明多个线程竞争锁。

    若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。

  4. 重量级锁

    升级为重量级锁时,锁标志的状态值变为“10”,此时 Mark Word 中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。

整体的锁状态升级流程如下:

综上,偏向锁通过对比 Mark Word 解决加锁问题,避免执行 CAS 操作。而轻量级锁是通过用 CAS 操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。

5.2 公平与非公平锁

公平锁:是指多个线程按照申请锁的顺序来获取锁,类似排队, FIFO

非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。在高并发的情况下,有可能会造成优先级反转、饥饿现象或者等待很久才能获取锁。其优点是,可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获取锁,CPU 不辞唤醒所有线程。

并发包中的 ReentrantLock 类,其内部有一个内部类 Sync,Sync 继承自 AQS(AbstractQueueSynchronized), 添加锁或释放锁的发部分操作实际上都在 Sync 中实现。它有公平锁 FairSync 和非公平锁 NonfairSync 两个子类。ReentrantLock 可以通过设置构造函数中的参数,来指定当前创建的锁的类型,默认为非公平锁。

Synchronized 是一种非公平锁。

公平锁与非公平锁源码:

可以看出,公平锁与非公平锁的 lock 方法唯一的区别就在于获取同步状态时多了一个限制条件:hasQueuePredecessors()。

在 hasQueuedPredecessors 方法中,主要做的事情是判断当前线程是否是同步队列的头元素。如果是,返回 true,否则返回 false。

综上:公平锁就是通过同步队列来实现多个线程按照申请锁的顺序来获取锁,从而实现公平的特性。非公平锁加锁时不考虑等待队列问题,直接尝试获取锁,因此存在后申请却先获取锁的情况。

5.2 可重入锁(递归锁)

指的是同一线程外层函数获得锁之后,内层函数仍然能获取该锁的代码,在同一个线程,在外层方法获取锁的时候,在进入内层方法会自动获取锁。也就是说,线程可以进入任何一个它已经拥有的锁所同步的代码块。可重入锁最大的作用就是能避免死锁。

可重入锁(ReentrantLock)与非可重入锁(NonReentrantLock)对比:

可重入锁在多次获取锁时,不会出现死锁,而非可重入锁会出现死锁。ReentrantLock 与 NonReentrantLock 都继承自 AQS,其父类 AQS 中维护了一个同步状态 status 来计数重入次数,status 初始状态为 0。

当线程尝试获取锁时,可重入锁先尝试获取并更新 status 值,如果 status == 0 表示没有其他线程在执行同步代码,则把 status 置为 1,当前线程开始执行。如果 status != 0,则判断当前线程是否是获取到这个锁的线程,如果是的话执行 status+1,且当前线程可以再次获取锁。而非可重入锁是直接去获取并尝试更新当前 status 的值,如果 status != 0 的话会导致其获取锁失败,当前线程阻塞。

释放锁时,可重入锁同样先获取当前 status 的值,在当前线程是持有锁的线程的前提下。如果 status-1 == 0,则表示当前线程所有重复获取锁的操作都已经执行完毕,然后该线程才会真正释放锁。而非可重入锁则是在确定当前线程是持有锁的线程之后,直接将 status 置为 0,将锁释放。

ReentrantLock 重入锁示例:

public class ReentrantLockDemo {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(phone, "t1").start();
        new Thread(phone, "t2").start();
    }
}

class Phone implements Runnable {
    Lock lock = new ReentrantLock();

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

    public void set() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " set lock");
        } finally {
            lock.unlock();
        }
    }

    public void get() {
        lock.lock();
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " get lock");
            set();
        } finally {
            lock.unlock();
            lock.unlock();
        }
    }
}

Synchronized 可重入锁示例:

public class ReentrantLockDemo {
    public static void main(String[] args) {
		lock1();
    }

    private synchronized static void lock1() {
        System.out.println("lock1");
        lock2();
    }

    private synchronized static void lock2() {
        System.out.println("lock2");
    }
}

5.3 自旋锁

指尝试获取锁的线程,在未获取到锁时,不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少了线程上下文切换的消耗,缺点是循环获取锁会消耗 CPU。在 CAS 中,UnSafe 类中的 getAndAddInt 方法就是采用的这种方式来更新值。

import java.util.concurrent.atomic.AtomicReference;

public class SpinLockDemo {
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    public static void main(String[] args) {
        SpinLockDemo spinLockDemo = new SpinLockDemo();
        new Thread(() -> {
            spinLockDemo.lock();
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLockDemo.unLock();
        }, "Thread1").start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            spinLockDemo.lock();
            spinLockDemo.unLock();
        }, "Thread2").start();

    }

    public void lock() {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() + " come in");
        Thread thread1 = null;
        while (!atomicReference.compareAndSet(thread1, thread)) {
            thread1 = atomicReference.get();
        }
        System.out.println(thread.getName() + " lock success");
    }

    public void unLock() {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() + " unlock");
        atomicReference.compareAndSet(thread, null);
        System.out.println(thread.getName() + " unlock success");
    }
}

5.4 独占锁、共享锁、互斥锁

独占锁(写锁):指该锁一次只能被一个线程所持有,对 ReentrantLock 和 Synchronized 而言,都是独占锁

共享锁(读锁):指该锁可以被多个线程所持有

对 ReentrantReadWriteLock,其读锁是共享锁,其写锁是独占锁

读锁(共享锁)能够保证高并发读是非常高效的,但是读写、写读、写写的过程都是互斥的。

独占锁与共享锁都是通过 AQS 来实现的,通过实现不同的方法,来实现独占或共享。

ReentrantReadWriteLock 的部分源码:

我们看到 ReentrantReadWriteLock 有两把锁:ReadLock 和 WriteLock,由词知意,一个读锁一个写锁,合称“读写锁”。再进一步观察可以发现 ReadLock 和 WriteLock 是靠内部类 Sync 实现的锁。Sync 是 AQS 的一个子类,这种结构在 CountDownLatch、ReentrantLock、Semaphore 里面也都存在。

在 ReentrantReadWriteLock 里面,读锁和写锁的锁主体都是 Sync,但读锁和写锁的加锁方式不一样。读锁是共享锁,写锁是独享锁。读锁的共享锁可保证并发读非常高效,而读写、写读、写写的过程互斥,因为读锁和写锁是分离的。所以 ReentrantReadWriteLock 的并发性相比一般的互斥锁有了很大提升。

那读锁和写锁的具体加锁方式有什么区别呢?在了解源码之前我们需要回顾一下其他知识。 在最开始提及 AQS 的时候我们也提到了 state 字段(int 类型,32 位),该字段用来描述有多少线程获持有锁。

在独享锁中这个值通常是 0 或者 1(如果是重入锁的话 state 值就是重入的次数),在共享锁中 state 就是持有锁的数量。但是在 ReentrantReadWriteLock 中有读、写两把锁,所以需要在一个整型变量 state 上分别描述读锁和写锁的数量(或者也可以叫状态)。于是将 state 变量“按位切割”切分成了两个部分,高 16 位表示读锁状态(读锁个数),低 16 位表示写锁状态(写锁个数)。如下图所示:

写锁的加锁源码:

protected final boolean tryAcquire(int acquires) {
	Thread current = Thread.currentThread();
	int c = getState(); // 取到当前锁的个数
	int w = exclusiveCount(c); // 取写锁的个数w
	if (c != 0) { // 如果已经有线程持有了锁(c!=0)
    // (Note: if c != 0 and w == 0 then shared count != 0)
		if (w == 0 || current != getExclusiveOwnerThread()) // 如果写线程数(w)为0(换言之存在读锁) 或者持有锁的线程不是当前线程就返回失败
			return false;
		if (w + exclusiveCount(acquires) > MAX_COUNT)    // 如果写入锁的数量大于最大数(65535,2的16次方-1)就抛出一个Error。
      throw new Error("Maximum lock count exceeded");
		// Reentrant acquire
    setState(c + acquires);
    return true;
  }
  if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) // 如果当且写线程数为0,并且当前线程需要阻塞那么就返回失败;或者如果通过CAS增加写线程数失败也返回失败。
		return false;
	setExclusiveOwnerThread(current); // 如果c=0,w=0或者c>0,w>0(重入),则设置当前线程或锁的拥有者
	return true;
}
  • 这段代码首先取到当前锁的个数 c,然后再通过 c 来获取写锁的个数 w。因为写锁是低 16 位,所以取低 16 位的最大值与当前的 c 做与运算( int w = exclusiveCount©; ),高 16 位和 0 与运算后是 0,剩下的就是低位运算的值,同时也是持有写锁的线程数目。
  • 在取到写锁线程的数目后,首先判断是否已经有线程持有了锁。如果已经有线程持有了锁(c!=0),则查看当前写锁线程的数目,如果写线程数为 0(即此时存在读锁)或者持有锁的线程不是当前线程就返回失败(涉及到公平锁和非公平锁的实现)。
  • 如果写入锁的数量大于最大数(65535,2 的 16 次方-1)就抛出一个 Error。
  • 如果当且写线程数为 0(那么读线程也应该为 0,因为上面已经处理 c!=0 的情况),并且当前线程需要阻塞那么就返回失败;如果通过 CAS 增加写线程数失败也返回失败。
  • 如果 c=0, w=0 或者 c>0, w>0(重入),则设置当前线程或锁的拥有者,返回成功!

tryAcquire()除了重入条件(当前线程为获取了写锁的线程)之外,增加了一个读锁是否存在的判断。如果存在读锁,则写锁不能被获取,原因在于:必须确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。

因此,只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦被获取,则其他读写线程的后续访问均被阻塞。写锁的释放与 ReentrantLock 的释放过程基本类似,每次释放均减少写状态,当写状态为 0 时表示写锁已被释放,然后等待的读写线程才能够继续访问读写锁,同时前次写线程的修改对后续的读写线程可见。

接着是读锁的代码:

protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;                                   // 如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态
    int r = sharedCount(c);
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            firstReaderHoldCount++;
        } else {
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;
        }
        return 1;
    }
    return fullTryAcquireShared(current);
}

可以看到在 tryAcquireShared(int unused)方法中,如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态。如果当前线程获取了写锁或者写锁未被获取,则当前线程(线程安全,依靠 CAS 保证)增加读状态,成功获取读锁。读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少的值是“1<<16”。所以读写锁才能实现读读的过程共享,而读写、写读、写写的过程互斥。

此时,再回头看一下互斥锁 ReentrantLock 中公平锁和非公平锁的加锁源码:

我们发现在 ReentrantLock 虽然有公平锁和非公平锁两种,但是它们添加的都是独享锁。根据源码所示,当某一个线程调用 lock 方法获取锁时,如果同步资源没有被其他线程锁住,那么当前线程在使用 CAS 更新 state 成功后就会成功抢占该资源。而如果公共资源被占用且不是被当前线程占用,那么就会加锁失败。所以可以确定 ReentrantLock 无论读操作还是写操作,添加的锁都是都是独享锁。

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCache myCache = new MyCache();
        for (int i = 0; i < 30; i++) {
            int finalI = i;
            new Thread(() -> myCache.put(String.valueOf(finalI), UUID.randomUUID().toString()), "Thread" + finalI).start();
        }

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        for (int i = 0; i < 30; i++) {
            int finalI = i;
            new Thread(() -> myCache.get(String.valueOf(finalI)), "Thread" + finalI).start();
        }
    }
}

class MyCache {
    private volatile Map<String, Object> map = new HashMap<>();
    Lock readLock = new ReentrantReadWriteLock().readLock();
    Lock writeLock = new ReentrantReadWriteLock().writeLock();

    /**
     * 由于在写入数据之前添加了写锁,所以一次只能有一个线程执行写操作
     * @param key
     * @param value
     */
    public void put(String key, Object value) {
        System.out.println(Thread.currentThread().getName() + " is writing, the key is: " + key);
        writeLock.lock();
        try {
            map.put(key, value);
        } finally {
            writeLock.unlock();
        }
        System.out.println(Thread.currentThread().getName() + " has written successfully");
    }

    /**
     * 在获取数据之前,添加读锁。由于读锁是共享锁,多个线程可以同时获取数据
     * @param key
     */
    public void get(String key) {
        System.out.println(Thread.currentThread().getName() + " is getting, the key is: " + key);
        readLock.lock();
        try {
            Object value = map.get(key);
            System.out.println(Thread.currentThread().getName() + " has read, the value is: " + value);
        } finally {
            readLock.unlock();
        }
    }
}

线程八锁

结论

  1. 普通同步方法锁定的是当前实例对象(this),简称对象锁。一个对象锁在同一时刻只能被一个线程持有,当某个线程持有了某个对象锁(调用该对象的普通同步方法),其他想要调用该对象的普通同步方法的线程,只能等待。

  2. 静态同步方法锁定的是当前类的 Class 对象(每个类只有一个),简称类锁。一个类锁在同一时刻只能被一个线程持有,当某个线程某个类锁(调用该类中的静态同步方法),其他想要调用该类中静态同步方法的线程,只能等待。

  3. 对一个类而言,对象锁可以有多把,而类锁只有一把

  4. 一个对象,在不同的线程中,同时调用普通同步方法和普通方法,线程之间互不影响

  5. 不同线程中,一个线程调用普通同步方法,另一个线程调用静态同步方法,线程之间互不影响

一、同一个实例对象,两个线程,调用不同的普通同步方法

代码:


public class LockTest {
    public static void main(String[] args) {

        Phone phone = new Phone();
        new Thread(phone::sendEmail, "ThreadA").start();

        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(phone::sendMsg, "ThreadB").start();

    }
}

class Phone {

    public synchronized void sendEmail() {
        System.out.println("sendEmail");
    }

    public synchronized void sendMsg() {
        System.out.println("sendMsg");
    }

    public void sendWeChat() {
        System.out.println("send WeChat");
    }
}

执行结果:

image-20210313131313892

分析:

  • 同一个实例对象,两个线程,调用不同的普通同步方法

  • 一个对象中如果有多个被 synchronized 修饰的普通方法,在某一时刻,一个线程调用了其中某一个同步方法,则其他需要调用同步方法的线程都只能等待。

  • 换句话说,在同一时刻,对于同一个对象,只能有一个线程去访问 synchronized 方法。

  • 此时锁的是当前对象(this),被锁定后,其他线程都不能进入到当前对象的其他 synchronized 方法

二、同一个实例对象,两个线程(某个线程短暂睡眠),调用不同的普通同步方法

public class LockTest {
    public static void main(String[] args) {

        Phone phone = new Phone();
        new Thread(phone::sendEmail, "ThreadA").start();

        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(phone::sendMsg, "ThreadB").start();

    }
}

class Phone {

    public synchronized void sendEmail() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("sendEmail");
    }

    public synchronized void sendMsg() {
        System.out.println("sendMsg");
    }

    public void sendWeChat() {
        System.out.println("send WeChat");
    }
}

执行结果:

image-20210313131313892

分析:

  • 同一个实例对象,两个线程,调用不同的普通同步方法
  • 一个对象中如果有多个被 synchronized 修饰的普通方法,在某一时刻,一个线程调用了其中某一个同步方法,则其他需要调用同步方法的线程都只能等待。
  • 换句话说,在同一时刻,对于同一个对象,只能有一个线程去访问 synchronized 方法。
  • 此时锁的是当前对象(this),被锁定后,其他线程都不能进入到当前对象的其他 synchronized 方法

三、同一个实例对象,两个线程,线程 A 调用普通同步方法(短暂阻塞),线程 B 调用普通方法

public class LockTest {
    public static void main(String[] args) {

        Phone phone = new Phone();
        new Thread(phone::sendEmail, "ThreadA").start();

        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(phone::sendWeChat, "ThreadB").start();

    }
}

class Phone {

    public synchronized void sendEmail() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("sendEmail");
    }

    public synchronized void sendMsg() {
        System.out.println("sendMsg");
    }

    public void sendWeChat() {
        System.out.println("send WeChat");
    }
}

执行结果:

image-20210313131413008

分析:

  • 同一个实例对象,两个线程,线程 A 调用普通同步方法(短暂阻塞),线程 B 调用普通方法
  • 线程 A 与线程 B 的运行互不影响
  • 换句话说,即使同步方法被阻塞,普通方法依然能够正常运行(线程 A 使用手机,线程 B 在玩手机壳

四、两个对象,两个线程,调用不同的普通同步方法

public class LockTest {
    public static void main(String[] args) {

        Phone phone1 = new Phone();
        Phone phone2 = new Phone();
        new Thread(phone1::sendEmail, "ThreadA").start();

        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(phone2::sendMsg, "ThreadB").start();

    }
}

class Phone {

    public synchronized void sendEmail() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("sendEmail");
    }

    public synchronized void sendMsg() {
        System.out.println("sendMsg");
    }

    public void sendWeChat() {
        System.out.println("send WeChat");
    }
}

执行结果:

image-20210313131450092

分析:

  • 两个对象,两个线程,调用不同的普通同步方法

  • 线程之间互不影响

  • 因为调用普通同步方法时,锁定的是当前对象(this),由于是两个对象,所以此时的锁不是同一把锁,因此两个线程互不影响

五、两个线程,调用不同的静态同步方法


public class LockTest {
    public static void main(String[] args) {

        Phone phone1 = new Phone();
        //Phone phone2 = new Phone();
        new Thread(Phone::sendEmail, "ThreadA").start();

        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(Phone::sendMsg, "ThreadB").start();

    }
}

class Phone {

    public static synchronized void sendEmail() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("sendEmail");
    }

    public static synchronized void sendMsg() {
        System.out.println("sendMsg");
    }

    public void sendWeChat() {
        System.out.println("send WeChat");
    }
}

执行结果:

image-20210313131313892

分析:

  • 两个线程,调用不同的静态同步方法
  • 在某个时刻,类中的某一个静态同步方法被线程调用,其他需要调用静态同步方法的线程都只能等待
  • 在同一时刻,只能有一个线程调用类中的静态同步方法
  • 此时锁的是当前类的 Class 对象,每个类只有一个 Class 对象

六、两个线程,调用不同的静态同步方法


public class LockTest {
    public static void main(String[] args) {

        Phone phone1 = new Phone();
        Phone phone2 = new Phone();
        new Thread(Phone::sendEmail, "ThreadA").start();

        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(Phone::sendMsg, "ThreadB").start();

    }
}

class Phone {

    public static synchronized void sendEmail() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("sendEmail");
    }

    public static synchronized void sendMsg() {
        System.out.println("sendMsg");
    }

    public void sendWeChat() {
        System.out.println("send WeChat");
    }
}

image-20210313131313892

分析:

  • 两个线程,调用不同的静态同步方法
  • 在某个时刻,类中的某一个静态同步方法被线程调用,其他需要调用静态同步方法的线程都只能等待
  • 在同一时刻,只能有一个线程调用类中的静态同步方法
  • 此时锁的是当前类的 Class 对象,每个类只有一个 Class 对象

七、两个线程,一个调用普通同步方法,一个调用静态同步方法

public class LockTest {
    public static void main(String[] args) {

        Phone phone1 = new Phone();
        //Phone phone2 = new Phone();
        new Thread(Phone::sendEmail, "ThreadA").start();

        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(phone1::sendMsg, "ThreadB").start();

    }
}

class Phone {

    public static synchronized void sendEmail() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("sendEmail");
    }

    public synchronized void sendMsg() {
        System.out.println("sendMsg");
    }

    public void sendWeChat() {
        System.out.println("send WeChat");
    }
}

执行结果:

image-20210313131450092

分析:

  • 两个线程,一个调用普通同步方法,一个调用静态同步方法
  • 两个线程互不影响
  • 调用普通同步方法的线程,锁定的是当前实例对象;调用静态同步方法的线程,锁定的是当前类对象。
  • 两个线程持有的是两把不同的锁,因此两个线程互不影响

八、两个线程,一个调用普通同步方法,一个调用静态同步方法


public class LockTest {
    public static void main(String[] args) {

        Phone phone1 = new Phone();
        Phone phone2 = new Phone();
        new Thread(Phone::sendEmail, "ThreadA").start();

        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(phone1::sendMsg, "ThreadB").start();

    }
}

class Phone {

    public static synchronized void sendEmail() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("sendEmail");
    }

    public synchronized void sendMsg() {
        System.out.println("sendMsg");
    }

    public void sendWeChat() {
        System.out.println("send WeChat");
    }
}

执行结果:

image-20210313131450092

分析:

  • 两个线程,一个调用普通同步方法,一个调用静态同步方法
  • 两个线程互不影响
  • 调用普通同步方法的线程,锁定的是当前实例对象;调用静态同步方法的线程,锁定的是当前类对象。
  • 两个线程持有的是两把不同的锁,因此两个线程互不影响
    mail, “ThreadA”).start();
    try {
        Thread.sleep(100);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    new Thread(phone1::sendMsg, "ThreadB").start();

}

}

class Phone {

public static synchronized void sendEmail() {
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("sendEmail");
}

public synchronized void sendMsg() {
    System.out.println("sendMsg");
}

public void sendWeChat() {
    System.out.println("send WeChat");
}

}


执行结果:



[外链图片转存中...(img-wNdxi6FZ-1626327886804)]



> 分析:
>
> -   两个线程,一个调用普通同步方法,一个调用静态同步方法
> -   两个线程互不影响
> -   调用普通同步方法的线程,锁定的是当前实例对象;调用静态同步方法的线程,锁定的是当前类对象。
> -   两个线程持有的是两把不同的锁,因此两个线程互不影响
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值