第一种 : 互斥同步
何谓同步?在多线程编程中,同步就是一个线程进入监视器(可以认为是一个只允许一个线程进入的盒子),其他线程必须等待,直到那个线程退出监视器为止。
在实现互斥同步的方式中,最常使用的就是Synchronized 关键字。
synchronized实现同步的基础就是:Java中的每一个对象都可以作为锁。
具体表现为:1.普通同步方法,锁是当前实例对象
2.静态同步方法,锁是当前类的Class对象
3.同步方法块,锁是Synchronized括号里匹配的对象
如何实现? synchronized经过编译之后,会在同步块的前后生成 monitorenter 和monitorexit这两个字节码指令。这两个字节码指令之后有一个reference类型(存在于java虚拟机栈的局部变量表中,可以根据reference数据,来操作堆上的具体对象)的参数来指明要锁定和解锁的对象。根据虚拟机规范,在执行monitorenter 指令时,首先会尝试获取对象的锁,如果该对象没有被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加一。若获取对象失败,那当前线程就要阻塞等待,知道对象锁被另一个线程释放。
synchronized用的锁是存放在对象头里面的,在jdk1.6之后,锁一共有四种状态:无锁状态,偏向锁状态(在对象头和栈帧中的锁记录里存储偏向锁的线程id),轻量级锁状态(将对象头的mark word复制到当前线程的栈帧的锁记录中,使用CAS操作将对象头中的markWord指向栈帧中的锁记录,如果成功,则线程就拥有了对象的锁。如果两个以上的线程争用一把锁的话,则膨胀为重量级锁),重量级锁状态。
因为之前我一直都很迷惑,所以我接下来讲一讲这一方面 :
大家应该都知道,java 在虚拟机中除了线程计数器,java虚拟机栈 是线程私有的,其余的java堆,方法区,和运行时常量池都是线程共享的内存区域。java堆是存储对象和数组的,但是对象在内存中的存储布局可以分为三块区域:对象头,实例数据(对象真正存储的有效信息,程序代码中所定义的各个类型的字段内容),对齐填充。
为什么说synchronized的锁是存放在对象头里面呢?因为对象头里面也存储了两部分信息:第一部分呢,存储对象自身的运行时数据,包括哈希码,GC分代年龄,锁状态标识位,线程持有的锁,偏向锁Id,偏向时间戳等数据。第二部分是类型指针,虚拟机通过这个来确定该对象是哪个类的实例。
如何判断该对象有没有被锁?对象头里面锁状态的标志位会发生变化,当其他线程查看synchronized 锁定的对象时,会查看该对象的对象头的标志位有没有发生变化,若标志位为01,则表示未锁定,为00时,则表示轻量级锁定,为10时,则为重量级锁定状态。为01时,则为偏向锁,为11时,则为GC标记状态。
除了synchronized 关键字之外,还可以使用JUC包下的重入锁来实现同步 。
第二种方法就是:非阻塞同步
因为使用synchronized的时候,只能有一个线程可以获取对象的锁,其他线程就会进入阻塞状态,阻塞状态就会引起线程的挂起和唤醒,会带来很大的性能问题,所以就出现了非阻塞同步的实现方法。
先进行操作,如果没有其他线程争用共享数据,那么操作就成功了,如果共享数据有争用,就采取补偿措施(不断地重试)。
我们想想哈,互斥同步里实现了 操作的原子性(这个操作没有被中断) 和 可见性(对数据进行更改后,会立马写入到内存中,其他线程在使用到这个数据时,会获取到最新的数据),那怎么才能不用同步来实现原子性和可见性呢?
CAS是实现非阻塞同步的计算机指令,它有三个操作数:内存位置,旧的预期值,新值,在执行CAS操作时,当且仅当内存地址的值符合旧的预期值的时候,才会用新值来更新内存地址的值,否则就不执行更新。
使用方法:使用JUC包下的整数原子类decompareAndSet()和getAndIncrement()方法
缺点 :ABA 问题 版本号来解决
只能保证一个变量的原子操作,解决办法:使用AtomicReference类来保证对象之间的原子性。可以把多个变量放在一个对象里。
第三种:无同步方案
线程本地存储:将共享数据的可见范围限制在一个线程中。这样无需同步也能保证线程之间不出现数据争用问题。
经常使用的就是ThreadLocal类
ThreadLocal类 最常见的ThreadLocal使用场景为 用来解决数据库连接、Session管理等。
- public T get() { }
- public void set(T value) { }
- public void remove() { }
- protected T initialValue() { }
get()方法是用来获取ThreadLocal在当前线程中保存的变量副本,set()用来设置当前线程中变量的副本,remove()用来移除当前线程中变量的副本,initialValue()是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法
其实引起线程不安全最根本的原因 就是 :线程对于共享数据的更改会引起程序结果错误。线程安全的解决策略就是:保护共享数据在多线程的情况下,保持正确的取值。