在计算机操做系统中,并发在宏观上是指在同一时间段内,同时有多道程序在运行。 一个程序能够对应一个进程或多个进程,进程有独立的存储空间。一个进程包含一个或多个线程。线程堆空间是共享的,栈空间是私有的。一样,在一个进程中,宏观上有多个线程同时运行。(微观上在单cup系统中,同一时刻,只有一个程序在运行。)html
基于以上原理,线程在并发运行时,对共享数据的操做存在数据同步问题。java
1.什么样的数据会被存储在线程共享空间堆里?微信
对象,当使用new 关键字建立一个对象时,这个对象就被存储在堆里。网络
2.并发问题:多线程
以一个例子来讲明:并发
建立测试类Test.java,测试类中有一个方法对变量sum加1操做,建立两个线程tA和tB分别执行这段代码:oracle
1 public class Test { 2 private int sum = 0; 3 public void add(){ 4 try { 5 System.out.println("线程:"+Thread.currentThread().getName()+"执行加1开始,sum当前值为:"+sum); 6 Thread.sleep(2000);//这两秒表明对其余数据进行操做所耗费的时间 7 sum++; 8 System.out.println("线程:"+Thread.currentThread().getName()+"执行加1结束,sum的值为:"+sum); 9 } catch (InterruptedException e) { 10 // TODO Auto-generated catch block 11 e.printStackTrace(); 12 } 13 } 14 public static void main(String [] args){ 15 final Test test = new Test(); 16 Thread tA = new Thread(new Runnable() { 17 18 @Override 19 public void run() { 20 // TODO Auto-generated method stub 21 test.add(); 22 } 23 }); 24 Thread tB = new Thread(new Runnable() { 25 26 @Override 27 public void run() { 28 // TODO Auto-generated method stub 29 test.add(); 30 } 31 }); 32 tA.start(); 33 tB.start(); 34 } 35 }
运行结果:app
线程:Thread-1执行加1开始,sum当前值为:0 线程:Thread-0执行加1开始,sum当前值为:0 线程:Thread-1执行加1结束,sum的值为:1 线程:Thread-0执行加1结束,sum的值为:1
从以上数据来看,这个结果明显不对,两次加操做,最后的值应是2。异步
缘由分析:两个线程前后执行add方法时,拿到的数据都是0,再对共享数据加1,最后结果都是1。jvm
1.普通同步方法,锁加在当前实例对象上。
2.静态方法,锁加载当前类对象上。
3.同步方法块,锁住的是synchronized (xxx)括号里的对象。
解析: 从1.0开始,java中的每个对象都有一个内部锁(这是加锁的基础)。
对于普通同步方法,若是一个方法使用synchronized 关键字声明,那么对象锁将保护整个方法,做用的对象是调用这个方法的对象。当某一个线程运行到该方法时,须要检查有没有其余线程正在使用这个方法,有的话须要等待那个线程运行完这个方法后在运行,没有的话,须要锁定这个方法,而后运行。
对于静态方法声明为synchronized ,若是调用这种方法,由于静态方法是类方法,其做用的范围是整个方法,做用的对象是这个类的全部对象,该方法得到到相关的类对的内部锁,所以,没有其余线程能够调用同一个类的同步静态方法。
对于同步代码块,被修饰的代码块称为同步语句块,其做用的范围是大括号{}括起来的代码,做用的对象是调用这个代码块的对象,也就是括号里面的对象,synchronized 括号里能够反射获取类的对象,例如本示例中能够写成 synchronized (this),也能够写成Test.class 。
2.2.1. 普通同步方法:
以上面代码为例,只须要给普通方法上加上synchronized 关键字,其余代码不变,只贴改变了的代码.
1 public synchronized void add(){ 2 try { 3 System.out.println("线程:"+Thread.currentThread().getName()+"执行加1开始,sum当前值为:"+sum); 4 Thread.sleep(2000);//这两秒表明对其余数据进行操做所耗费的时间 5 sum++; 6 System.out.println("线程:"+Thread.currentThread().getName()+"执行加1结束,sum的值为:"+sum); 7 } catch (InterruptedException e) { 8 // TODO Auto-generated catch block 9 e.printStackTrace(); 10 } 11 }
运行结果:
1 线程:Thread-0执行加1开始,sum当前值为:0 2 线程:Thread-0执行加1结束,sum的值为:1 3 线程:Thread-1执行加1开始,sum当前值为:1 4 线程:Thread-1执行加1结束,sum的值为:2
从以上数据能够看出,做用的对象是调用这个方法的对象,当第一个线程执行完此方法,第二个线程才开始执行。
在 ..\bin\com\test 目录下找到类对应的class文件,个人是Test.class, 使用 Javap -v Test.class 命令查看字节码信息,以下:
经过上面的截图能够看到,在add()方法上加了一个 ACC_SYNCHRONIZED 标识,JVM在解析的时候,根据这个标识实现方法同步。
2.2.2.静态方法
先看问题,将上面的代码改造为两个对象,代码以下:
1 package com.test; 2 3 public class Main { 4 public static int i = 0; 5 public static void add() { 6 try { 7 System.out.println("线程"+Thread.currentThread().getName()+"调用add()方法前,i的值为:"+i); 8 Thread.sleep(2000); 9 i=i+1; 10 System.out.println("线程"+Thread.currentThread().getName()+"调用add()方法后,i的值为:"+i); 11 }catch (InterruptedException e){ 12 e.printStackTrace(); 13 } 14 } 15 public static void main(String[] args) { 16 17 final Main min1 = new Main(); //对象1 18 final Main min2 = new Main(); //对象2 19 Thread ta = new Thread(new Runnable() { 20 public void run() { 21 min1.add(); 22 } 23 }); 24 Thread tb = new Thread(new Runnable() { 25 public void run() { 26 min2.add(); 27 } 28 }); 29 ta.start(); 30 tb.start(); 31 32 } 33 }
运行结果:
1 线程Thread-0调用add()方法前,i的值为:0 2 线程Thread-1调用add()方法前,i的值为:0 3 线程Thread-0调用add()方法后,i的值为:1 4 线程Thread-1调用add()方法后,i的值为:2
从运行结果能够看出,两个线程是交叉执行的,可是结果倒是正确的,没有什么问题。可是,若是将线程增长到五个再看一下:
1 package com.test; 2 3 public class Main { 4 public static int i = 0; 5 public static void add() { 6 try { 7 System.out.println("线程"+Thread.currentThread().getName()+"调用add()方法前,i的值为:"+i); 8 Thread.sleep(2000); 9 i=i+1; 10 System.out.println("线程"+Thread.currentThread().getName()+"调用add()方法后,i的值为:"+i); 11 }catch (InterruptedException e){ 12 e.printStackTrace(); 13 } 14 } 15 public static void main(String[] args) { 16 17 final Main min1 = new Main(); //对象1 18 final Main min2 = new Main(); //对象2 19 final Main min3 = new Main(); //对象3 20 final Main min4 = new Main(); //对象4 21 final Main min5 = new Main(); //对象5 22 Thread ta = new Thread(new Runnable() { 23 public void run() { 24 min1.add(); 25 } 26 }); 27 Thread tb = new Thread(new Runnable() { 28 public void run() { 29 min2.add(); 30 } 31 }); 32 33 Thread tc = new Thread(new Runnable() { 34 public void run() { 35 min3.add(); 36 } 37 }); 38 Thread td = new Thread(new Runnable() { 39 public void run() { 40 min4.add(); 41 } 42 }); 43 Thread te = new Thread(new Runnable() { 44 public void run() { 45 min5.add(); 46 } 47 }); 48 tc.start(); 49 td.start(); 50 te.start(); 51 ta.start(); 52 tb.start(); 53 54 } 55 }
运行结果:
1 线程Thread-3调用add()方法前,i的值为:0 2 线程Thread-4调用add()方法前,i的值为:0 3 线程Thread-2调用add()方法前,i的值为:0 4 线程Thread-0调用add()方法前,i的值为:0 5 线程Thread-1调用add()方法前,i的值为:0 6 线程Thread-0调用add()方法后,i的值为:1 7 线程Thread-1调用add()方法后,i的值为:3 8 线程Thread-3调用add()方法后,i的值为:2 9 线程Thread-2调用add()方法后,i的值为:5 10 线程Thread-4调用add()方法后,i的值为:4
一眼就看出有问题了,缘由再也不讨论。再看给add()方法加锁后的状况:
1 public static synchronized void add() { 2 try { 3 System.out.println("线程"+Thread.currentThread().getName()+"调用add()方法前,i的值为:"+i); 4 Thread.sleep(2000); 5 i=i+1; 6 System.out.println("线程"+Thread.currentThread().getName()+"调用add()方法后,i的值为:"+i); 7 }catch (InterruptedException e){ 8 e.printStackTrace(); 9 } 10 }
运行结果:
1 线程Thread-2调用add()方法前,i的值为:0 2 线程Thread-2调用add()方法后,i的值为:1 3 线程Thread-1调用add()方法前,i的值为:1 4 线程Thread-1调用add()方法后,i的值为:2 5 线程Thread-0调用add()方法前,i的值为:2 6 线程Thread-0调用add()方法后,i的值为:3 7 线程Thread-4调用add()方法前,i的值为:3 8 线程Thread-4调用add()方法后,i的值为:4 9 线程Thread-3调用add()方法前,i的值为:4 10 线程Thread-3调用add()方法后,i的值为:5
这个结果就顺眼多了,再看字节码状况:
通过查看发现,这个加锁方式和普通方法加锁方式同样,都是加了一个标志。
2.2.3.同步方法块
将add()方法改造以下:
1 public void add(){ 2 synchronized (this) { 3 try { 4 System.out.println("线程:"+Thread.currentThread().getName()+"执行加1开始,sum当前值为:"+sum); 5 Thread.sleep(2000); //这两秒表明对其余数据进行操做所耗费的时间 6 sum++; 7 System.out.println("线程:"+Thread.currentThread().getName()+"执行加1结束,sum的值为:"+sum); 8 } catch (InterruptedException e) { 9 // TODO Auto-generated catch block 10 e.printStackTrace(); 11 } 12 13 } 14 }
运行结果:
1 线程:Thread-1执行加1开始,sum当前值为:0 2 线程:Thread-1执行加1结束,sum的值为:1 3 线程:Thread-0执行加1开始,sum当前值为:1 4 线程:Thread-0执行加1结束,sum的值为:2
以上运行结果正常,查看字节码信息:
为了查看信息量小,将同步代码块内的代码屏蔽掉,而后编译,查看字节码信息以下:
同步方法块和以上两种就不同了,是经过 monitorenter 和 monitorexit 给对象 this 也就是Test类的对象加锁和解锁。
若是给同步代码块内添加任意可执行代码,状况就变了,好比加一句打印语句(不上图,自行想象),字节码信息以下:
竟然出现两条monitorexit ,可是只有一个monitorenter,这就是锁的重入性,什么意思呢?对于同一个类的对象,线程在执行一个任务时,会获取一次锁,当执行完会释放锁,若是这个线程还要继续执行这个对象的其余任务,是不须要从新获取锁的,但执行完任务就要释放锁,顾名思义,锁的重入性。
综上,synchronized 加锁的方法就是使用ACC_SYNCHRONIZED 和 monitorenter—monitorexit 实现的,那么,接下来,就研究研究这三个词是什么。
2.2.4 synchronized 原理
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.11.10 中的解释是这样的:
同步方法在运行时常量池的method_info结构中经过ACC_SYNCHRONIZED标志区分,该标志由方法调用指令检查。当调用设置了ACC_SYNCHRONIZED的方法时,执行线程进入监视器,调用方法自己,并退出监视器,不管方法调用是正常仍是忽然完成。在执行线程拥有监视器期间,没有其余线程能够输入它。若是在调用synchronized方法期间抛出异常而且synchronized方法不处理异常,则在异步从同步方法中从新抛出以前,将自动退出该方法的监视器。
这里有个关键词:监视器。监视器是什么呢? 监视器又名monitor。每一个对象都是一个监视器锁,当对象监视器锁被占用时,对象就是锁定状态,其余线程不能对其操做,当占用被解除时,其余线程就能够获取此对象。
那么上面三个词是怎么工做的呢?
a.monitorenter—monitorexit
monitorenter 和 monitorexit 是线程执行同步代码块时执行的指令,当线程执行同步代码块时会占用监视器,执行monitorexit 指令会解除监视器。
b.ACC_SYNCHRONIZED
当JVM调用方法时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,若是设置了,执行线程将先获取monitor,获取成功以后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其余任何线程都没法再得到同一个monitor对象。
因此,两种方法本质没有区别。
要明白监视器怎么工做的,就得研究研究对象。
对象在内存中存储的布局能够分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。对象头主要包括两部分(对象其余信息在此不作研究) markword 和 klass ,与锁有关的信息就存储子在markword中 ,以下图:
在64位虚拟机下,Mark Word是64bit大小的,其存储结构以下:
图片来自网络,若有雷同,纯属巧合。
从图片能够看出,对象头的后两位存储了锁的标志。初始状态是01,标识位加锁。偏向锁的标志位存储的是占用当前对象的线程的ID。
2.2.5 synchronized 优化
从上面的例子中,能够体会到,当有多个线程访问同步代码块时,若是每一个线程执行几秒,那么将会很消耗时间,故而,须要对锁进行优化。
高效 并发是从JDK1.5到JDK1.6的一个重要改进,目前的优化技术有 适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁 等 这些技术是为了在线程之间更高效地共享数据,以解决竞争问题,从而提升程序的执行效率。
自旋锁与自适应锁:
若是物理机器上有一个以上的处理器,能让两个或两个以上的线程同时并行执行,就可让后面请求锁的那个线程稍微等待一下,但不放弃处理器的执行时间,看看持有线程的锁是否很快就会释放锁,为了让线程等待,只须要让线程执行一个忙循环即自旋,这就是自旋锁。
自旋锁在JDK 1.4.2中引入,默认关闭,可使用-XX:+UseSpinning开启,在JDK1.6中默认开启。默认次数能够经过参数-XX:PreBlockSpin来调整。
若是所被占用的时间很短,自旋等待的效果就会很是的好,反之,自旋的线程只会白白得消耗处理器资源,而不会作任何有用的工做,反而带来性能上的浪费,所以,自选锁等待的时间必需要有必定的限制,若是自旋锁超过了限定的次数仍然没有成功得到锁,就应当使用传统的方式挂起锁了,默认次数是10。为了解决这个问题,引入自适应锁,JDK 1.6引入了更加聪明的自旋锁,即自适应自旋锁。所谓自适应就意味着自旋的次数再也不是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,线程若是自旋成功了,那么下次自旋的次数会更加多,由于虚拟机认为既然上次成功了,那么这次自旋也颇有可能会再次成功,那么它就会容许自旋等待持续的次数更多。反之,若是对于某个锁,不多有自旋可以成功,那么在之后要或者这个锁的时候自旋的次数会减小甚至省略掉自旋过程,以避免浪费处理器资源。
锁消除:
锁消除是Java虚拟机在JIT编译时,经过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,经过锁消除,能够节省毫无心义的请求锁时间。
锁粗化:
若是一系列的连续操做都是对同一对象反复加锁和解锁,甚至加锁操做时出如今循环体中,那便是没有线程竞争,频繁地进行互斥同步操做也会致使没必要要的性能损耗,若是虚拟机探测到有这样一串零碎的操做都对同一个对象加锁,将会把加锁同步范围扩展到整个操做序列的外部,就扩展到第一个append()操做以前直至最后一个append()操做以后,这样只须要加锁一次就好。
轻量级锁:
轻量级锁是JDK1,6中加入的新型锁机制,它是在没有多线程竞争的前提下,减小传统的重量级锁使用操做系统互斥量产生的性能消耗。
偏向锁:
在JVM1.6中引入了偏向锁,偏向锁主要解决无竞争下的锁性能问题,首先咱们看下无竞争下锁存在什么问题:
如今几乎全部的锁都是可重入的,也即已经得到锁的线程能够屡次锁住/解锁监视对象,按照以前的HotSpot设计,每次加锁/解锁都会涉及到一些CAS操做(好比对等待队列的CAS操做),CAS操做会延迟本地调用,所以偏向锁的想法是一旦线程第一次得到了监视对象,以后让监视对象“偏向”这个线程,以后的屡次调用则能够避免CAS操做,说白了就是置个变量,若是发现为true则无需再走各类加锁/解锁流程。
2.2.6 内部锁条件的局限性:
(1)不能中断一个正在试图得到锁的线程
(2)试图得到锁时不能设定超时
(3)每一个锁仅有单一的条件,多是不够的。
欢迎扫码关注个人微信公众号,或者微信公众号直接搜索Java传奇,不定时更新一些学习笔记!