juc并发编程入门(六)

61 篇文章 1 订阅

我们来看下对象的内存布局

Object o=new Object();

Object在方法区;

o在栈里;

new Object在堆;

在HotSpot虚拟机里,对象在堆内存中的存储布局,可以划分为三个部分:对象头(Header),实例数据(Instance Data)和对齐填充(Padding)(保证8个字节的倍数);

对象头里面包含对象标记(Mark Word),类元信息(又叫类型指针);

对象标记里面包含哈希码,GC标记,GC次数,同步锁标记,偏向锁持有者;

类型指针指向方法区的类元信息Klass

如果只有class A没有数据,那么就是只有一个对象头的实例对象;

class A{

}

如果class A有数据,那么不但有对象头,还有实例数据

class A{

int age;

}

实例数据:存放类的属性(Field)数据信息,包括父类的属性信息;

对齐填充

虚拟机要求对象起始地址位置必须是8字节的整数倍,填充数据不是必须存在的,仅仅是为了字节对齐这部分内存按8字节补充对齐;

一句话就是,这个字段如果不是8个字节的,给他补齐。

我们在代码加入

  <!--分析对象在jvm的大小和布局-->
        <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.9</version>
        </dependency>
 public static void main(String[] args) throws Exception {
        //对象的详细信息
        System.out.println(VM.current().details());
        System.out.println("------------------");
        // Objects are 8 bytes aligned. 对象是8字节对齐的。
        System.out.println(VM.current().objectAlignment());
    }

可以看到分析出来的字节信息

Object o=new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());

我们可以看到loss due to the next object alignment 下一次对象对齐造成的损失,就是不够,补齐

在看下面的代码

class A{
    char age;
    double num;
}

public class Producer {

