《多线程》

每一个任务就是一个进程,每个进程内部至少有一个线程在运行中。线程是程序执行的一个路径,每一个线程都有自己的局部变量表,程序技术器,以及各自的生命周期。

1.创建一个线程,并且重写它的run方法,将行为方法交给它执行。

2.启动新的线程,只有调用了Thread的start方法,才代表派生了一个新的线程,否则Thread和其他普通的Java对象没有什么区别,start方法是一个立即返回方法,并不会让程序陷入阻塞

使用jconsole查看正在运行的线程:(C:\Program Files\Java\jdk1.8.0_73\bin\jconsole)

线程的生命周期:new,runnable,running ,blocked,terminated

线程new状态:
使用关键字new创建一个thread对象时,此时它并不处于执行状态,因为没有调用start方法启动该线程,线程状态为new,它只是thread对象的状态,new状态通过start方法进入runnable状态。

线程runnable状态:线程对象进入runnable状态必须调用start方法启动该线程,但不一定是立即执行,线程的执行与否和进程一样都要听CPU的调度,这个中间状态就是runnable,即几倍执行的资格。由于存在running状态,所以不会直接进入blocked和terminater状态,即使线程的执行逻辑调用wait,sleep或其他blockde的IO操作,也必须先获得CPU的状态执行权才可以。

线程的running状态:
一旦CPU通过轮训或其他方式从任务可执行队列中选中了线程,那么此时它才能真正的执行自己的逻辑代码。一个正在running状态的线程事实上也是runnable的,但是反过来不成立。

running状态中,线程的状态切换:
1.直接进入terminated状态,比如调用jdk的stop方法,或者判断某个逻辑标识
2.进入某个阻塞的IO操作,比如因网络数据的读写而进入了blocked状态。
3.获取某个锁资源,从而加入到该锁的阻塞队列中而进入了blocked状态。
4.由于CPU的调度器轮询使该线程放弃执行,进入runnable状态。
5.线程主动调用yield方法,放弃CPU执行权,进入runnable状态。

线程的blocked状态:

线程在blocked状态中可以切换的状态:
1.直接进入terminated状态,比如调用jdk的stop方法或者意外死亡(JVM Crash)。

2.线程阻塞的操作结束,比如读取了想要的数据字节进入到runnable状态。

3.线程完成了指定时间的休眠,进入到了runnable状态。

4.wait中的线程被其他线程notify/notifyAll唤醒,进入runnable状态。

5.线程获取到了某个锁资源,进入runnable状态。

6.线程在阻塞过程中被打断,比如其他线程调用了interrupt方法,进入runnable状态。

线程的terminated状态:terminated是线程的最终状态,此状态中线程将不会切换到其他任何状态,线程的整个生命周期都结束了。

进入terminated状态的场景:
1.线程运行正常结束,结束生命周期
2.线程运行出错意外结束
3.JVM Crash导致所有的线程都结束。

重复启动线程会抛IllegalThreadStateException异常。

共享资源static修饰的变量生命周期很长(有时会出现线程安全的问题),
java的Runnable接口专门用于解决该问题,将线程的控制和业务逻辑的运行彻底分离开来。

重写Thread类的run方法和Runnable接口的run方法区别是:Thread类的run方法是不能共享的,也就是A线程不能把B线程的run方法当做自己的执行单元,而使用Runnable接口就能实现这一点,使用同一个Runnable的实例构造不同的Thread实例。

这个demo多运行几次,或者max的值从50增加到500,1000或者更大都会出现一个号码出现两次的情况,也会出现某个号码根本不会出现的情况,更会出现超过最大值的情况,这是因为共享资源index存在线程安全的问题。

构造线程时起一个名字有利于排查问题和线程的跟踪,如果没有我线程显示的指定一个名字,那么线程会以Thread-做为前缀和一个自增数字进行组合,这个自增数字在整个JVM进程中将会不断自增。

1.一个线程的创建肯定是由另一个线程完成的。
2.被创建线程的父线程是创建它的线程。
main函数所在的线程由JVM创建,所有的线程其父线程都是main线程。

线程组:
1.main线程所在的ThreadGroup称为main。
2.构造一个线程的时候如果没有显示的指定ThreadGroup,那么它将会和父线程同属于一个ThreadGroup。

