java内置锁

在java中,每个对象都有一个锁,称为内置锁

java对象的结构

对于一个java对象,组成有:

对象头

由3部分组成:

1.mark word

存储着线程锁状态,GC情况,hashcode.不受指针压缩影响.

64位的对象头mark word.

lock是锁的标记位,biased_lock是偏向锁标记.lock+biased_lock组合可以表示锁的状态.

2.类对象指针,存放方法区中class对象的地址.

3.array length(数组长度)

对象为数组的时候,需要.

指针压缩

在64位操作系统下,默认会进行指针压缩.减少空间消耗.压缩类型主要有:

1.class对象的指针.(静态变量)

2.Object对象的指针.(成员变量)

3.普通对象数组的指针.

对象体

存储成员变量的属性值.

对齐字节

用来保证java对象占用的字节是8的倍数.

自增运算不是线程安全的

package com.lenovo.javautils.lock;

import com.lenovo.javautils.threads.MyThreadPoolExecutor;
import lombok.Data;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadPoolExecutor;

public class AddTest {
    @Data
    static class SelfAdd {
        private int a;

        public void add() {
            a++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutor poolExecutor = MyThreadPoolExecutor.INSTANCE.getInstance();
        CountDownLatch countDownLatch = new CountDownLatch(2);
        SelfAdd selfAdd = new SelfAdd();
        poolExecutor.execute(() -> {
            for (int i = 0; i < 10; i++) {
                selfAdd.add();
            }
            countDownLatch.countDown();
        });
        poolExecutor.execute(() -> {
            for (int i = 0; i < 10; i++) {
                selfAdd.add();
            }
            countDownLatch.countDown();
        });
        countDownLatch.await();
        System.out.println(selfAdd.getA());
    }
}

上面的例子是启动2个线程去执行i++操作,运行结果如下:

11:15:47.391 [MyThreadPoolExecutor2] INFO com.lenovo.javautils.threads.MyThreadPoolExecutor - thread start:MyThreadPoolExecutor2
11:15:47.391 [MyThreadPoolExecutor1] INFO com.lenovo.javautils.threads.MyThreadPoolExecutor - thread start:MyThreadPoolExecutor1
11:15:47.394 [MyThreadPoolExecutor2] INFO com.lenovo.javautils.threads.MyThreadPoolExecutor - thread:MyThreadPoolExecutor2, end,useTime:0ms
17
11:15:47.394 [MyThreadPoolExecutor1] INFO com.lenovo.javautils.threads.MyThreadPoolExecutor - thread:MyThreadPoolExecutor1, end,useTime:0ms

可以看出,结果不是预期的20,还是17.

原因分析:

自增运算符是一个复合操作,包含了2个jvm指令:内存取值,寄存器计算,内存放值.

2个线程取值同时为1的时候,通过计算得到2,然后同时放值,a=2,而不是a=3.

临界区资源与临界区代码块

简介

临界区资源是多个线程共同访问的公共资源.临界区代码块是访问临界区资源的代码块.

比如a++自增例子中,a是临界区资源,a++是临界区代码块.

在并发的情况下,线程需要申请到临界区资源,然后执行临界区代码块,最后释放资源.

在java中,使用synchronize关键字使对临界代码块进行保护.

 @Data
    static class SelfAdd {
        private int a;
        private int b;

        public synchronized void addSyn() {
            b++;
        }

        public void add() {
            a++;
        }
    }

这样b++,得到的结果就是一个正确的结果.

synchronized

java中的关键字,可以获取java对象的内置锁,从而达到对临界区代码进行排他性保护.

1.synchronized代码块:

细粒度的对象锁.

2.synchronized方法:

粗粒度的对象锁

3.synchronized静态方法:

粗粒度的类锁.该类所有的实例对象都会被锁住.

 @Data
    static class SelfAdd {
        private int a;
        private int b;
        private final Integer lockA = 1;
        private final Integer lockB = 1;
        private static int c;

        public void add() {
            a++;
        }

        public synchronized void addSyn() {
            b++;
        }

        /**
         * 粗粒度的锁方法
         */
        public synchronized void addMulSyn() {
            b++;
            a++;
        }

        public void addMul1Syn() {
            //细粒度的锁代码块
            synchronized (lockA) {
                a++;
            }
            synchronized (lockB) {
                b++;
            }
        }
        //类锁
        public static synchronized void addCSyn() {
            c++;
        }
    }

执行过程:

1.检测锁状态和偏向锁标记,如果偏向锁标记是1,lock是01.表示是偏向锁状态.

2.检查偏向锁mark work的线程id,如果是抢锁线程,可快速放行,执行同步代码块.

3.如果不是,则通过cas尝试获取锁,如果获取到锁,则把mark work的线程id修改为抢锁线程id.此时还是偏向锁状态.

4.如果cas竞争锁失败,则撤销偏向锁,升级为轻量级锁.

5.通过cas将mark word修改为抢锁线程的锁记录指针,修改成功,抢锁线程获取到锁,此时还是轻量级锁.

6..如果获取失败,就膨胀为重量级锁.此时所有抢锁线程堵塞.

无锁

java对象刚创建,还没有线程来调用.

此时偏向锁标记是0,锁状态是01.

偏向锁

一块代码只被一个线程访问.此时偏向锁标记1,锁状态是01.

线程A访问对象的时候,如果是无锁对象,会把mark work里面的线程id设置为访问线程,然后线程A再次访问的时候,自动获取锁.

轻量级锁

又称自旋锁.当偏向锁由2个线程同时访问的时候,这2个线程会自旋等待获取对象锁.哪个线程获取到了锁,该锁的mark work的锁记录指针就会指向哪个线程栈桢的锁记录.

分类:

1.普通自旋锁

原地循环等待10次,去尝试获取锁.

2.自适应自旋锁

根据上一次获取锁的时间+结果,来调整下次自旋的时间.

主要适用于锁竞争不激烈,并且线程持有锁时间很短的情况.

重量级锁

使用操作系统底层的互斥锁,导致用户态和核心态的转换,开销较大.当竞争过于激烈,线程无法通过自旋获取到锁,对象锁就会变为重量级锁.

原理

每个java对象都有一个监视器ObjectMonitor,跟随着对象的创建和销毁.

Cxq:线程竞争队列,存放所有请求资源的线程.

EntryList:存放有资格成为候选者的线程.

WaitSet:存放使用了object.wait()的线程.可以被notify()或者notifyAll()唤醒,重新进入entrylist中.

多个线程访问的时候,会先进入线程竞争队列,Cxq可以多线程访问,因此当owner thread释放锁的时候,会把cxq中的线程迁移到entyList中,然后entryList的第一个线程重新竞争锁,因此重量级锁是非公平的锁.

这种通过竞争获取锁的行为可以提高系统的吞吐量.

线程间通信

通过wait和notify来进行通信.

Last updated

Was this helpful?