JVM学习记录-线程安全与锁优化(一)

前言

线程:程序流执行的最小单元。线程是比进程更轻量级的调度执行单位,线程的引入,可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源(内存地址、文件I/O等),又可以独立调度(线程是CPU调度的基本单位)。

Java语言定义了5中线程状态,在任意一个时间点,一个线程只能有且只有其中的一种状态,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):已终止线程的线程状态,线程已经结束执行。

这5种状态在遇到特定的事件的时候会相互转换。

线程安全

一个比较严谨线程安全定义:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确地结果,那么这个对象就是线程安全的。

Java语言中的线程安全

研究线程安全,需要限定于多个线程之间存在共享数据访问这个前提。Java语言中各种操作共享数据分为以下5类:

不可变

在JDK1.5以后,Java语言中不可变的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再采取任何的线程安全保障措施。如果一个基本数据类在定义时使用final关键字修饰它,就可以保证它时不可变的。如果final修饰的是一个对象,需要保证对象的方法不会对其状态产生影响才行。例如:String类的substring()、concat()这些方法不会影响原来的值,只会生成一个新的字符串。

保证对象方法不会对其状态产生影响的实现方式有很多,最简单是将对象中带有状态的属性用final修饰。

例如Integer类中的实现代码:

   /**
     * The value of the {@code Integer}.
     *
     * @serial
     */
    private final int value;

    /**
     * Constructs a newly allocated {@code Integer} object that
     * represents the specified {@code int} value.
     *
     * @param   value   the value to be represented by the
     *                  {@code Integer} object.
     */
    public Integer(int value) {
        this.value = value;
    }

Java中除了String类、Integer类,还有其他的Long、Double等包装类,以及BigInteger和BigDecimal等大数据类型,都符合不可变要求的类型。

绝对线程安全

绝对线程安全,是指绝对的符合前面提到的线程安全的定义,多线程永远调用对象时永远都能获得正确的结果。但是为了实现这个绝对要付出的代价是很大的,在Java中标注自己是线程安全的类,绝大多数都不是绝对线程安全的,例如Vector类,java.util.Vector是一个线程安全类,它的add()、get()、size()等都是被synchronized修饰的,但这并不能保证它是绝对安全的。

如下代码:

public class Test {

    private static Vector<Integer> vector = new Vector<Integer>();

    public static void main(String[] args){

        while (true){
            for(int index = 0;index < 10;index++){
                vector.add(index);
            }
            //移除元素的线程
            Thread removeThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i<vector.size(); i++){
                        vector.remove(i);
                    }
                }
            });

            //打印元素的线程
            Thread printThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i<vector.size(); i++){
                        System.out.println(vector.get(i));
                    }
                }
            });

            removeThread.start();
            printThread.start();
       //别创建太多线程,出现异常就手动停止运行吧,不然会一直执行下去。
            while (Thread.activeCount()>5);
        }

    }

}

运行结果:

Exception in thread "Thread-229" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 15
    at java.util.Vector.get(Vector.java:748)
    at com.eurekaclient2.client2.shejimoshi.JVM.Test$2.run(Test.java:38)
    at java.lang.Thread.run(Thread.java:748)

尽管Vector的方法都是同步的,但是在多线程环境下,若不在调用方法端做额外的同步措施的话,仍然不是线程安全的,因为若另一线程恰好在错误的时间里删除了一个元素,导致序号i已经不再可用的话,再用i访问数组就会抛出一个ArrayIndexOutOfBoundsException。

解决方法如下(将移除和打印都设置为同步):

public class Test {

    private static Vector<Integer> vector = new Vector<Integer>();

    public static void main(String[] args){

        while (true){
            for(int index = 0;index < 10;index++){
                vector.add(index);
            }
            //移除元素的线程
            Thread removeThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized(vector){
                        for (int i = 0; i<vector.size(); i++){
                            vector.remove(i);
                        }
                    }
                }
            });

            //打印元素的线程
            Thread printThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized(vector){
                        for (int i = 0; i<vector.size(); i++){
                            System.out.println(vector.get(i));
                        }
                    }
                }
            });

            removeThread.start();
            printThread.start();

            while (Thread.activeCount()>5);
        }

    }

}

相对线程安全

我们通常所讲的线程安全就是指的相对线程安全,需要保证对象单独的操作时线程安全的,不需要做额外的保障措施。但若是对于一些特定属性的连续调用,就可能会需要在调用端添加额外的同步措施。Java语言中,大部分的线程安全类都是相对线程安全的,例如Vector、HashTable以及Collections的synchronizedCollection()方法包装的集合等。

