多线程笔记2——并发之共享模型

内容:Synchronized及底层原理;ReentrantLock

1.临界区:一段代码块如果存在对共享资源的多线程读写操作称这个代码块为临界区

为了避免临界区的竞争条件发生,有两种方法:

阻塞式:synchronized, Lock(ReentrantLock对象)       非阻塞式:原子变量

2.synchronized关键字:实际是用对象锁保证了临界区代码的原子性

两种用法:锁住代码块 或者 加在方法上,当加在方法上时:

加在普通方法上等效于: synchronized(this){}    本质是锁 this实例对象

加在静态方法上等效于: synchronized(类.class){}     本质是锁 类对象

3.变量的线程安全分析

成员变量和静态变量:看该变量是否共享,且是否有读写操作(如果只有读操作,则线程安全)

局部变量:线程安全(因为栈帧是每个线程私有的);但是如果局部变量引用了同一个堆对象,不安全。

如果子类继承了父类,也可能引起线程安全问题。所以private和final的修饰很重要(闭合原则),可以防止子类调用父类对象引起的线程安全问题。

4.常见的线程安全类

String;Integer等包装类;StringBuffer;Random;Vector;Hashtable;java.util.concurrent包下的类

String,Integer等都属于不可变类,都是线程安全的。

eg. String为什么是final类,为了防止子类覆盖引用父类,使得线程不安全

5.使用lock解决临界区的线程安全问题(Reentrantlock)

相对于Synchronized的优点:可中断,支持锁超时,支持公平锁,支持多个条件变量;同时与synchronized一样都支持可重入

基本语法:lock()加锁, unlock()解锁

reentrantLock.lock();    //获取锁
try{
    //临界区
}finally{
    reentrantLock.unlock();    //释放锁
}

(1)可重入:指同一个线程可重复递归调用锁。

(2)可打断:在阻塞队列等待的过程中可以被其他线程调用interrupt()方法打断。

private static ReentrantLock lock = new ReentrantLock();

public static void main(String[] args){
    new Thread(() -> {
        try{
            //如果没有竞争,此方法会获取Lock对象的锁
            //如果有竞争,会进入阻塞队列,可以被其他线程用interrupt方法打断
            lock.lockInterruptibly();
        }catch(InterruptedException e){
            e.printStackTrace();
        }
    }).start();
}

(3)锁超时:设置线程等待时间。tryLock(等待时间,时间单位)或tryLock()方法

(4)公平性:synchronized会在阻塞队列中随机选取一个,而不是先来先到顺序,因此是不公平锁。

                  ReentrantLock默认也是不公平锁,但可以设置成公平锁:ReentrantLock lock = new ReentrantLock(true)

(5)条件变量:synchronized中也有条件变量:线程满足条件时继续执行,不满足条件时进入waitList等待

                     ReentrantLock支持多个条件变量(waitList)

static ReentrantLock lock = new ReentrantLock();

public void static main(String[] args){
    
    Condition condition1 = lock.newCondition();    //创建两个条件变量
    Condition condition2 = lock.newCondition();

    lock.lock();
    condition1.await();    //效果等同于wait()

}


其他线程可以用condition1.signal()或者condition1.signalAll()方法唤醒

6.线程活跃性问题:活锁,死锁,饥饿

(1)死锁:一个线程同时需要获取多把锁,容易产生死锁

eg1. Thread1已经获得了A对象锁,接下来想获得B对象锁

        Thread2已经获得了B对象锁,接下来想获得A对象锁

        此时发生死锁

eg2. 哲学家就餐问题

定位死锁方法:jconsole工具或者先用jps定位线程ID,再用jstack定位死锁

(2)活锁:两个线程互相改变对方的结束条件,最后谁也无法结束

static volatile int count = 10;

new Thread(() -> {
    while(count > 0){
        count--;
    }
}).start();

new Thread(() -> {
    while(count < 20){
        count++;
    }
}).start();

(3)饥饿:一个线程由于优先级太低,始终得不到CPU调度执行,无法结束

7.互斥和同步

互斥:使用synchronized或者lock达到共享资源互斥,代码原子性

同步:使用wait/notify或者Lock的条件变量完成线程间通信

8.Synchronized底层原理

8.1 对象头

每个对象都有对象头:

eg. Integer对象大小为:8byte(对象头)+4byte(数据)+4byte(自动补齐) = 16字节

      int大小为:4byte  = 4字节

对象头:以32位虚拟机为例。MarkWord:存储该对象的信息,KlassWord:指针指向该对象的类型

普通对象:MarkWord(32bits) + KlassWord(32bits)   共64bits8个字节

数组对象:MarkWord(32bits) + KlassWord(32bits) + arraylength(32bits)

MarkWord具体细节(下图):以Normal为例,分别记录:hashcode值,垃圾回收分代年龄,是否是偏向锁,锁标志位

