Java线程深入学习(一)

目录

 

一、线程概述

1.1 线程相关概念

1.2 线程的创建和启动

1.3 线程的常用方法

1.4 线程的生命周期

1.5 多线程编程的优势与存储的风险

二、线程安全问题

2.1 原子性

2.2 可见性

2.3 有序性

2.4 Java内存模型


一、线程概述

1.1 线程相关概念

   1.进程Process:是计算机的程序关于某数据集合上的一次运行活动,是操作系统进行资源分配与调度的基本单位。可以把进程简单地理解为正在操作系统中运行的一个程序。

   2.线程Thread:是进程的一个执行单元,一个线程就是进程中一个单一顺序的控制流,进程的一个执行分支。进程是线程的容器,一个进程至少有一个线程,也可以有多个线程。

   3.在操作系统中是以进程为单位分配资源,如虚拟存储空间、文件扫描符等;每个线程都有各自的线程栈,自己的寄存器环境,自己的线程本地存储。

   4.JVM启动时会创建一个主线程,该主线程负责执行main方法,主线程就是运行main方法的线程。

   5.父线程与子线程:java中的线程是不孤立的,线程之间存在一些联系。如果在线程A中创建了线程B,称线程B为线程A的子线程,相应的线程A为线程B的父线程。

   6.注意串行、并发与并行的区别:

        并发可以提高处理事务的效率,即一段时间内可以处理或者完成更多的事情。并行可以看成

      一种更为严格、理想的并发。

        从硬件角度来说,如果是单核CPU,一个处理器一次只能执行一个线程的情况下,处理器可

      以使用时间片轮转技术,可以让CPU快速地在各个线程之间进行切换,对于用户来说,感觉就

      是三个线程在同时执行;如果是多核CPU,可以为不同的线程分配不同的CPU内核。

1.2 线程的创建和启动

   1.在java中,创建一个线程就是创建一个Thread类(或其子类)的对象(实例)。java.lang.Thread

      Thread类有两个常用的构造方法

                ①Thread()

                ②Thread(Runnable)

      对应了两种线程的创建方式

                ①定义Thread类的子类

                ②定义一个Runnable接口的实习类

      这两种创建线程的方式没有本质的区别。都要重写run()方法,run()方法体中的代码就是子线程

    要执行的任务。(Thread类本身也实现了Runnable接口)

   2.启动线程:start()方法

      调用线程的start方法来启动线程,实质就是请求JVM运行相应的线程,这个线程具体在什么时

    候运行由线程调度器Scheduler决定。注意:start方法调用结束并不意味着子线程开始运行

      新开启的线程会自动执行run方法

      如果开启了多个线程,各个线程start()调用的顺序并不一定就是线程的启动顺序。多线程的运

    行结果是随机的。

1.3 线程的常用方法

   1.currentThread()方法

        Thread.currentThread()方法可以获得当前线程。java中的任何一段代码都是执行在某个线程

      当中的,执行当前代码的线程就是当前线程。同一段代码可能被不同的线程执行,因此当前线

      程是相对的,该方法的返回值是在代码实际运行时候的线程对象。

   2.setName()/getName()方法

        thread.setName(线程名称):设置线程名称。

        thread.getName(); 返回线程名称。

        通过设置线程名称,有助于程序调试,提高程序的可读性,建议为每个线程都设置一个能够

      体现线程功能的名称。

   3.isAlive()方法

        thread.isAlive(); 判断当前线程是否处于活动状态。

        活动状态就是线程已启动并且尚未终止。

   4.sleep()方法

        Thread.sleep(millis); 让当前线程休眠指定的毫秒数

        当前线程就是Thread.currentThread()返回的线程。利用sleep()方法可以设计计时器。

   5.getId()方法

        thread.getId(); 可以获得线程的唯一标识。

        注意:某个编号的线程运行结束后,该编号可能被后续创建的线程使用。重启JVM之后,同

      一个线程的编号可能不一样。

   6.yield()方法

        Thread.yield(); 放弃当前的CPU资源。

   7.setPriority()方法

        thread.setPriority(num); 设置线程的优先级。

        java线程的优先级取值范围是1~10,如果超出这个范围会抛出异常illegalArgumentException.

        在操作系统中,优先级较高的线程获得CPU的资源越多。线程优先级本质上只是给线程调度

      器一个提示信息,以便于调度器决定先调用哪些线程。不过不能保证优先级高的线程先运行

        java优先级设置不当或者滥用可能会导致某些线程永远无法得到运行,即产生了线程饥饿。线

      程优先级并不是设置的越高越好,一般情况下使用普通的优先级即可,即在开发时不必设置线

      程的优先级。默认优先级是5

        线程的优先级具有继承性,在A线程中创建了B线程,则B线程的优先级与A线程是一样的。

   8.interrupt()方法

        thread.interrupt(); 中断线程,但是仅仅是在当前线程打一个停止标志,并不是真正的停止线

      程。

      isInterrupt()方法

        thread.isInterrupt(); 判断当前线程是否中断,返回boolean类型。

   9.setDaemon()方法

        thread.setDaemon(true); 设置线程为守护线程

        java中的线程分为用户线程与守护线程。守护线程是为其他线程提供服务的线程,如垃圾回收

      器GC就是一个典型的守护线程。守护线程不能单独运行,当JVM中没有其他用户线程,只有

      守护线程时,守护线程会自动销毁,JVM会退出。设置守护线程的代码要放在线程启动即start

      方法前

   10.getState()方法

        获得当前线程的状态,是一个枚举类型。

