java并发下

  1. 安全性问题

线程安全性很难给出一个确切定义,核心概念就是正确性,当多个线程访问某个类,这个类始终能表现出正确行为,该类就是线程安全的。编写线程安全的代码,核心要对状态访问操作进行管理(共享的和可变的状态)。无状态对象是线程安全的(不包含任务域、不包含对其他类中域的引用)。

对于多线程访问可变的状态变量未使用合适同步,修复方式:

  1. 不在线程间共享该状态变量
  2. 该状态变量修改为不可变变量
  3. 访问状态变量使用同步

线程stack,堆。

    1. 内存模型

Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。

 

      1. 原子性

分析下以下哪些为原子操作

x = 10;         //语句1          

y = x;         //语句2

x++;           //语句3

x = x + 1;     //语句4

1为原子,2.3.4不是

 

      1. 可见性

当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

另外,通过synchronizedLock也能够保证可见性,synchronizedLock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

 

      1. 有序性

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原则具备传递性。

 

 

    1. 竞态条件

并发编程中,由于不恰当执行时序出现不正确的结果的情况

 延迟初始化

 读取-修改-写入操作

 

如何避免静态条件

   在某个线程修改变量时,通过某种方式防止其他线程使用此变量,确保其他线程只能在修改操作完成前或者完成后读取和修改状态,而不是在修改状态过程中。

 

      1. 使用线程安全对象代替非线程安全域

 AtomicLong….

 

好处:简单方便  弊端:涉及多个变量间的操作,变量状态间有关系时依然有问题

原理:轮询,记录变量是否被修改,被修改,重新读取执行操作,保证修改期间没其他线程操作修改过。

 

 

      1. 加锁机制

 

内置锁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方法类似,puttake是同一个锁,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. 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. 线程封闭
      1. 栈封闭

局部变量(方法中的变量)的固有属性之一就是封闭在线程中。它们位于执行线程的栈中,其他线程无法访问这个栈,避免使用同步,也就没有线程安全的问题。

方法发布的变量需要确保安全,返回对象的深拷贝或者不可变对象。

      1. ThreadLocal

ThreadLocal类为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的绑定机制,是每一个线程都可以独立地改变自己的副本,而不会与其他副本冲突。

对于多线程资源问题,同步机制采用了以时间换空间的方式,而ThreadLocal采用了以空间换时间的方式。前者仅提供了一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

理解ThreadLocal和类创建是类似的,Threadlocal是新建多个空间存类里边的ThreadLocal对象,为了避免多线程使用相同的数据,也可以在新建多个对象,新建线程的时候关联不同对象。适用于不需要要求可见性的场景.

 

  1. 同步工具类

线程访问代码段的限制,通过对代码段访问的限制实现线程间的协作和等待。

    1. 闭锁 CountDownLatch

一个等待N个线程结束 

Countdown()

Await()

比如:老板等待多个工人工作做完,然后检查。

多个线程持有相同的CountDownLatch对象,等待的线程(老板)使用await,等待线程(工人)countDwon().

    1. 循环屏障 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

    1. 信号量 Semaphore

   acquire()

release()

   用于控制并发线程数量

比如:一个文件的并发访问数量,停车场并发停车容量

 

同步工具文章分享: https://www.cnblogs.com/shijiaqi1066/p/3412338.html

 

  1. 线程间通信
    1. 轮询,使用volatile。

在一个线程中使用轮询判断变量条件,另一个线程完成后修改对应变量值.

不推荐:轮询会提高CPU使用率,一直运行,非通知机制。

    1. 回调。可以拿数据

两个线程各自有对象引用,可以随时通过引用调用对方方法(停止对方线程)。

回调的核心就是回调方将本身即this传递给调用方,这样调用方就可以在调用完毕之后告诉回调方它想要知道的信息。

     Eg:龟兔赛跑

    1. Synchronized ,wait,notify ,lock ,condition等方法。
    2. 线程同步工具类。
    3. Callable阻塞获取线程返回数据
  1. 多线程练习
    1. 三个线程顺序打印ABC

  Wait notify机制实现通信,同生产者消费者类似。

此处wait和notify指定不同的对象,可以显示唤醒在此锁上的线程。同condition类似。

重点理解唤醒谁,等待被谁唤醒,锁都是一把。

    1. 生产者消费者,生产者负责往篮子放苹果,消费者负责吃苹果,控制篮子最多五个苹果。
    2. 龟兔赛跑[线程间回调通信]
    3. 生产者消费者KFC

https://www.cnblogs.com/pureEve/p/6524366.html

    1. 线程同步工具类

 

同步方法和同步块比较:优先使用同步块,较少锁的代码范围,较少锁带来的性能损耗。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值