注:文章参考了各类大神和机构的内容。已标出。
引子:
在一个list中有过亿条的Integer类型的值,如何更快的计算这些值的总和?
要学习的东西那么多!扶我起来。。
慢慢来深入,慢慢来领略大牛的思想融汇。
注:本文汇集各方材料而成,部分不知出处
一.线程
想使用多线程?线程之间如何交互呢?有哪些原则?注意点?顺序?
Visibility:通过并发线程修改变量值, 必须将线程变量同步回主存后, 其他线程才能访问到。
Ordering:通过java提供的同步机制或volatile关键字, 来保证内存的访问顺序。(操作系统有自己的顺序)
Cache coherency :它是一种管理多处理器系统的高速缓存区结构,其可以保证数据在高速缓存区到内存的传输中不会丢失或重复。---cpu缓存机制的好与坏
Happens-beforeordering(规则):synchronized,volatile,final,java.util.concurrent.lock|atomic(应对方法)
1.可见性
Java Memory Model (JAVA 内存模型)描述线程之间如何通过内存(memory)来进行交互。 具体说来, JVM中存在一个主存区(Main Memory或Java Heap Memory),对于所有线程进行共享,而每个线程又有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作并非发生在主存区,而是发生在工作内存中,而线程之间是不能直接相互访问,变量在程序中的传递,是依赖主存来完成的。
从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:
1、线程A把本地内存A中更新过的共享变量刷新到主内存中去。
2、线程B到主内存中去读取线程A之前已更新过的共享变量。
下面通过示意图来说明这两个步骤:
何为可见性:
从上图可知,如果线程A对共享变量X进行了修改,但是线程A没有及时把更新后的值刷入到主内存中,而此时线程B从主内存读取共享变量X的值,所以X的值是原始值,那么我们就说对于线程B来讲,共享变量X的更改对线程B是不可见的
package com.ming.test;
public class TestVisible {
private static boolean ready;
private static int number;
private static int count;
private static class ReaderThread extends Thread {
public void run() {
while(!ready) {
System.out.println(System.currentTimeMillis()+"计数"+count++);
Thread.yield();
}
System.out.println(number);
}
}
public static void main(String[] args) {
for(int i=0;i < 100;i++)
new ReaderThread().start();
number = 11;
ready = true;
}
}
。。。
1469690434615计数218
11
11
1469690434614计数214
1469690434614计数211
11
1469690434614计数212
1469690434614计数210
11
1469690434614计数209
11
1469690434614计数208
1469690434614计数207
11
。。。
结果不止100次
分析:--CPU内部缓存,指令优化执行顺序
理论上如果某个线程打印出了11,说明main方法已经执行完毕,即变量ready的值已经设置为true了,那么这以后其它的线程打印的结果应该都是11了,但这里的结果是有些线程读取的ready值仍然为false,这就说明了java虚拟机会对线程中使用到的变量进行缓存,所以就出问题了。
java虚拟机缓存变量,是出于性能的考虑,并且在单线程程序中,或者不存在共享变量的多线程程序中,这都不会出现问题。但是,在有共享变量的多线程程序中,就会发生问题,这里就涉及到共享对象的可见性了,也就是在没有使用同步机制的情况下,一个线程对某个共享对象的修改,并不会立即被其它的线程读取到。上面的代码之所以会出问题,就是因为ReaderThread线程,没有读取到main线程对ready变量修改后的值。要解决上述问题,可以通过在main方法和ReaderThread线程中的run方法中,给访问number和ready值的代码块中加锁来解决。
注意:网上大神说可能打印出0,另外一种结果打印出0来,这个暂时还不是很明白,书中的解释是java虚拟机的内存模型允许编译器对操作顺序进行重排序,并将数值缓存在寄存器中,这样可能在读取到ready修改后的值后,却仍然读取了number的旧值,从而打印出了int的默认值0来。
现在模拟不出来,是不是JDK版本升级了?本人测试7.0
为了保证值一致:---volatile注: 如果一个基本变量被volatile修饰,编译器将不会把它保存到寄存器中,而是每一次都去访问内存中实际保存该变量的位置上。这一点就避免了没有volatile修饰的变量在多线程的读写中所产生的由于编译器优化所导致的灾难性问题。所以多线程中必须要共享的基本变量一定要加上volatile修饰符。当然了,volatile还能让你在编译时期捕捉到非线程安全的代码。
package com.ming.test;
public class TestVisible {
private volatile static boolean ready;
private volatile static int number;
private static int count;
private static class ReaderThread extends Thread {
public void run() {
while(!ready) {
System.out.println(System.currentTimeMillis()+"计数"+count++);
Thread.yield();
}
System.out.println(number);
}
}
public static void main(String[] args) {
for(int i=0;i < 10;i++)
new ReaderThread().start();
number = 11;
ready = true;
}
}
1469690848951计数6
11
1469690848950计数4
11
11
11
11
11
1469690848951计数15
11
11
。。。。
注:在调试过程中遇到的问题
package com.ming.test;
public class TestVisible {
private volatile static boolean ready;
private volatile static int number;
private volatile static int count;
private static class ReaderThread extends Thread {
public void run() {
synchronized(this){
while(!ready) {
count = count+1;
System.out.println(Thread.currentThread().getName()+"计数"+count);
Thread.yield();
Thread.currentThread().stop();
}
System.out.println(number);
}
}
}
public static void main(String[] args) {
for(int i=0;i < 10;i++)
new ReaderThread().start();
number = 11;
ready = true;
}
}
Thread-3计数4
Thread-5计数6
Thread-1计数2
Thread-2计数3
Thread-4计数5
Thread-6计数7
Thread-8计数8
Thread-7计数8
11
count值相同的原因??
2.原子性
定义:
原子是世界上的最小单位,原子性由此而来,具有不可分割性。比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作时原子操作。再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。java的concurrent包下提供了一些原子类,我们可以通过阅读API来了解这些原子类的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。
3.锁与同步
锁:内部锁、互斥锁、分离锁、闭锁、顺序锁、读写锁、独占锁、分拆锁、重入锁
先看看synchronized
一、synchronized的实现方案
synchronized比较简单,语义也比较明确,尽管Lock推出后性能有较大提升,但是基于其使用简单,语义清晰明了,使用还是比较广泛的,其应用层的含义是把任意一个非NULL的对象当作锁。当synchronized作用于方法时,锁住的是对象的实例(this),当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带,因此静态方法锁相当于类的一个全局锁,当synchronized作用于一个对象实例时,锁住的是对应的代码块。在Sun的HotSpot JVM实现中,其实synchronized锁还有一个名字:对象监视器。
当多个线程一起访问某个对象监视器的时候,对象监视器会将这些请求存储在不同的容器中。(获得锁的流程)
1、 Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中
2、 Entry List:Contention List中那些有资格成为候选资源的线程被移动到Entry List中