【Java基础总结】-了解Java线程调度、并发安全及锁优化

Java内存模型

Java虚拟机提供的同步机制

  • synchronized关键字
  • java.util.concurrent包
  • volatile关键字 (最轻量级的同步机制)

对于volatile型变量的特殊规则

当一个变量定义为volatile之后,它将具备两种特性:第一是保证此变量对所有线程的可见性,这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。第二个语义是禁止指令重排序优化,普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。

ps 指令重排序:为了使处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执行优化,处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果一致,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致,因此,如果存在一个计算任务依赖另外一个计算任务的中间结果,那么其顺序性并不能靠代码的先后顺序来保证。

由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然要通过加锁(使用synchronized或java.util.concurrent中的原子类)来保证原子性。

  • 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
  • 变量不需要与其他的状态变量共同参与不变约束

Java线程并发过程如何处理原子性、可见性和有序性这三个特征

  • 原子性:由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store和write,我们大致认为基本数据类型的访问读写是具备原子性的。synchronized关键字和java.util.concurrent包能保证原子性。
  • 可见性:可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从内存刷新变量这种依赖主内存作为传递媒介的方式来实现可见性的。无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是,volatile的特殊规则保证了新值能立即同步到主内存以及每次使用前立即从主内存刷新。除了volatile之外,Java还有两个关键字能实现可见性,即synchronized和final。
  • 有序性:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指“线程内变现为串行的语义”,后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。
    Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile关键字本身就包含了禁止指令重排序的语义,而synchronized则是由“一个变量在同一个时刻只允许一条线程对其进行lock操作”这条规则获得的,这条规则决定了持有同一个锁的两个同步块只能串行地进行。

Java与线程

线程的实现

  • 使用内核线程实现
    每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核就叫做多线程内核。
    内核线程实现的优缺点:由于内核线程的支持,每个轻量级进程都成为一个独立的调度单元,即使有一个轻量级进程在系统调用中阻塞 ,也不会影响整个进程继续工作。它的局限性在于:系统调用的代价相对较高,需要在用户态和内核态之间来回切换;其次每个轻量级进程都需要有一个内核线程支持,要消耗一定的内核资源,因此一个系统支持轻量级进程的数量是有限的。
    轻量级进程与内核线程之间1:1的关系
  • 使用用户线程实现
    用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。如果程序实现得当,这种线程不需要切换到内核态,因此操作可以是非常快速且低消耗的,也可以支持规模更大的线程数量。
    实用用户线程的优势在于不需要系统内核支援,劣势也在于没有系统内核的支援,所有的线程操作都需要用户程序自己处理。有一些复杂的问题诸如“阻塞如何处理”、“多处理系统中如何将线程映射到其他处理器上”这类问题解决起来将会异常困难,甚至不可能完成。现在使用用户线程的程序越来越少了。
    进程与用户线程之间1:N的关系
  • 使用用户线程加轻量级进程混合实现
    在这种混合模式下,既存在用户线程,也存在轻量级进程。用户线程还是完全建立在用户空间中,因此用户线程的创建、切换、析构等操作依然廉价,并且可以支持大规模的用户线程并发。而操作系统提供支持的轻量级进程则作为用户线程和内核线程之间的桥梁,这样可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级线程来完成,大大降低了整个进程被完全阻塞的风险。
    用户线程与轻量级进程的数量比是不定的,即为N:M的关系。

线程的调度

线程调度是指系统为线程分配处理器使用权的过程。主要调度方式有两种,分别是协同式线程调度和抢占式线程调度。

1.如果使用协同式调度的多线程系统, 线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,要主动通知系统切换到另一个线程上。优点:实现简单,而且由于线程要把自己的事情干完后才会进行线程切换,切换操作对线程自己是可知的,所以没有说明线程同步的问题。劣势:线程的执行时间是不可控的,甚至如果一个线程编写有问题,一直不告诉系统进行线程切换,那么程序就会一直阻塞在那儿,导致系统相当不稳定。