线程兼容

线程兼容是指对象本身不是线程安全的,但是可以通过在调用端使用同步措施来保证对象在并发环境中可以安全的使用。Java中大部分类都是线程兼容的,如ArrayList、HashMap等等。

线程对立

线程对立指无论调用端是否采用了同步措施,都无法在多线程环境中并发使用代码。这种代码是有害的,应尽量避免。常见的线程对立操作有System.setIn()、System.setOut()和System.runFinalizersOnExit()等。

线程安全的实现方法

线程安全的实现主要有以下几个方法:

互斥同步

通过互斥来实现同步,临界区、互斥量、信号量都是主要的互斥实现方法。在Java中最基本的互斥同步手段就是synchronized关键字,synchronized关键字通过编译后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,这两个字节码都需要一个reference类型的惨呼是来指明要锁定和解锁的对象。若在程序中为synchronized指明了对象参数,那就是这个对象的reference,若没有指明,则根据synchronized修饰的是实例方法或类方法,来获取对应的对象或Class对象来作为锁对象。

在虚拟机规范中要求,在执行monitorernter指令时,首先要尝试获取对象的锁。如若此对象没被锁定或当期线程已经拥有了此对象的锁,则把锁的计数器加1,响应的在执行monitorexit指令时会将锁计数器减1,当计数器为0时,锁就会被释放。若获取锁失败,那么当前线程就要进入阻塞状态,直到对象锁被另外一个线程释放为止。

有两点需要注意的是:

  • synchronized同步快对于同一条线程来说是可重入的,不会出现自己把自己锁死的问题。
  • 同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。

除了synchronized之外,还可以使用java.util.concurrent包中的重入锁(ReentrantLock)来实现同步。用法很相似,只是代码写法上有区别,ReentrantLock表现为API层面的互斥锁(lock()和unlock()方法配合try/finally()语句块来完成),synchronized表现为原生语法层面的互斥锁。不过ReentrantLock比synchronized增加了一些高级功能,主要有以下3项:等待可中断、可实现公平锁,以及锁可以绑定多个条件。

  • 等待可中断是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行时间非常长的同步块很有帮助。
  • 公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;非公平锁,在释放时任何一个等待线程都有机会获得锁。synchronized是非公平锁,ReentrantLock默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。
  • 锁绑定多个条件是指一个ReentrantLock对象可以同时绑定多个Condition对象,而synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外的添加一个锁,而ReentrantLock则无须这样做,只需要多次调用newCondition()方法即可。

非阻塞同步

互斥同步最主要的问题就是进行现场阻塞和唤醒锁带来的性能问题,因此这种同步也称为阻塞同步(Block Synchronization)。从处理问题的方式上来说,互斥同步属于一种悲观的并发策略,那么相对而言的就有了另一种基于冲突检测的乐观并发策略,通俗的解释就是先执行操作,如果没有其他线程争用共享数据,那操作就成功了;如果有线程争用共享数据,那就再采取其他补偿措施(常见的补偿措施就是不断重试,直到成功为止),这种乐观的并发策略不需要把线程挂起,因此也被称为非阻塞同步(Non-Block Synchronization)

在进行操作和冲突检测时,需要保证这两个步骤的原子性,这个时候如果靠同步互斥,那就也成悲观并发了,所以只能靠硬件来完成这个保证,硬件保证一个从语义上开起来需要多次操作的行为只通过一条处理器指令就能完成,此类指令常用的有:

  • 测试并设置(Test-and-Set)。
  • 获取并增加(Fetch-and-Increment)。
  • 交换(Swap)。
  • 比较并交换(Compare-and-Swap)CAS
  • 加载链接/条件存储(Load-Linked/Store-COnditional)。

无同步方案

可重入代码

如果一个方法,它的返回结果是可以预测的,只要输入了相同的数据,就都能返回相同的结果,那它就满足可重入性的要求,当然也就是线程安全的。这个方法就是可重入代码,在这段代码可以在执行的任何时刻中断它,转而去执行另外一段代码,而在控制权返回后,原来的程序不会出现任何错误。

线程本地存储

如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行?如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。例如大部分的消息队列的架构模式(生产者-消费者)都符合这个特点。

原文地址:https://www.cnblogs.com/jimoer/p/9119419.html

时间: 2024-08-28 23:54:44

JVM学习记录-线程安全与锁优化(一)的相关文章

JVM学习记录-线程安全与锁优化(二)