    public static void main(String[] args) throws Exception {
        A o=new A();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}

alignment/padding gap 对齐/填充间隙

接下来我们来看下Synchronized与锁升级

谈谈你对Synchronized的理解?

在高并发时,能用无锁数据结构,就不要用锁,能锁代码块,就不要锁整个方法,能用对象锁,就不要使用类锁;

Synchronized的锁升级你聊聊?

用锁能够实现数据的安全性,但是会带来性能下降,无锁能够基于线程并行提升程序性能,但是会带来安全性下降;

锁的升级过程:无锁-》偏向锁-》轻量级锁-》重量级锁;

Synchronized锁:由对象头中的Mark word根据锁标志位的不同而被复用及锁升级策略;

无锁标志位: 0 0 1

偏向锁标志位:1 0 1

轻量级锁 标志位:0 0

重量级锁 标志位: 1 0

偏向锁:Mark word存储的是偏向的线程id;

轻量级锁:Mark word存储的是指向线程栈中Lock Record的指针;

重量级锁: Mark word 存储的是指向堆中的monitor对象的指针;

我们可以看到在无锁的状态下,对象头Mark word标记的就是001

当执行hashcode的时候下面的部分发生了变化,也就是hashcode存在对象头中

程序不会有锁的竞争

无锁:初始状态,一个对象被实例化后,如果还没有被任何线程竞争锁,那么他就为无锁状态(001);

偏向锁:单线程竞争

当线程A第一次竞争到锁时,通过操作修改Mark word中的偏向线程id,偏向模式。

如果不存在其他线程竞争,那么持有偏向锁的线程将永远不需要进行同步;

当一段同步代码一直被同一个线程多次访问,由于只有一个线程那么该线程在后序访问时便会自动获得锁;

锁总是同一个线程持有,很少发送竞争,也就是说锁总是被第一个占用他的线程拥有,这个线程就是锁的偏向线程;

说白了就是一个线程抢到的资源比其他线程抢到的资源多,就是偏向锁。

在Git bash里面可以看到偏向锁的信息

java -XX:+PrintFlagsInitial |grep BiasedLock*

UseBiasedLocking=true 就是开启偏向锁

BiasedLockingStartupDelay=0 就是程序在启动的时候立刻启动,没有延迟

我们在代码看到,当前是轻量级锁 0 0

那么我们在vm中设置一下-XX:BiasedLockingStartupDelay=0 ,再次启动

可以看到变成了101 偏向锁

我们也可以在代码中加入延迟时间5秒,不设置vm参数,他的默认时间是4秒,所以我们要超过4秒

才会变成偏向锁1 0 1

注意,要在对象new之前加sleep

我们在看下不加锁,加了延迟5秒时间

可以看到,锁状态为101是偏向锁状态了,只是由于o对象未用synchronized加锁,所以线程id是空的,其余数据跟上述无锁状态一样

偏向锁带线程id的情况,第一行中后面不再是0了,有了线程id的值

偏向锁的撤销

偏向锁使用一种等到竞争出现才释放锁的机制,只有当其他线程竞争锁时,持有偏向锁的原来线程才会被撤销;

撤销需要等待全局安全点(该时间点上没有字节码正在执行),同时检查持有偏向锁的线程是否还在执行;

1.第一个线程正在执行sync方法处于同步块,他还没有执行完,其他线程来抢夺,该偏向锁会被取消掉并出现锁升级;

此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得改轻量级锁。

2.第一个线程执行完成sync方法退出同步块,则将对象头设置成无锁状态并撤销偏向锁,重新偏向;

一句话说明:如果A线程正在运行,B线程进来了,同时争抢资源,锁升级为轻量级锁,如果A退出了,那么先变成无锁状态,然后再重新成为偏向锁

轻量级锁:多线程竞争,但是任意时刻最多只有一个线程竞争,即不存在竞争太过激烈的情况,也就没有线程阻塞;

轻量级锁的主要作用

有线程来参与锁的竞争,但是获取锁的冲突时间极短,本质就是自旋锁CAS

当轻量级的自旋锁达到了一定的自旋次数还没有成功,那么升级为重量级锁

可以看到,在多个线程抢夺一个对象的时候,变成了重量级锁1 0

当调用了hashcode立马就会从偏向锁变成轻量级锁0 0

在同步代码块中写hashcode方法,会升级为重量级锁

当没有加锁的时候,就是无锁状态,当加了锁之后,如果开启5秒延迟就是偏向锁。

如果存在另一个线程进来抢占资源,那么升级为轻量级锁,当轻量级自旋次数达到上限之后,

升级为重量级锁,这就是锁升级的过程。

接下来我们来看下AQS

AQS 是抽象的队列同步器 AbstractQueuedSynchronizer的简写

和AQS有关的类

ReentrantLock;

CountDownLatch;

ReentrantReadWriteLock;

Semaphore;

可以看到ReentrantLock里面有一个Sync类继承了AQS

可以看到上面的类里面都有一个抽象的类Sync继承了AQS

在ReentrantLock,CountDownLatch,ReentrantReadWriteLock,Semaphore的源码中,

都有一个抽象的类Sync继承了AbstractQueuedSynchronizer,简称(AQS)抽象队列同步器,

加锁的时候会抢占资源,抢到资源的往下进行,抢不到资源的就会阻塞,阻塞就会排队,

排队那么就会进入我们的AQS,AQS底层的数据结构就是Node链表,在Node排队;

AQS的state 0是空闲,大于0就是有人在使用;

通过自旋等待;

state变量判断是否阻塞;

从尾部入队;

从头部出队;

我们来看下非公平锁

new ReentrantLock();

可以sync对应的就是Sync类

我们可以看到加锁的方法对应的也是sync

可以看到我们实现的lock锁都是由sycn的lock来实现的

可以看到公平锁的lock也是Sync的lock

不管是公平锁还是非公平锁都会进入acquire方法

在公平锁里面就是多了一个hasQueuedPredecessors方法

具有排队的前置任务

如果为true,那么说明前面有排队的,如果为false说明前面没有排队的

公平锁加锁时判断等待队列中是否存在有效节点的方法

接下来我们来看下ReentrantReadWriteLock 读写锁

读写锁定位为:一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程;

我们来看下,在可重入锁的场景下

class A{
    Lock lock=new ReentrantLock();