1.4 线程的生命周期

   1.线程的生命周期是线程对象的生老病死,即线程的状态。线程生命周期可以通过getState()方法

    获得,线程的状态是Thread.State枚举类型定义的,有以下几种:

        ①NEW:新建状态。创建了线程对象,在调用start()方法启动之前的状态。

        RUNNABLE:可运行状态。它是一个复合状态,包含READYRUNNING两个状态。

      READY状态时该线程可以被线程调度器进行调度使它处于RUNNING状态,RUNNING状态标

      识该线程正在执行。Thread.yield()方法可以把线程由RUNNING状态转换为READY状态。

        ③BLOCKED:阻塞状态。线程发起阻塞的I/O操作,或者申请由其他线程占用的独占资源,

      此时线程会转换为BLOCKED阻塞状态。处于阻塞状态的线程不会占用CPU资源。当阻塞I/O操

      作执行完成,或者线程获得了其申请的资源,线程可以转换为RUNNABLE。

        ④WAITING:等待状态。线程执行了object.wait()、thread.join()方法会把线程转换未

      WAITING等待状态,执行object.notify()方法,或者加入的线程执行完毕,当前线程会转换为

      RUNNABLE状态。

        ⑤TIMED_WAITING:等待状态。与WAITING状态类似,都是等待状态。区别在于处于该状

      态的线程不会无限的等待,如果线程没有在指定的时间范围内完成期望的操作,该线程自动转

      换为RUNNABLE。

        ⑥TERMINATED:终止状态。线程结束处于终止状态。

1.5 多线程编程的优势与存储的风险

   1.多线程编程的优势

        ①提高系统的吞吐率Throughout。多线程编程可以使一个进程有多个并发(concurrent,即同

      时进行)的操作;

        ②提高响应性Responsiveness。Web服务器会采用一些专门的线程负责用户的请求处理,缩

      短了用户的等待时间。

        ③充分利用多核Multicore处理器资源。通过多线程可以充分地利用CPU资源。

   2.多线程编程存在的问题与风险

        ①线程安全(Thread Safe)问题。多线程共享数据时,如果没有采用正确的并发访问控制措

      施,就可能会产生数据一致性问题,如读取脏数据(过期的数据)、丢失数据更新。

        ②线程活性(Thread Liveness)问题。由于程序自身的缺陷或者由于资源稀缺性导致线程一直

      处于非RUNNABLE状态,这就是线程活性问题,常见的活性故障有以下几种:

                a.死锁Deadlock

                b.锁死Lockout

                c.活锁Livelock

                d.饥饿Starvation

        ③上下文切换(Context Switch)。处理器从执行一个线程切换到执行另外一个线程,需要消耗

      系统资源。

        ④可靠性。可能会由于一个线程导致JVM意外终止,其他的线程也无法执行。

二、线程安全问题

   非线程安全主要是指多个线程对同一个对象的实例变量进行操作时,会出现值被更改,值不同步的情况。

   线程安全问题表现为三个方面:原子性、可见性、有序性。

2.1 原子性

   原子Atomic就是不可分割的意思。不可分割有两层含义:

        ①访问(读、写)某个共享变量的操作从其他线程来看,该操作要么已经执行完毕,要么尚未发

      生,即其他线程看不到当前线程操作的中间结果;

        ②访问同一组共享变量的原子操作是不能够交错的。

   java有两种方式实现原子性:一种是使用;另一种利用处理器的CAS(Compare and Swap)指令。锁具有排他性,保证共享变量在某一时刻只能被一个线程访问;CAS指令直接在硬件(处理器和内存)层次上实现,看作是硬件锁。

   java中有一些线程安全的类,比如AtomicInteger类,保证了操作的原子性。

2.2 可见性

   在多线程环境中,一个线程对某个共享变量进行更新之后,后续其他的线程可能无法立即读到这个更新的结果,这就是线程安全问题的另外一种形式:可见性visibility。

   如果一个线程对共享变量更新后,后续访问改变量的其他线程可以读到更新的结果,称这个线程对共享变量的更新对其他线程可见,否则称这个线程对共享变量的更新对其他线程不可见。

   多线程程序因为可见性问题可能会导致其他线程读取到了旧数据(脏数据)

2.3 有序性

   有序性Ordering是指在任何情况下一个处理器上运行的一个线程所执行的内存访问操作在另外一个处理器运行的其他线程看来是乱序的(Out of Order)。处理器为了提高运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的

   乱序是指内存访问操作的顺序看起来发生了变化。

   看如下例子:

        线程A执行程序:


context = loadContext();//1
inited = true; //2

        线程B执行程序:

while(!inited ){
 sleep
}
doSomethingwithconfig(context);//3

        因为对于线程A来说,语句1和语句2顺序发生变化不影响结果,可能就会先执行语句2再执行

      语句1,但是如果线程A先执行了语句2导致线程B中的循环结束了,而还未执行语句1时,此刻

      线程B执行了语句3,但是此时共享数据context还未初始化,从而会引起错误。

2.3.1 重排序

   在多核处理器的环境下,编写的顺序结构,这种操作执行的顺序可能是没有保障的:

        ①编译器可能会改变两个操作的先后顺序。

        ②处理器可能不会按照目标代码的顺序执行。

   这种一个处理器上执行的多个操作,在其他处理器来看它的顺序与目标代码指定的顺序可能不一样,这种现场称为重排序。重排序是对内存访问有序操作的一种优化,可以在不影响单线程程序正确的情况下提升程序的性能,但是可能对多线程程序的正确性产生影响,即可能导致线程安全问题。

   重排序与可见性问题类似,不是必然出现的。

   与内存操作顺序有关的几个概念:

        ①源代码顺序:源码中指定的内存访问顺序。

        ②程序顺序:处理器上运行的目标代码所指定的内存访问顺序。

        ③执行顺序:内存访问操作在处理器上的实际执行顺序。

        ④感知顺序:给定处理器所感知到的该处理器以及其他处理器的内存访问操作的顺序。

   可以把重排序进行如下分类:

        ①指令重排序:主要是由JIT编译器、处理器引起的程序顺序与执行顺序不一致。

        ②存储子系统重排序:是由高速缓存、写缓冲器引起的感知顺序与执行顺序不一致。

2.3.2 指令重排序

   在源码顺序与程序顺序不一致或者程序顺序与执行顺序不一致的情况下,我们就说发生了指令重排序(Instruction Reorder)。指令重排是一种动作,确实对指令的顺序做了调整,重排序的对象指令。javac编译器一般不会执行指令重排序,而JIT编译器可能执行指令重排序。处理器也可能执行指令重排序,使得执行顺序与程序顺序不一致。指令重排不会对单线程程序的结果正确性产生影响,可能导致多线程程序出现非预期的结果。

2.3.3 存储子系统重排序

   存储子系统是指写缓冲器与高速缓存。高速缓冲Cache是CPU中为了匹配与主内存处理速度不匹配而设计的一个告诉缓存;写缓冲器(Write Buffer)是用来提高写高速缓存操作的效率。

   即使处理器严格按照执行顺序执行两个内存访问操作,在存储子系统的作用下,其他处理器对这两个操作的感知顺序与程序顺序不一致,即这两个操作的顺序看起来像是发生了变化,这种现象称为存储子系统重排序。存储子系统重排序并没有真正的对指令执行顺序进行调整,而是造成一种指令执行顺序被调整的感觉现象,是内存操作的结果。

   从处理器角度来看,读内存就是从指定的RAM地址中加载数据到寄存器,称为Load操作;写内存就是把数据存储到指定的地址标识的RAM存储单元中,称为Store操作。内存重排序有以下四种可能:

        ①LoadLoad重排序:一个处理器先后执行两个读操作L1--->L2,其他处理器对两个内存操作

          的感知顺序可能是L2--->L1。

        ②StoreStore重排序:一个处理器先后执行两个写操作W1--->W2,其他处理器对两个内存操

          作的感知顺序可能是W2--->W1。

        ③LoadStore重排序:一个处理器先执行读内存操作L1再执行写内存操作W1,其他处理器对

          两个内存操作的感知顺序可能是W1--->L1。

        ④StoreLoad重排序:一个处理器先执行写内存操作W1再执行读内存操作L1,其他处理器对

          两个内存操作的感知顺序可能是L1--->W1。

   内存重排序与具体的处理器微架构有关,不同架构的处理其所允许的内存重排序不同。内存重排序可能会导致线程安全问题。

2.3.4 貌似串行语义

   JIT编译器、处理器、存储子系统是按照一定的规则对指令、内存操作的结果进行重排序,给单线程程序造成一种假象---指令是按照源码的执行顺序执行的,这种假象称为貌似串行语义。可以保证单线程程序执行结果的正确性,但是不能保证多线程环境程序的正确性。

   为了保证貌似串行语义,有数据依赖关系的语句不会被重排序,只有不存在数据依赖关系的语句才会被重排序。如果两个操作指令访问同一变量,且其中一个操作指令为写操作,那么这两个操作之间就存在数据依赖关系。

   存在控制依赖关系的语句允许重排,一条语句(指令)的执行结果会决定另一条语句(指令)能否被执行,这两条语句(指令)存在控制依赖关系,比如if语句块允许重排,可能存在处理器先执行if语句块,再判断if条件是否称立。

2.3.5 保证内存访问的顺序性

   可以使用volatile关键字、synchronized关键字实现有序性。

2.4 Java内存模型

   

PS:根据动力节点课程整理,如有侵权,联系删除。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值