程序计数器:程序计数器在JVM中的作用是用于存放当前线程将要执行的字节码指令,分支,循环,跳转,异常处理等信息,在任何时候, 一个处理器只执行其中一个线程中的指令,为了能够在CPU时间片轮转切换上下文之后顺利回到正确的执行位置,每条线程都需要具有一个独立的程序计数器,各个线程之间互不影响,程序计数器是私有的。

Java虚拟机栈:Java虚拟机栈是线程私有的,它的生命周期和线程相同,是在JVM运行时创建的,在线程中,方法在执行的时候都会创建一个名为栈帧的数据结构,主要用于存放局部变量表,操作栈,动态链接,方法出口等信息。

本地方法栈:Java提供了调用本地方法的接口(java native interface),即C/C++程序,在程序的执行过程中,经常会碰到调用JNI方法的情况,比如网络通信,文件操作的底层,String的intern都是JNI方法,JVM为本地方法所划分的内存区域就是本地房发展,自由度非常高,也是线程自由的内存区域。

堆内存:堆内存是JVM中最大的一块内存区域,被所有的线程所共享,Java在运行期间创建的所有对象几乎都存放在该内存区域,该内存区域是垃圾回收器重点照顾的区域,堆内存也被称为“GC堆”。堆内存细分为新生代和老年代,更细点划分为Eden区,From Survivor区,To Survivor区。

方法区:方法区也是被多个线程所共享的内存区域,它主要用于存储已经被虚拟机加载的类信息,常量,静态变量,即时编译器JIT编译后的代码等数据,持久代。

Java8元空间:Java8开始,持久代内存被彻底删除,取而代之的是元空间。jdk7中存放持久代内存区域,而在jdk8中,该内存区域被Meta Space取代,元空间是堆内存的一部分,JVM为每个类加载器分配一块内存块列表,进行线性分配,块的大小取决于类加载器的类型,sun/反射/代理对应的类加载器块会小一些,之前的版本会单独卸载回收某个类,而现在则是GC过程中发现某个类加载器已经具备回收的条件,则会将整个类加载器相关的元空间全部回收,这样就可以减少内存碎片,节省GC扫描和压缩的时间。

程序计数器是比较小的一块内存,该内存不会出现任何溢出异常的,和线程创建,运行,销毁等关系比较大的是虚拟机内存栈,而且栈内存划分的大小将直接决定在一个JVM进程中可以创建多少个线程。

正常情况下,若JVM中没有一个非守护线程,则JVM的进程就会退出。守护线程具备自动结束生命周期的特性,而非守护线程则不具备这个特点。

sleep是一个静态方法,它有两个重载方法,其中一个需要传入毫秒数,一个需要毫秒数和纳秒数。

sleep方法会使当前线程进入指定毫秒数的休眠,暂停执行,虽然给定了一个休眠的时间,但是最终要以系统的定时器和调度器的精度为准。但它不会放弃monitor锁的所有权。

在使用Thread.sleep的时候,可以使用TimeUnit来替代,sleep可以做的事情,TimeUnit全部都能实现,并且功能更加强大。

yield方法:调用yield方法会使当前线程从running状态切换到runnable状态,  yield方法会提醒调度器愿意放弃当前的CPU资源,如果CPU的资源不紧张,则会忽略这种提醒。yield只是一个提示,CPU调度器并不会保证每次都能满足yield提示。

yield和sleep的区别:

1.sleep会导致当前线程暂停指定的时间,没有CPU时间片的消耗。
2.yield只是对CPU调度器的一个提示,如果CPU调度器没有忽略这个提示 ,那么它会导致线程上下文的切换。
3.sleep会使线程短暂block,会在给定的时间内释放CPU资源。
4.yield会使当前线程从running状态切换到runnable状态。
5.sleep几乎百分之百的晚餐了给定时间的休眠,而yield的提示并不能一定保证。
6.一个线程sleep另一个线程调用interrupt会捕获到中断信号,而yield则不会。

线程的优先级:线程的优先级设置,只是一个提示作用,并不一定完全起作用。线程的优先级不能小于1不能大于10,如果指定的线程优先级大于线程所在group的优先级,那么指定的优先级将会失效,取而代之的是group的最大优先级。


1.对于root用户,它会提示操作系统你想要设置的优先级别,否则它会被忽略。
2.如果CPU比较忙,设置优先级可能会获得更多的CPU时间片,但是闲时优先级的高低几乎不会有任何作用。