    public void write(){
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+",开始写入");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+",写完了");
        }finally {
            lock.unlock();
        }
    }

    public void read(){
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+",开始读取");
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+",读完了");
        }finally {
            lock.unlock();
        }
    }
}

public class Producer {


    public static void main(String[] args) throws Exception {
        A a=new A();

        for (int i = 0; i <5 ; i++) {
            new Thread(()->{
                a.write();
            },String.valueOf(i)).start();
        }

        for (int i = 0; i <5 ; i++) {
            new Thread(()->{
                a.read();
            },String.valueOf(i)).start();
        }



    }
}

可以看到重入锁场景下,写锁执行完了,读锁才开始读取,做不到读读共享

就是线程0读完了,线程1才能读

我们来看下读写锁的代码

class A{
   // Lock lock=new ReentrantLock();
    ReentrantReadWriteLock lock=new ReentrantReadWriteLock();
    public void write(){
        lock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName()+",开始写入");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+",写完了");
        }finally {
            lock.writeLock().unlock();
        }
    }

    public void read(){
        lock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName()+",开始读取");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+",读完了");
        }finally {
            lock.readLock().unlock();
        }
    }
}

public class Producer {


    public static void main(String[] args) throws Exception {
        A a=new A();

        for (int i = 0; i <5 ; i++) {
            new Thread(()->{
                a.write();
            },String.valueOf(i)).start();
        }

        for (int i = 0; i <5 ; i++) {
            new Thread(()->{
                a.read();
            },String.valueOf(i)).start();
        }

        Thread.sleep(1000);
        //再次写入
        for (int i = 0; i <3 ; i++) {
            new Thread(()->{
                a.write();
            },String.valueOf(i)).start();
        }


    }
}

可以看到读读共享了,就是0,1,2,3,4线程同时读取了

写锁饥饿问题 就是读锁的次数比较多,写锁的次数比较少;

在写锁之间 我们还可以获取到读锁,我们来看下代码

public class Producer {


    public static void main(String[] args) throws Exception {
        ReentrantReadWriteLock readWriteLock=new ReentrantReadWriteLock();

        readWriteLock.writeLock().lock();

        System.out.println("写锁");

        readWriteLock.readLock().lock();

        System.out.println("读锁");

        readWriteLock.writeLock().unlock();

        readWriteLock.readLock().unlock();
    }
}

在写锁里面 包含这读锁,在写锁外面 释放读锁,这就是锁降级 降级为读锁

也叫做写后读

我们把读锁放在写锁上面

 public static void main(String[] args) throws Exception {
        ReentrantReadWriteLock readWriteLock=new ReentrantReadWriteLock();

        readWriteLock.readLock().lock();
        System.out.println("读锁");

        readWriteLock.writeLock().lock();
        System.out.println("写锁");

        readWriteLock.writeLock().unlock();

        readWriteLock.readLock().unlock();
    }

可以看到读锁升级写锁的时候,卡死了,所以在读锁没有读完之前,不能进行写锁

有没有比读写锁更快的锁?

邮戳锁StampedLock,也叫票据锁,是对读写锁的优化;

stamp(戳记,long类型)

代表了锁的状态,当stamp返回零时,表示线程获取锁失败,并且当释放锁或者转换锁的时候,都要传入最初获取的stamp值;

所有获取锁的方法,都返回一个邮戳(Stamp),Stamp为零表示获取失败,其余都表示成功;

所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致;

StampedLock是不可重入的,危险(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁);

StampedLock有三种访问模式:

1.Reading(读模式悲观):功能和读写锁的读锁类似;

2.Writing(写模式):功能和读写锁的写锁类似;

