我们写的程序,最后是会扔在多线程环境下运行的,数据安全才是重中之重。
线程安全问题会直接导致数据异常,从而影响业务功能的正常使用,所以这个问题还是非常严重的。
创建一个银行账户类Account :
Balance 直译:平衡;余额;
public class Account {
private String actno; //账号
private double balance; //余额
public Account() {
}
public Account(String actno, double balance) {
this.actno = actno;
this.balance = balance;
}
public String getActno() {
return actno;
}
public void setActno(String actno) {
this.actno = actno;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
//取款的方法
public void withdraw(double money){
//取款之前的余额
double before=this.getBalance();
//取款之后的余额
double after=before-money;
try {
//模拟网络延迟,使得百分百出现线程安全问题
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//更新余额
//第一个线程执行到了这里,但是还没来得及执行这行代码,第二个进到这个withdraw方法来了,此时一定出问题。
this.setBalance(after);//省略了set、get方法
System.out.println(Thread.currentThread().getName()+"——>取款"+money+",剩余"+after);
}
}
创建线程进行取款操作。模拟数据共享(同一个账户)会引起的线程安全问题
//多线程取款操作
public static void main(String[] args) {
Account account = new Account();
account.setActno("小羽毛");
account.setBalance(10000);
//实现Runnable接口的线程
new Thread(() -> {
//线程业务代码
account.withdraw(1000);
},"withdraw-money-1").start();
//实现Runnable接口的线程
new Thread(() -> {
//线程业务代码
account.withdraw(2000);
},"withdraw-money-2").start();
}
运行结果:
应该是剩余7000才对,,,但是结果不如人意啊
也可以执行出很离的操作。。最后居然剩余9000
为什么synchronized括号里的同步监视器是this呢?
1、()里的this是同步监视器,线程开始执行同步代码块之前,必须先获取同步监视器的锁定
2、当同步代码块执行完成之后,该线程会释放对该同步监视器的锁定
//取款的方法
public void withdraw(double money) {
//这里为什么是this?同步代码块范围能不能再小点。。。
synchronized (this) {
//取款之前的余额
double before = this.getBalance();
//取款之后的余额
double after = before - money;
try {
//在同步代码块里就算线程睡眠了(sleep)也得等它醒过来,别的线程进不去的
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//更新余额
//第一个线程执行到了这里,但是还没来得及执行这行代码,第二个进到这个withdraw方法来了,此时一定出问题。
this.setBalance(after);//省略了set、get方法
System.out.println(Thread.currentThread().getName() + "——>取款" + money + ",剩余" + after);
}
}
正确结果:不会再出现上面那样荒唐的结果了
synchronized关键字包括两种用法:synchronized 方法和 synchronized 块
同步方法:使用synchronized 关键字修饰⼀个⽅法
// 关键字在实例⽅法上,锁为当前实例(调用方法的对象)
public synchronized void instanceLock() {
// code
}
// 关键字在静态⽅法上,锁为当前Class对象
public static synchronized void classLock() {
// code
}
}
具体案例就是“8锁问题”,看的时候请关注一个重要的问题,synchronized出现在不同的位置分别锁的是哪个对象?
public class Lock_8 {
public static void main(String[] args) throws Exception {
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(() -> {
try {
phone.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
}, "AA").start();
Thread.sleep(100);
new Thread(() -> {
try {
phone.sendEmail(); //步骤B
//phone.getHello(); //步骤C
//phone2.sendEmail(); //步骤D
} catch (Exception e) {
e.printStackTrace();
}
}, "BB").start();
}
}
class Phone {
public synchronized void sendSMS() throws Exception {
//TimeUnit.SECONDS.sleep(4); //步骤A
System.out.println("------sendSMS");
}
public synchronized void sendEmail() throws Exception {
System.out.println("------sendEmail");
}
public void getHello() {
System.out.println("------getHello");
}
}
1、标准访问,先打印短信还是邮件
此时,步骤ACD注释。。。只有步骤B放开
关注点在于锁谁?
由于此时线程AA和BB都是调用同步成员方法,所以锁的是调用该方法的对象。BB和AA中使用的是同一个对象锁phone,所以存在竞争关系。
2、睡眠4秒在短信方法内,先打印短信还是邮件
步骤CD注释。。。步骤AB放开
sleep是持有锁的睡眠,因为 AA和BB中使用相同的锁(phone对象),所以会等4s后在控制台依次打印发短信和发邮件
如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁
3、普通的hello方法,是先打短信还是hello
步骤BD注释。。。步骤AC放开
因为步骤C:phone.getHello(),调用的这个方法根本就没有锁。所以不会存在锁的竞争
,就会先打印getHello了,4s后再打印sendSMS
4、现在有两部手机,先打印短信还是邮件
2部手机的意思即使,放开步骤A和步骤D。注释步骤B和步骤C。
步骤D,phone2.sendEmail();是另外一个phone2对象,所以和上面phone对象不构成锁的竞争关系,所以也是先发邮件,因为在发短信里等了4s
告诉我们:
不同实例对象的非静态同步方法因为用的是不同对象的锁,所以毋须等待其他实例对象的非静态同步方法释放锁,就可以获取自己的锁。
5、两个静态同步方法,1部手机,先打印短信还是邮件
这时候变一下,加俩个static关键字
public static synchronized void sendSMS() throws Exception { TimeUnit.SECONDS.sleep(4); //步骤A System.out.println("------sendSMS"); } public static synchronized void sendEmail() throws Exception { System.out.println("------sendEmail"); }
步骤CD注释。。。步骤AB放开
由于此时线程AA和BB都是调用同步静态方法,所以锁的是类模板对象,即Phone.class对象
所以此时AA和BB中持有同一把锁,存在竞争关系
则会等待4s,依次打印发短信个发邮件
6、两个静态同步方法,2部手机,先打印短信还是邮件
步骤BC注释。。。步骤AD放开
效果和5是一样的,因为锁的是Phone.class,所以不管在AA还是BB,由于调用的都是同步静态方法,所以是存在竞争关系的。 会在4s后依次发短信和发邮件
告诉我们:
所有的静态同步方法用的是同一把锁——类对象本身。不管是不是同一个实例对象,只要是一个类的对象,一旦一个静态同步方法获取锁之后,其他对象的静态同步方法,都必须等待该方法释放锁之后,才能获取锁。
7、1个静态同步方法,1个普通同步方法,1部手机,先打印短信还是邮件
步骤CD注释。。。步骤AB放开
不妨取掉发短信的static关键字
public synchronized void sendSMS() throws Exception { TimeUnit.SECONDS.sleep(4); //步骤A System.out.println("------sendSMS"); } public static synchronized void sendEmail() throws Exception { System.out.println("------sendEmail"); }
此时,AA和BB线程俩使用的一个是调用方法的对象即phone,一个是类模板对象Phone.class,根本就不会存在锁的竞争,所以会先发邮件,再在4s后发送短信
8、1个静态同步方法,1个普通同步方法,2部手机,先打印短信还是邮件
步骤BC注释。。。步骤AD放开。
效果和7是一样的,理由也是一样的,俩种不同的锁之间不存在竞争关系
告诉我们:
静态同步方法(Class对象锁)与非静态同步方法(实例对象锁)之间是不会有竞态条件的
synchronized 方法是对整个方法进行加锁。若将一个大的方法声明为synchronized 将会大大影响效率
// 关键字在代码块上,锁为括号⾥⾯的对象
public void blockLock() {
Object o = new Object();
synchronized (o) {
// code
}
同步监视器,也叫共享对象。每一个java对象(特指共享对象)都有它自己的锁。⼀个锁同⼀时间只能被⼀个线程持有。
同步代码块,何为同步?
线程同步是线程之间按照⼀定的顺序执⾏,为了达到线程同步,可以使⽤锁来实现。
通过上⾯的例⼦我们可以看到,下⾯这两个写法其实是等价的作⽤:
// 关键字在实例⽅法上,锁为当前实例
public synchronized void instanceLock() {
// code
}
// 关键字在代码块上,锁为括号⾥⾯的对象
public void blockLock() {
synchronized (this) {
// code
}
}
同理,下⾯这两个⽅法也应该是等价的:
// 关键字在静态⽅法上,锁为当前Class对象
public static synchronized void classLock() {
// code
}
// 关键字在代码块上,锁为括号⾥⾯的对象
public void blockLock() {
synchronized (this.getClass()) {
// code
}
}
可重入锁 又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁。
Java中ReentrantLock和synchronized 都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
例如下列伪代码:
class A{ public synchronized void aa{ ...... bb(); ...... } public synchronized void bb{ ...... } } A a = new A(); a.aa();
A类中有两个普通同步方法aa()和bb(),因为都再实例方法上加了synchronized。所以方法执行都需要对象a的锁。
1、如果是不可重入锁的话
aa方法首先获取到锁,aa方法在执行的过程中需要调用bb方法,此时锁被aa方法占有,bb方法无法获取到锁,这 样就会导致bb方法无法执行,aa方法也无法执行,出现了死锁情况。
2、可重入锁可避免这种死锁的发生。
公平锁即尽量以请求锁的顺序来获取锁。同时有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该锁,这种就是公平锁。
非公平锁即无法保证锁的获取是按照请求锁的顺序进行的,这样就可能导致某个或者一些线程永远获取不到锁。
synchronized
是非公平锁,它无法保证等待的线程获取锁的顺序。
对于ReentrantLock
和ReentrantReadWriteLock
,默认情况下是非公平锁,但是可以设置为公平锁。
死锁:A线程和B线程互相持有锁,各自在加锁的代码中有需要获取对方的锁导致死锁
Object o = new Object()//会占用堆内存多少个字节
在HotSpot虛拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header) 、实例数据(Instance Data)和对齐填充( Padding)。
对齐填充,保证对象大小是8个字节的倍数,具体后面说
因为虚拟机规定:对象起始地址必须是8字节的整数倍。
填充数据不是必须存在的,仅仅是为了字节对齐,保证对象大小是8个字节的倍数
实例数据:存放类的属性(Field)数据信息,包括父类的属性信息
对象头又有2部分构成:
如果是数组在对象头会多一个部分表示数组的长度
对象头中的对象标记Mark Word中有什么
类元信息(类型指针klass pointer)存储的是指向该对象类元数据(kass)的首地址。
对象头中的类型指针指向方法区中的klass类元数据,JVM通过这个类型指针确定对象是哪个类的实例
MarkWord被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据 它会根据对象的状态复用自己的存储空间,也就是说在运行期间MarkWord里存储的数据会随着锁标志位的变化而变化。
JOL用来分析对象在jvm的大小和布局
org.openjdk.jol
jol-core
0.9
随便定义个类,主方法中输出以下内容
public static void main(String[] args) {
//VM的细节详细情况
System.out.println(VM.current().details());
//证明:所有的对象分配的字节都是8的整数倍
System.out.println(VM.current().objectAlignment()
}
synchronized升级原理
把之前重量级锁变成在一定条件下使用偏向锁以及使用轻量级(自旋锁CAS)的形式,而不是无论什么情况都使用重量级锁
用锁能够实现数据的安全性,但是会带来性能下降。无锁能够基于线程并行提升程序性能,但是会带来安全性下降。
Java6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁。
java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入, 需要在户态与核心态之间切换,这种切换会消耗大量的系统资源,
由对象头中的Mark Word根据锁标志位的不同而被复用及锁升级策略
在64位系统中,Mark Word占了8个字节(64bit),类型指针占了8个字节,一共是16个字节
对象年龄占了4个bit,所以最大是1111,故分代年龄最大就是15,然后从新生代进入老年代。
cms_free垃圾标识
锁指向
无锁:初始状态,一个对象被实例化后,如果还没有被任何线程竞争锁,那么它就为无锁状态(001)
new 一个对象,演示无锁状态的markword中各种标志位
public static void main(String[] args) { Object o = new Object(); System.out.println(ClassLayout.parseInstance(o).toPrintable()); }
控制台打印:倒着看,从右下角到左上角看,所以看到是001,无锁
31位hashcode怎么体现?
调用hashcode才会有该值,否则无
public static void main(String[] args) { Object o = new Object(); System.out.println("10进制:"+o.hashCode()); System.out.println("16进制:"+Integer.toHexString(o.hashCode())); System.out.println("2进制:"+(Integer.toBinaryString(o.hashCode()))); System.out.println(ClassLayout.parseInstance(o).toPrintable()); }
不管是看二进制还是16进制,我们都看得出来要从下往上,从右往左看。每一个
经过以往的研究发现⼤多数情况下锁不仅不存在多线程竞争,⽽且总是由同⼀线程多次获得,于是引⼊了偏向锁。
偏向锁:java允许cpu偏向某一个线程,让它一直执行,而不是用户态和内核态的切换
偏向锁会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。也即偏向锁在资源没有竞争情况下消除了同步语句,直接提高程序性能
当对象的锁第一次被某个线程持有的时候,对象头Mark Word会记录下偏向线程ID。
这样偏向线程就一直持有着锁,后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接会去检查锁的MarkWord里面是不是放的自己的线程ID。
注意:线程执行完并不会主动释放偏向锁,下次来的时候也就不需要重新加锁了
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁
用锁的线程只有一个,偏向锁几乎没有额外开销,性能极高
偏向锁实现原理
⼀个线程在第⼀次进⼊同步块时,会在对象头和栈帧中的锁记录⾥存储锁的偏向的
线程ID。当下次该线程进⼊这个同步块时,会去检查锁的Mark Word⾥⾯是不是放
的⾃⼰的线程ID。
如果是,表明该线程已经获得了锁,以后该线程在进⼊和退出同步块时不需要花费CAS操作来加锁和解锁 ;如果不是,就代表有另⼀个线程来竞争这个偏向锁。这个时候会尝试使⽤CAS来替换Mark Word⾥⾯的线程ID为新线程的ID,这个时候要分两种情况:
成功,表示之前的线程不存在了, Mark Word⾥⾯的线程ID为新线程的ID,锁不会升级,仍然为偏向锁;
失败,表示之前的线程仍然存在,那么暂停之前的线程,设置偏向锁标识为
0,并设置锁标志位为00,升级为轻量级锁,会按照轻量级锁的⽅式进⾏竞争
锁。
多个线程在不同时段获取同⼀把锁,即不存在锁竞争的情况,也就没有线程阻塞。针对这种情况,JVM采⽤轻量级锁来避免线程的阻塞与唤醒。
有线程来参与锁的竞争,但是获取锁的冲突时间极短,轻量级锁本质是自旋锁CAS。轻量级锁是为了在线程近乎交替执行同步块时提高性能,并不会去阻塞线程
锁怎么升级为轻量级锁
有大量的线程参数锁的竞争,冲突性很高。重量级锁会阻塞线程
锁升级为轻量级或重量级锁后,Mark Word中保存的分别是线程栈帧里的锁记录指针和重量级锁指针,已经没有位置再保存哈希码、GC分代年龄了,那么这些信息被移动到哪里去了呢?
锁消除
从JIT角度看相当于无视它,synchronized(o)不存在了, 这个锁对象并没有被共用扩散到其它线程使用,极端的说就是根本没有加这个锁对象的底层机器码,消除了锁的使用
这么写无意义。表面上语法没问题,但是JIT会无视
锁粗化
假如方法中首尾相接,前后相邻的都是同一个锁对象,那么JIT编译器就会把这几个synchronized块合并成一个大块,加粗加大范围,一次申请锁使用即可,避免次次的申请和释放锁,提升了性能
JIT编译器锁粗话后