学习资料:《深入理解计算机系统》,《Java高并发程序设计》,《Java并发编程实战》,《Java并发编程的艺术》,《Java核心技术卷1》多线程一章,极客时间王宝令的Java并发编程实战课程…
以下大部分阐述来自上述书籍与课程中个人认为很重要的部分,也有部分心得体会。
进程:操作系统对一个正在运行的而程序的一种抽象。在一个系统上可以同时运行多个进程,而每个进程都好像在独占地使用硬件。(进程是线程的容器)而并发运行,则是说一个进程的指令和另一个进程的指令是交错执行的。这种机制叫做上下文切换。进程可以通过多核处理器来并行!
举例:当你双击一个exe程序时,这个.exe文件的指令就会被加载,那么你就能得到一个关于这个程序的进程。进程是活的,是正在被执行的,你可以通过任务管理器看到你电脑正在执行的进程。
线程:一个进程由将多个称为线程的执行单元组成,每个线程都运行在进程的上下文中,并共享同样的代码和全局数据。因为多线程之间比多进程之间更容易共享数据,也因为线程一般来说都比进程更高效。线程可以实现宏观上的“同时”执行,实际上是快速切换线程来达到几乎同时执行的效果。也可以称线程为轻量级进程,它是程序执行的最小单位。
多进程与多线程的本质区别:每个进程都拥有自己的一整套变量,而线程则共享数据。多线程比多进程开销小得多。
同步 Synchronous方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。
异步 Asynchronous方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作。
区别:同步就是要等到整个流程全部结束,而异步只是传递一个接下来要去做什么什么事情的消息,然后就会去干其他事。
并行 Parallel:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。
并行二定律
1.Amdahl定律 加速比=1/[F+(1-F)/n](n为处理器储量,F为并行比例) 由此可见,为了提高系统的速度,仅仅增加CPU处理器数量不一定能起到有效的作用。需要根本上修改程序的串行行为,提高系统内并行化的模块比重。
2.Gustafson定律 加速比=n-F(n-1) 如果串行化比例很小,并行化比例很大,那么加速比就是处理起个数,只要不断累加处理起,就能获得更快的速度
并发 ConCurrent:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,我们会用TPS或者QPS来反应这个系统的处理能力。
区别:并行就是同时进行,并发则是一个做一点,然后另一个再做一点。
死锁:所有线程互相占用了对方的锁,导致所有线程挂起。
饥饿:某些线程因为某些原因(优先级过低)无法获得所需的资源,导致无法运行。
活锁:两个线程互相释放资源给对方,从而导致没有一个线程可以同时拿到所有资源正常执行。(出电梯时,和一个进电梯的人互相谦让,导致进电梯的人进不了,出电梯的人出不去)
分工指的是如何高效地拆解任务并分配给线程。类似于“烧水泡茶”问题。
同步指的是线程之间如何协作。当某个条件不满足时,线程需要等待,当某个条件满足时,线程需要被唤醒执行。
互斥则是保证同一时刻只允许一个线程访问共享资源。也就是所谓的“线程安全”。核心技术为“锁”。
可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到。
volatiole变量的写先于读发生,保证了它的可见性。
多核时代,每颗 CPU 都有自己的缓存,当多个线程在不同的 CPU 上执行时,这些线程操作的位置是不同的 CPU缓存,他们之间不具有可见性。
原子性:一个或者多个操作在 CPU 执行的过程中不被中断的特性。
举例
例1:同时向一个变量发起两次修改请求,可能会导致变量修改失败。
补充 若要对一个变量进行操作,至少需要三条 CPU 指令:
指令 1:把变量从内存加载到 CPU 的寄存器;
指令 2:在寄存器中修改变量;
指令 3:将结果写入内存/缓存。
在线程A将结果写入内存之前,线程B可能已经读入了初始的变量值。 然后线程A将修改结果写入内存后,线程B也将结果写入内存。这会导致线程A的修改被完全覆盖,因为线程B的初始值读入的是线程A修改之前的变量值。
例2:在32位的系统上,读写long(64位数据)
使用双线程同时对long型数据进行写入或读取。
如果新建多个线程同时改变long型数据的值,最后的值可能是乱码。因为是并行读入的,所以可能读的时候错位了。
有序性:程序按照代码的先后顺序执行。 编译器为了优化性能,有时候会改变程序中语句的先后顺序,例如程序中:“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 的成员变量就可能触发空指针异常。
我们已经知道,导致可见性的原因是缓存,导致有序性的原因是编译优化,那解决可见性、有序性最直接的办法就是禁用缓存和编译优化,但是这样问题虽然解决了,我们程序的性能可就堪忧了。
合理的方案应该是按需禁用缓存以及编译优化。那么,如何做到“按需禁用”呢?对于并发程序,何时禁用缓存以及编译优化只有程序员知道,那所谓“按需禁用”其实就是指按照程序员的要求来禁用。所以,为了解决可见性和有序性问题,只需要提供给程序员按需禁用缓存和编译优化的方法即可。
Java 内存模型是个很复杂的规范,本质上可以理解为,Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法。具体来说,这些方法包括 volatile、synchronized 和 final 三个关键字,以及六项 Happens-Before 规则。
volatile 关键字并不是 Java 语言的特产,古老的 C 语言里也有,它最原始的意义就是禁用 CPU 缓存
多线程的内存模型:main memory(主存)、working memory(线程栈),在处理数据时,线程会把值从主存load到本地栈,完成操作后再save回去(volatile关键词的作用:每次针对该变量的操作都激发一次load and save)。
针对多线程使用的变量如果不是volatile或者final修饰的,很有可能产生不可预知的结果(另一个线程修改了这个值,但是之后在某线程看到的是修改之前的值)。其实道理上讲同一实例的同一属性本身只有一个副本。但是多线程是会缓存值的,本质上,volatile就是不去缓存,直接取值。在线程安全的情况下加volatile会牺牲性能。
为了解决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块中的方法获取了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() 操作的返回
打开JAVA的Thread类里的State枚举类,可以看到
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
线程在Running的过程中可能会遇到阻塞(Blocked)情况
1.调用join()和sleep()方法,sleep()时间结束或被打断,join()中断,IO完成都会回到Runnable状态,等待JVM的调度。
2.调用wait(),使该线程处于等待池,直到notify()/notifyAll(),线程被唤醒被放到锁定池,释放同步锁使线程回到Runnable
3.对Running状态的线程加同步锁(Synchronized)使其进入锁定池,同步锁被释放进入(Runnable)。
WAITING和TIMED_WAITING都是等待状态,区别是通过wait()进入WAITING等待是notify()或者notifyAll(),通过join()进入则等待目标线程结束;TIMED_WAITING是在进行一个有时限的等待。
Thread t1 = new Thread(){
@Override
public void run() {
..do something
}
};
t1.start();
一般的类要实现线程,可以继承Thread类,当然也可以使用Runnable接口。最常使用的还是用正则表达式重写run函数。
构造方法:public Thread(Runnable targert)
不建议用已经被废弃的stop() ,因为它会自动释放被终止对象的锁。
推荐使用的是
volatile boolean stopme =false;
public void stopMe(){
stopme = true;
}
...
while(true){
if(stopme){
break;
}
. . do something
}
三个方法
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();
}
wait()和notify()是Object类的方法。
在一个实例对象上,在一个synchronzied语句中,调用wait()方法后,当前线程就会在这个对象上等待。直到有其他线程执行了notify()/notifyAll()。