设置线程上下文类加载器:
public ClassLoader getContextClassLoader获取线程上下文的类加载器,即这个线程是由哪个类加载器加载的,如果是在没有修改线程上下文类加载器的情况下,则保持与父线程同样的类加载器。

public void setContextClassLoader设置该线程的类加载器,这个方法可以打破Java类加载器的父委托机制。

线程中断interrupt:
1.public void interrupt()
2.public static boolean interrupted();
3.public boolean isInterrupted();

使线程进入阻塞状态的方法(调用当前线程的interrupt方法可以打断阻塞):
1.Object的wait方法。
2.Object的wait(long)方法。
3.Object的wait(long,int)方法。
4.Thread的sleep(long)方法
5.Thread的sleep(long,int)方法
6.Thread的join方法
7.Thread的join(long)方法
8.Thread的join(long,int)方法
9.InterruptibleChannel的io操作。
10.Selector的wakeup方法。

如果一个线程已经是死亡状态,那么尝试对它的中断interrupt会直接被忽略。

Thread的join方法是一个可中断的方法,如果有其他线程执行了对当前线程的interrupt操作,它会捕获到中断信号,并且擦除线程的interrupt标识。

关闭线程的方法:
1.stop
2.线程正常结束
3.捕获中断信号关闭线程。
4.使用voatile开关控制。

进程假死:进程虽然存在,但是没有日志输出,程序不进城任何的作业。程序出现的这样的情况,绝大部分原因就是某个线程阻塞了,或者线程出现了死锁的情况。可以借助jstack,jconsole,jvisualvm等工具。

synchronized关键字可以防止线程干扰和内存一致性错误,如果一个对象对多个线程是可见的,那么对该对象的所有读或者写都将通过同步的方式来进行。

1.synchronized关键字提供了一种锁的机制,能够确保共享变量的互斥访问,从而防止数据不一致问题的出现。

2.synchronized关键字包括monitor enter和monitor exit两个JVM指令,它能够保证在任何时候线程执行到monitor enter成功之前都必须从主内存中获取数据,而不是从缓存中,在monitor exit成功之后,共享变量被更新后的值必须刷入主内存。

3.synchronized的指令严格遵守Java happens-before规则,一个monitor exit指令之前必定要有一个monitor enter。

synchronized用法,synchronized可以用于对代码块或方法进行修饰,而不能用于对class以及变量进行修饰。

synchronized关键字提供了一种互斥机制,即同一时刻,只能有一个线程访问同步资源。

1.如果monitor的计数器为0,则意味着该monitor的lock还没有被获得,某个线程获得之后将立即对该计数器加一,从此该线程就是这个monitor的所有者了。

2.如果一个已经拥有该monitor所有权的线程重入,则会导致monitor计数器再次累加。

3.如果monitor已经被其他线程所拥有,则其他线程尝试获取该monitor的所有权时,会被陷入阻塞状态直到monitor计数器变为0,才能再次尝试获取对monitor的所有权。

Monitor exit: 释放对monitor的所有权,想要释放对某个对象关联的monitor的前提是,你曾经获得了所有权。释放monitor所有权的过程就是将monitor的计数器减一,如果计数器的结果为0,则该线程不再拥有对该monitor的所有权,即解锁。

synchronized作用域太大:由于synchronized关键字存在排他性,即所有的线程必须串行的经过synchronized保护的共享区域,如果synchronzed作用域越大,则代表着其效率越低,甚至还会丧失并发的优势。synchronized关键字应该尽可能的只作用于共享资源(数据)的读写作用域。

多个锁的交叉很容易引起线程出现死锁的情况,程序并没有任何错误输出,但就是不工作。

程序死锁的原因以及诊断:

1.程序死锁

1.交叉锁可导致程序出现死锁【线程A持有R1的锁等待获取R2的锁,线程B持有R2的锁等待获取R1的锁】

2.内存不足:当并发请求系统可用内存时,如果此时系统内存不足,则可能会出现死锁的情况(两个线程都需要内存,但是剩余的内存不够,两个线程可能都在等待彼此能够释放内存资源)。

3.一问一答式的数据交换。

4.数据库锁:无论是数据库表锁,还是行锁,比如某个线程执行for update 语句退出了实物,其他线程访问该数据库时都将陷入死锁。

