* 运行速度 cpu>内存>磁盘
* 当数据量小时 串行化中cpu等待的时间(因为要和内存交互)小于多线程中cpu上下文+创建线程的时间
* 当数据量大时 串行化中cpu等待的时间(因为要和内存交互)大于多线程中cpu上下文+创建线程的时间
*
* 当数据量相同时,线程也不是越多越快(因为有线程等待的时间会变长)
*
* 总结:1,多线程不一定比单线程快
* 2.当任务不变时候,线程也不是越多越快
* Thread.yield(); 向调度程序提示当前线程愿意放弃其当前对处理器的使用。调度程序可以随意忽略此提示。
影响线程安全的三方面因素:可见性,有序性,原子性
可见性
反例
public static Boolean stop = false;//任务是否停止,普通变量
public static void main(String[] args) throws Exception {
Thread thread1 = new Thread(() -> {
while (!stop) { //stop=false,不满足停止条件,继续执行
//do someting
}
System.out.println("stop=true,满足停止条件。" +
"停止时间:" + System.currentTimeMillis());
});
thread1.start();
Thread.sleep(100);//保证主线程修改stop=true,在子线程启动后执行。
stop = true; //true
System.out.println("主线程设置停止标识 stop=true。" +
"设置时间:" + System.currentTimeMillis());
jmm 模型
所有的共享变量都存储于主内存。这里所说的变量指的是实例变量和类变量,不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。
只有当工作内存的变量失效后才会从主内存同步
什么时候失效呢
- 线程释放锁时候
- 线程切换时
- cpu空闲时
内存可见性问题
内存可见性是指当一个线程修改了某个变量的值,其它线程总是能知道这个变量变化。也就是说,如果线程 A 修改了共享变量 V 的值,那么线程 B 在使用 V 的值时,能立即读到 V 的最新值。
解决: 使用 volatile 关键字 synchronized lock加锁 CAS操作(原子操作类)。
使用 volatile 修饰共享变量后,每个线程要操作变量时会从主内存中将变量拷贝到本地内存作为副本,当线程操作变量副本并写回主内存后,会通过 CPU 总线嗅探机制告知其他线程该变量副本已经失效,需要重新从主内存中读取。
嗅探机制工作原理:在多核处理器架构上,所有的处理器是共用一条总线的,都是靠此总线来和主内存进行数据交互,每个处理器会通过嗅探器来监控总线上的数据来检查自己缓存内的数据是否过期,如果发现自己缓存行对应的地址被修改了,就会将此缓存行置为无效。当处理器对此数据进行操作时,就会重新从主内存中读取数据到缓存行
volatile 注意
-
volatile只能保证基本类型变量的内存可见性,对于引用类型,无法保证引用所指向的实际对象内部数据的内存可见性。关于引用变量类型详见:Java的数据类型。
-
volilate只能保证共享对象的可见性,不能保证原子性:假设两个线程同时在做x++,在线程A修改共享变量从0到1的同时,线程B已经正在使用值为0的变量,所以这时候可见性已经无法发挥作用,线程B将其修改为1,所以最后结果是1而不是2。
synchronized 锁四种状态(锁升级) 无锁 偏向锁 (记录线程id) 轻量级锁(cas) 重量级锁
有序性
反例
public class MemoryReorderingExample {
private static int x=0,y=0;
private static int a=0,b=0;
public static void main(String[] args) throws InterruptedException {
int i=0;
while(true){
i++;
x=0;y=0;
a=0;b=0;
Thread t1=new Thread(()->{
a=1;
x=b;
});
Thread t2=new Thread(()->{
b=1;
y=a;
});
t1.start();
t2.start();
t1.join();
t2.join();
String result="第"+i+"次("+x+","+y+")";
if(x==0&&y==0){
System.out.println(result);
break;
}
}
}
}
解决: **使用 volatile 关键字 synchronized lock加锁 **。
禁止指令重排序是实现有序性的一种方式,截止JDK1.8, Java 里只有 volatile 变量是能实现禁止指令重排的。
synchronized 和 volatile 有序性不同也是因为其实现原理不同:
synchronized 靠操作系统内核互斥锁实现的,相当于 JMM 中的 lock 和 unlock。退出代码块时一定会刷新变量回主内存
volatile 靠插入内存屏障指令防止其后面的指令跑到它前面去了
使用 volatile 修饰变量时,Java 编译器在生成字节码时,会在指令序列中插入内存屏障指令来禁止特定类型的处理器重排序内存屏障是一组处理器指令。
- 在每个 volatile 读操作的后面插入 LoadLoad 屏障和 LoadStore 屏障。
- 在每个 volatile 写操作的前后分别插入一个 StoreStore 屏障和一个 StoreLoad 屏障。
也就是说,编译器不会对 volatile 读与 volatile 读后面的任意内存操作重排序;编译器不会对 volatile 写与 volatile 写前面的任意内存操作重排序。
原子性原子(atomic)本意是“不能被进一步分割的最小粒子”,而原子操作(atomic operation)意为“不可被中断的一个或一系列操作”。
解决:synchronized lock加锁 CAS操作
大部分情况下基本类型的赋值操作是原子性的 比如32位机器上double赋值会分两次
volatile 不能保证原子性
volatile 属性的读写操作都是无锁的,它不能替代 synchronized,因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁上,所以说它是低成本的。volatile 只能作用于属性,我们用 volatile 修饰属性,这样编译器就不会对这个属性做指令重排序。
volatile 使用场景: 能够保证的场景非常少
- 对变量的写操作不依赖于当前值。
- 该变量没有包含在具有其他变量的不变式中。
CAS 实现原子操作的三大问题
ABA 问题 单纯的赋值操作是原子性的。 在变量前面追加上版本号,每次变量更新的时候把版本号加 1,
循环时间长开销大 如果 JVM 能支持处理器提供的 pause 指令,那么效率会有一定的提升
只能保证一个共享变量的原子操作 是把多个共享变量合并 成一个共享变量来操作。
对任意单个使用 volatile 修饰的变量的读 / 写是具有原子性,但类似于 flag = !flag 这种复合操作不具有原子性。简单地说就是,单纯的赋值操作是原子性的。
单例模式为什么要加volatile
双重检查锁是从方法加synchronized演化而来的 当演化到
private static Singleton instance;
public static Singleton getInstance() {
if (instance==null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();//3
}
}
}
return instance;
}
这一步时,为什么要加volatile呢
首先创建一个对象分为三个步骤:
1、分配内存空间
2、初始化对象
3、讲内存空间的地址赋值给对象的引用
但是上面我讲了,jvm可能会对代码进行重排序,所以2和3可能会颠倒,
就会变成 1 —> 3 —> 2的过程,
那么当第一个线程A抢到锁执行初始化对象时,发生了代码重排序,3和2颠倒了,这个时候对象对象还没初始化,但是对象的引用已经不为空了,
所以当第二个线程B遇到第一个if判断时不为空,这个时候就会直接返回对象,但此时A线程还没执行完步骤2(初始化对象)。就会造成线程B其实是拿到一个空的对象。造成空指针问题。
volatile 变量的读/写和 CAS 可以实现线程之间的通信。把这些特性整合在一起,就 形成了整个 concurrent 包得以实现的基石