java从基础知识(十)java多线程(下)

  首先介绍可见性、原子性、有序性、重排序这几个概念 

原子性:即一个操作或多个操作要么全部执行并且执行的过程不会被任何因素打断,要么都不执行。

可见性:一个线程对共享变量值的修改,能够及时地被其它线程看到

  共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量

  每个线程都有自己的工作内存,存有主内存中共享变量的副本,当工作内存中的共享变量改变,会主动刷新到主内存中,其它工作内存要使用共享变量时先从主内存中刷新共享变量到工作内存,这样就保证了共享变量的可见性。

  

可见性的实现方法:

  1、synchronized两条规则

    线程解锁前,必须要把共享变量的最新值刷新到主内存中

    线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(加锁与解锁需要是同一把锁)

    总之,线程解锁前对共享变量的修改在下次加锁时对其他线程可见。

  2、volatile

    只能保证内存的可见性,不能保证操作的原子性。

有序性:即程序执行的顺序按照代码的先后顺序执行。

    在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

    重排序:代码书写的顺序与实际执行的顺序不同,指令重排序是编译器或处理器为了提高程序性能而做的优化

    1、编译器优化的重排序(编译器优化)

    2、指令集并行重排序(处理器优化)

    3、内存系统重排序(处理器优化)

    as-if-serial:无论如何重排序,程序执行的结果应该与代码顺序执行的结果一致(java编译器、运行时和处理器都会保证java在单线程下遵循as-if-serial语义)

    重排序不会给单线程带来内存可见性问题

    多线程中程序交错执行,重排序可能会造成内存可见性问题

线程不可见的原因:

    1、线程的交叉执行(需要原子性)

    2、重排序结合线程交叉执行(需要原子性)

    3、共享变量更新后的值没有在工作内存与贮存间及时更新(需要内存可见性)

1、线程同步

  线程同步是保证多个线程安全访问竞争资源的一种手段。

  1.1、通过synchronized关键字(修饰方法、代码块)

  synchronized保证锁内操作的原子性,内存的可见性。

  县城执行互斥代码的过程:获取互斥锁、清空工作内存、从住内存中拷贝最新变量到工作内存、执行代码、将更改后的共享变量的值刷新到住内存、释放互斥锁。

  上述代码可以保证两个单词不被拆分,但不能保证其顺序,通过join方法可实现顺序输出。如果去掉synchronized两个单词将被拆分输出。

  注意:当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)代码块

       当一个线程访问object的一个synchronized(this)同步代码块时,其它线程对object中所有其它synchronized(this)代码块的访问将会被阻塞。

     当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。

  1.2、通过域变量(volatile)实现线程同步(变量)

  volatile能够保证内存的可见性,不能保证操作的原子性。

  volatile确保变量每次使用的时候是从主存中获取,而不是每个线程各自的工作内存,volatile具有synchronized关键字的“可见性”,但是没有synchronized关键字的“并发正确性”(因为线程对共享资源的读写不具有原子性),也就是说不保证线程执行的有序性。

  volatile实现内存的可见性是通过内存屏障和禁止重排序优化来实现的,对volatile变量执行写操作时,会在写操作后加入一条store屏障指令(刷新变量到主内存),对volatile变量执行读操作时会在都操作前加入一条load屏障指令(重主内存中读取变量)。

  上述代码的输出结果很多时候都小于500,这是由于number++的非原子性操作导致的。也就是说A线程从主内存read到number修改后还没load到主内存中,这时B线程从主内存中也read到number,导致主内存中number有时候会被覆盖掉,因而输出结果会有小于500的情况。

  为了保证上述number++的原子性,可使用synchronized、ReentrantLock(java.util.concurrent.locks包下)、AtomicInterger(java.util.concurrent.atomic包下)三种方式实现。

  synchronized实现同步上面已介绍,这里不再累述。

  ReentrantLock方式如下:

  运行上述代码,我们发现结果为确定的500。

  注意:共享数据的访问权限都必须定义为private

     java中没有提供检测与避免死锁的专门机制,但应用程序员可以采用某些策略防止死锁的发生

     java中对共享数据操作的并发控制是采用加锁技术

  1.3、通过重入锁实现线程同步

    参考1.2中volatile原子性问题解决办法ReentrantLock方式的代码。

  1.4、通过局部变量实现线程同步

  ava.lang.ThreadLocal,线程局部变量,把一个共享变量变为一个线程的私有对象。不同线程访问一个ThreadLocal类的对象时,锁访问和修改的事每个线程变量各自独立的对象。通过ThreadLocal可以快速把一个非线程安全的对象转换成线程安全的对象。(同时也就不能达到数据传递的作用了)。引用代码

  1.5、通过阻塞队列实现线程同步

  前面5种同步方式都是在底层实现的线程同步,但是我们在实际开发当中,应当尽量远离底层结构。 使用javaSE5.0版本中新增的java.util.concurrent包将有助于简化开发。本小节主要是使用LinkedBlockingQueue<E>来实现线程的同步。(引用代码)

  1.6、通过原子变量实现线程同步

  需要使用线程同步的根本原因在于对普通变量的操作不是原子的,util.concurrent.atomic包中提供了创建了原子类型变量的工具类AtomicInteger 表可以用原子方式更新int的值。

  上述代码输出结果为确定的500。

2、数据交换

  由于线程的运行和结束是不可预料的,因此,在传递和返回数据时就无法象函数一样通过函数参数和return语句来返回数据。

  1)通过构造方法传递数据

  当传递数据过多时,构造方法会显得特别臃肿,因此可以使用变量方法的方式。

  2)通过变量和方法传递数据

  3)通过回调函数传递数据

  上面讨论的两种向线程中传递数据的方法是最常用的。但这两种方法都是main方法中主动将数据传入线程类的。然而,在有些应用中需要在线程运行的过程中动态地获取数据,这种情况可以使用回调函数方式。

3、线程死锁

  所谓死锁: 是指两个或两个以上的进程(线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外部处理作用,它们都将无限等待下去。

  产生原因:

    1、系统资源不足,导致线程对资源的竞争引起

    2、进程的推进顺序不恰当

    3、资源分配不当

  死锁产生的条件:

    1、互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。

    2、请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。

    3、不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。

    4、环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。

  一个死锁的例子

  解决死锁的办法

  1、预防死锁:设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或者几个

  2、避免死锁:而是在资源的动态分配过程中,用某种方法去防止系统进入不安全状态

  3、检测和解除死锁:先检测再解除。此方法允许系统在运行过程中发生死锁,但可通过系统所设置的检测机构(检测方法包括定时检测、效率低时检测、进程等待时检测等。),及时地检测出死锁的发生,并精确地确定与死锁有关的进程和资源,采取适当措施,从系统中将已发生的死锁清除掉。

4、synchronized和volatile的比较

  volatile不需要加锁,比synchronized更轻量级,不会阻塞线程

  从内存可见性角度讲,volatile读相当于加锁,写相当于解锁

  synchronized即能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性