5.文件锁:某线程获得了文件锁意外退出,其他读取该文件的线程也将会进入死锁直到系统释放文件句柄资源。

6.死循环引起的死锁:程序由于代码或者对异常处理的不当,进入死循环,查看线程堆栈信息不会发现任何死锁的迹象,但是程序不工作,CPU占有率又居高不下,这种死锁一般称为系统假死,是一种最为致命最难排查的死锁现象。

hashMap,如果在多线程同时写操作的情况下不对其进行同步化封装,则很容易出现死循环引起的死锁,程序运行一段时间后CPU等资源居高不下。hashMap不具备线程安全的能力,可以使用ConcurrentHashMap或者使用Collections.synchronizedMap来代替。

1.wait和sleep方法都可以使线程进入阻塞状态。
2.wait和sleep方法都是可中断方法,被中断后都会收到中断异常。
3.wait是Object的方法,sleep是Thread特有的方法。
4.wait方法的执行必须在同步方法中进行,而sleep则不需要。
5.线程在同步方法中执行sleep方法时,并不会释放monitor的锁,而wait方法则会释放monitor的锁。
6.sleep方法短暂休眠之后会主动退出阻塞,而wait方法则需要被其他线程中断后才能退出阻塞。

多线程间通信:多线程间通信需要用到Object的notifyAll方法,和notify类似,都可以唤醒由于调用了wait方法而阻塞的线程,但是notify只能唤醒其中一个线程,而notifyAll方法则可以同时唤醒全部的阻塞线程,被唤醒的线程仍需要继续争抢monitor的锁。

synchronized关键字提供了一种排他式的数据同步机制,某个线程在获取monitor lock的时候可能会被阻塞,这种阻塞的缺陷:
1.无法控制阻塞时长
2.阻塞不可被中断

显示锁BooleanLock:它具备synchronized关键字所有功能的同时又具备可中断和lock超时的功能。

UncaughtExceptionHandler:线程在执行单元中是不允许抛出checked异常的,而且线程运行在自己的上下文中,派生它的线程将无法直接获得它运行中出现的异常。所以,Java提供了一个UncaughtExceptionHandler接口,当线程在运行过程中出现异常时,会回调UncaughtExceptionHandler接口,从而得知是哪个线程在运行时出错,以及出现了什么样的错误。

Hook:可以帮助程序获得进程中断信号,有机会在进程退出之前做一些资源释放的动作,或者一些警告通知。但,如果强制杀死进程,那么进程将不会收到任何中断信号。

Hook线程应用场景以及注意事项:

1.hook线程只有在收到退出信号的时候会被执行,如果在kill的时候使用了参数-9,那么hook线程不会得到执行,进程将会立即退出,因此.lock文件将得不到清理。

2.hook线程也可以执行一些资源释放的工作,比如关闭文件句柄,socket链接,数据库connection等。

3.尽量不要在hook线程中执行一些耗时非常长的操作,因为其会导致程序迟迟不能退出。

utils包提供了ExecutorService线程池的实现,主要目的是为了重复利用线程,提高系统效率。Thread是一个重量级的资源,创建,启动以及销毁都是比较消耗系统资源的,对线程的管理,尤其是数量的控制更能直接决定程序的性能。

线程池:里面存放着已经创建好的线程,当有任务提交给线程池执行时,池子中的某个线程会主动执行该任务。如果池子中的线程数量不够应付数量众多的任务时,则需要自动扩充新的线程到池子中,但是该数量是有限的。当任务比较少的时候,池子中的线程能够自动回收,释放资源。

任务队列:用于缓存提交的任务。
线程数量管理功能:一个线程池必须能够很好的管理和控制线程数量,可通过三个参数来实现,比如创建线程池时初始的线程数量init;线程池自动扩充时最大的线程数量max;在线程池空闲时需要释放线程但是也需要维护一定数量的活跃数量或者核心数量core。三者的关系init<=core<=max。

任务拒绝策略:如果线程数量已经达到上限并且任务队列已满,则需要有相应的拒绝策略来通知任务提交者。

线程工厂:主要用于个性化定制线程,比如将线程设置为守护线程以及设置线程名称等。

QueueSize:任务队列主要存放提交的Runnable,但是为了防止内存溢出,需要有limit数量对其进行控制。

keepedalive时间: 该时间主要决定线程各个重要参数自动维护的时间间隔。