2.如果使用抢占式调度的多线程系统,那么每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定。在这种实现线程调度的方式下,线程的执行时间是系统可控的,也不会 有一个线程导致整个进程阻塞的问题,Java使用的线程调度方式就是抢占式调度。

状态转换

Java语言定义了5种线程状态,在任意一个时间点,一个线程只有且只有其中一种状态:

  • 新建(New):创建后还未启动的线程处于这种状态
  • 运行(Runnable):Runnable包括操作系统线程状态中的Running和Ready,也就是处于此状态的线程有可能正在执行,也有可能正在等待着CPU为它分配执行时间。
  • 无限期等待(Waiting):处于这种状态的线程不会被分配CPU执行时间,它们要等待被其他线程显示地唤醒。以下方法会让线程陷入无限期的等待状态
    • 没有设置Timeout参数的Object.wait()方法。
    • 没有设置Timeout参数的Thread.join()方法。
    • LockSupport.park()方法。
  • 期限等待(Timed Waiting):处于这种状体的线程也不会被分配CPU执行时间,不过无需等待被其他线程显示地唤醒,在一定时间之后它们会由系统自动唤醒。以下方法会让线程进入限期等待状态:
    • Thread.sleep()方法
    • 设置了Timeout参数的Object.wait()方法。
    • 设置了Timeout参数的Thread.join()方法。
    • LockSupport.parkNanos()方法。
    • LockSupport.parkUntil()方法。
  • 阻塞(Blocked):线程被阻塞了,”阻塞状态”与”等待状态”的区别是:”阻塞状态”在等待着获取到一个排他锁,这个事件将在另外一个线程放弃这个锁的时候方式;而”等待状态”则是在等待一段时间,或者唤醒动作的发生。在程序等待进入同步区域的时候,线程将进入这种状态。
  • 结束(Terminated):已终止线程的线程状态,线程已经结束执行。

线程安全的实现方法

1.互斥同步
同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个线程使用。(互斥是方法,同步是目的)
互斥实现方式:临界区、互斥量、信号量
在Java中,互斥同步手段有:
①synchronized关键字
②使用java.util.concurrent包中的重入锁(ReentrantLock)

2.非阻塞同步
先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施(最常见的补偿措施就是不断地重试,知道成功为止)

3.无同步方案
同步只是保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性。
①可重入代码:执行的任意时刻中断,再调用不会出现任何错误。
②线程本地存储:把共享数据的可见范围限制在同一个线程之内,这样,无需同步也能保证线程之间不出现数据争用的问题。

锁优化

以下这些技术都是为了在线程之间更高效地共享数据,以及解决竞争问题,从而提高程序的执行效率。

  • 自旋锁和自适应自旋
    在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。如果物理机有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的哪个线程”稍等一下“,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。

    在JDK1.6中引入了自适应的自旋锁。自适应意味着自旋的时间不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

  • 锁消除
    锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在数据共享竞争的锁 进行消除。

  • 锁粗化
    原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小—只在共享数据的实际作用域中才进行同步,这样是为了是的需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽可能拿到锁。

  • 轻量级锁
    轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
    轻量级锁能提升程序同步性能的依据是”对于绝大部分的锁,在整个同步周期内都是不存在竞争的“,这是一个经验数据。如果没有竞争,轻量级锁使用CAS操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外使用CAS操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。

  • 偏向锁
    偏向锁也是JDK1.6中引入的一项锁优化,它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。偏向锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。
    偏向锁可以提高带有同步但无竞争的程序性能。它同样是一个带有效益权衡性质的优化,也就是说,它并不一定总是对程序运行有利,如果程序中大多数的锁总是被多个不同的线程访问,那偏向模式就是多余的。

    原文作者:java锁
    原文地址: https://blog.csdn.net/Coder__CS/article/details/78669672
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