线程安全
目录
- Java线程安全
- 线程安全实现方法
Java线程安全
线程安全严谨定义:当多个线程访问一个对象时,如果不用考虑线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方法进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象是线程安全的。
Java语言中的各种操作共享的数据分为以下5类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立
1,不可变
不可变对象一定是线程安全的,因为只要一个不可变对象被正确构建出,那么其外部的可见状态就永远不会改变,看不到它在多个线程之中处于不一致的状态。在java中,有两种情况:
- 如果共享数据是一个基本数据类型,那么只要在定义时使用final关键字修饰即可;
- 若是一个对象,那么就需要保证对象的行为不会对其状态产生任何影响才行。例如String类,当调用subString(),replace()等这些方法不会影响其原来的值,只会返回一个新构造的字符串对象。
想要保证一个对象行为不影响自己状态,很简单的可以将对象中带有状态的变量都声明为final。Number中Long、Double、BigInteger等是不可变的,而AtomicInteger和AtomicLong则并非不可变(至于为什么,后续看了源码再来解析)。
2,绝对线程安全
绝对的线程安全是难以达到的,一般java中的线程安全类都不是觉得安全的。比如vector,其操作方法基本都是被synchronized修饰的,虽然方法修饰为同步的,但也不意味着调用它时不需要同步手段了。
比如分析下列例子
1 public class aaa { 2 private static Vector<Integer> vector = new Vector<Integer>(); 3 4 public static void main(String[] args) { 5 while(true){ 6 for(int i=0;i<10;i++){ 7 vector.add(i); 8 } 9 Thread removeThread = new Thread(new Runnable() { 10 @Override 11 public void run() { 12 for(int i=0;i<vector.size();i++){ 13 vector.remove(i); 14 } 15 } 16 }); 17 Thread printThread = new Thread(new Runnable() { 18 @Override 19 public void run() { 20 for(int i=0;i<vector.size();i++){ 21 System.out.println(vector.get(i)); 22 } 23 } 24 }); 25 removeThread.start(); 26 printThread.start(); 27 while(Thread.activeCount()>20); 28 } 29 } 30 }
当一个remove线程和一个get线程一直在对vector做修改的时候,虽然get和remove在vector中确实是线程安全的方法,但是不能保证在调用端依然线程安全。这个例子就表现了这一点,for循环的部分就不是线程安全的,可能当某一时刻remove线程确实对vector执行了remove操作,这也是线程安全的,但是整体从11-16行不是安全的,可能此时执行权由print拿到,他刚好要访问这个刚才被删除了的元素,于是就会产生下面的ArrayIndexOutOfBoundsException异常。
所以说方法虽然是安全的,但是调用端不一定安全,需要加安全措施。
3,线程相对安全
我们在调用时不需要做额外的保障措施,但是对于一些特定顺序额连续调用,就可能需要在调用端使用额外同步手段保证调用正确性。
4,线程兼容
线程兼容:对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用。一般说一个类不是线程安全地,就是这种情况。比如与Vector,HashTable对应地ArrayList和HashMap。
5,线程对立
无论调用端是否采取了同步措施,都无法再多线程环境中并发使用的代码。比如suspend()和resume(),两个线程同时持有一个线程对象,一个尝试取中断,另一个尝试去恢复。
线程安全的实现方法
1,互斥同步
互斥即同一时刻只能有一个线程访问或修改共享变量。互斥实现同步,临界区,互斥量,信号量都是主要的互斥实现方式。
最常见的互斥手段是synchronized关键字,它是经过编译之后会在同步块的前后分别形成monitorenter和monitorexit两个字节码指令。
通过引用对象头中的参数Monitor Address来指明要锁定的和解锁的对象,若未指定,则看其修饰的是实例方法还是类方法,去对应的对象实例或Class对象来作为锁对象。
执行monitorenter指令时
首先尝试获取对象的锁。若该对象没被锁定,或者当前线程已经拥有了那个对象的锁,锁的计数器加1
执行monitorexit指令时,
会将锁计数器减1。
当计数器为0时,锁就被释放了
若获取对象锁失败,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。
两个注意点:
1,synchronized同步块对同一条线程来说时可重入的。
2,同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入
ReetrantLock 与sychronized很相似,他们都具备一样的线程重入特性。
区别:
1,前者为API层面的互斥锁,另一个表现为原生语法互斥锁。
2,reentrantLock有一些高级功能:
i) 等待可中断
指当持有锁的线程长期不释放锁的时候,真该等待的线程可以选择放弃等待,处理其他事情,对处理执行事件非常长的同步块很有帮助。
ii) 可实现公平锁
iii) 锁可以绑定多个条件
即可以绑定多个Condition对象,而在synchronized中,锁对象的wai()、notify()、或notifyAll()方法可以实现一个隐含的条件,但要和多个条件关联时,就需要额外地添加一个锁。
2,非阻塞同步
互斥同步最主要的问题就是进行线程阻塞和唤醒锁带来的性能问题,因此这种同步也称为阻塞同步。
待续。。。