类的加载就是将class文件中的二进制数据读取到内存中,然后将该字节流所代表的静态存储结构转换为方法区中运行时的数据结构,并且在堆内存中生成一个该类的Java.lang.Class对象,作为访问方法区数据结构的入口。

类的加载过程一般分为三个阶段:加载阶段,连接阶段,初始化阶段。

加载阶段:主要负责查找并且加载类的二进制数据文件,即class文件。

连接阶段:连接阶段做的工作分为:验证,准备,解析,初始化阶段。
   验证:主要是确保类文件的正确性,比如class的版本,class文件的魔术因子是否正确。
   准备:为类的静态变量分配内存,并且为其初始化默认值。
   解析:把类中的符号引用转换为直接引用。
   初始化阶段:为类的静态变量赋予正确的初始值。

JVM对类的初始化是一个延迟的机制,使用的是lazy的方式,当一个类在首次使用的时候才会被初始化,在同一个运行时包下,一个class只会被初始化一次。JVM虚拟机规范规定了,每个类或者接口被Java程序首次主动使用时才会对其进行初始化。

JVM主动使用类的6种场景:
1.通过new关键字会导致类的初始化(会导致类的加载并且最终初始化)
2.访问类的静态变量,包括读取和更新会导致类的初始化。
3.访问类的静态方法,会导致类的初始化。
4.对某个类进行反射操作,会导致类的初始化
5.初始化子类会导致父类的初始化。
6.启动类。

双亲委派机制:当一个类加载器被调用量loadClass之后,它并不会直接将其加载,而是先交给当前类加载器id父加载器尝试加载,直到最顶层的父加载器,然后再依次向下进行加载。

JDK的核心库中提供了很多SPI(service provider interface),常见的SPI包括JDBC,JCE,JNDI,JAXP,JBI,不提供具体的实现,具体的实现由第三方厂商提供。

volatile:原子性,可见性,有序性。

java提供了三种保证有序性的方式:
1.使用volatile关键字来保证有序性
2.使用synchronized关键字来保证有序性
3.使用显示锁lock来保证有序性

可见性:
1.读线程Reader从主内存中获取初始值init_value的值为0,并且将其缓存到本地工作内存中。

2.写线程Updater将初始值init_value的值在本地工作内存中修改为1,然后立即刷新到主内存中。

3.读线程Reader在本地工作内存中的初始值init_value失效。

4.由于读线程Reader工作内存中的初始值init_value失效,因此需要到主内存中重新读取初始值init_value。

顺序性:为了保证顺序性,直接禁止JVM和处理器对volatile关键字修饰的指令重排序,但对于volatile前后无以来关系的指令则可以随便排序。

volatile和synchronized的区别:
使用伤的区别:
1.volatile 关键字只能用于修饰实例变量或者类变量,不能用于修饰方法以及方法参数和局部变量,常量等。
2.synchronized关键字不能用于对变量的修饰,只能用于修饰方法或者语句块。
3.volatile修饰的变量可以为null,synchronized关键字同步语块的monitor对象不能为null。

对原子性的保证:
1.volatile无法保证原子性
2.由于synchronized是一种排他机制,因此被synchronized关键字修饰的同步代码是无法被中途打断的,因此其能够保证代码的原子性。

对可见性的保证:
1.两者都可以保证共享资源在多线程间的可见性,但是实现机制完全不同。
2.synchronized借助于JVM指令monitor enter和monitor exit 对通过排他的方式使得同步代码串行化,在monitor exit时所有共享资源都将会被刷新到主内存中。
3.相比于synchronized关键字,volatile使用机器指令(偏硬件)的方式迫使其他线程工作内存中的数据失效,不得到主内存中进行再次加载。

有序性:
1.volatile关键字禁止JVM编译器以及处理器对其进行重排序,所以它能够保证有序性。
2.虽然synchronized关键字所修饰的同步方法也可以保证顺序性,但是这种顺序性是以程序的串行化执行换来的,在synchronized关键字所修饰的代码块中代码指令也会发生指令重排序的情况。

其他:
1.volatile不会使线程陷入阻塞
2.synchronized关键字会使线程进入阻塞状态。


 

资料参考:

《Java高并发程序设计》,《Java高并发编程详解2018》,《Java多线程编程实战指南-核心篇》

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值