并发2
1.1 竞态条件
并发编程中,由于不恰当执行时序出现不正确的结果的情况
延迟初始化
读取-修改-写入操作
如何避免静态条件
在某个线程修改变量时,通过某种方式防止其他线程使用此变量,确保其他线程只能在修改操作完成前或者完成后读取和修改状态,而不是在修改状态过程中。
1.1.1 使用线程安全对象代替非线程安全域
AtomicLong….
好处:简单方便 弊端:涉及多个变量间的操作,变量状态间有关系时依然有问题
原理:轮询,记录变量是否被修改,被修改,重新读取执行操作,保证修改期间没其他线程操作修改过。
1.1.2 加锁机制
内置锁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();
}
}
1.1.3 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
1.2 线程封闭
1.2.1 栈封闭
局部变量(方法中的变量)的固有属性之一就是封闭在线程中。它们位于执行线程的栈中,其他线程无法访问这个栈,避免使用同步,也就没有线程安全的问题。
方法发布的变量需要确保安全,返回对象的深拷贝或者不可变对象。
1.2.2 ThreadLocal类
ThreadLocal类为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的绑定机制,是每一个线程都可以独立地改变自己的副本,而不会与其他副本冲突。
对于多线程资源问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供了一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
理解ThreadLocal和类创建是类似的,Threadlocal是新建多个空间存类里边的ThreadLocal对象,为了避免多线程使用相同的数据,也可以在新建多个对象,新建线程的时候关联不同对象。适用于不需要要求可见性的场景.
2 同步工具类
线程访问代码段的限制,通过对代码段访问的限制实现线程间的协作和等待。
2.1 闭锁 CountDownLatch
一个等待N个线程结束
Countdown()
Await()
比如:老板等待多个工人工作做完,然后检查。
多个线程持有相同的CountDownLatch对象,等待的线程(老板)使用await,等待线程(工人)countDwon().
2.2 循环屏障 CyclicBarrier
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
2.3 信号量 Semaphore
acquire()
release()
用于控制并发线程数量
比如:一个文件的并发访问数量,停车场并发停车容量
同步工具文章分享: https://www.cnblogs.com/shijiaqi1066/p/3412338.html
3 线程间通信
3.1 轮询,使用volatile。
在一个线程中使用轮询判断变量条件,另一个线程完成后修改对应变量值.
不推荐:轮询会提高CPU使用率,一直运行,非通知机制。
3.2 回调。可以拿数据
两个线程各自有对象引用,可以随时通过引用调用对方方法(停止对方线程)。
回调的核心就是回调方将本身即this传递给调用方,这样调用方就可以在调用完毕之后告诉回调方它想要知道的信息。
Eg:龟兔赛跑
3.3 Synchronized ,wait,notify ,lock ,condition等方法。
3.4 线程同步工具类。
3.5 Callable阻塞获取线程返回数据
4 多线程练习
4.1 三个线程顺序打印ABC
Wait notify机制实现通信,同生产者消费者类似。
此处wait和notify指定不同的对象,可以显示唤醒在此锁上的线程。同condition类似。
重点理解唤醒谁,等待被谁唤醒,锁都是一把。
4.2 生产者消费者,生产者负责往篮子放苹果,消费者负责吃苹果,控制篮子最多五个苹果。
4.3 龟兔赛跑[线程间回调通信]
4.4 生产者消费者KFC
https://www.cnblogs.com/pureEve/p/6524366.html
浙公网安备 33010602011771号