线程安全性很难给出一个确切定义,核心概念就是正确性,当多个线程访问某个类,这个类始终能表现出正确行为,该类就是线程安全的。编写线程安全的代码,核心要对状态访问操作进行管理(共享的和可变的状态)。无状态对象是线程安全的(不包含任务域、不包含对其他类中域的引用)。
对于多线程访问可变的状态变量未使用合适同步,修复方式:
- 不在线程间共享该状态变量
- 该状态变量修改为不可变变量
- 访问状态变量使用同步
线程stack,堆。
Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。
分析下以下哪些为原子操作
x = 10; //语句1
y = x; //语句2
x++; //语句3
x = x + 1; //语句4
1为原子,2.3.4不是
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
在Java里面,可以通过volatile关键字来保证一定的“有序性”(具体原理在下一节讲述)。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。
happens-before原则(先行发生原则):
程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作。
volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作。
传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C。
线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作。
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行。
对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始。
8条原则摘自《深入理解Java虚拟机》,这8条规则中,前4条规则是比较重要的,后4条规则都是显而易见的。
解释一下前4条规则:
对于程序次序规则来说,理解就是一段程序代码的执行在单个线程中看起来是有序的。注意,虽然这条规则中提到“书写在前面的操作先行发生于书写在后面的操作”,这个应该是程序看起来执行的顺序是按照代码顺序执行的,因为虚拟机可能会对程序代码进行指令重排序。虽然进行重排序,但是最终执行的结果是与程序顺序执行的结果一致的,它只会对不存在数据依赖性的指令进行重排序。因此,在单个线程中,程序执行看起来是有序执行的,这一点要注意理解。事实上,这个规则是用来保证程序在单线程中执行结果的正确性,但无法保证程序在多线程中执行的正确性。
第二条规则也比较容易理解,也就是说无论在单线程中还是多线程中,同一个锁如果处于被锁定的状态,那么必须先对锁进行了释放操作,后面才能继续进行lock操作。
第三条规则是一条比较重要的规则,也是后文将要重点讲述的内容。直观地解释就是,如果一个线程先去写一个变量,然后一个线程去进行读取,那么写入操作肯定会先行发生于读操作。
第四条规则实际上就是体现happens-before原则具备传递性。
并发编程中,由于不恰当执行时序出现不正确的结果的情况
延迟初始化
读取-修改-写入操作
如何避免静态条件
在某个线程修改变量时,通过某种方式防止其他线程使用此变量,确保其他线程只能在修改操作完成前或者完成后读取和修改状态,而不是在修改状态过程中。
AtomicLong….
好处:简单方便 弊端:涉及多个变量间的操作,变量状态间有关系时依然有问题
原理:轮询,记录变量是否被修改,被修改,重新读取执行操作,保证修改期间没其他线程操作修改过。
内置锁Synchronized
内置锁(内置):可以对方法加,对代码块加。进入同步代码块自动获得,退出同步代码块自动释放,同一时刻只有一个线程可以访问锁保护的变量。
1.是互斥锁,线程A获得锁,线程B无法获取,进入阻塞
2.是可以重入的,子类改写父类synchronized方法,调用super.XXX()。
锁使用不当很容易引起死锁等活跃性问题,同时也导致严重的性能问题。(减小锁的粒度,对于耗时长的计算尽量不要使用锁)
锁不仅仅是互斥性(防止某个线程正在使用对象状态其他线程修改该状态),更是可见性,线程在同步代码块中会从主内存中读取最新值到线程stack中。(线程修改了对象状态后,其他线程能看到,get方法也要加)
重排序问题: http://blog.csdn.net/u012312373/article/details/44983523
显示锁ReentrantLock(独占锁)
显示锁定、解锁(支持公平锁,等待时间,可中断,非块结构[锁的释放和锁获取在一个块]的锁粒度更细)
Lock lock = new ReentrantLock();
….
Lock.lock()
try{
..
}
Finally
{
Lock.unlock;
}
也是互斥锁,除了使用上需要手动功能同内置锁相同。
读写锁(共享锁)
ReentrantLock /ReentrantReadWriteLick 显示锁/读写的显示锁
支持同时读,但是同时刻只能多个写
ReadWriteLock lock = new ReentrantReadWriteLock();
Lock r = lock.readLock();
Lock w = lock.writeLock();
r.lock();
try{
doRead
}
Finally
{
r.unLock();
}
取多少次锁,就必须释放多少次锁,对内置锁和显示锁都适用。
等待通知比较
内置锁/条件队列
!!!共享锁和内置锁注意wait使用在while里边,不能使用if,之前wait
的线程可能随时被唤醒,if不会重新判断条件,导致出错~~参考KFC代码
Public synchoronized void put(V v)
{
While(isFull())
wait();
doPut(v);
notifyALL();
}
Take方法类似,put和take是同一个锁,notifyALL会唤醒等待在此锁上的所有线程,但是不一定是take线程能获取到锁并执行。
显示锁/Condition
在锁上添加了条件,一个锁可以有多个条件,在一个条件上可以有多个线程等待,await会在此条件上等待,并且释放锁。SingalAll会唤醒等待在此条件上的所有线程。
Eg:教练在体育馆教导是上午【条件】打篮球,下午【条件】打羽毛球【共享一个场地,可看成一把锁】。
Lock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
Public void put(V v)
{
Lock.lock();
Try{
While(count==items.length)
notFull.await; --等待容器不为满
…做put动作
notEmpty.signal() --put后通知等待不为空的线程唤醒
}
Finally{
Lock.unlock();
}
}
Public void take(V v)
{
Lock.lock();
Try
{
While(count==0)
noEmpty.await; --等待容器不为空
…做take动作
notFull.signal() --take后通知等待不为满的线程唤醒
}
Finally{
Lock.unlock();
}
}
-
-
- Volatile变量
-
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
Volatile不保证原子性,保证的是可见性和一定的有序性。
原子性测试:
可见性:
//线程1
boolean stop = false;
while(!stop){
doSomething();
}
//线程2
stop = true;
这段代码有可能导致无法中断线程。在前面已经解释过,每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。
那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。
但是用volatile修饰之后就变得不一样了:
第一:使用volatile关键字会强制将修改的值立即写入主存;
第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。
那么在线程2修改stop值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。
那么线程1读取到的就是最新的正确的值。
一定的有序性:
//x、y为非volatile变量
//flag为volatile变量
x = 2; //语句1
y = 0; //语句2
flag = true; //语句3
x = 4; //语句4
y = -1; //语句5
由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。
并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。
原理和机制
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
使用场景
通常来说,使用volatile必须具备以下2个条件:
1)对变量的写操作不依赖于当前值
2)该变量没有包含在具有其他变量的不变式中
上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。
使用示例:
1.状态标记量
volatile boolean flag = false
while(!flag){
doSomething();
}
public void setFlag() {
flag = true;
}
volatile boolean inited = false;
//线程1:
context = loadContext();
inited = true;
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
2.double check
class Singleton{
private volatile static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if(instance==null) {
synchronized (Singleton.class) {
if(instance==null)
instance = new Singleton();
}
}
return instance;
}
}
Volatile及其内存模型好文链接: http://www.importnew.com/18126.html
局部变量(方法中的变量)的固有属性之一就是封闭在线程中。它们位于执行线程的栈中,其他线程无法访问这个栈,避免使用同步,也就没有线程安全的问题。
方法发布的变量需要确保安全,返回对象的深拷贝或者不可变对象。
ThreadLocal类为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的绑定机制,是每一个线程都可以独立地改变自己的副本,而不会与其他副本冲突。
对于多线程资源问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供了一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
理解ThreadLocal和类创建是类似的,Threadlocal是新建多个空间存类里边的ThreadLocal对象,为了避免多线程使用相同的数据,也可以在新建多个对象,新建线程的时候关联不同对象。适用于不需要要求可见性的场景.
线程访问代码段的限制,通过对代码段访问的限制实现线程间的协作和等待。
一个等待N个线程结束
Countdown()
Await()
比如:老板等待多个工人工作做完,然后检查。
多个线程持有相同的CountDownLatch对象,等待的线程(老板)使用await,等待线程(工人)countDwon().
await()
N个线程间互相等待
比如赛跑的时候,等待每个人准备ok,一起出发,每位player停止在await位置,调用3此大家同时开始跑。单cpu看谁有cpu使用权。
CyclicBarrier barri = new CyclicBarrier(3);
可初始化另一个线程,用于ok后启动。
//CyclicBarrier barri = new CyclicBarrier(3,new Refereee());
Player cuncun = new Player("cuncun",barri);
Player leilei = new Player("leilei",barri);
Player haihai = new Player("haihai",barri);
比较屏障和闭锁:从apI上能看出,屏障是到某个时刻多个线程同时开始。无await
acquire()
release()
用于控制并发线程数量
比如:一个文件的并发访问数量,停车场并发停车容量
同步工具文章分享: https://www.cnblogs.com/shijiaqi1066/p/3412338.html
在一个线程中使用轮询判断变量条件,另一个线程完成后修改对应变量值.
不推荐:轮询会提高CPU使用率,一直运行,非通知机制。
两个线程各自有对象引用,可以随时通过引用调用对方方法(停止对方线程)。
回调的核心就是回调方将本身即this传递给调用方,这样调用方就可以在调用完毕之后告诉回调方它想要知道的信息。
Eg:龟兔赛跑
Wait notify机制实现通信,同生产者消费者类似。
此处wait和notify指定不同的对象,可以显示唤醒在此锁上的线程。同condition类似。
重点理解唤醒谁,等待被谁唤醒,锁都是一把。
https://www.cnblogs.com/pureEve/p/6524366.html
同步方法和同步块比较:优先使用同步块,较少锁的代码范围,较少锁带来的性能损耗。