在多线程中同时进行i++操作 不能保证i的原子性。i++ 可以看作为为以下几个步骤
1.读取i的值
2.计算i+1
3.赋值
在多线程下 可能还在没有来得及赋值 其他线程已经复制,再赋值就是脏数据
synchronized则能保证原子性。synchronized 一个线程获得锁对象则会将对象标记为锁定状态。执行完毕之后释放锁
synchronize的三个特性使用方式
原子性
如i++ 分为读取 计算 复制, 在这3步没有执行完毕之前 其他线程不能执行
可见性
在原子性的前提下,因为释放锁会将最新值刷入主内存。保证后面获取所得线程获取到的是最新的值
有序性
Java允许编译器和处理器对指令进行重排,但是指令重排并不会影响单线程的顺序,它影响的是多线程并发执行的顺序性
可重入性
synchronized和ReentrantLock都是可重入锁。当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁。通俗一点讲就是说一个线程拥有了锁仍然还可以重复申请锁。
synchronize的三种使用方式
- 修饰实例方法
- 修饰静态方法
- 修饰代码块
修饰实例方法
.... public synchronized void account(){ i++; } ....
修饰静态方法
... public static synchronized void account(){ i++; } ...
修饰代码块
使用this
public void account(){ synchronized (this) { i++; } }
使用对象
int i=0; Object lockObj=new Object(); public void account(){ synchronized (lockObj) { i++; } }
使用class
public void account(){ synchronized (Accounting.class) { i++; } }
sychronized原理
反编译
* @author liqiang * @date 2020/3/30 15:52 * @Description: (what) * (why) * (how) */ public class Accounting implements Runnable { int i=0; public void account(){ synchronized (this) { i++; } } @Override public void run() { for (int j=0;j<2000;j++){ account(); } } public int getI() { return i; } public static void main(String[] args) throws InterruptedException { Accounting accounting= new Accounting(); Thread t=new Thread(accounting,"a1"); Thread t2=new Thread(accounting,"a2"); t.start(); t2.start(); t.join();//主线程挂起等待这个线程执行完毕在网下执行 t2.join(); System.out.print(accounting.getI()); } }
1.编译class文件
javac -encoding UTF-8 Accouting.java //先运行编译class文件命令
2.打印
javap -v Accouting.class //再通过javap打印出字节文件
public void account(); descriptor: ()V flags: ACC_PUBLIC Code: stack=3, locals=3, args_size=1 0: aload_0 1: dup 2: astore_1 3: monitorenter //monintorenter指令 4: aload_0 5: dup 6: getfield #2 // Field i:I 9: iconst_1 10: iadd 11: putfield #2 // Field i:I 14: aload_1 15: monitorexit //monitorexit指令 16: goto 24 19: astore_2 20: aload_1 21: monitorexit 22: aload_2 23: athrow 24: return Exception table: from to target type 4 16 19 any 19 22 19 any LineNumb
在进入monitorenter指令后 线程将持有Monitor,在进入monnitorexit指令后将释放Monitor对象
monitor的实现类是ObjectMonitor
主要成员包括_WaitSet _EntryList _Owner 用来保存ObjectWaiter对象(每个等待锁的线程都会封装成ObjectWaiter)
当多线程同时访问一段同步代码块会首先进入_EntryList状态为block,获得锁的线程则会设置到_oWner 同时monitor对象的count+1
如果调用线程的wait方法则清空_oWner count-1 同时当前线程进入_WaitSet等待被唤醒
如果当前线程执行完毕也将清空_Owner count-1
其他block线程再次发起竞争
monitor结构
ObjectMonitor() {
_header = NULL;
_count = 0; //记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
线程中断
public void interrupt();//只能中断阻塞线程 非阻塞线程只会给当前线程增加中断标志,线程还是会继续往下执行 public boolean isInterrupted(); //判断线程是否中断 public static boolean interrupted();//判断线程是否中断并且会清除中断状态
package com.liqiang.sychronize; public class Accounting implements Runnable { int i = 0; public void account() { } @Override public void run() { try { while (true) { Thread.sleep(2000); System.out.println("1"); } } catch (InterruptedException e) { e.printStackTrace(); } } public int getI() { return i; } public static void main(String[] args) throws InterruptedException { Accounting accounting = new Accounting(); Thread t = new Thread(accounting, "a1"); t.start(); Thread.sleep(4000); t.interrupt(); System.out.println(t.isInterrupted()); t.join();//主线程挂起等待这个线程执行完毕在往下执行 } }
interrupt方法只能中断阻塞线程(需要try捕获异常 否则会一直执行下去)非阻塞线程需要我们手动中断
package com.liqiang.sychronize; public class Accounting implements Runnable { int i = 0; public void account() { } @Override public void run() { while (true) { if(Thread.currentThread().isInterrupted()){ break; } System.out.println("1"); } } public int getI() { return i; } public static void main(String[] args) throws InterruptedException { Accounting accounting = new Accounting(); Thread t = new Thread(accounting, "a1"); t.start(); Thread.sleep(4000); t.interrupt(); System.out.println(t.isInterrupted()); t.join();//主线程挂起等待这个线程执行完毕在网下执行 } }
sleep和wait的区别
Thread.sleep 与wait的区别 wait会将线程锁释放 线程保存到monitor的 _waitSet里面 等待被唤醒,当调用notify() 唤醒线程,唤醒线程并不是立即执行,而是重新获取锁(注:调用wait必须已经持有锁), sleep是线程休眠并不释放锁
jdk1.6的锁优化
在 JDK1.6 JVM 中,对象实例在堆内存中被分为了三个部分:对象头、实例数据和对齐填充。其中 Java 对象头由 Mark Word、指向类的指针以及数组长度三部分组成。
Mark Word记录了对象和锁的相关信息
锁升级主要由Mark Word 中的锁标志位和释放偏向锁标志位,Synchronized 同步锁就是从偏向锁开始的,随着竞争越来越激烈,偏向锁升级到轻量级锁,最终升级到重量级锁。
偏向锁
偏向锁主要用来优化同一线程多次申请同一个锁的竞争。在某些情况下,大部分时间是同一个线程竞争锁资源
线程执行同步代码块的时候 只需要到Mark Word 中去判断一下是否有偏向锁指向它的 ID,无需再进入 Monitor 去竞争对象了。当对象被当做同步锁并有一个线程抢到了锁时,锁标志位还是 01,“是否偏向锁”标志位设置为 1,并且记录抢到锁的线程 ID,表示进入偏向锁状态。
一旦出现其它线程竞争锁资源时,偏向锁就会被撤销。偏向锁的撤销需要等待全局安全点,暂停持有该锁的线程,同时检查该线程是否还在执行该方法,如果是,则升级锁,反之则被其它线程抢占
偏向锁生成的流程:摘自《极客时间-java调优实战》
public class Accounting implements Runnable { int i=0; public void account(){ synchronized (this) { i++; } } @Override public void run() { for (int j=0;j<2000;j++){ account(); } } public int getI() { return i; } public static void main(String[] args) throws InterruptedException { Accounting accounting= new Accounting(); Thread t=new Thread(accounting,"a1"); t.start(); t.join();//主线程挂起等待这个线程执行完毕在网下执行 Thread t2=new Thread(accounting,"a2"); t2.start(); t2.join(); System.out.print(accounting.getI()); } }
上面这种情况虽然有2个线程 但是不存在锁竞争所以偏向锁 就发生了优化的作用。但是,在高并发场景下,当大量线程同时竞争同一个锁资源时,偏向锁就会被撤销,发生 stop the word 后, 开启偏向锁无疑会带来更大的性能开销,这时我们可以通过添加 JVM 参数关闭偏向锁来调优系统性能,
-XX:-UseBiasedLocking //关闭偏向锁(默认打开) 或者 -XX:+UseHeavyMonitors //设置重量级锁
轻量级锁
当有另外一个线程竞争获取这个锁时,由于该锁已经是偏向锁,当发现对象头 Mark Word 中的线程 ID 不是自己的线程 ID,就会进行 CAS 操作获取锁,如果获取成功,直接替换 Mark Word 中的线程 ID 为自己的 ID,该锁会保持偏向锁状态;如果获取锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。轻量级锁适用于线程交替执行同步块的场景,绝大部分的锁在整个同步周期内都不存在长时间的竞争。
public class Accounting implements Runnable { int i=0; public void account(){ synchronized (this) { i++; } } @Override public void run() { for (int j=0;j<2000;j++){ account(); } } public int getI() { return i; } public static void main(String[] args) throws InterruptedException { Accounting accounting= new Accounting(); Thread t=new Thread(accounting,"a1"); t.start(); Thread t2=new Thread(accounting,"a2"); t2.start(); t2.join(); t.join();//主线程挂起等待这个线程执行完毕在网下执行 System.out.print(accounting.getI()); } }
虽然a1和a2存在竞争,但是当a1执行期间 a2 cas自旋重试获取锁(固定重试次数 如果重试失败则暂停所有线程 开始升级锁)。a1不是耗时操作0.几毫秒释放后a2继续执行
所以针对存在竞争但是竞争不大的情况下轻量级锁才能发挥做用
偏向锁升级为轻量级锁图 摘自《极客时间java性能调优实战》
自旋锁与重量级锁
轻量级锁 CAS 抢锁失败,线程将会被挂起进入阻塞状态。如果正在持有锁的线程在很短的时间内释放资源,那么进入阻塞状态的线程无疑又要申请锁资源。JVM 提供了一种自旋锁,可以通过自旋方式不断尝试获取锁,从而避免线程被挂起阻塞。这是基于大多数情况下,线程持有锁的时间都不会太长,毕竟线程被挂起阻塞可能会得不偿失。从 JDK1.7 开始,自旋锁默认启用,自旋次数由 JVM 设置决定,这里我不建议设置的重试次数过多,因为 CAS 重试操作意味着长时间地占用 CPU。自旋锁重试之后如果抢锁依然失败,同步锁就会升级至重量级锁,锁标志位改为 10。在这个状态下,未抢到锁的线程都会进入 Monitor,之后会被阻塞在 _WaitSet 队列中。、
重量级锁图,摘自:《极客时间-java调优实战》
锁粗化
就是在 JIT 编译器动态编译时,如果发现几个相邻的同步块使用的是同一个锁实例,那么 JIT 编译器将会把这几个同步块合并为一个大的同步块,从而避免一个线程“反复申请、释放同一个锁“所带来的性能开销。
优化前
public void account(){ ArrayList<Integer> arrs=new ArrayList<>(); for(int i=0;i<10;i++){ synchronized (Accounting.class){ arrs.add(i); } } }
优化后
public void account() { synchronized (Accounting.class) { ArrayList<Integer> arrs = new ArrayList<>(); for (int i = 0; i < 10; i++) { arrs.add(i); } } }
锁消除
public void account() { StringBuilder stringBuilder=new StringBuilder(); stringBuilder.append("1"); }
个人理解 虽然StringBuilder的append是同步方法,但是StringBuilder是方法内变量 不存在锁竞争 就会吧同步锁去掉(偏向锁)