以下内容主要来源于元动力
6.5线程安全的讨论: 6.5.1 CPU多核缓存架构视频链接
CPU缓存为了提高程序运行的性能,现代CPU在很多方面会对程序进行优化。CPU的处理速度是很快的,内存的速度次之,硬盘速度最慢。在CPU处理内存数据中,内存运行速度太慢,就会拖累CPU的速度。为了解决这样的问题,CPU设计了多级缓存策略
CPU分为三级缓存: 每个CPU都有L1,L2缓存,但是L3缓存是多核公用的。
CPU查找数据的顺序为: CPU -> L1 -> L2 -> L3 ->内存 ->硬盘
进一步优化,CPU每次读取一个数据,并不是仅仅读取这个数据本身,而是会读取与它相邻的64个字节的数据,称之为【缓存行】,因为CPU认为,我使用了这个变量,很快就会使用与它相邻的数据,这是计算机的局部性原理。
这种多级缓存的结构下,会出现一个线程修改的值对其他线程可能不可见。比如两个CPU读取了一个缓存行,缓存行里有两个变量,一个x一个y。第一颗CPU修改了x的数据,还没有刷回主存,此时第二颗CPU,从主存中读取了未修改的缓存行,而此时第一颗CPU修改的数据刷回主存,这时就出现,第二颗CPU读取的数据和主存不一致的情况。
6.5.2 JMM-java 内存模型:Java虚拟规范中曾经试图定义一种Java内存模型,来屏蔽各种硬件和操作系统的内存访问之间的差异,以实现让Java程序在各种平台上都能达到一致的内存访问效果。在此之前,主流程序语言直接使用物理内存和操作系统的内存模型,会由于不同平台的内存模型的差异,可能导致程序在一套平台上发挥完全正常,而在另一套平台上并发经常发生错误,所以在某种常见的场景下,必须针对平台来进行代码的编写
这里的内存模型和我们的运行时数据是从不同的角度去分析java对内存的使用的。两者表达的含义和目的不同。在java内存模型当中一样会存在可见性和指令重排的问题
6.5.3JMM模型中存在的问题-
指令重排
先看代码:
package com.itheima.example18; public class OutOfOrderExecution { private static int x=0,y=0; private static int a=0,b=0; private static int count=0; private static volatile int NUM=0; public static void main(String[] args) throws InterruptedException { long start=System.currentTimeMillis(); for(;;){ Thread t1=new Thread(new Runnable(){ @Override public void run(){ a=1;//1 x=b;//2 } }); Thread t2=new Thread(new Runnable(){ @Override public void run(){ b=1;//3 y=a;//4 } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("一共执行了:"+(count++)+"次"); if(x==0 && y==0){ long end=System.currentTimeMillis(); System.out.println("耗时:"+(end-start)+"毫秒,("+x+","+y+")"); break; } a=0;b=0;x=0;y=0; } } }在多线程中会出现指令重排,单线程中不会,所谓指令重排就是执行的顺序不同(代码中注释1234的代码)
执行了一百多万次也没有出来。。。。。不等了(太烫了)
解决指令重排的方法是使用内存屏障:
在Java语言中我们可以使用volatile关键字来保证一个变量在一次读写操作时的避免指令重排,【内存屏障】是在我们的读写操作之前加入一条指令,当CPU碰到这条指令后必须等到前边的指令执行完成才能继续执行下一条指令。
-
可见性
先看代码:
package com.itheima.example18; public class Test { private static boolean isover=false; private static int number=0; public static void main(String[] args) { Thread t1=new Thread(new Runnable(){ @Override public void run(){ while(!isover){} System.out.println(number); } }); t1.start(); ThreadUtils.sleep(1000); number=50; isover=true; } }运行上面的代码,并没有输出number的值,线程会一直循环下去,这是因为多线程之间可见性的问题。在单线程环境中,如果向某个变量写入某个值,在没有其他写入操作的影响下,那么你总能取到你写入的那个值。然而在多线程环境中,当你的读操作和写操作在不同的线程中执行时,情况就并非你想象的理所当然,也就是说不满足多线程之间的可见性,所以为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。
然后键入volatile关键字
private volatile static boolean isover=false;
我们可以看到输出number的值
程序成功退出,volatile能强制对改变量的读写直接在主存中操作,从而解决了不可见的问题。
小总结:volatile 禁止指令重排 内存的可见性
-
线程争抢:
先看代码:
package com.itheima.example18; public class Test { private static int count=0; public static void adder(){ count++; } public static void main(String[] args) throws InterruptedException { Thread t1=new Thread(()->{ for(int i=0;i<10000;i++){ adder(); } }); Thread t2=new Thread(()->{ for(int i=0;i<10000;i++){ adder(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("最后的结果是:"+count); } }输出结果为
并没有输出20000,而且每输出的结果都不一样都为10000以上的数字,说明一个线程的结果对另一个线程不可见
就上面代码来说,主存将count=1给了t1和t2,然后我第一个线程t1将count加到2时,将count=2刷回主存然后继续count++,但是t2这时得到的count还是1,并且将count=2刷回主存,这就相当于浪费了一次count++,所以就加不到20000了。
这就很好的反映了线程争抢。
然后解决线程争抢问题的最好的方案就是【加锁】
public synchronized static void adder(){//加锁得到结果:
卖票示例:
package com.itheima.example18; public class Ticket implements Runnable{ private static Integer count=100; String name; public Ticket(String name) { this.name = name; } @Override public void run(){ while (Ticket.count>0){ ThreadUtils.sleep(100); System.out.println(name+"出票一张,还剩"+count--+"张票!"); }} public static void main(String[] args) { Thread one=new Thread(new Ticket("一号窗口")); Thread two=new Thread(new Ticket("一号窗口")); one.start(); two.start(); }}输出:
package com.itheima.example18; public class Ticket implements Runnable{ private static Integer count=100; String name; public Ticket(String name) { this.name = name; } @Override public void run(){ while (Ticket.count>0){ ThreadUtils.sleep(100); synchronized (Ticket.class){//上锁,同步,关于synchronized以后再说 System.out.println(name+"出票一张,还剩"+count--+"张票!");} } } public static void main(String[] args) { Thread one=new Thread(new Ticket("一号窗口")); Thread two=new Thread(new Ticket("一号窗口")); one.start(); two.start(); }输出:
-
数据不可变
在Java当中,一切不可变的对象(immutable)一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再进行任何线程安全保障的措施,比如final关键字修饰的基础数据类型,再比如说咱们的Java字符串儿。只要一个不可变的对象被正确的构建出来,那外部的可见状态永远都不会改变,永远都不会看到它在多个线程之中处于不一致的状态,带来的安全性是最直接最纯粹的。
-
互斥同步
在Java中最基本的互斥同步手段,就是synchronized字段,除了synchronize的之外,我们还可以使用ReentrantLock等工具类实现。
-
非阻塞同步
互斥同步面临的主要问题是,进行线程阻塞和唤醒带来的性能开销,因此这种同步也被称为阻塞同步,从解决问题的方式上来看互斥同步是一种【悲观的并发策略】,其总是认为,只要不去做正确的同步措施,那就肯定会出现问题,无论共享的数据是否真的出现,都会进行加锁。这将会导致用户态到内核态的转化、维护锁计数器和检查是否被阻塞的线程需要被唤醒等等开销。
-
无同步方案
在我们这个工作当中,还经常遇到这样一种情况,多个线程需要共享数据,但是这些数据又可以在单独的线程当中计算,得出结果,而不被其他的线程所影响,如果能保证这一点,我们就可以把共享数据的可见范围限制在一个线程之内,这样就无需同步,也能够保证个个线程之间不出现数据征用的问题,说人话就是数据拿过来,我用我的,你用你的,从而保证线程安全,比如说咱们的ThreadLocal。ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。通过get和set方法就可以得到当前线程对应的值。
在多线程并发编程中 synchronized 一直是元老级角色,很多人都会称呼它为重量级锁。但是,随着 Java SE 1.6 对synchronized 进行了各种优化之后,有些情况下它就并不那么重,Java SE 1.6 中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁。
synchronized 有三种方式来加锁,分别是:
-
修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
-
静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
-
修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁
| 分类 | 具体分类 | 被锁对象 | 伪代码 |
|---|---|---|---|
| 方法 | 实例方法 | 调用该方法的实例对像 | public synchronized void method (){} |
| 方法 | 静态方法 | 类对象Class对象 | public static synchronized void method(){} |
| 代码块 | this | 调用该方法的实例对象 | synchronized(this){} |
| 代码块 | 类对象 | 类对象 | synchronized(demo.class){} |
| 代码块 | 任意的实例对象 | 创建的任意对象 | Object lock=new Object(); synchronized(lock){} |
视屏链接
6.6.3 死锁:死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
Java 死锁产生的四个必要条件:
-
互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
-
不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
-
请求和保持,即当资源请求者在请求其他资源的同时保持对原有资源的占有。
-
循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
解决死锁问题的方法是:一种是用synchronized,一种是用Lock显式锁实现。
下面代码模拟:
package com.itheima.example19;
import com.itheima.example18.ThreadUtils;//自建的工具类
import java.util.Date;
public class lockTest {
public static final Object obj1=new Object();
public static final Object obj2="obj1";
public static void main(String[] args) {
new Thread(()->{
synchronized (obj1){
System.out.println(Thread.currentThread().getName()+"获取了一号锁");
ThreadUtils.sleep(100);
synchronized (obj2){
System.out.println(Thread.currentThread().getName()+"获取了二号锁");
}
}
},"thread1").start();
new Thread(()->{
synchronized (obj2){
System.out.println(Thread.currentThread().getName()+"获取了二号锁");
ThreadUtils.sleep(100);
synchronized (obj1){
System.out.println(Thread.currentThread().getName()+"获取了一号锁");
}
}
},"thread2").start();
}
}
输出:



