1.观察多线程的不安全案例。public static int count0; public static void main(String[] args) throws InterruptedException{ Thread t1new Thread(()-{ for (int i 0; i 50000; i) { count; } }); Thread t2new Thread(()-{ for (int i 0; i 50000; i) { count; } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); //并发执行 }在单线程中我们可以知道最后输出的结果是10w而多线程输出的结果每次都不一致并且不正确。2.线程安全的概念以及它不安全的原因。2.1线程安全的概念如果多线程环境下代码运行的结果是符合我们预期的即在单线程环境应该的结果则说明这个程序是线程安全的。2.2线程不安全的原因2.2.1线程调度是随机的根本原因如上述代码中的count看起来是一行代码实际上分为三步1load把内存中 count的值加载到cpu的寄存器2) add把寄存器中的内容13) save把寄存器中的内容保存回内存上由于多线程环境下线程调度是随机的。则对于t1t2两个线程下就会有多种顺序。可能的的顺序是t1的三步先完成再是t2这是理想的正确顺序。但也可能是t1的load完成了t2的三步接着来了然后t2的剩下两步再进行。有很多种顺序。2.2.2多个线程同时修改同一个变量刚开始count为0t1load之后左边格子为0紧接着t2三步完成右边格子以及count变成1。如下图此时t1线程开始1并且保存到内存上面此时这个count被覆盖成t1的结果也就是1。这个结果是不正常的。这就是丢失更新属于典型的线程不安全问题2.2.3修改操作不是原子的什么是原子性举个例子一群人排队买东西轮到我的时候我已经买好商品付了钱但还没有拿到商品突然有人插在我前面拿走了我的商品这就是我多付了一次钱被别人打断了我的操作这就是不具备原子性。一句话总结原子性就是“一气呵成不可打断”确保多线程并发操作共享资源时结果是可预期且一致的。2.2.4内存可见性可见性指一个线程对共享变量值的修改能够及时被其他线程看到。public class Demo22 { //内存可见性造成的线程安全问题 public static int flag0; public static void main(String[] args) { Thread t1new Thread(()-{ while (flag0){ } System.out.println(线程结束); }); Thread t2new Thread(()-{ Scanner innew Scanner(System.in); System.out.println(请输入flag的值); flagin.nextInt(); }); t1.start(); t2.start(); } }由此上代码可知虽然输入了非零的值但线程根本没有结束t1仍然执行。一个线程读取一个线程修改修改线程修改的值并没有被读线程读到这就是没有保证内存可见性的问题。2.2.5指令重排序什么是重排序你要做 3 件事1. 穿裤子 2. 系腰带 3. 拿钥匙出门正常顺序穿裤子 → 系腰带 → 拿钥匙出门CPU/编译器觉得太慢我给你优化一下于是它发现拿钥匙和穿裤子不冲突于是重排拿钥匙 → 穿裤子 → 系腰带结果一样你出门了。单线程完全没问题多线程场景一乱立刻出大事现在变成两个人配合你负责穿裤子、系腰带你朋友负责看你有没有穿好裤子再给你递包正常顺序不乱排1. 你穿裤子 2. 你系腰带 ​3. 朋友看到你穿好裤子 → 递包给你正常指令重排后 CPU 为了快把顺序改成1. 系腰带 ​2. 穿裤子你朋友看到你系上腰带了他以为你裤子穿完了于是立刻递包给你。结果你裤子还没穿腰带先系了朋友以为你好了提前递包编译器会在逻辑不变的前提下调整你代码执行的先后顺序以达到提升性能的效果3.如何解决线程不安全问题常见3.1synchronized关键字—监视器锁monitor lock3.1.1synchronized的特性1互斥synchronized会起到互斥的效果某个线程执行到某个对象的synchronized中时其他线程如果也执行到同一个对象synchronized就会阻塞等待。进入到synchronized修饰的代码块相当于加锁退出到synchronized修饰的代码块synchronized用的锁是存在Java对象头里的。2可重入synchronized同步代码块对同一条线程是可重入的不会出现自己把自己锁死的问题。理解“把自己锁死”一个线程没有释放然后又尝试再次加锁。第一次加锁加锁成功。第二次加锁锁已经被占用阻塞等待。但是由于这个线程没有解锁第二次加锁然后就阻塞等待了这时候你可能想解锁但是解不了锁的第二次 lock 会把线程卡死在原地后面的 unlock 根本跑不到所以永远解不了锁只能自己锁死自己这就是不可重入锁。举个例子就是你锁了门钥匙却在房间里面你无法越过房门而进入房间拿到钥匙所以你就只能锁死自己你进不去了。而Java的synchronized是可重入锁没有这种的问题。for (int i 0; i 50000; i) { synchronized (locker) { synchronized (locker) { count; } } }在可重入锁的内部包含“线程持有者”和“计数器”的两个信息。如果某个线程加锁的时候发现锁已经被人占用但是占用的刚好正是自己那么仍然可以继续获取到锁并且让计数器自增。解锁的时候计数器递减为0的时候才真正释放锁。synchronized使用实例1修饰代码块public static void main(String[] args) throws InterruptedException{ Object locker new Object(); Thread t1new Thread(()-{ for (int i 0; i 50000; i) { synchronized (locker) { count; } } }); Thread t2new Thread(()-{ for (int i 0; i 50000; i) { synchronized (locker) { count; } } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); }2直接修饰普通方法public class Test { // synchronized 普通方法 public synchronized void method() { System.out.println(进入方法); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(退出方法); } public static void main(String[] args) { Test t new Test(); // 同一个对象 // 两个线程用同一个对象会排队 new Thread(() - t.method()).start(); new Thread(() - t.method()).start(); } }3修饰静态方法public class Test { // 静态同步方法 public static synchronized void staticMethod() { System.out.println(进入静态方法); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(退出静态方法); } public static void main(String[] args) { // 即使是不同对象也会排队 Test t1 new Test(); Test t2 new Test(); new Thread(() - Test.staticMethod()).start(); new Thread(() - Test.staticMethod()).start(); } }3.2ReentrantLock可重入互斥锁ReentrantLock和synchronized定位类似都是用来实现互斥效果保证线程安全。ReentrantLock的用法lock加锁如果获取不到锁就死等trylock超时时间加锁如果获取不到锁等待一定时间就放弃加锁。unlock解锁ReentrantLock lock new ReentrantLock(); ----------------------------------------- lock.lock(); try { // working } finally { lock.unlock() }ReentrantLock和synchronized的区别synchronized是一个关键字是JVM内部实现的ReentrantLock是标准库中的一个类在JVM外实现的。synchronized使用时不需要手动释放锁。ReentrantLock使用时需要手动释放锁。使用起来更灵活但是也容易遗漏unlock。synchronized申请锁失败时会死等。ReentrantLock可以通过trylock的方式等待一段时间就放弃。synchronized是非公平锁ReentrantLock默认是非公平锁但可以通过构造方法传入一个true开启公平锁模式。synchronized唤醒的是一个随机的线程ReentrantLock可以精确控制唤醒某个指定的线程。如何选择使用哪个锁锁竞争不激烈—synchronized锁竞争激烈—ReentrantLock如果使用公平锁—ReentrantLock3.3volatile关键字1volatile可以保证内存可见性每个线程都有自己的工作内存本地缓存所有线程共享主内存。线程读变量主内存—复制到工作内存—线程用线程写变量线程改工作内存—时机不确定才刷回主内存volatile如何保证内存可见性写操作立马刷回主内存读操作时每次都重新从主内存读不从本地缓存中读。2volatile可以禁止指令重排序3.4CASCompare and swap比较并交换3.4.1CAS涉及到的操作假设内存中的原数据V线程之前读到的V的值A需要修改的新值B1.比较线程保存的A和内存的V是否相等比较2.如果比较结果相等就把新值B写入内存中的V3.返回操作是否成功当多个线程同时对某个资源进行CAS操作只能有一个线程操作成功但是并不会阻塞其他线程其他线程只会收到操作失败的信号。3.4.2CAS的应用1实现原子类标准库中提供了java.util.concurrent.atomic包对boolean,int,long类型进行了封装里面的类都是基于这种方式来实现的。典型的就是Atomicinteger类其中的getAndincrement相当于i不安全需要加锁但加锁效率低操作。所以使用原子类的目的是避免加锁。AtomicInteger atomicInteger new AtomicInteger(0); // 相当于 i atomicInteger.getAndIncrement();2实现自旋锁什么是自旋锁线程拿不到锁时不进入阻塞不放弃CPU而是在原地循环重试自旋直到获取到锁。public class SpinLock { private Thread owner null; public void lock(){ // 通过 CAS 看当前锁是否被某个线程持有. // 如果这个锁已经被别的线程持有, 那么就⾃旋等待. // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程. while(!CAS(this.owner, null, Thread.currentThread())){ } } public void unlock (){ this.owner null; } }3.4.3CAS的ABA问题假设存在两个线程t1和t2.有⼀个共享变量num,初始值为A.接下来,线程t1想使⽤CAS把num值改成Z,那么就需要• 先读取num的值,记录到oldNum变量中.• 使⽤CAS判定当前num的值是否为A,如果为A,就修改成Z.但是,在t1执⾏这两个操作之间,t2线程可能把num的值从A改成了B,⼜从B改成了A线程t1的CAS是期望num不变就修改.但是num的值已经被t2给改了.只不过又改成A了.这个时候t1究竟是否要更新num的值为Z呢?到这⼀步,t1线程无法区分当前这个变量始终是A,还是经历了⼀个变化过程.3.4.4ABA引起的bug假设你的银行卡现在有100元你现在想要取款50元ATM创建了两个线程。正常情况下是一个线程成功扣款50元一个失败。正常的过程1. 存款100.线程1获取到当前存款值为100,期望更新为50;线程2获取到当前存款值为100,期望更新为50.2. 线程1执行扣款成功,存款被改成50.线程2阻塞等待中.3. 轮到线程2执行了,发现当前存款为50,和之前读到的100不相同,执⾏失败.异常的过程1. 存款100.线程1获取到当前存款值为100,期望更新为50;线程2获取到当前存款值为100,期望更 新为50.2. 线程1执行扣款成功,存款被改成50.线程2阻塞等待中.3. 在线程2执⾏之前,你的朋友正好给你转账50,账⼾余额变成100!!4. 轮到线程2执⾏了,发现当前存款为100,和之前读到的100相同,再次执行扣款操作这个时候,扣款操作被执行了两次!!!都是ABA问题搞的鬼!!3.4.5怎样解决ABA的问题核心思路是在CAS比对时加入“版本信息”确保值未被修改。CAS操作在读取旧值的同时,也要读取版本号.真正修改的时候,如果当前版本号和读到的版本号相同,则修改数据,并把版本号1.如果当前版本号高于读到的版本号.就操作失败(认为数据已经被修改过了).3.5使用线程安全类集合类Vector线程安全的ArrayList几乎所有的方法都加了synchronized不推荐使用HashTable线程安全的HashMap方法全加 synchronized不推荐使用ConcurrentHashMapHashMap的高度优化版本重点读操作不加锁但是使用了volatile保证从内存读取结果写操作加锁。加锁方式仍然是使用synchronized但是不是锁整个对象是相当于锁桶使用每个链表的头结点作为锁对象。充分利用CAS特性。比如size属性通过CAS来更新避免效率低。优化扩容方式化整为零发现需要扩容的线程只需要创建一个新的数组同时只搬几个元素过去扩容期间新老数组同时存在多线程协同扩容不是像HashMap一样单线程全量搬分段迁移把数组分成小任务块搬完之后老数组被回收新数组存在。StringBuffer所有方法加了synchronized锁不推荐使用