多线程和高并发(1)

    技术2022-07-11  92

    线程概念和状态

    程序 进程: 一个程序里不同的执行路径(同时执行) 线程 协程(纤程)

    多线程基本使用

    使用run调用的方法就是普通的方法, 不是多线程

    启动线程的三种方式(面试题) 1. Thread 从Thread继承 2. Runnable 实现Runable接口 3. Executors.newCachedThrad 通过线程池来启动 注: 使用线程池启动本质和使用Thread同源

    线程的运行分为六个状态 1. new状态:新建线程对象 2. Runnable状态: Runnable有两个状态组成, 一个是Ready状态, 就绪状态, 任务处于CPU的等待队列中; 一个是Running状态, CPU运行任务 3. teminated状态: 线程结束或中断 4. timewaiting状态: 时间结束自动唤醒 5. waiting状态: 睡眠(等待被唤醒) 6. blocked状态:被锁定状态

    synchronized

    同步方法和非同步方法可以同时调用

    写数据加锁, 读数据不加锁行不行?主要看业务允不允许, 如果读数据只是查看, 或业务允许脏读就没有问题, 加锁会使多线程的效率变得极低!!!

    一个同步方法可以调用另外一个同步方法, 一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁, 也就是说synchronized获得的锁是可重入的

    public class test1 extends Thread{ public synchronized void m1() { System.out.println("m1"); try { sleep(1000); } catch(InterruptedException e) { e.printStackTrace(); } m2(); System.out.println("m1结束"); } public synchronized void m2() { System.out.println("m2"); try { sleep(1000); } catch(InterruptedException e) { e.printStackTrace(); } System.out.println("m2结束"); } public static void main(String[] args) { test1 t1 = new test1(); Thread t = new Thread() { public void run() { synchronized(t1) { t1.m1(); } try { Thread.sleep(1000); } catch(InterruptedException e) { e.printStackTrace(); } } }; t.start(); } }

    如果synchronized不是可重入,上述程序中m1调用m2就会发生死锁, 当子类super父类方法时也会发生死锁.所以synchronized必须是可重入的.

    程序在执行过程中如果出现异常, 默认情况下锁会被释放. 所以, 在并发处理的过程中, 有异常要小心, 不然会发生不一致的情况.(在一个synchronized块中发生了异常, 此时锁会被释放, 其他的线程就会乱入)

    synchronized的底层实现

    早期JDK 重量级锁-OS层 后来的改进 锁升级的概念 锁升级有四个阶段 无锁->偏向锁->自旋锁->重量级锁

    偏向锁就是记录第一个线程的ID, 有多个线程但没有争议的时, 此时是没有上锁的. 只是记录了首个线程的ID, 该线程再次使用时是没有锁状态的.

    当有多个线程存在线程争议时, 这时锁就会升级为自旋锁. 自旋锁就是其他线程在一旁循环等待锁的释放(在循环期间占用CPU资源但不访问OS), 通常是自旋10次, 10次之后会升级为重量级锁(系统锁等待期间不占用CPU), 在自旋期间如果锁释放了, 还需要看公平和非公平锁, 如果是公平锁, 那么最先来的自旋锁就会拿到锁, 如果是非公平锁, 那么是所有自旋锁来抢这个锁.

    开发过程中, 在执行时间长, 线程多的情况下系统锁更适用. 执行时间短, 线程少的情况下自旋锁更有优势, 当有大量线程时(几万个线程)自旋的话CPU也会爆炸. 锁的等级只升不降.

    关于锁升级可以看马士兵的我就是厕所所长

    各个锁的优缺点

    volatile boolean running = true;

    volatile

    保证线程可见性 MESI缓存一致协议(靠CPU的缓存一致协议保证线程可见性)

    首先会把flag复制一份到线程的工作区中, 先将工作区的副本进行修改, 然后去修改堆内存中的flag. 然后堆中的flag值被修改了,那么线程的工作区的flag副本也需要更新(该过程不好控制), 这就是线程之间的不可见

    public class test1 extends Thread{ // volatile boolean running = true; void m() { System.out.println("m start"); while(running) { //执行内容 } System.out.println("m end"); } public static void main(String[] args){ test1 t1 = new test1(); new Thread(t1::m).start(); try { Thread.sleep(1000); } catch(InterruptedException e) { e.printStackTrace(); } t1.running = false; } }

    这就是volatile的作用, 一个线程的改变, 其他线程马上就能读到

    禁止指令重排序(CPU) DCL单例Double Check LockMgr06.java

    单例模式

    单例模式的双重检查

    public class danli { private static volatile danli instance;//volatile private danli() { } public static danli getinstance() { if(instance==null) { //双重判断避免线程出错 synchronized(danli.class) { if(instance==null) { try { Thread.sleep(1000); } catch(InterruptedException e) { e.printStackTrace(); } instance = new danli(); } } } return instance; } public void m() { System.out.println("m"); } public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(()->{ System.out.println(danli.getinstance().hashCode()); }).start(); } } }

    如果instance没有加volatile会出错, 在指令重排序上可能会出错!!!

    JVM中new一个对象的过程

    假如成员变量a是int类型 第一步, 将a的初始值置为0 第二步, 将初始化中a真正的值再次赋予a 第三步, 将内容赋值给引用对象 禁止指令重排序就是让a的初始化过程是按正常流程运行. 如果在给a初始化值得过程中指令重排序了, 可能会把a = 0 赋值给引用对象, 在超高并发状态下没有加volatile会出现这种问题.

    在开发中最普通的饿汉单例模式是最为常用的, 上述单例模式纯属研究最完美的开发.

    public static synchronized Danli getInstance() { if(instance==null){ instance = new Danli(); } return instance;

    原子性相关

    import java.util.ArrayList; import java.util.List; public class test2 { volatile int count = 0; void m() {for(int i = 0;i<10000;i++)count++;} public static void main(String[] args) { test2 t2 = new test2(); List<Thread> threads = new ArrayList<Thread>(); for(int i = 0;i<10;i++) { threads.add(new Thread(t2::m)); } threads.forEach((o)->o.start()); threads.forEach((o)->{ try { o.join(); } catch(InterruptedException e) { e.printStackTrace(); } } ); System.out.println(t2.count); } }

    count的值并没达到理想的10万, 因为count++不是原子性的. count++ 这个行为,事实上是有3个原子性操作组成的。 步骤 1. 取 count 的值 步骤 2. count + 1 步骤 3. 把新的值赋予count 这三个步骤,每一步都是一个原子操作,但是合在一起,就不是原子操作。就不是线程安全的。 volatile并不能保证多个线程共同修改running变量时所带来的不一致问题, volatile不能替代synchronized

    解决count++原子性, 还是需要把方法设为线程安全.

    synchronized void m() {for(int i = 0;i<10000;i++)count++;}

    锁优化

    锁细化

    synchronized代码块中的语句越少越好

    volatile int count = 0; synchronized void m1() { try { //业务逻辑1 Thread.sleep(1000); } catch(InterruptedException e) { e.printStackTrace(); } count++; try { //业务逻辑2 Thread.sleep(1000); } catch(InterruptedException e) { e.printStackTrace(); } }

    在整个锁中需要保证的只有count的值, 所以对锁细化

    void m2() { try { //业务逻辑1 Thread.sleep(1000); } catch(InterruptedException e) { e.printStackTrace(); } synchronized(this) { count++; } try { //业务逻辑2 Thread.sleep(1000); } catch(InterruptedException e) { e.printStackTrace(); }

    与锁细化相对的就是锁粗化, 细化和粗化分场合使用

    锁定对象

    锁定某对象, 如果o的属性发生改变, 不影响锁的使用. 但是如果o变成另一个对象, 则锁定的对象发生改变. 所有要避免将锁定的对象和引用变成另外的对象

    final Object o = new Object(); void m(){ synchronized(o){ ... } } o = new Object();

    字符串相关

    不要以字符串常量作为锁定对象 在下面的例子中, m1和m2其实锁定的是同一个对象(字符串缓冲池) 这种情况还会发生比较诡异的现象, 比如你用到了一个库类, 在该类库中代码锁定了字符串"hello", 但是你读不到源码, 所以你在自己的代码中也锁定了"hello" ,这时候就有可能发生非常诡异的死锁阻塞

    String s1 = "hello"; String s2 = "hello"; void m1() { synchronized(s1) { } } void m2() { synchronized(s2) { } } }

    loadfence原语指令和storefence原语指令

    CAS(乐观锁)

    乐观锁, 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改 所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据 可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量 像数据库, 如果提供类似于write_condition机制的其实都是提供的乐观锁。

    Compare And Set/Swap 无锁优化 自旋

    解决同样的问题的更高效的方法, 使用AtomXXX类 AtomXXX 类本身方法都是原子性的, 但不能保证多个方法连续调用是原子性的

    import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; //以Atom开头的都是以CAS来保证线程安全的 public class test2 { /*volatile int count = 0;*/ AtomicInteger count = new AtomicInteger(); /*volatile*/void m() { for(int i =0;i<10000;i++)count.incrementAndGet(); } public static void main(String[] args) { test2 t2 = new test2(); List<Thread> threads = new ArrayList<>(); for (int i = 0; i < 10; i++) { threads.add(new Thread((t2::m))); } threads.forEach((o)->o.start()); threads.forEach((o)->{ try { Thread.sleep(1000); } catch(InterruptedException e) { e.printStackTrace(); } }); System.out.println(t2.count); } }

    count的值为100000

    实现过程 cas(V, Expected,update) V是要被修改的值, Expected期望当前的值, update更改后的值 被修改的值如果与期望值不同, 说明有其他线程修改过这个值, cas会再次再试一次或失败. 严格来说CAS()只有两个参数, 期望值和修改后的值. 因为cas是cpu原语支持, 所有在 V == Expected的过程中不可能被打断, 所以不会发生在比较过程中V的值发生了改变.

    ABA问题 把一个值从0修改到1, 再从1修改到0. 在这个中间过程中另外一个线程使用cas发现值被修改过. 如果是基础类型是无所谓的, 但如果是引用类型, 你的手机屏幕碎了, 返厂修好后手机修好了, 手机还是原来的那个手机吗? 解决ABA最好的办法就是加版本号AtomicStampedReference

    悲观锁

    顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。

    Unsafe类

    Unsafe类是一个单例模式

    直接操作内存 allocateMemory putXX freeMemory pageSize 直接生成实例 allocateInstance 直接操作类或实例变量 objectFieldOffsetgetIntgetObject CAS相关操作 compareAndSwapObject JKD 1.8weakcompareAndSetObject Int Long JDK10.0

    与c和c++的指针类似 c->malloc free c+±>new delete

    有锁和无锁的选择是在效率上做抉择, 无锁肯定比有锁效率高得多

    Processed: 0.010, SQL: 9