一、多线程
1.线程同步
我们知道线程之间是共享数据的,这就有可能出现“ 同一个资源,多个人都想使用”的情况。 多个线程访问同一个对象,并且某些线程还想修改这个对象。 这时候,我们就需要用到“线程同步”。 线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用。
/*** 多线程操作,未同步* Created by bzhang on 2019/3/13.*/public class TestSynchronize implements Runnable{ private static Integer count = 1000; public void get(int i){ count = count-i; System.out.println(Thread.currentThread() + ":" + count); } @Override public void run() { if (count > 600) { try { Thread.sleep(500); get(500); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("不够600了:"+count); } public static void main(String[] args) { new Thread(new TestSynchronize()).start(); new Thread(new TestSynchronize()).start(); }}
其结果是:
Thread[Thread-1,5,main]:500不够600了:500Thread[Thread-0,5,main]:0不够600了:0
可见,不足200时仍能够操作,与期望结果不符合。为了解决这个问题,不得不对线程使用锁,即在同一时间内只允许一个线程访问count变量,其他线程必须等待前一个线程访问完之后才允许使用该变量。
/*** 多线程操作,已同步* Created by bzhang on 2019/3/13.*/public class TestSynchronize implements Runnable{ private static Integer count = 1000; public void get(int i){ count = count-i; System.out.println(Thread.currentThread() + ":" + count); } @Override public void run() { synchronized (count) { if (count > 600) { try { Thread.sleep(500); get(500); } catch (InterruptedException e) { e.printStackTrace(); } } } System.out.println("不够600了:"+count); } public static void main(String[] args) { new Thread(new TestSynchronize()).start(); new Thread(new TestSynchronize()).start(); }} 结果是:Thread[Thread-0,5,main]:500不够200了:500不够200了:500
二、锁
1.synchronized关键字
Java中为了解决同一数据对象被多个线程同时访问造成冲突的问题引入了synchronized关键字。其用法主要有synchronized方法和synchronized块。
/*** synchronized修饰的都是对象,不论是synchronized方法还是synchronized块* 即锁只能是锁对象,这个对象可以是this,临界资源对象,class对象等。* Created by bzhang on 2019/3/13.*/public class UseSynchronized { private int age; private Object o = new Object(); private static int count = 0; private static Object obj = new Object(); //被synchronized修饰的方法,相当于锁this,即m1方法与m2方法锁的对象是相同的 public synchronized void m1(){ System.out.println(new Date()); System.out.println(Thread.currentThread().getName()+":age="+ age++); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } //锁当前对象的synchronized块 public void m2(){ synchronized (this){ System.out.println(new Date()); System.out.println(Thread.currentThread().getName()+":age="+ age++); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { UseSynchronized u = new UseSynchronized(); new Thread(new Runnable() { @Override public void run() { u.m2(); } }).start(); new Thread(new Runnable() { @Override public void run() { u.m1(); } }).start(); }}
其运行结果如下,不论m1先执行还是m2先执行,后执行的方法都必须等待先执行的方法执行完才能执行,说明m1和m2锁的对象时同一个。
Wed Mar 13 17:19:07 CST 2019Thread-0:age=0Wed Mar 13 17:19:09 CST 2019Thread-1:age=1
若锁的不是同一个对象,
/*** Created by bzhang on 2019/3/13.*/public class UseSynchronized { private int age; private Object o = new Object(); private static int count = 0; private static Object obj = new Object(); public static void main(String[] args) { UseSynchronized u = new UseSynchronized(); new Thread(new Runnable() { @Override public void run() { u.m2(); } }).start(); new Thread(new Runnable() { @Override public void run() { u.m3(); } }).start(); } //m3方法锁的是o对象 public void m3(){ synchronized (o){ System.out.println(new Date()); System.out.println(Thread.currentThread().getName()+":age="+ age++); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } } } //m2方法锁的仍是this对象 public void m2(){ synchronized (this){ System.out.println(new Date()); System.out.println(Thread.currentThread().getName()+":age="+ age++); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } } }}
运行结果是如下,可见锁的对象不同,不会起到同步效果,m2与m3方法基本是同一时刻运行,没有sleep的过程,这也说明了同步方法只影响锁定同一对象(资源)的同步方法,并不影响其他线程调用非同步方法或调用其他锁资源的同步方法。
Wed Mar 13 17:41:17 CST 2019Thread-1:age=0Wed Mar 13 17:41:17 CST 2019Thread-0:age=1
当锁的是静态方法时,其本质是对class对象进行同步修饰操作。
/*** Created by bzhang on 2019/3/13.*/public class UseSynchronized { private int age; private Object o = new Object(); private static int count = 0; private static Object obj = new Object(); public static void main(String[] args) { UseSynchronized u = new UseSynchronized(); new Thread(new Runnable() { @Override public void run() { u.m4(); } }).start(); new Thread(new Runnable() { @Override public void run() { u.m5(); } }).start(); } //锁class对象 public static void m5(){ synchronized (UseSynchronized.class){ System.out.println(new Date()); System.out.println(Thread.currentThread().getName()+":count="+ count++); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } } } //修饰静态方法 public static synchronized void m4(){ System.out.println(new Date()); System.out.println(Thread.currentThread().getName()+":count="+ count++); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } }} 其结果如下,可以看出两个静态方法之间存在同步操作,说明锁的是同一个对象。Wed Mar 13 17:51:42 CST 2019Thread-0:count=0Wed Mar 13 17:51:44 CST 2019Thread-1:count=1
进行同步操作的根本目的是为了保证操作的原子性,即假设有变量k=0,在多线程环境下对其进行50次的自增操作,其结果恒为k=50,则说明操作具有原子性,若存在k为其他值,则操作不具有原子性。示例:
public class AtmoicTest { private int count = 0; public void add(){ System.out.println(Thread.currentThread().getName()+":"+count); try { TimeUnit.MILLISECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } count = count + 1; } public int getCount(){ return count; } public static void main(String[] args) { AtmoicTest test = new AtmoicTest(); for (int m=0;m<5;m++){ new Thread(new Runnable() { @Override public void run() { for (int i=0;i<10;i++){ test.add(); } } }).start(); } try { TimeUnit.SECONDS.sleep(1); System.out.println("结果是:"+test.getCount()); } catch (InterruptedException e) { e.printStackTrace(); } }}//结果是:47
当对add方法进行同步加锁操作后,即可保证结果的正确及count自增操作的原子性,另外对add方法加锁只能保证该方法的原子性,若有add2方法也是对count进行自增操作,那么count结果仍可能不正确,要取决与add2方法是否也实现了同步操作。
2.锁的重入
锁的重入:同一个线程,多次访问不同或相同的同步代码,这些代码若锁的是同一资源/对象,那么锁可重入。即当一个线程拥有了某一锁资源,可在其拥有该锁资源的期间对锁同一资源的代码任意访问。
/*** Created by bzhang on 2019/3/13.*/public class ReenterTest { public synchronized void m1(){ System.out.println("m1"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } m2(); System.out.println("m1 gg"); } public synchronized void m2() { System.out.println("m2"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("m2 gg"); } public static void main(String[] args) { ReenterTest test = new ReenterTest(); new Thread(new Runnable() { @Override public void run() { test.m1(); } }).start(); new Thread(new Runnable() { @Override public void run() { test.m2(); } }).start(); }}其结果:m1m2m2 ggm1 ggm2m2 gg
子类重写父类的同步方法,若重写的方法也需要同步,需要重写增加同步操作,否则不能同步。子类的同步方法中可以调用父类的同步方法,相当于重入。
/*** Created by bzhang on 2019/3/13.*/public class ParentReenterTest extends P { public void visit(){ super.m1(); } @Override public synchronized void m1() { System.out.println(new Date()+"----son"); super.m1(); System.out.println("son gg"); } public static void main(String[] args) { ParentReenterTest test = new ParentReenterTest(); new Thread(new Runnable() { @Override public void run() { test.m1(); } }).start(); new Thread(new Runnable() { @Override public void run() { test.visit(); } }).start(); }}class P { public synchronized void m1(){ System.out.println(new Date()+"-----parent"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } }}//子类中重写m1方法不带synchronized,结果:Wed Mar 13 21:06:38 CST 2019-----parentWed Mar 13 21:06:38 CST 2019----sonWed Mar 13 21:06:39 CST 2019-----parentson gg//子类中重写m1方法带synchronized,结果:Wed Mar 13 21:09:03 CST 2019----sonWed Mar 13 21:09:03 CST 2019-----parentson ggWed Mar 13 21:09:04 CST 2019-----parent
3.锁与异常
当在同步方法中发生异常时,持有锁资源的线程会自动释放锁,不会影响到其他线程获取锁资源。
/*** Created by bzhang on 2019/3/13.*/public class ExceptionTest { public synchronized void m1(int k){ while (k>0){ System.out.println(Thread.currentThread().getName()+"----m1"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } if (k--==6){ int res = 2/0; } } } public static void main(String[] args) { ExceptionTest test = new ExceptionTest(); new Thread(new Runnable() { @Override public void run() { test.m1(7); } }).start(); new Thread(new Runnable() { @Override public void run() { test.m1(4); } }).start(); }}//运行结果:Thread-0----m1Thread-0----m1Thread-1----m1Exception in thread "Thread-0" java.lang.ArithmeticException: / by zero at com.bzhang.thread.ExceptionTest.m1(ExceptionTest.java:18) at com.bzhang.thread.ExceptionTest$1.run(ExceptionTest.java:28) at java.lang.Thread.run(Thread.java:748)Thread-1----m1Thread-1----m1Thread-1----m1
4.锁对象改变
synchronized锁的对象在同步期间如果发生改变,将会导致同步失效,但不影响后续对同步代码执行,即锁对象变更后,对后续的访问仍能保持同步。但对锁对象改变时刻的持有锁对象的线程影响巨大,因为此时,新的锁对象是重新开放竞争的。
/*** Created by bzhang on 2019/3/13.*/public class ChangeTest { Object object = new Object(); public void m1(){ synchronized (object){ System.out.println(Thread.currentThread().getName()+":"+object+"**"+new Date()); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+":"+object+"**"+new Date()); } } public static void main(String[] args) { ChangeTest test = new ChangeTest(); new Thread(new Runnable() { @Override public void run() { test.m1(); } }).start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } test.object = new Object(); System.out.println("main:"+ test.object); new Thread(new Runnable() { @Override public void run() { test.m1(); } }).start(); }}//其结果:Thread-1线程并没有在3秒之后才能访问m1方法Thread-0:java.lang.Object@d46d875**Wed Mar 13 21:56:52 CST 2019main:java.lang.Object@677327b6Thread-1:java.lang.Object@677327b6**Wed Mar 13 21:56:53 CST 2019Thread-0:java.lang.Object@677327b6**Wed Mar 13 21:56:55 CST 2019Thread-1:java.lang.Object@677327b6**Wed Mar 13 21:56:56 CST 2019
三、锁的底层实现及种类
1.锁的底层实现
在JVM中,对象在内存中存储的布局分为3块区域:对象头(Header)、实例数据(Instance Data)和填充数据(Padding)。
1.对象头分为两个部分:第一部分用于存储对象自身的运行时,如hashcode、GC分代年龄、锁状态标志、线程持有的锁等,称为Mark Word;另一部分则是类型指针,即对象指向他的类元数据的指针(JVM通过这个指针来确定对象是哪个类的实例),称为Class MetaData Address。
2.实例数据:对象真正存储的有效信息的部分,其中还包括继承自父类的信息。
3.填充数据:该部分并不是必须存在的,也无特殊含义,仅仅是为了占位。Java中对象的大小必须是8字节的整数倍,当对象大小不是8字节的整数倍时,就用填充数据来补齐。
实现synchronized锁对象的基础是Java中的对象头,锁对象一般是记录在对象头中的Mark Word。当执行synchronized同步方法或同步代码快时,会在对象头中记录锁标记,锁标记指向的是monitor对象(管程或监视器锁)的起始地址,每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如 monitor 可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。
在Java虚拟机中(hotspot)monitor的由ObjectMonitor实现的,其数据结构大致如下(仅列出用到的,C++实现):
ObjectMonitor() { _header = NULL; _count = 0; //计数器,记录当前拥有该锁的线程重入的次数。 _waiters = 0, _owner = NULL; //用于记录当前持有锁的线程,即执行线程 _WaitSet = NULL; //调用了wait()方法,会被加入到_WaitSet队列中,_WaitSet队列用于记录和管理等待访问的线程 _WaitSetLock = 0 ; _EntryList = NULL ; //处于等待锁block状态的线程,会被加入到_EntryList队列中,用于记录和管理想要获取锁的线程 }
当多线程并发访问同步代码时,首先进入_EntryList等待线程竞争获取锁标记,当某一线程获取到锁对象的monitor后,monitor中的_owner会记录此线程,并将_count计数+1,代表锁定。其他线程则继续在_EntryList中阻塞等待;若当前执行线程调用wait()方法,则monitor中的_onwer将置为null,计数器_count归零,代表当前执行线程放弃锁,该线程进入_WaitSet队列中阻塞(阻塞到调用notify或notifyAll方法为止,唤醒后进入_EntryList队列等待竞争锁);若线程执行完同步代码,则_count会减为0(_count记录重入锁的次数,每进入一次+1,每出一次-1,最终同步代码执行完,_count=0),_owner恢复为null,释放锁。
synchronized同步语句块的底层实现使用的是monitorenter 和 monitorexit指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置。当执行monitorenter指令时,当前线程将试图获取锁对象所对应的 monitor的持有权,当锁对象的 monitor的_count为 0,那线程可以成功取得 monitor,并将_count值+1,获取锁成功。倘若其他线程已经拥有锁对象的monitor的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放monitor(锁)并设置_count值为0 ,其他线程将有机会持有monitor。值得注意的是编译器将会确保无论方法通过何种方式完成,方法中调用过的每条monitorenter指令都有执行其对应 monitorexit 指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时monitorenter和monitorexit指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行monitorexit指令。
synchronized同步方法的底层实现与同步块不同,它并不是由monitorenter 和 monitorexit指令来实现同步的,而是由方法调用指令读取运行时常量池中的方法的 ACC_SYNCHRONIZED标记( ACC_SYNCHRONIZED指明该方法为同步方法 )来隐 式实现的。 当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词), 然后在执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放monitor。
2.锁的种类
Java中锁的种类大致可分为4种:偏向锁,自旋锁,轻量级锁,重量级锁。
- 偏向锁:是一种编译解释锁,它是针对加锁操作的优化手段。很多时候,所不仅不存在线程竞争,往往总是由统一线程多次获取,因此为了减少同一线程获取锁的代价引入了偏向锁。其核心是,当锁对象第一次被线程获取时,那么这个锁就进入偏向模式,Mark Word变为偏向锁,记录下线程id以及是否是偏向锁。这样这个线程就一直持有锁,以后请求锁的线程若还是同一线程时,无需在做任何同步操作,直接获取锁,省去大量申请锁的操作,提高程序的性能。若请求锁的线程改变(意味着发生了竞争),那么锁就不在偏向某一线程,偏向锁失效,需要膨胀为轻量级锁。
- 轻量级锁:是一种过度锁,当不满足偏向锁时,虚拟机并不会直接膨胀成重量级锁,它会先尝试轻量级锁,即Mar Word结构变为轻量级锁的结构。其原理是:使用标记 ACC_SYNCHRONIZED来记录获取到锁的线程, ACC_UNSYNCHRONIZED标记记录为获取到锁的线程,就是说只有两个线程争抢锁标记的时候,优先使用轻量级锁。两个线程也是会出现重量级锁的。
- 自旋锁:也是一种过度锁。当获取锁的过程中,未获取到锁时,为了提高效率,JVM会进行若干次空循环后再次申请锁,而不是直接将线程阻塞挂起。自旋锁是基 于 在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁。若循环过后还不能得到锁,那么就将该线程挂起。
- 重量级锁:就是synchronized。
锁的使用方式一般是:先偏向锁,不满足则膨胀为轻量级锁,还不满足在膨胀为重量级锁,锁只能升级不能降级。
锁的消除,JVM根据上下文环境判定同步代码不可能存在锁的竞争,直接放弃同步信息,消除synchronized的影响。