3.Optimistic reading(乐观读模式):无锁机制,类似数据库中的乐观锁,支持读写并发,很乐观的认为读取时没人修改,假如被修改再实现升级为悲观读模式;

接下来我们来看下StampedLock的代码,操作读写锁

class A{
    //邮戳锁 票据锁
    StampedLock stampedLock=new StampedLock();

    public void write(){
        //写锁   stamp戳记 当stamp返回0时,表示线程获取锁失败
        long stamp=stampedLock.writeLock();
        try {
            System.out.println(Thread.currentThread().getName()+",开始写入");
            System.out.println(Thread.currentThread().getName()+",写完了");
        }finally {
            //释放写锁  当释放写锁的时候都要传入最初的戳记
            stampedLock.unlockWrite(stamp);
        }
    }

    public void read(){
        long stamp=stampedLock.readLock();
        try {
            System.out.println(Thread.currentThread().getName()+",开始读取");
            for (int i = 0; i <4 ; i++) {
                try {
                    //阻塞1秒
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+",正在读取中");
            }
            System.out.println(Thread.currentThread().getName()+",读完了");
        }finally {
            stampedLock.unlockRead(stamp);
        }
    }
}

public class Producer {


    public static void main(String[] args) throws Exception {
        A a=new A();
        new Thread(()->{
            a.read();
        },"t1").start();


        new Thread(()->{
            a.write();
        },"t2").start();
    }
}

可以看到,读完了才能写锁

我们在来看下邮戳锁的乐观读模式

在读锁的情况下,也能进行写锁

validate

如果自发出给定标记后未完全获取锁,则返回true,如果标记为0,则始终返回false

如果图章代表当前持有的锁,则始终返回true.

返回true,就代表没有修改

tryOptimisticRead 尝试乐观读

一个有效的乐观读取标记,如果是完全锁定则为0;

class A{
    //邮戳锁 票据锁
    StampedLock stampedLock=new StampedLock();
    int num=0;

    public void write(){
        //写锁   stamp戳记 当stamp返回0时,表示线程获取锁失败
        long stamp=stampedLock.writeLock();
        try {
            System.out.println(Thread.currentThread().getName()+",开始写入");
            num=10;
            System.out.println(Thread.currentThread().getName()+",写完了");
        }finally {
            //释放写锁  当释放写锁的时候都要传入最初的戳记
            stampedLock.unlockWrite(stamp);
        }
    }

    public void read(){
        long stamp=stampedLock.readLock();
        try {
            System.out.println(Thread.currentThread().getName()+",开始读取");
            for (int i = 0; i <4 ; i++) {
                try {
                    //阻塞1秒
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+",正在读取中");
            }
            System.out.println(Thread.currentThread().getName()+",读完了");
        }finally {
            stampedLock.unlockRead(stamp);
        }
    }

    //乐观读
    public void aa(){
        long stamp=stampedLock.tryOptimisticRead();
        for (int i = 0; i < 4; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+",正在读取,是否有人修改过:"+stampedLock.validate(stamp));
        }
        if(!stampedLock.validate(stamp)){
            //有人修改过 乐观读 变为悲观读
            try {
                //重新赋值
                stamp=stampedLock.readLock();
                System.out.println("结果为:"+num);
            }finally {
                stampedLock.unlockRead(stamp);
            }
        }
        System.out.println("读完毕结果为:"+num);

    }
}

public class Producer {


    public static void main(String[] args) throws Exception {
        A a=new A();
        new Thread(()->{
            a.aa();
        },"t1").start();

        Thread.sleep(3000);
        new Thread(()->{
            System.out.println("写进入");
            a.write();
        },"t2").start();
    }
}

可以看到t1在读取的时候,t2也能写入了

StampedLock不支持重入;

StampedLock的悲观读锁和写锁都不支持条件变量Condition;

使用StampedLock一定不要调用中断操作,不要调用interrupt方法;

juc并发编程入门(七)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值