前言 高效并发是程序员们写代码时一直所追求的,HotSpot虚拟机开发团队也为此付出了很多努力,为了在线程之间更高效地共享数据,以及解决竞争问题,HotSpot开发团队做出了各种锁的优化技术常见的有:自适应自旋锁(Adaptive Spinning).锁消除(Lock Elimination).锁粗化(Lock Coarsening).轻量级锁(Lightweight Locking)和偏向锁(Biased Locking)等. 自旋锁与自适应自旋 互斥同步对性能最大的影响是阻塞的实现,线程的挂

Java并发编程学习:线程安全与锁优化

本文参考<深入理解java虚拟机第二版> 一.什么是线程安全? 这里我借<Java Concurrency In Practice>里面的话:当多个线程访问一个对象,如果不考虑这些线程在运行时环境下的调度和交替执行,也不需要额外的同步,或者调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象是线程安全的. 我的理解:多线程访问一个对象,任何情况下,都能保持正确行为,就是对象就是安全的. 我们可以将Java语言中各种操作共享的数据分为以下5类:不可变.

jvm(13)-线程安全与锁优化(转)

0.1)本文部分文字转自“深入理解jvm”, 旨在学习 线程安全与锁优化 的基础知识: 0.2)本文知识对于理解 java并发编程非常有用,个人觉得,所以我总结的很详细: [1]概述 [2]线程安全 1)线程安全定义:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的:(干货——线程安全定义) [2.1]java 语言中的线程安全(干货——java

深入理解Java虚拟机(第三版)-14. 线程安全与锁优化

14. 线程安全与锁优化 1. 什么是线程安全? 当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替进行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的 2. Java语言中的线程安全 我们将Java语言下的线程安全分为以下五类:不可变.绝对线程安全.相对线程安全.线程兼容和线程对立. 1.不可变:不可变一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要进行任何线程安全保障措施

线程安全与锁优化

线程安全与锁优化 线程安全 java语言中的线程安全 不可变 相对线程安全 绝对线程安全 线程兼容 线程对立 线程安全的实现方法 互斥同步 非阻塞同步 无同步方案 锁优化 自旋与自适应自旋 锁消除 锁粗化 轻量级锁 偏向锁 线程安全与锁优化 线程安全 <Java Concurrency In Practice>的作者Brian Goetz对"线程安全"有一个比较恰当的定义:"当多个线程访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进

JVM-并发-线程安全与锁优化

线程安全与锁优化 1.线程安全 (1)当多个线程访问一个对象时,如果不考虑这些线程在执行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象时是线程安全的. (2)Java语言中的线程安全 a)可以将Java语言中各种操作共享的数据分为5类:不可变,绝对线程安全,相对线程安全,线程兼容和线程对立 b) 在Java语言中不可变的对象一定是线程安全的,无论是对象的方法实现还是方的调用者,都不需要再采用任何的线程安全

深入理解java虚拟机-第13章-线程安全与锁优化

第十三章 线程安全与锁优化 线程安全 java语言中的线程安全 1 不可变.Immutable 的对象一定是线程安全的 2 绝对线程安全 一个类要达到不管运行时环境如何,调用者都不需要额外的同步措施,通常需要付出很大甚至是不切实际的代价,在java api中标注自己是线程安全的类,大多数都不是绝对的线程安全 3 相对线程安全 4 线程兼容  对象本身不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全使用 5 线程对立 线程安全的实现方法 1 互斥同步 Murua

Java虚拟机--线程安全和锁优化

Java虚拟机--线程安全和锁优化 线程安全 线程安全:当多线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的. Java中,线程安全体现在多个线程访问同一个共享数据,如果一段代码中根本不会与其他线程共享数据,可以说不存在线程安全的问题. 线程安全的安全程度,由强至弱排序,可以分为以下5类. 不可变 不可变的对象一定是线程安全的,final关键字可以实现

一夜搞懂 | JVM 线程安全与锁优化

前言 本文已经收录到我的 Github 个人博客,欢迎大佬们光临寒舍: 我的 GIthub 博客 学习导图 一.为什么要学习内存模型与线程? 之前我们学习了内存模型和线程,了解了 JMM 和线程,初步探究了 JVM 怎么实现并发,而本篇文章,我们的关注点是 JVM 如何实现高效 并发编程的目的是为了让程序运行得更快,提高程序的响应速度,虽然我们希望通过多线程执行任务让程序运行得更快,但是同时也会面临非常多的挑战,比如像线程安全问题.线程上下文切换的问题.硬件和软件资源限制等问题,这些都是并发编程