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?