锁信息是存储在对象的MarkWord中的,复习对象结构
对象的结构图
对象的结构是由对象头和对象数据以及数据对齐(padding,凑够8字节的倍数 加快读取速度)3部分组成。
对象头
对象头由markword,klass,数组长度(只有数组有,记录数组的长度信息)。
1.markword记录存储自身运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit 4字节和64bit 8字节,官方称它为“MarkWord”。
2.klass 一个指针,指向方法区的类信息。
3.数组长度
实例数据
如果基本类型直接存储,引用类型存储的是引用指针。
建立对象是为了使用对象,我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象。由于reference类型在Java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位、访问堆中的对象的具体位置,所以对象访问方式也是取决于虚拟机实现而定的。目前主流的访问方式有使用句柄和直接指针两种
句柄池中指针分别指向实例引用和方法去的对象类型,java栈(栈帧的本地变量表)中的reference指向的是句柄池中的对象指针,对象指针指向的是实例对象,java中的reference可以看成指针的指针,简接引用。
直接引用,在java栈中(栈帧的本地变量表)的reference直接指向存储指向java堆中实例对象。是直接引用,访问速度快。
对齐
HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍加快运行读取速度。
计算对象大小
1.在32位系统下,存放Class指针的空间大小是4字节,MarkWord是4字节,对象头为8字节。
2.在64位系统下,存放Class指针的空间大小是8字节,MarkWord是8字节,对象头为16字节。
3.64位开启指针压缩的情况下,存放Class指针的空间大小是4字节,MarkWord是8字节,对象头为12字节。 数组长度4字节+数组对象头8字节(对象引用4字节(未开启指针压缩的64位为8字节)+数组markword为4字节(64位未开启指针压缩的为8字节))+对齐4=16字节。
4.静态属性不算在对象大小内。
锁的使用
如果向一个变量写入值, 而这个变量接下 来可能会被另一个线程取, 或者,从一个变量读值,而这个变量可能是之前被另一个线程写入的, 此时必须使用同步
可重入锁,锁条件
package com.example.BankExample;
import java.util.Arrays;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Bank {
private final double[] accounts;// 账户数组
private Lock bandLock;// 锁
private Condition sufficientFunds;// 锁条件,账户余额充足
/**
* 初始化银行数据
* @param n 账户总数量
* @param initialBalance 每个账户初始数额
*/
public Bank(int n, double initialBalance) {
accounts = new double[n];
Arrays.fill(accounts, initialBalance);
bandLock = new ReentrantLock();
sufficientFunds = bandLock.newCondition();
}
public void transfer(int from, int to, double amount) throws InterruptedException {
bandLock.lock();
// 临界区
try {
while (accounts[from] < amount) {
sufficientFunds.await();// 获得了锁,但因为等待锁上的一个条件,
//进入了锁条件对象的等待队列(等待队列是没资格竞争锁的),进入等待状态,
// 当另一个线程唤醒(singnalAll)后,从等待队列中释放,回到该位置
}
System.out.println(Thread.currentThread());
// 转移
accounts[from] -= amount;
System.out.printf("%10.2f from %d to %d", amount, from, to);
accounts[to]+=amount;
System.out.printf("总金额: %10.2f%n", getTotalBalance());
sufficientFunds.signalAll();// 通知锁的条件对象的等待列中移除线程,
// 使所有等待集中的线程结束等待状态,进入阻塞状态竞争锁
}finally {
bandLock.unlock();
}
}
public double getTotalBalance(){
bandLock.lock();
try {
double sum=0;
for(double a:accounts){
sum+=a;
}
return sum;
}finally {
bandLock.unlock();
}
}
public int size(){
return accounts.length;
}
}
synchronized关键字加锁:保障原子性和可见性
除了Lock和Lock Condition,还可以用synchronized上锁,而使用synchronized关键字,实际上是利用了java为每个对象提供的一个内部锁(对象头的markword 堆内存中前8个字节中存了每个对象的锁信息-记录哪个线程在用锁,上哪种类型的锁和hashcode等信息)来控制线程同步。synchronized加锁是一个无锁到重量级锁过程,先无锁,然后偏向锁(相当于在锁对象markword上贴上线程的指针信息,如果只有这个线程访问特定资源时根本不用加锁的),自旋锁(cas-更新前读取值和初始读取的值进行比较如果一样就更新,不一样就重新读取新值,下次更新前再比较如此往复-会出现aba问题,解决用版本号进行更新,并将线程栈中的lockrecord信息更新到锁对象的markword中)当多个线程竞争不激烈时,或者自旋次数不多时,就是加的自旋锁,当竞争激烈,线程自旋次数增多时,会非常消耗cpu资源(适用于执行时间短,竞争少的时候),这时最后向操作系统请求mutex重量级的锁(放在阻塞队列中不耗cpu,适用于执行时间长,竞争多的时候),这种锁系统是有限的,比较耗资源,比较慢,由用户态转成内核态。底层的实现是汇编代码有一句lock cmpxchg,如果是多核cpu就加锁。
synchronized加锁方式
1.synchronised(A.class)锁住堆内存中的A的Class对象(class是一个关键字,Class是一个类,这个Class的对象在jvm类加载时初始化,每个类只有一个Class类对象,A.class就是对A的Class类对象的引用。它存储了A的属性 方法结构信息,在java进行类加载的时候将首先将每个类的信息即Class对象-A.class加载进堆内存中)
2.synchronized(object) 对一个对象加锁,这个对象可以this当前类对象,即synchronized(this)
3.synchronized(this)这种方法又等同方法上加synchronized-public
synchronized void run(){},要注意用的是哪个对象的锁,如果持有的锁不一样,线程的安全是得不到保障的。
4. static synchronized 锁的是 当前类的Class对象,在堆内存中只有一个
5. 有继承关系时,锁住的是子类对象
wait对应的await()和singnalAll() 。使用的时候必须 在一个同步方法或同步代码块,必须持有锁对象。
其实最好利用java.util.concurrent 包中的一种机制,可以处理所有的加锁
package com.example.BankExample;
import java.util.Arrays;
public class Bank2 {
// 相较于synchronized,Lock和Lock的Condition对象是一种普遍的锁,
// 而使用synchronized关键字,实际上是利用了java 1.0以后
// 为每个对象提供的一个内部锁(对象)和其单条件对象来控制线程同步。
// 区别Lock和Condition synchronized 的await()和singnalAll()
// 等待方法是object.wait() 唤醒方法 notifyAll()。使用的时候必须
// 在一个同步方法或同步代码块,必须持有锁对象。
// 其缺点包括:
// 1.不能中断一个正在试图获得锁的线程 2.试图获得锁时不能设定超时
// 3.每个锁仅有单一的条件, 可能是不够的 优点代码简洁。最好利用
// java.util.concurrent 包中的一种机制,可以处理所有的加锁
private final double[] accounts;
public Bank2(int n,double initialBalance){
accounts=new double[n];
Arrays.fill(accounts, initialBalance);
}
// 等同于synchronized(this) 实际上是
// this.intrinsidock.1ock();
// try
// {
// method body
// }
// finally { this.intrinsicLock.unlockO; }
public synchronized void transfer(int from ,int to ,double amount) throws InterruptedException {
while (accounts[from]<amount){
this.wait();
}
System.out.println(Thread.currentThread());
accounts[from]-=amount;
System.out.printf("%10.2 from %d to %d", amount,from,to);
accounts[to]+=amount;
System.out.printf("总金额 %10.2",getBalance());
notifyAll();
}
// 等同于synchronized(this)
public synchronized double getBalance(){
double sum=0;
for(double a:accounts){
sum+=a;
}
return sum;
}
public int size(){
return accounts.length;
}
}
volatile首先可以保证线程的可见性,一旦有程序更新了值,其他线程重新读取新值,再者可以阻止指令重排,jvm-虚拟机标准上是加内存屏障(load-load,load-store,store-store,store-lock)防止指令重排-cpu认为两行指令互换不影响最后结果有可能发生重排,hotspot实现是通过加锁lock实现。volatile不能保障原子性。cpu读取内存的方式是内存行(比如64个字节)可能包含多个数据,那么如果加了volatile,如果读取x,y x y都是volatile; x变化,会导致cpu再次读取y,同样y变化,cpu会再次读取x。可以将x y放在不同内存行(在临近的内存中补一些无关的数据)中去,会提高cpu处理的速度。
// volatile在单例模式下也有应用
class VolatileSingleton {
private volatile static VolatileSingleton singleton;
private VolatileSingleton (){}
public static VolatileSingleton getSingleton() {
if (singleton == null) {
synchronized (VolatileSingleton.class) { // 细化锁,锁太粗就太耗性能
if (singleton == null) {// 获取锁后,如果不判断,就有可能别的线程new,后获取不到最新的new出来的对象
singleton = new Singleton();
}
}
}
return singleton;
}
}
JMM:java内存模型
定义JMM的目的
Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储 到内存和从内存中取出变量这样的底层细节。此处的变量(Variables)与Java编程中所说的变量有所区别,它包括了实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,自然就不会存在竞争问题。
java的工作内存(JMM)和JVM内存模型有关系吗?
主内存、工作内存与本书第2章所讲的Java内存区域中的Java堆、栈、方法区等并不是同一个层次的内存划分,这两者基本上是没有关系的。Java内存模型规定了所有的变量都存储在主内存(Main Memory)中(此处的主内存与介绍物理硬件时的主内存名字一样,两者也可以互相类比,但此处仅是虚拟机内存的一部 分)。每条线程还有自己的工作内存(Working Memory,可与前面讲的处理器高速缓存类 比,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的 所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主 内存来完成。
计算机内部寄存器最快其次是l1一级缓存,l2二级缓存,l3三级缓存,内存,硬盘,就像mysql数据库慢了要加缓存的道理,计算机搞这么多缓存是因为速度差的原因,离cpu处理越近,就越快
线程更改共享变量的不可见性:
共享内存不可见性:线程B写入的值对线程A不可见
synchronized volatile的深层语义(工主内存和工作内存理解)
1.synchronized是可以保证可见性的,双重检查锁之所以用synchronized是为了保证原子性,volatile是为了禁止指令重排,防止new的过程中指令重排,对象未初始化完成被其他线程获
2.JMM中关于synchronized有如下规定,线程加锁时,必须清空工作内存中共享变量的值,从而使用共享变量时需要从主内存重新读取;线程在解锁时,需要把工作内存中最新的共享变量的值写入到主存,以此来保证共享变量的可见性。(ps这里是个泛指,不是说只有在退出synchronized时才同步变量到主存)
3.总结下语义就synchronized可以解决共享变量内存可见性问题。进入synchronized块的内存语义是把在synchronized块内使用到的变量从线程的工作内存中清除,这样在synchronized块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取。退出synchronized块的内存语义是把在synchronized块内对共享变量的修改刷新到主内存。会造成上下文切换的开销,独占锁,降低并发性。该关键字可以确保对一个变量的更新对其他线程马上可见。当一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。volatile的内存语义和synchronized有相似之处,具体来说就是,当线程写入了volatile变量值时就等价于线程退出synchronized同步块(把写入工作内存的变量值同步到主内存),读取volatile变量值时就相当于进入同步块(先清空本地内存变量值,再从主内存获取最新值)。不能保证原子性
java开线程有三种方式1.继承Thread 重写run方法,执行start函数 2.实现Runnable接口,重写Runnable的run方法,其实Thread本身也实现了Runnable接口的run方法,也是Runnable,使用new Thread(Runnable obj) 新建Thread 对象,执行 start方法 ,其实Runnable是一个函数式接口,java中通过匿名内部类-重写接口的方法 new Runnable对象,或者通过lambda表达式-对应函数式接口中的函数形式,来构造一个Runnable对象 。而c#中可以传入一个委托,因为委托的赋值Mydel del=new Mydel(fun) 可以等价于Mydel del=fun,所以可以直接传入一个函数签名,而java8可以类似的通过Thread thread=new Thread(t::fun)来传入参数或者是lambda表达式,只要符合Runnable函数式接口的run方法的形式 public void run(){3.Executors.newCahcedThreadPool 线程池来创建线程
阻塞队列和等待队列是不一样的,阻塞队列是没有获取锁时,进入阻塞状态,有获取锁的资格;等待队列是没有资格获取锁的,只有从等待队列中出来(wait时间到或者interrupt或notify才能从等待队列中出来)才有资格获得锁,进入Runnable状态,之后根据是否获取锁,没有获取锁的线程进入阻塞队列。
线程状态变化:wait一般在同步代码块中,会让获取锁的线程进入等待队列中,等待队列是无权获取锁的,notify或者wait时间到(不必notify)后才能从等待队列中出来,进入阻塞队列,竞争锁,获取锁后才进入Runnable状态。等待队列比阻塞队列低一级含义是wait后再等待队列中获取锁的资格都没有。wait(notify)必须持有锁,所以其必须用在同步代码块中;
当线程sleep wait join调用interrupt方法线程会报异常(如果不在sleep wait join就只是isInterrupted状态发生了变化)可以加try catch (catch之后isInterrupted状态清除恢复到原来的false)catch之后的操作用户可以自己定义处理;
对wait的线程,调用interrupt后会重新获得锁然后抛出InterruptedException异常。sleep(如果原来线程持有锁,sleep之后不会释放锁)时间到或者interrupt (要try catch,不try catch jvm会报错,退出运行)重新回到Runnable状态,yield比较少用,让出cpu一下,然后有可能还会被系统调度,再次运行。线程的终止,可以run结束,或者报出异常终止,终止的线程是不可以重新启动的。
线程的脏读
结合下边代码,加锁的set方法执行过程中,由于线程执行赋新值(0->8),需要时间(Thread.sleep),未赋新值前,被其他未加锁方法读到(同类GoodsPrice中未加锁方法不受锁限制可以随意执行)没有赋值之前的值0,过一会后才读到真正的值8,那么这种现象就叫脏读。解决脏读的方法就是在同类GoodsPrice读方法加锁,但效率要慢。
package com.example.threadTest;
public class DirtyThreadTest {
public static void main(String[] args) {
GoodsPrice goodsPrice = new GoodsPrice("蒜瓣", 0);
new Thread(() -> goodsPrice.setNameAndPrice(8)).start();
new Thread(goodsPrice::getPrice).start();// 读值
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
new Thread(goodsPrice::getPrice).start();// 读值
}
}
class GoodsPrice {
private final String name;
private float price;
public GoodsPrice(String name, float price) {
this.name = name;
this.price = price;
}
synchronized void setNameAndPrice(float price) {
// 处理一些问题
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
this.price = price;
}
void getPrice() {// synchronized
System.out.println(this.name + ":" + this.price);
}
}
运行结果: 蒜瓣:0.0 蒜瓣:8.0 加了synchronized: 蒜瓣:8.0 蒜瓣:8.0
synchronized是可重入锁:父子类调用是同一把锁,锁对象是同一个子类锁对象
class GoodsPrice {
private final String name;
private float price;
public GoodsPrice(String name, float price) {
this.name = name;
this.price = price;
}
synchronized void setNameAndPrice(float price) {
try {
Thread.sleep(2000);
}
catch (InterruptedException e) {
}
this.price = price;
getPrice();
}
synchronized void getPrice() {
System.out.println(this.name + ":" + this.price);
}
}
异常时线程的乱入:当程序发生异常时,默认会释放锁,其他线程会获得锁接着执行下去
package com.example.threadTest;
public class ThreadWhenException {
public static void main(String[] args) {
T t = new T();
new Thread(t::m, "t1").start();
new Thread(t::m, "t2").start();
System.out.println("main所在线程结束");
}
}
class T {
private int count = 0;
synchronized void m() {
while (true) {
count++;
System.out.println(Thread.currentThread().getName() + " " + count);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count == 5) {// 当程序发生异常时,默认会释放锁,其他线程会获得锁接着执行下去,其他线程就会接着5向下(count++后是6) 执行
int i = 8 / 0;
System.out.println(Thread.currentThread().getName() + " " + count);
}
}
}
}
结果
t1 1
main所在线程结束
t1 2
t1 3
t1 4
t1 5
t2 6
Exception in thread “t1” java.lang.ArithmeticException: / by zero
at com.example.threadTest.T.m(ThreadWhenException.java:26)
at java.lang.Thread.run(Thread.java:748)
t2 7
t2 8