学习资料:《深入理解计算机系统》,《Java高并发程序设计》,《Java并发编程实战》,《Java并发编程的艺术》,《Java核心技术卷1》多线程一章,极客时间王宝令的Java并发编程实战课程…
以下大部分阐述来自上述书籍与课程中个人认为很重要的部分,也有部分心得体会,如有不准确的地方,欢迎评论区告诉我。后续还会更新并发包,并发算法等各种并发相关笔记,点点关注不迷路!ヽ(✿゚▽゚)ノ
一.概念辨析
1.进程 & 线程
进程:操作系统对一个正在运行的程序的一种抽象。在一个系统上可以同时运行多个进程,而每个进程都好像在独占地使用硬件。(进程是线程的容器)
而并发运行,则是说一个进程的指令和另一个进程的指令是交错执行的。这种机制叫做上下文切换。
举例:当你双击一个exe程序时,这个.exe文件的指令就会被加载,那么你就能得到一个关于这个程序的进程。进程是活的,是正在被执行的,你可以通过任务管理器看到你电脑正在执行的进程。
线程:一个进程由将多个称为线程的执行单元组成,每个线程都运行在进程的上下文中,并共享同样的代码和全局数据。因为多线程之间比多进程之间更容易共享数据,也因为线程一般来说都比进程更高效。线程可以实现宏观上的“同时”执行,实际上是快速切换线程来达到几乎同时执行的效果。也可以称线程为轻量级进程,它是程序执行的最小单位。多核处理器中,同一个进程的不同线程也可以在不同的cpu上同时运行,此时就需要考虑通过锁,来保证操作的原子性。
多进程与多线程的本质区别:每个进程都拥有自己的一整套变量,而线程则共享数据。多线程比多进程开销小得多。(当然也要看具体的操作系统,Windows和Linux是不同的,Windows开进程开销大,Linux开线程开销大,因此 Windows 多线程学习重点是要大量面对资源争抢与同步方面的问题,Linux 下的学习重点大家要学习进程间通讯的方法)
2.同步 & 异步
同步 Synchronous方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。
异步 Asynchronous方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作。
举例:
比如有一个计算圆周率小数点后 100 万位的方法pai1M(),这个方法可能需要执行俩礼拜,如果调用pai1M()之后,线程一直等着计算结果,等俩礼拜之后结果返回,就可以执行 printf(“hello world”)了,这个属于同步;如果调用pai1M()之后,线程不用等待计算结果,立刻就可以执行 printf(“hello world”),这个就属于异步。
区别:同步就是要等到整个流程全部结束,而异步只是传递一个接下来要去做什么什么事情的消息,然后就会去干其他事。
同步,是 Java 代码默认的处理方式。如果你想让你的程序支持异步,可以通过下面两种方式来实现:
1.调用方创建一个子线程,在子线程中执行方法调用,这种调用我们称为异步调用;
2.方法实现的时候,创建一个新的线程执行主要逻辑,主线程直接return,这种方法我们一般称为异步方法。
3.并行 & 并发
并行 Parallel:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。
并行二定律
1.Amdahl定律 加速比=1/[F+(1-F)/n](n为处理器储量,F为并行比例) 由此可见,为了提高系统的速度,仅仅增加CPU处理器数量不一定能起到有效的作用。需要根本上修改程序的串行行为,提高系统内并行化的模块比重。
2.Gustafson定律 加速比=n-F(n-1) 如果串行化比例很小,并行化比例很大,那么加速比就是处理起个数,只要不断累加处理起,就能获得更快的速度
并发 ConCurrent:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,我们会用TPS或者QPS来反应这个系统的处理能力。
总述:
并行就是同时进行,并发则是上下文快速切换(进程的运行不仅仅需要CPU,还需要很多其他资源,如内存,显卡,GPS,磁盘等等,统称为程序的执行环境,也就是程序上下文。)。单核处理器中,为了避免一个进程一直在使用CPU,调度器快速切换CPU给不同进程,于是在使用者看来程序是在同时运行,这就是并发,而实际上CPU在同一时刻只在运行一个进程。
CPU进程无法同时刻共享,但是出现一定要共享CPU的需求呢?此时线程的概念就出现了。线程被包含在进程当中,进程的不同线程间共享CPU和程序上下文。(共享进程分配到的资源)单CPU进行进程调度的时候,需要读取上下文+执行程序+保存上下文,即进程切换。
如果这个CPU是单核的话,那么在进程中的不同线程为了使用CPU核心,则会进行线程切换,但是由于共享了程序执行环境,这个线程切换比进程切换开销少了很多。在这里依然是并发,唯一核心同时刻只能执行一个线程。
如果这个CPU是多核的话,那么进程中的不同线程可以使用不同核心,真正的并行出现了。
4.死锁、饥饿、活锁
死锁:所有线程互相占用了对方的锁,导致所有线程挂起。
死锁四条件:
1.互斥,共享资源 X 和 Y 只能被一个线程占用;
2.占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;
3.不可抢占,其他线程不能强行抢占线程 T1 占有的资源;
4.循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。解决方法:破坏死锁后三个条件的任意其一(互斥一般不应被破坏)。
1.对于“占用且等待”这个条件,我们可以一次性申请所有的资源,这样就不存在等待了。若申请不成功,wait();申请成功了,notifyAll()。
2.对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。
3.对于“循环等待”这个条件,可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后自然就不存在循环了。
饥饿:某些线程因为某些原因(优先级过低)无法获得所需的资源,导致无法运行。
活锁:两个线程互相释放资源给对方,从而导致没有一个线程可以同时拿到所有资源正常执行。(出电梯时,和一个进电梯的人互相谦让,导致进电梯的人进不了,出电梯的人出不去)
解决方法:让等待时间设定为随机值。
为了处理这种异常情况,可以通过 jstack 命令或者Java VisualVM这个可视化工具将 JVM 所有的线程栈信息导出来,完整的线程栈信息不仅包括线程的当前状态、调用栈,还包括了锁的信息。
5.锁 & 监视器(管程)
锁为实现监视器提供必要的支持。
锁是对象内存堆中头部的一部分数据。JVM中的每个对象都有一个锁(或互斥锁),任何程序都可以使用它来协调对对象的多线程访问。如果任何线程想要访问该对象的实例变量,那么线程必须拥有该对象的锁(在锁内存区域设置一些标志)。所有其他的线程试图访问该对象的变量必须等到拥有该对象的锁有的线程释放锁(改变标记)。锁,应是私有的、不可变的、不可重用的。
细粒度锁:用不同的锁对受保护资源进行精细化管理,能够提升性能。
1) 锁用来保护代码片段,任何时刻只能有一个线程执行被保护 的代码。
2) 锁可以管理试图进入被保护代码的线程
3) 锁可以拥有一个或者多个相关的条件对象
4) 每个条件对象管理那些已经进入被保护的代码段,但还不能运行的线程
用锁的最佳实践
永远只在更新对象的成员变量时加锁
永远只在访问可变的成员变量时加锁
永远不在调用其他对象的方法时加锁
监视器 Monitor是一中同步结构,它允许线程同时互斥(使用锁)和协作,即使用等待集(wait-set)使线程等待某些条件为真的能力。监视器也可以叫管程,意思是管理共享变量以及对共享变量的操作过程,让他们支持并发。管程和信号量是等价的,所谓等价指的是用管程能够实现信号量,也能用信号量实现管程。但是管程更容易使用,所以 Java 选择了管程。管程理论上能解决一切并发问题。
他们是应用于同步问题的人工线程调度工具。讲其本质,首先就要明确monitor的概念,Java中的每个对象都有一个管程,来监测并发代码的重入。在非多线程编码时该管程不发挥作用,反之如果在synchronized 范围内,管程发挥作用。
管程至少有两个等待队列。一个是进入管程的等待队列一个是条件变量对应的等待队列。后者可以有多个。
wait/notify/notifyAll必须存在于synchronized块中。并且,这三个关键字针对的是同一个管程。这意味着wait之后,其他线程可以进入同步块执行。
当某代码并不持有管程的使用权时,去wait或notify,会抛出java.lang.IllegalMonitorStateException。也包括在synchronized块中去调用另一个对象的wait/notify,因为不同对象的管程不同,同样会抛出此异常。
管程三大模型:Hasen 模型、Hoare 模型和 MESA 模型
1)Hasen 模型,要求 notify() 放在代码的最后,这样 T2 通知完 T1 后,T2 就结束了,然后 T1再执行,这样就能保证同一时刻只有一个线程执行。
2)Hoare 模型,T2 通知完 T1 后,T2 阻塞,T1 马上执行;等 T1执行完,再唤醒 T2,也能保证同一时刻只有一个线程执行。但是相比 Hasen 模型,T2 多了一次阻塞唤醒操作。
3)MESA 管程,T2 通知完 T1 后,T2 还是会接着执行,T1 并不立即执行,仅仅是从条件变量的等待队列进到入口等待队列里面。这样做的好处是 notify()不用放到代码的最后,T2 也没有多余的阻塞唤醒操作。但是也有个副作用,就是当 T1 再次执行的时候,可能曾经满足的条件,现在已经不满足了,所以需要以循环方式检验条件变量。
Hasen 是执行完,再去唤醒另外一个线程。
Hoare,是中断当前线程,唤醒另外一个线程,执行完再去唤醒。
Mesa是进入等待队列(不一定有机会能够执行)。
MESA 管程特有的编程范式:
while(条件不满足) {
wait();
}
Java 参考了 MESA 模型,语言内置的管程(synchronized)对 MESA 模型进行了精简。MESA 模型中,条件变量可以有多个,Java 语言内置的管程里只有一个条件变量,而 Java SDK 并发包实现的管程支持多个条件变量,不过并发包里的锁,需要开发人员自己进行加锁和解锁操作。
二.并发编程的核心问题
分工指的是如何高效地拆解任务并分配给线程。类似于“烧水泡茶”问题。
同步指的是线程之间如何协作。当某个条件不满足时,线程需要等待,当某个条件满足时,线程需要被唤醒执行。
互斥则是保证同一时刻只允许一个线程访问共享资源。也就是所谓的“线程安全”。核心技术为“锁”。
三.并发的隐患
【补充】
数据竞争: 当多个线程同时访问同一数据,并且至少有一个线程会写这个数据的时候,如果我们不采取防护措施,那么就会导致并发 Bug。
竞态条件,指的是程序的执行结果依赖线程执行的顺序。当你看到代码里出现 if 语句的时候,就应该立刻意识到可能存在竞态条件,因为可能同时在一个状态的时候,有多个线程通过了这个条件,然而他们之后的操作都可能使得这个条件不成。解决办法就是让这些方法之间互斥,例如可以采用单例模式,然后让所有方法以单例为加锁对象同步。
1.缓存导致的可见性问题
可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到。
volatiole变量的写先于读发生,保证了它的可见性。
多核时代,每颗 CPU 都有自己的缓存,当多个线程在不同的 CPU 上执行时,这些线程操作的位置是不同的 CPU缓存,他们之间不具有可见性。
2.线程切换带来的原子性问题
原子性:一个或者多个操作在 CPU 执行的过程中不被中断的特性。
“原子性”的本质其实不是不可分割,不可分割只是外在表现,其本质是多个资源间有一致性的要求,操作的中间状态对外不可见。
举例
例1:同时向一个变量发起两次修改请求,可能会导致变量修改失败。
补充
若要对一个变量进行操作
至少需要三条 CPU 指令:指令 1:把变量从内存加载到 CPU 的寄存器;
指令 2:在寄存器中修改变量;
指令 3:将结果写入内存/缓存。
在线程A将结果写入内存之前,线程B可能已经读入了初始的变量值。 然后线程A将修改结果写入内存后,线程B也将结果写入内存。这会导致线程A的修改被完全覆盖,因为线程B的初始值读入的是线程A修改之前的变量值。
例2:在32位的系统上,读写long(64位数据)
使用双线程同时对long型数据进行写入或读取。
如果新建多个线程同时改变long型数据的值,最后的值可能是乱码。因为是并行读入的,所以可能读的时候错位了。
3.编译带来的有序性
有序性:程序按照代码的先后顺序执行。 编译器为了优化性能,有时候会改变程序中语句的先后顺序,例如程序中:“a=6;b=7;”编译器优化后可能变成“b=7;a=6”,有时候会导致意想不到的bug。
(指令重排对于CPU处理性能是十分必要的)
举例
在 Java 领域一个经典的案例就是利用双重检查创建单例对象,例如下面的代码:在获取实例 getInstance() 的方法中,我们首先判断 instance 是否为空,如果为空,则锁定 Singleton.class 并再次检查 instance 是否为空,如果还为空则创建 Singleton 的一个实例。
public class Singleton {
static Singleton instance;
static Singleton getInstance(){
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
假设有两个线程 A、B 同时调用 getInstance() 方法,他们会同时发现 instance == null ,于是同时对Singleton.class 加锁,此时 JVM 保证只有一个线程能够加锁成功(假设是线程 A),另外一个线程则会处于等待状态(假设是线程B); 线程 A 会创建一个 Singleton 实例,之后释放锁,锁释放后,线程 B 被唤醒,线程 B再次尝试加锁,此时是可以加锁成功的,加锁成功后,线程 B 检查 instance == null 时会发现,已经创建过 Singleton实例了,所以线程 B 不会再创建一个 Singleton 实例。
这看上去一切都很完美,无懈可击,但实际上这个 getInstance() 方法并不完美。问题出在哪里呢?出在 new 操作上,我们以为的new 操作应该是:
分配一块内存 M;
在内存 M 上初始化 Singleton 对象;
然后 M 的地址赋值给 instance 变量。
但是实际上优化后的执行路径却是这样的:
分配一块内存 M;
将 M 的地址赋值给 instance 变量;
最后在内存 M 上初始化 Singleton 对象。
优化后会导致什么问题呢?
我们假设线程 A 先执行 getInstance() 方法,当执行完指令 2 时恰好发生了线程切换,切换到了线程B 上; 如果此时线程 B 也执行 getInstance() 方法,那么线程 B 在执行第一个判断时会发现 instance != null ,所以直接返回 instance。 而此时的 instance 是没有初始化过的,如果我们这个时候访问 instance 的成员变量就可能触发空指针异常。
解决方案1:将Instance声明为volatitle,前面的重排序在多线程环境中将会被禁止
public class Singleton {
private static volatile Singleton sInstance;
public static Singleton getInstance() {
if (sInstance == null) {
synchronized (Singleton.class) {
if (sInstance == null) {
sInstance = new Singleton();
}
}
}
return sInstance;
}
private Singleton() {}
}
解决方案2:静态内部类
public class Singleton {
private Singleton(){};
private static class Inner{
private static Singleton SINGLETION=new Singleton();
}
public static Singleton getInstance(){
return Inner.SINGLETION;
}
}
静态内部类不会随着外部类的初始化而初始化,他是要单独去加载和初始化的,当第一次执行getInstance方法时,Inner类会被初始化。
静态对象SINGLETION的初始化在Inner类初始化阶段进行,类初始化阶段即虚拟机执行类构造器()方法的过程。
虚拟机会保证一个类的()方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的()方法,其它线程都会阻塞等待。
解决方法3:原子类 AtomicInteger
详情请见文末java并发包中的原子类
四.解决原子性,可见性和有序性
我们已经知道,导致可见性的原因是缓存,导致有序性的原因是编译优化,那解决可见性、有序性最直接的办法就是禁用缓存和编译优化,但是这样问题虽然解决了,我们程序的性能可就堪忧了。
合理的方案应该是按需禁用缓存以及编译优化。那么,如何做到“按需禁用”呢?对于并发程序,何时禁用缓存以及编译优化只有程序员知道,那所谓“按需禁用”其实就是指按照程序员的要求来禁用。所以,为了解决可见性和有序性问题,只需要提供给程序员按需禁用缓存和编译优化的方法即可。
Java 内存模型是个很复杂的规范,本质上可以理解为,Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法。具体来说,这些方法包括 volatile、synchronized 和 final 三个关键字,以及六项 Happens-Before 规则。
volatile
volatile 关键字并不是 Java 语言的特产,古老的 C 语言里也有,它最原始的意义就是禁用 CPU 缓存。当你使用了这个变量,就等于告诉了虚拟机,这个变量极有可能会被某些程序或线程修改。为了确保这个变量被修改后,应用程序范围内的所有线程都能看到这个改动,虚拟机必须得进行一些特殊的手段。
volatile是轻量级的synchronized,如果使用恰当,它比synchronized的使用和执行成本更低,因为他不会引起上下文切换和调度。
多线程的内存模型:main memory(主存)、working memory(线程栈),在处理数据时,线程会把值从主存load到本地栈,完成操作后再save回去(volatile关键词的作用:每次针对该变量的操作都激发一次load and save)。
针对多线程使用的变量如果不是volatile或者final修饰的,很有可能产生不可预知的结果(另一个线程修改了这个值,但是之后在某线程看到的是修改之前的值)。其实道理上讲同一实例的同一属性本身只有一个副本。但是多线程是会缓存值的,本质上,volatile就是不去缓存,直接取值。在线程安全的情况下加volatile会牺牲性能。但相比较synchronized和锁,性能更佳。与普通变量相比,就是写入的操作慢一点,因为会加入许多内存屏障指令。
内存屏障(memory barrier)是一个CPU指令。基本上,它是这样一条指令:
a) 确保一些特定操作执行的顺序;
b) 影响一些数据的可见性(可能是某些指令执行后的结果)。编译器和CPU可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。内存屏障另一个作用是强制更新一次不同CPU的缓存。例如,一个写屏障会把这个屏障前写入的数据刷新到缓存,这样任何试图读取该数据的线程将得到最新值,而不用考虑到底是被哪个cpu核心或者哪颗CPU执行的。
内存屏障(memory barrier)和volatile什么关系?上面的虚拟机指令里面有提到,如果你的字段是volatile,Java内存模型将在写操作后插入一个写屏障指令,在读操作前插入一个读屏障指令。这意味着如果你对一个volatile字段进行写操作,你必须知道:1、一旦你完成写入,任何访问这个字段的线程将会得到最新的值。2、在你写入前,会保证所有之前发生的事已经发生,并且任何更新过的数据值也是可见的,因为内存屏障会把之前的写入值都刷新到缓存。
1.原 子 性
volatile并不能直接替代锁的作用,他只能保证可见性和有序性,但volatile保证不了原子性操作!!!!!
修改一个变量值的JVM指令:
mov 0xc(%r10),%r8d ; Load
inc %r8d ; Increment
mov %r8d,0xc(%r10) ; Store
lock addl $0x0,(%rsp) ; StoreLoad Barrier
从Load到store到内存屏障,一共4步,其中最后一步jvm让这个最新的变量的值在所有线程可见,也就是最后一步让所有的CPU内核都获得了最新的值,但中间的几步(从Load到Store)是不安全的,中间如果其他的CPU修改了值将会丢失。
最简单的一个例子是调用多次一个线程进行i++操作:
一个变量i被volatile修饰,两个线程想对这个变量修改,都对其进行自增操作也就是i++,i++的过程可以分为三步,首先获取i的值,其次对i的值进行加1,最后将得到的新值写回到缓存中。
线程A首先得到了i的初始值100,但是还没来得及修改,就阻塞了,这时线程B开始了,它也得到了i的值,由于i的值未被修改,即使是被volatile修饰,主存的变量还没变化,那么线程B得到的值也是100,之后对其进行加1操作,得到101后,将新值写入到缓存中,再刷入主存中。根据可见性的原则,这个主存的值可以被其他线程可见。
问题来了,线程A已经读取到了i的值为100,也就是说读取的这个原子操作已经结束了,所以这个可见性来的有点晚,线程A阻塞结束后,继续将100这个值加1,得到101,再将值写到缓存,最后刷入主存,所以即便是volatile具有可见性,也不能保证对它修饰的变量具有原子性。
除此之外,还有一个很重要的点。在JAVA中,Integer 属于不变对象,也就是说,如果你要修改一个值为1的Integer对象,实际上是新建一个值为2的Interger对象,i=Integer.valueOf(i.intValue()+1),
2.可见性 & 顺序性
为了解决volatile所带来的可能的可见性问题,jdk1.5以后添加了Happens-Before 规则,它规定了哪些指令不能重排。
Happens-Before
1)程序顺序原则:一个线程内保证语义的串行性。
2)volatile规则:volatile变量的写先于读发生,这保证了它的可见性 。
3)传递性:A先于B,B先于C,那么A必然先于C。
4)管程中锁规则:解锁必须在加锁前。
5)线程的start()方法先于它的每一个动作。
6)线程的所有操作先于线程的终结(Thread.join())。
7)线程的中断先于被中断线程的代码。
8)对象的构造函数的执行、结束先于finalize()方法。
1)程序顺序原则
符合单线程里面的思维:程序前面对某个变量的修改一定是对后续操作可见的
2 & 3)volatile 变量规则+传递性
举例:
如果线程 B 读到了“v=true”,那么线程 A 设置的“x=42”对线程 B 是可见的。也就是说,线程 B 能读到 x = 42 。
4)管程中锁的规则
管程:是一种通用的同步原语,在 Java 中指的就是 synchronized,synchronized 是 Java 里对管程的实现。synchronized 关键字及 wait()、notify()、notifyAll() 这三个方法都是管程的组成部分。
举例
在多线程环境下,synchronized块中的方法获取了lock实例的monitor,如果实例相同,那么只有一个线程能执行该块内容
public class Thread1 implements Runnable {
Object lock;
public void run() {
synchronized(lock){// 此处自动加锁
..do something
}
}// 此处自动解锁
}
也可以直接用于方法: 相当于上面代码中用lock来锁定的效果,实际获取的是Thread1类的monitor。更进一步,如果修饰的是static方法,则锁定该类所有实例。
public class Thread1 implements Runnable {
public synchronized void run() {
..do something
}
}
5)线程 start() 规则
如果线程 A 调用线程 B 的 start() 方法(即在线程 A 中启动线程 B),那么该 start() 操作 Happens-Before 于线程 B 中的任意操作。
6)线程 join() 规则
如果在线程 A 中,调用线程 B 的 join() 并成功返回,那么线程 B 中的任意操作 Happens-Before 于该 join() 操作的返回
synchronized
关键字synchronized的作用是实现线程之间的同步,他的工作是对同步的代码枷锁,使得每一次,只能有一个线程进入同步块,从而保证线程之间的安全性。
Java 编译器会在 synchronized 修饰的方法或代码块前后自动加上加锁 lock() 和解锁 unlock()
三种使用方法:
1.指定加锁对象
2.直接作用于实例方法,锁定的是当前实例对象 this。
3.直接作用于静态方法,锁定的是当前类的 Class 对象
class X {
// 修饰非静态方法
synchronized void foo() {
// 临界区
}
// 修饰静态方法
synchronized static void bar() {
// 临界区
}
// 修饰代码块
Object obj = new Object();
void baz() {
synchronized(obj) {
// 临界区
}
}
}
实例方法要求thread指向的接口是同一个,
而静态方法则不需要。
五.多线程
【拓】
1.为什么要用多线程?
提升 CPU 和 I/O 设备的利用率。
2.线程数量最佳为多少?
对于 CPU 密集型的计算场景,理论上“线程的数量 =CPU 核数”就是最合适的。不过在工程上,线程的数量一般会设置为“CPU 核数
+1”,这样的话,当线程因为偶尔的内存页失效或其他原因导致阻塞时,这个额外的线程可以顶上,从而保证 CPU 的利用率。
对于 I/O 密集型的计算场景,最佳线程数 =CPU 核数 * [ 1 +(I/O 耗时 / CPU 耗时)]
3.局部变量需要多线程吗?
不需要。局部变量存放在线程各自的的调用栈里,而每个线程都有自己独立的调用栈,不会共享,所以自然也就没有并发问题。
1.线程的状态
打开JAVA的Thread类里的State枚举类,可以看到
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
1)Runnable to Blocked
只有一种场景会触发这种转换,就是线程等待 synchronized 的隐式锁。synchronized 修饰的方法、代码块同一时刻只允许一个线程执行,其他线程只能等待,这种情况下,等待的线程就会从 RUNNABLE 转换到 BLOCKED 状态。而当等待的线程获得 synchronized 隐式锁时,就又会从 BLOCKED 转换到 RUNNABLE 状态。
2)Runnable to Waiting
1.获得 synchronized 隐式锁的线程,调用无参数的 Object.wait() 方法。
2.调用无参数的 Thread.join() 方法。其中的 join() 是一种线程同步方法,例如有一个线程对象 thread A,当调用 A.join() 的时候,执行这条语句的线程会等待 thread A 执行完,而等待中的这个线程,其状态会从 RUNNABLE 转换到 WAITING。当线程 thread A 执行完,原来等待它的线程又会从 WAITING 状态转换到 RUNNABLE。
3.调用 LockSupport.park() 方法。其中的 LockSupport 对象,Java 并发包中的锁,都是基于它实现的。调用 LockSupport.park() 方法,当前线程会阻塞,线程的状态会从 RUNNABLE 转换到 WAITING。调用 LockSupport.unpark(Thread thread) 可唤醒目标线程,目标线程的状态又会从 WAITING 状态转换到 RUNNABLE。
3)Runnable to TIMED_WAITING
WAITING和TIMED_WAITING都是等待状态,TIMED_WAITING 和 WAITING 状态的区别,仅仅是触发条件多了超时参数。.
4)New to Runnable
重写run或者实现Runnable接口或者继承Thread
5)Runnable to Terminated
线程执行完 run() 方法后,会自动转换到 TERMINATED 状态,当然如果执行 run() 方法的时候异常抛出,也会导致线程终止。有时候我们需要强制中断 run() 方法的执行,可以调用 interrupt() 方法。
2.线程的基本操作(JAVA)
1)新建一个线程
Thread t1 = new Thread(){
@Override
public void run() {
..do something
}
};
t1.start();
一般的类要实现线程,可以继承Thread类,当然也可以使用Runnable接口。最常使用的还是用正则表达式重写run函数。
构造方法:public Thread(Runnable targert)
2)终止线程
不建议用已经被废弃的stop() ,因为它会自动释放被终止对象的锁。
推荐使用的是用一个volatile修饰的布尔型变量来决定是否退出。
volatile boolean stopme =false;
public void stopMe(){
stopme = true;
}
...
while(true){
if(stopme){
break;
}
. . do something
}
3)线程中断
三个方法
1)public void Thread.interrupt()
通知目标线程中断,也就是设置中断标志位。
2)public boolean Thread.isInterrupted()
判断当前线程是否被中断,也就是检查中断标志位
3)public static boolean Thread.interrupted()
判断是否被中断,并清除当前中断标志位
Thread.sleep()方法会让当前线程休眠若干时间,它会抛出InterruptedException中断异常。此时,它会清除中断标记。所以我们应该在异常处理中再次将其中断。
try{
Thread.sleep(2000);
} catch (InterruptedException e) {
System.ot,println("xxx");
Thread.currentThread().interrupt();
}
4)等待和通知
wait()和notify()是Object类的方法。
在一个实例对象上,在一个synchronzied语句中,调用wait()方法后,当前线程就会在这个对象上等待,并释放锁。直到有其他线程执行了notify()/notifyAll()。使用notify()/notifyAll()后,会唤醒处于等待状态的线程,是他们的状态变为Runnable。
wait()会释放锁而sleep()不会。
挂起(suspend)和继续执行(resume)已被废弃。
5)等待线程结束和谦让
join()是等待方法,该方法将一直等待到它调用的线程终止。
Join方法实现是通过wait()在当前线程对象实例上。 当main线程调用t.join()时候,main线程会获得线程对象t的锁(wait 意味着拿到该对象的锁),调用该对象的wait(等待时间),直到该对象唤醒main线程 ,比如退出后。这就意味着main线程调用t.join时,必须能够拿到线程t对象的锁。
用途:在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。(个人理解就是wait() + notify())
不要在应用程序中,在Thread对象实例上使用类似wait()方法或者notify()方法等,可能会和API互相影响。
yield()是谦让方法,它会让当前线程让出CPU,但线程还是会进行CPU资源的争夺。如果你觉得一个线程不是非常重要,又害怕占用过多资源,可以使用它来给其他线程更多的工作机会。
6)线程组
如果线程数量很多,而且功能分配明确,可以将相同功能的线程放置在同一个线程组里。
activeCount()返回活动线程总数(不精确的)
list()返回线程组中所有线程的信息。
public class Test implements Runnable{
public static void main(String[] args) {
ThreadGroup tg = new ThreadGroup("GroupName");
Thread t1 = new Thread(tg,new Test(),"ThreadName1");
Thread t2 = new Thread(tg,new Test(),"ThreadName2");
t1.start();
t2.start();
System.out.println(tg.activeCount());
tg.list();
}
@Override
public void run() {
String groupAndName = Thread.currentThread().getThreadGroup().getName()+" - "+Thread.currentThread().getName();
while(true){
System.out.println("I am "+groupAndName);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
7.守护线程
守护线程是系统的守护者,他会在后台完成一些系统性的服务。如果应用内只有守护线程,那么JAVA虚拟机就会退出。
t.setDaemon(true);
t.start();
设置守护线程必须得在start之前,不然该线程会被当作用户线程,永远无法停止。
8.线程优先级
内置三个优先级,数字越大优先级越高。【1,10】
高优先级的线程 倾向于 更快地完成。
至于问什么是倾向而不是一定,是因为JAVA线程的实现是采用1:1线程模型,也就是每个轻量级进程都有一个操作系统内核的支持,但问题是 不是所有的操作系统都有10个优先级,比如说windows中只有七个,那么当然就无法满足严格的优先级顺序。
除此之外,windows还有优先级推进器,就是一个线程被切换频繁时,系统可能会越过优先级去为它分配执行时间,从而减少线程频繁切换带来的损耗。总而言之,不能太过依赖于优先级。
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;