线程安全能够归纳为三个方面:原子性、可见性和有序性。java
原子性:对于涉及共享变量的操做看作一个总体,在同一时间内,只能由一个线程执行,在其它线程看来,这部分操做要么还没有开始,要么已经完成。Java中,基本类型除了long和double,其它类型变量的写操做都是原子性的。安全
可见性:一个线程修改了共享变量后,其它线程可以当即看见改变后的值。多线程
有序性:即程序按照代码的前后顺序执行。咱们写好的代码在执行的时候不必定是按照顺序的,由于虚拟机编译的时候,在保证输出结果不变的状况下可能对代码进行优化,也就是常说的指令重排。在单线程的状况下不会有什么影响,可是多线程的环境下则会有隐患。ide
先来看Java内存模型性能
线程先从主内存读取到变量,对变量进行修改以后再刷新回主内存。当使用了volatile以后:线程直接读写主内存。测试
这就保证了共享变量在线程间的可见性。一个常见的例子就是使用volatile修饰的变量,线程A经过改变变量的值去中止另外一个正在运行的线程B。优化
class MyTask implements Runnable{ private volatile boolean flag = true; public void stop(){ flag = false; } @Override public void run() { System.out.println("====进入循环====" + flag); while (flag){ } System.out.println("====中止循环====" + flag); } }
测试this
import java.util.concurrent.*; public class Main { public static void main(String[] args) throws Exception { ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 15, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5)); MyTask task = new MyTask(); executor.execute(task); Thread.sleep(2000); System.out.println("外部调用stop"); task.stop(); } }
若是不加volatile关键字,则不能中止线程。由于其它线程不能当即看见改变。另外volatile也保证了有序性。spa
对于volatile变量的写操做,JVM会在该操做以前加入一个释放屏障,操做以后加入一个存储屏障。操作系统
释放屏障禁止了volatile写操做和该操做以前的任何读写操做进行重排序。这就保障了实际执行顺序和源代码顺序同样,即保障了有序性。
volatile虽然可以保障有序性,可是不具备锁那样的排他性,只可以保证所修饰变量写操做的原子性,不能保证其余操做的原子性。
对于volatile的读操做,JVM会在该操做以前加入一个加载屏障,操做以后加入一个获取屏障。
volatile总结:
volatile变量的写操做与该操做之前的任何读写操做不会被重排序。
volatile变量的读操做与该操做之后的任何读写操做不会被重排序。
volatile只能保证可见性和有序性,对于包含多个操做的共享区域,不能保证线程安全。
先来个简单代码
package com.demo.tools; public class Demo { public void hello(){ synchronized(this) { System.out.println("Hello"); } } }
编译以后进入classes目录,查看编译后的结构。
能够看到在代码中用synchronized包起来的代码块先后有两个东西:monitorenter和monitorexit,这就涉及了一个叫作Monitor的东西:咱们知道对象在内存中分为三个区域(对象头、实例变量,填充数据),而这个Monitor则存储在对象头里。
当执行到monitorenter,线程尝试获取锁(也就是抢占Monitor);也由于Monitor存在对象头里,因此解释了为何Java中任意对象均可以做为锁。其中还有个计数器,当为0的时候表明能够获取,当线程获取到了,计数器+1,当执行到monitorexit的时候,线程不占有这个锁,计数器-1。因为Synchronized是可重入锁,也就是在持有当前锁的基础上继续获取当前锁,是能够的,这个时候,计数器继续+1,退出同步区域则-1,直到为0。
同时Synchronized还维护了一个入口集(Entry Set),这个集合存放等待的线程。
引伸
JDK1.6的时候,JVM团队作了一系列的锁优化,因此如今的synchronized在一些状况中性能不比ReentrantLock差:
咱们都知道线程的开销主要是上下文切换,挂起线程和恢复线程的操做都须要转入内核态中完成。而不少状况是:共享数据的锁定状况只会持续很短的一段时间,根本不值得挂起恢复。自旋锁由此而来:
1. 自旋锁:线程不放弃处理器的执行时间,为了让线程等待,而是去执行一个忙循环(自旋)。
固然,虽然避免了上下文切换的开销,可是它也是占用处理器时间的:若是锁占用时间很短,那么就达到了自旋的目的;反之占用时间很长,那么自旋线程只会白白浪费处理器资源。因此自旋一般都有个限制,自旋次数默认是10次。
2. 在JDK1.6中引入了适应性自旋:
自适应意味着自旋的时间再也不固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
若是在同一个锁对象上,自旋等待刚刚成功得到过锁,而且持有锁的线程正在运行中,那么虚拟机就会认为此次自旋也颇有可能再次成功,进而它将容许自旋等待持续相对更长的时间,好比100个循环。
若是对于某个锁,自旋不多成功得到过,那在之后要获取这个锁时将可能省略掉自旋过程,以免浪费处理器资源。说白了就是前事不忘后事之师,看到前面那个自旋成功,则本次也认为可以自旋成功;若是前面自旋失败,则本次也八九不离十失败,干脆省略自旋,直接挂起。
3. 锁消除:指虚拟机即时编译器在运行时,对一些代码上被检测到不可能存在共享数据竞争的锁进行消除。
4. 偏向锁:这个锁会偏向于第一个得到它的线程,若是在接下来的执行过程当中,该锁没有被其余的线程获取,则持有偏向锁的线程将永远不须要再进行同步。当有另一个线程去尝试获取这个锁时,偏向模式就宣告结束,进而转变为轻量级锁。偏向锁能够提升带有同步但无竞争的程序性能。
5. 轻量级锁:“轻量级”是相对于使用操做系统互斥量来实现的传统锁而言的,所以传统的锁机制就称为“重量级”锁。
对于synchronized的升级过程:
①:第一次执行到synchronized代码块的时候,锁对象是偏向锁。当线程执行完同步代码块,不释放锁,若是下一次又执行到了同步代码块,先判断持有锁的线程是否是本身(当前持有锁的线程ID存储在对象头里),若是是本身就不用从新加锁;不然就是有人来抢了,此时偏向锁升级为轻量级锁。
②:在轻量级锁上继续竞争,没有抢到锁的线程自旋,抢到锁的线程把锁对象的对象头里的线程ID更改成本身。自旋的线程在白白的消耗CPU,这种状态叫busy-waiting,超过了最大自旋次数的限制后,会将轻量级锁升级为重量级锁。后面再来线程尝试获取锁,发现是个重量级锁,就把本身挂起,等待被唤醒恢复。
总结:之因此说synchronized很差,由于在1.6以前的synchronized直接是重量级锁,而后锁的是整个对象,不如ReentrantLock零活粒度细。而如今的synchronized通过优化以后性能以及很好了,ConcurrentHashMap都在用。还有就是synchronized只能按照偏向锁->轻量级锁->重量级锁的顺序逐渐升级(锁膨胀),不容许降级。
另外,synchronized和ReentrantLock都是可重入锁(容许同一个线程屡次获取同一把锁);synchronized是不可中断锁,Lock接口的实现类好比ReentrantLock都是可中断锁。
volatile只能修饰变量,synchronized能够修饰方法和代码块。多线程访问volatile不会阻塞,synchronized会阻塞。volatile只保证了变量在多个线程之间的可见性,但不能保证原子性;而synchronized能够保证原子性,也能够间接保证可见性,由于它会将工做内存和主内存中的数据作同步处理。