多线程笔记2——并发之共享模型_第1张图片

多线程笔记2——并发之共享模型_第2张图片

8.2 synchronized底层:Monitor

Monitor也称监视器或者管程。每个java对象都可以关联一个Monitor对象(该对象是操作系统提供的),MarkWord会改变。

注:如果加在静态方法上,则 类.class 类对象也是对象。

多线程笔记2——并发之共享模型_第3张图片

注:如果不加synchronized则不会关联monitor

字节码角度:

对象加锁前:会先执行dup,复制该对象(包含原始MarkWord等信息)到变量槽slot中。

加锁:执行monitorenter,将lock对象MarkWord置为Monitor指针。

执行完毕后解锁:从slot取出原始信息恢复原来MarkWord信息,唤醒Entrylist

8.3 锁

(1)轻量级锁

使用场景:多线程的访问时间是错开的(无竞争)

步骤:

首先栈帧中会创建锁记录(Lock Record)对象,锁记录中包含:该对象的地址 和 锁记录

多线程笔记2——并发之共享模型_第4张图片

加锁时,会把引用地址指向该对象,并尝试用CAS把锁记录与该对象的MarkWord做交换,如下图,该对象由的锁标志位由01(无锁)变成00(轻量级锁),表示已经加锁。

多线程笔记2——并发之共享模型_第5张图片

如果CAS失败,有两种情况:

如果其他线程已经持有了该Object的轻量级锁,表示有竞争,进入锁膨胀

如果是自己执行了synchronized重入,那么再添加一条锁记录作为锁重入的记数。

多线程笔记2——并发之共享模型_第6张图片

锁重入时会再添加一条锁记录(因为是自己调用,在同一栈帧中),内容为null。解锁时会通过是否为null判断是否结束,如果是null则表示有锁重入,继续解锁;如果不是null则需将MarkWord再恢复给对象头。(成功:说明解锁成功;失败:轻量级锁已经锁膨胀了,需要重量级锁解锁)

(2)锁膨胀

有竞争时会锁膨胀,即加锁时发现对象头的Markword已经变成00。如下图,Thead-1尝试加锁,发现锁已经被Thread-0占用。

多线程笔记2——并发之共享模型_第7张图片

Thead-1加锁失败,进行锁膨胀:首先thead-1会为该Object对象申请Monitor锁,让Object指向重量级锁地址(即Monitor对象),Monitor会记录原来该对象的信息,然后自己进入Monitor的EntryList BLOCKED。

注:此时Markword为 monitor地址(30bits) + 10(2bit)

多线程笔记2——并发之共享模型_第8张图片

当Thead-0执行完退出时,会执行解锁。但发现此时Object内的锁标志位不再是00(轻量级),而是10(重量级)。因此会进入重量级解锁流程,先找到Monitor对象,设置Owner为null,唤醒EntryList中的BLOCKED线程。

(3)自旋优化

在重量级锁竞争时,可以用自旋进行优化。当新线程Thead-2想继续加锁时,发现对象头的锁标志位是10(重量级),它会先尝试自旋而不是立即阻塞。

注:JAVA6之后自旋是自适应的,即如果刚刚自旋成功,则会认为现阶段自旋成功可能性高,多自旋几次。反之少自旋。

成功情况:

多线程笔记2——并发之共享模型_第9张图片

失败情况:

多线程笔记2——并发之共享模型_第10张图片

(4)偏向锁

在轻量级锁加锁的过程中,每次锁重入仍然需要执行CAS操作。即尝试用锁记录去替换MarkWord,但是发现原先记录属于同一个线程,因此替换失败。(尽管失败,仍然每次都会开销)。因此在JAVA6中引入偏向锁对轻量级锁的锁重入进行优化。

偏向锁:第一次使用CAS将该线程ID记录到对象的MarkWord中(取代了原来的锁记录)。

优化前:

多线程笔记2——并发之共享模型_第11张图片

优化后:

多线程笔记2——并发之共享模型_第12张图片

总结:

无竞争时,如果开启了偏向锁,会优先加偏向锁;当锁未释放其他线程来竞争时,会撤销偏向锁变成重量级锁。当锁已释放其他线程来加锁时,会撤销偏向锁变成轻量级锁。

无竞争时,如果没开启偏向锁,会加轻量级锁;当其他线程来竞争时,轻量级锁变成重量级锁

偏向锁撤销情况:

hashcode():如果开启了偏向锁,一开始的hashcode和age不会存在(这些位都用来记录threadID了)。此时如果调用hashcode方法,会撤销偏向锁

其他线程对该对象加锁:如果此时已经解锁则转轻量级锁;如果此时没有解锁存在竞争转重量级锁。

wait/notify方法:只有重量级锁才有,会转重量级锁

 

你可能感兴趣的:(JAVA多线程)