线程同步
机制简介
线程同步机制是一套用于协调线程之间的数据访问机制,保障线程安全。
Java平台提供线程同步机制包括:锁,volatile关键字,final关键字,static关键字,相关API,如Object.wait(),Object.notify()等。
锁概述
线程安全问题产生的前提是多个线程并发访问共享数据。
解决:将多个线程对共享数据的并发访问转换为串行访问,即一个共享数据一次只能被一个线程访问。锁就是利用这种思路来保障线程安全。
锁(Lock)可以理解为对共享数据进行保护的一个许可证。对于同一个许可证保护的共享数据来说,任何线程想要访问这些共享数据必须先持有该许可证。一个线程只有在持有许可证的情况下才能对这些共享数据进行访问;并且一个许可证某一时刻一次只能被一个线程持有;许可证线程在结束对共享数据的访问后必须释放其持有的许可证。
一线程在访问有锁的共享数据之前必须先获得锁,获得锁的线程称为锁的持有线程;一个锁在某一时刻只能被一个线程持有。持有线程在获得锁的之后,释放锁之前这段时间所执行的代码称为临界区
锁具有排他性(互斥性),即一个锁一次只能被一个线程持有。
JVM把锁分为内部锁和显示锁两种。内部锁通过synchronized关键字实现;显示锁通过java.concurrent.locks.Lock接口的实现类实现。
锁的作用
锁可以实现对共享数据的安全访问,保障线程的原子性,可见性与有序性。
保障原子性:
锁是通过互斥的特性保障原子性,一个锁一次只能被一个线程持有,保证了临界区一次只能被一个线程执行,使得临界区所执行的操作具有不可分割的特性,即具备了原子性。
保障可见性:
可见性是通过写线程冲刷处理器缓存和读线程刷新处理器缓存这两个动作进行实现的。在java平台的锁中,锁的获得隐含着刷新处理器缓存的动作,锁的释放隐含着冲刷处理器缓存的动作。
保障有序性
写线程在临界区所执行的操作在读线程所执行的临界区看来像是完全按照源码顺序执行的。
注意:
使用锁保障线程的安全性,必须满足以下条件:
1.访问共享数据时必须使用同一个锁。
2.即使是读取共享数据的线程也需要加同步锁。
锁相关概念
1.可重入性:一个线程持有该锁的时候能再次(或多次)申请该锁
void methodA(){ 申请A锁 methodB() 释放A锁 } void methodB(){ 申请A锁 。。。。。。 释放A锁 }
如果一个线程持有一个锁的时候还能成功申请该锁,称该锁是可重入的,否则该锁不可重入。
2.锁的争用与调度
Java平台内部锁属于非公平锁,显示Lock锁既支持公平锁也支持非公平锁。
3.锁的粒度
一个锁可以保护的共享数据的数量大小称为锁的粒度。
锁保护的共享数据量大,则粒度粗,反之则细
粒度过粗,线程申请锁会造成不必要等待;过细,会增加锁的调度。
4.内部锁:synchronized关键字
Java中的每个对象都有一个与之关联的内部锁。这种锁也称为监视器。这种内部锁是一种排他锁。保障原子性,可见性和有序性。
内部锁是通过synchronized关键字实现的。synchronized可以修饰代码块和方法
修饰代码块:
synchronized(对象锁){ 同步代码块,可以在同步代码块中访问共享数据 }
修饰实例方法就称为同步实例方法
修饰静态方法就称为同步静态方法
synchronized同步代码块:
注意: 线程同步必须使用用一个锁对象
也可以使用常量作为锁对象。在不同的方法中同步代码块也能实现同步。
synchronized同步实例方法:
默认就是用this作为锁对象。
synchronized同步静态方法:
同步代码块与同步方法的效率
同步代码块相对于同步方法的,锁的粗粒度更细,并发执行的效率越快。
脏读
概念:出现读取属性值的时候出现读取了中间值,不是修改:后的值
原因:对共享数据的修改和读取出现不同步的现象
解决:在修改方法和读取方法上都加锁。
public static void main(String[] args) throws InterruptedException { PublicValue publicValue = new PublicValue(); SubThread subThread = new SubThread(publicValue); subThread.start(); Thread.sleep(100); publicValue.getValue(); } static class SubThread extends Thread{ private PublicValue publicValue; public SubThread(PublicValue publicValue){ this.publicValue=publicValue; } @Override public void run() { publicValue.setValue("newMessi",35); } } static class PublicValue{ private String name="Messi"; private int age=34; public void getValue(){ System.out.println(Thread.currentThread().getName()+" get "+"name:"+name+" age:"+age); } public void setValue(String name ,int age){ this.name=name; try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } this.age=age; System.out.println(Thread.currentThread().getName()+ " set " + "name:"+name+" age:"+age); } }
异常结果:
解决:
正确结果:
死锁
在多线程程序中,同步时使用多个锁,如果获得锁的顺序不一致,就会导致死锁。出现相互等待情况。
解决方案:所有线程保证锁的顺序即可。
结果:
轻量级同步机制:volatile关键字
volatile作用
使变量在多个线程之间可见,解决线程的可见性问题
未加volatile关键字:
结果:
说明未读出来更新的flag。
添加volatile关键字:
结果:
说明读取出来更新的flag;
volatile也synchronized的区别
1.volatile是轻量级的线程同步实现,性能高于synchronized;
2.volatile只能修饰变量,synchronized可以修饰方法和代码块。
3.多线程访问volatile变量不会发生阻塞,而synchronized可以会阻塞。
4.volatile能保证数据的可见性,但是不能保证原子性。synchronize都能保证。
5.volatile解决的是变量在多线程之间的可见性。synchronized解决的是多线程之间访问公共资源的同步性。
6.volatile会解决指令重排序问题,进而保证有序性,synchronized能保证有序性,但不能保证指令重排序问题。
CAS(由硬件实现,保证原子性)
原理:在把数据更新到主内存时,再次读取主内存变量的值,如果现在的值与操作起始读取的值一样就进行更新。
ABA问题
CAS算法认为现在的值与操作起始读取的值一样,就认为这个变量没有被其他线程修改过。
实际上这种假设不一定成立,比如说有一个共享变量count=0
A线程count+10;
B线程count+10;
C线程count-20;
所以此时看到的count变量还是0,现在是否还认为count的值没被更新呢?这种结果是否能够被接受?这种共享变量经历了A-B-A的更新就是CAS中的ABA问题。
解决方案:
为共享变量引入一个修订号(时间戳),每次修改共享变量时,相应的修订号就会加1.ABA变量更新过程变成:[A,0]-[B,1]-[A,2],所以可以通过修订号判断共享变量是否被修订过。AtomicStampedReference就是基于这种思想产生的。