多线程学习记录(有想一起学习的小伙伴可以私信我)

多线程学习

一、线程实现的三种方式:文字表述

1.继承thread类,并重写thread类中的run()
2.实现runnable接口,实现runnable接口中的run(),(因为runnable接口中只有一个抽象方法,abrast run(),并且抽象接口是必须被实现的),所以也可以采用lambda表达式的写法去启动线程,比如new Thread(()->System.out.println(“线程已启动”)).start,
3.通过线程池的方式

二、启动线程的三种代码实现方式

1.new Thread().start
2.new Thread(Runnable target).start
3.new Thread(Runnable target).start lambad表达式的写法,具体见下方代码块
public class fd {
    // 实现线程的第一种方式,但是有局限性,因为java是单继承,继承了之后该类就不能再继承了
    static class myThread extends Thread {
        @Override
        public void run() {
            for (int i = 0; i <= 5; i++) {
                try {
                    TimeUnit.MILLISECONDS.sleep(1);
                    System.out.println("线程正在执行");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }
    }
    // 实现线程的第二种方式,实现runnable接口
    static class runnableImpl implements Runnable {

        @Override
        public void run() {
            System.out.println("实现runnable接口");
        }
    }
    public static void main(String[] args) {
         
        // 调用run方法并没有启动线程,仅仅只是调用了一个方法
        new myThread().run();
        // 通过继承Thread类的方式启动
        new myThread().start();
        // 通过实现runnable接口方式启动线程,只有
        // Thread类中才有start()方法,
        // 在源码中看到Thread类中有一个方法可以传入
        // Runnable接口类型的参数,
        // 所以可以通过下面这种方式进行启动
        new Thread(new runnableImpl()).start();
        // 这是通过lambda表达式的一种写法(lambda实际上
        // 就是对接口的一种实现,可以直接作为参数进行传递,
        // 是一种语法糖),
        // 也就是说这里使用lambda表达式对Runnable接口
        // 中的抽象方法进行了一个实现
//        new Thread(() ->
//        {
//            for (int i = 0; i <= 5; i++) {
//                try {
//                    TimeUnit.MICROSECONDS.sleep(1);
//                    System.out.println("runnable形式线程");
//                } catch (InterruptedException e) {
//                    e.printStackTrace();
//                }
//
//            }
//        }).start();
        for (int i = 0; i <= 5; i++) {
            try {
                TimeUnit.MICROSECONDS.sleep(1);
                System.out.println("主程序正在执行");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

    

三、三种线程相关方法

  1. Thread.sleep() 该方法指定该线程睡眠指定的时间,过了指定时间后,该线程回到就绪状态,等待cpu的调度,但不会释放锁
  2. Thread.yield()当前线程让出一下cpu,让其他线程进来执行,当前线程让出一下cpu后直接回到就绪状态,也就是等待队列,也不会释放锁
  3. Thread.join()将当前线程上的任务交给另一个线程进行执行,另一个线程执行完了回到当前线程。此处面试题:让t1,t2,t3三个线程有序执行。
public class ThreadLearn {

    public static void main(String[] args) {
//        testSleep();
        Thread t1 = new Thread(new TestJoin(null));
        Thread t2 = new Thread(new TestJoin(t1));
        Thread t3 = new Thread(new TestJoin(t2));
        t1.start();
        t2.start();
        t3.start();

    }

    public static class TestJoin implements Runnable {

        private Thread thread;

        public TestJoin(Thread target) {
            this.thread = target;
        }

        @Override
        public void run() {
            if (thread!=null){
                try {
                    thread.join();
                    System.out.println("thread join"+Thread.currentThread().getName());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            else {
                System.out.println("thread join"+Thread.currentThread().getName());
            }
        }
    }
}

四、线程的6种状态

  1. new—新建
    (new.start()启动线程)后
  2. Runnable运行状态—>运行状态包括就绪状态和执行状态
  3. TimedWating.有时间限制的等待状态
  4. Wating.无时间限制需要被唤醒的等待状态
  5. Blocked.阻塞状态,等待进入同步代码块的锁
  6. Teminater终止状态

五、关于线程的终止

  1. 线程执行完后自动终止
  2. stop() 已经被停用
  3. interrupt() 该方法也并不是真正的将线程停止,而是在当前线程至一个标记**(isInterrupt(),这个标记是Thread类中的一个静态方法),如果该标记为false,则线程为非中断状态,该标记为true,则线程为中断状态。并且这个标记并不会对当前线程造成任何影响,需要通过代码进行控制,也就是说在非阻塞的线程中,通过一直循环判断当前标记状态来确定当前线程是否需要进行中断,这里的中断指的就是停止运行,比如当前标记为false了,就不进入方法进行执行,线程就中断了第二种就是阻塞的线程,比如线程中有wait(),join(),sleep()等方法时,就是阻塞的线程,在这三个方法的源码中都抛出了一个interruptexception,所以当当前线程是阻塞的线程,并且调用了interrupt方法时,由于只有运行时的线程才会有isInterrupt()的状态,所以此时会抛出异常,从而执行catch块中的代码,并将isInterrupt状态改为false(非中断状态),结论:当阻塞式的线程调用了中断方法时可以提前结束阻塞的时间,并将状态改为false
interrupt结论:如果仅仅是在当前线程调用了interrupt方法,但不做任何处理的话,线程并不会被中断,所以需要循环判断当前线程的状态标志,根据状态标志做出相应的处理,阻塞式线程会提前结束阻塞的时间,并在catch块中捕获异常,并将状态标志改为false
引发的思考?
interrupt()能否终止wait()?

答:可以,并抛出interrupt异常,wait,join,sleep操作都会被提前终止,并抛出异常,改中断标记为false

sleep是否可以被notify唤醒

答:不能,但是sleep可以被interrupt打断,从而达到快速唤醒的目的。wait必须被唤醒


六、synchronized锁的理解

  • synchronized 锁的是对象,底层实现锁的是该对象对象头的前两位,一个对象的对象头是64位(如果是32位jvm就是32位)。
  • 一个对象的组成为:对象头,实例数据,对其填充
  • 加锁的三种方式:1.new Object,并对object加锁
public class SynchronizedLearn_01 {

    int i =1;
    Object o = new Object();

    public void testSync(){
        // 锁object对象,只有拿到这个锁之后,线程才能执行锁里面的代码
        synchronized (o){
            System.out.println(i);
        }
    }
}
  • 第二种方式:给当前对象加锁
public class Sync_02 {

    int i;
    int b;
    // 这个是给点钱对象加锁
    public void getSync() {
        synchronized (this) {
            System.out.println(i);
        }
    }
    
    // 上面这种写法可以变成这种
    public synchronized void getSync_same(){
        System.out.println("同样的效果");
    }

}
  • 第三种方式:给当前类加锁(每一个类通过类的加载器加载后,都会有一个当前类的对象)
public class Sync_03 {

    static int i = 10;

    // 这是就是类锁,将当前类的对象上锁
    public synchronized static void getSync() {
        i--;
    }

    // 等同于以下代码,在static方法里面是没有this的
    public static void getSyncSame() {
        synchronized (Sync_03.class) {
            i--;
        }
    }
}
六-一、sync锁的相关问题

先说结论:同步方法和非同步方法在两个线程中是可以同时执行的。

  1. 同步方法和非同步方法是否可以同时调用
    答:可以。非同步方法可以在同步方法的执行过程中被调用,因为非同步的方法不需要获取当前对象的锁
public class Sync_04 {

    // 同步和非同步方法是否可以同时调用
    public synchronized void getNum(){
        System.out.println(Thread.currentThread().getName()+"线程t1开始执行");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"线程t1执行结束");
    }

    public void getNumNone(){
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"非同步线程运行");
    }

    public static void main(String[] args) {
        Sync_04 T = new Sync_04();
        new Thread(T::getNum,"t1").start();
        new Thread(T::getNumNone, "t2").start();

        // 1.8之前的写法
//        new Thread(new Runnable() {
//            @Override
//            public void run() {
//                     T.getNum();
//            }
//        }).start();

    }
}

2.模拟问题:一个银行账户,只对写方法加锁,不对读方法加锁,这样行不行
答:这样会出现脏读,如果业务允许也可以。最好是能不加锁的地方就不加锁,加锁会使效率低100倍

public class Sync_Bank {

    /**
     * 模拟银行,只对写加锁,不对读加锁
     */
    String name ;
    double money;
    //对写方法加锁
    public synchronized void set(String name,double money){
        this.name = name;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.money = money;
    }
    // 根据传入的名字拿到钱
    public double get(String name){

        return money;
    }

    public static void main(String[] args) {
        Sync_Bank sb = new Sync_Bank();
        new Thread(()->sb.set("张三",100000)).start();
        System.out.println(sb.get("张三"));
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(sb.get("张三"));

    }
}

3.模拟问题:sync是否是可重入锁?
答:必须是。可重入锁是指,同一个线程去拿到两个加了当前对象锁的方法,这时是可以执行的,如果不是可冲入锁,那么此时就死锁了(例子:在一个加了锁的方法里去调用加了同样锁的另一个方法)

public class Sync_Reentry {
    // 可重入锁---在一个加锁的方法中,调用另一个加锁的方法

    public synchronized void getReentry(){
        System.out.println("第一个加锁的方法");
        getReentry2();
    }

    public synchronized void getReentry2(){
        System.out.println("第二个加锁的方法");
    }

    public static void main(String[] args) {
        Sync_Reentry sr = new Sync_Reentry();
        new Thread(sr::getReentry).start();
    }

}
  1. 模拟问题:子类继承父类,sync(this)是否是同一把锁?
    答:是的

  1. 模拟问题:程序抛出异常后,是否会释放锁?
    答:是,但是在用catch块捕获异常后,就不会释放锁了。比如说当前线程t1释放锁之后,t2线程就会进来执行
public class Sync_TryCath {

    // 程序抛出异常后,会释放锁
    public synchronized void count(){
        int i = 0;
        while (true){
            i++;
            System.out.println(Thread.currentThread().getName()+"线程正在执行");
            if(i==5){
                try {
                    int s = i/0; // 这里抛出异常后会释放锁,但是用catch捕获后不会
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }



    public static void main(String[] args) {
        Sync_TryCath st = new Sync_TryCath();

        new Thread(st::count,"t1").start();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(st::count,"t2").start();
    }
}
sync原理
  • 早起的时候每一次的获取锁都是重量级的,都需要去找操作系统内核,这时候从用户态切换到内核态势非常耗费资源的
  • 后期引入锁升级的概念:
    主要有以下几种锁: 1.偏向锁。2自旋锁(轻量级锁)。3重量级锁(os)

偏向锁

  • 偏向锁是一种乐观锁,实际上是没有上锁的,只是记录了一下线程ID
  • 大多数情况下,锁不仅不由多个线程竞争,往往都由一个线程来获取,所以引用偏向锁,当第一个线程获取锁时,会在Mark word中记录该线程的线程ID,并给一个一位的偏向锁标记。下一次该线程再来获取锁的时候只需要对比这个线程ID,就可以获取锁了
  • 线程默认是不会释放偏向锁的,当有其他线程来获取锁时,首先在达到全局安全点时,先暂停持有偏向锁的线程,然后检查该线程是否存活(因为有可能该线程已经执行完毕,但并不会释放锁),如果该线程已经处于销毁状态,那么将对象头至为无锁状态01,然后重新偏向新的线程,如果该线程处于活跃状态,撤销偏向锁,升级为轻量锁,标志为00,此时轻量锁由原持有偏向锁的线程持有,而在竞争的线程会进入自旋状态,等待获取锁。

自旋锁

  • 自旋锁主要是通过不断的循环去拿锁,默认循环10次
  • 优点:不会让线程频繁的挂起,运行,一直都是用户态,不用切换到内核态
  • 缺点:一直占用cpu

JDK1.6 自适应自旋锁

  • 根据上一个拿到该锁的线程的执行时间判断,使线程自旋时间变长,如果其他线程拿到锁很少成功的话,就直接忽略自旋操作

重量级锁

  • 争抢锁的线程从用户态转换成内核态,效率很低
问题:什么时候用自旋锁,什么时候用重量级锁?

答:加锁代码执行时间长,线程多的时候用重量级锁,执行时间短,线程数比较少的时候用自旋锁。(自旋锁一直占用cpu,重量级锁不占cpu,一直处于等待队列)

注意::在使用sync()的使用,锁的对象不能是String常量,Integer,Long,也就是说基本数据类型就别用了。Integer 的内部有特殊处理,每次值一改变就会产生新对象

锁优化

  • 锁细化—>尽量让锁作用在较少的代码块上,让只需要加锁的代码加锁
  • 锁粗话—>当对锁的竞争特别激烈的时候,让锁粗话,比如在一个方法中做了很多的锁细化,那么还不如让锁粗化,这样更能减少资源的浪费

volatile (只能保证线程的可见性,没有原子性,并不能代替sync)

Volatile主要有两个作用:

  • 1.保证线程可见性 (尽量在简单的变量上使用volatile,在对象上使用volatile,并不会观察到对象中成员变量的值得改变,比如在new一个ArrayList后,list中的数据改变了,并不会被立刻发现,线程睡一秒之后才可以被发现,list中的成员被改变了)
  • 2.禁止指令重排序 (原理是内存屏障)

保证线程可见性(一个线程将变量改变后,立刻被另一个线程发现)

  • 比如定义了一个变量,有两个线程去用这个变量,java中变量是存储在堆内存中的,堆内存相当于是一个公共的区域,当一个线程想要使用这个变量,他会先去堆内存中复制一份这个变量,然后在自己的线程中去改变这个变量,改变后立刻写会堆内存。但是第二个线程在使用这个变量的时候,不一定什么时候去读取这个改变之后的变量,这期间可能还在用字的复制的副本变量。
  • 本质上使用的是(原理)cpu的缓存一致性协议(MESI)不同的线程运行在不同的cpu上
public class V_01 {
    //volatile 测试
    // 这里在主线程中改为false,t1线程一直都没有发现,使用volatile就不会了,保证了线程的可见性
    volatile boolean aBoolean = true;

      void set(){
        System.out.println("线程"+Thread.currentThread().getName()+"开始了");
        while (aBoolean){
//            try {
//                TimeUnit.SECONDS.sleep(2);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
        }
        System.out.println("结束了");
    }

    public static void main(String[] args) {
        V_01 v = new V_01();
        new Thread(v::set,"t1").start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        v.aBoolean = false;
    }

禁止指令重排序

  • 以前的cpu执行指令是一条一条的执行,现在是并发的执行,比如一条指令执行了一半,另一条也开始执行了,所以cpu会对指令进行一个重新排序的可能,重排序后,和我们本来的执行顺序有可能是不一样的
  • 例子:单例模式(禁止指令重新排序的例子,以往经典面试题)
public class V_02 {

    // 单例模式:类加载到内存后,只加载一个单例
    // 这个是饿汉式,不管用没用到都先创建这个对象
    private static final V_02 v02 = new V_02();

    // 私有化构造方法
    private V_02(){};

    public static V_02 getIntance(){
        System.out.println("m");
        return v02;
    }

    public static void main(String[] args) {
        V_02.getIntance();
    }

}
public class V_03 {


    // 懒汉式
    private static V_03 v03;

    private V_03() {

    };

    public static V_03 getInstance() {
        if (v03 == null) {
            v03 = new V_03();
            System.out.println("我看到你了");
        }
        return v03;
    }

    public static void main(String[] args) {
        V_03.getInstance();
    }
}
线程安全的单例模式
  • 这里涉及到的问题就是要不要加volatile,如果不加的话,正常压测也很难测出问题,但在理论上还是会存在执行重排序的问题,只是很难测出
  • new 一个对象经过jvm编译后分为三个指令,1.申请一块内存(会给一个默认值) 2.给这个对象的成员变量初始化(就是你真正给的初始值) 3.把值给栈中的对象。所以在超高并发的情况下,有可能指令重排序后,第三步跑到了第二步的前面,在初始化后就给对象赋值了,也就是直接把初始化的值直接给了对象,这时就出问题了,所以要加volatile
public class V_04 {

    // 线程安全的懒汉式
    private volatile static V_04 v04;

    //  私有构造方法
    private V_04() {
    }


    // 提供一个对外的公用的访问方式,保证全局只有一个实例对象
    public static V_04 getInstance() {

        if (v04 == null) {
            synchronized (V_04.class) {
                if (v04 == null) {
                    v04 = new V_04();
                }
            }
        }
        return v04;
    }

}

AtomicInteger CAS操作(这个目前还么有理解)
public class AtomicInteger_1 {
    AtomicInteger count = new AtomicInteger(0);

    void m() {
        for (int i = 0; i < 1000; i++) {
            count.incrementAndGet();//count++
        }
    }

    public static void main(String[] args) {
        AtomicInteger_1 a1 = new AtomicInteger_1();

        List<Thread> threads = new ArrayList<>();

        for (int i=0;i<10;i++){
            threads.add(new Thread(a1::m));
        }

        threads.forEach(o->o.start());
        // 不知道问什么,这里必须规定执行的顺序,不然值就是不对的
        threads.forEach(o-> {
            try {
                o.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        System.out.println(a1.count);


    }

}

CAS(无锁优化 自旋)

  • Compare And Set
  • cas(V,Expected,NewValue)
    if(V(要改变的变量)==E(预期值))
    V=New
    otherwise try again or fail
  • cas是cpu原语支持的,执行的过程不会被打断,不用担心线程的问题

ABA 问题(比如一个Integer值为1,被一个线程改成了2,又被一个线程改成了1,应该怎么解决)

  • 答:加版本号cas(version) atomic类中是有这个类的。-----------ABA问题在基础数据类型上是没问题的,在对象类型是可能有问题的

所有的CAS操作都是unsafe这个类在支撑

unsafe类 == c,c++的指针
  • unsafe 可以直接操作内存,直接生成类示例,直接操作类变量或实例变量,执行cas相关操作
  • 该类是单例的,在jdk11之前是无法直接使用的,只能通过反射,但是在jdk11,可以直接调用
  • 马士兵试了一下,在视频中调用了一下,发现一运行就报错
lock(可以代替sync的锁,cas操作的锁)
  • lock中的reentrantLock(可重入锁)
  • lock.lock()如同sync,获取不到锁会一直等待中,无法打断(interrept()),除非上一个线程释放锁
public class ReentrantLock_01 {

    Lock lock = new ReentrantLock();
    // 可重入锁,必须手动释放锁
    void getNum() {
        lock.lock();
        try {
            for (int i = 0; i < 10; i++) {
                System.out.println(i);
                TimeUnit.SECONDS.sleep(1);
                if(i==2){
                    getNum2();
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    void getNum2(){
        lock.lock(); // sync(this)
        System.out.println("线程2start");
        lock.unlock();
    }

    public static void main(String[] args) {
        ReentrantLock_01 r = new ReentrantLock_01();
        new Thread(r::getNum).start();
    }
  • tryLock(尝试以设置的时间去获取锁,如果在固定时间获取不到,则返货false,并执行下面的代码,而不是像sync,获取不到锁就阻塞了,在等待时间内可以被interrept打断,并抛出异常)
public class TryLock01 {
    Lock lock = new ReentrantLock();

    void getNum() {
        lock.lock();
        try {
            for (int i = 0; i < 10; i++) {
                System.out.println(i);
                TimeUnit.SECONDS.sleep(1);
            }

        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    void getNum2(){
        boolean islock = false;
        try {
             islock = lock.tryLock(1, TimeUnit.SECONDS);// sync(this)
            System.out.println(islock);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            if(islock)lock.unlock();
        }

    }

    public static void main(String[] args) {
        TryLock01 r = new TryLock01();
        new Thread(r::getNum).start();
        new Thread(r::getNum2).start();

    }
}
  • LOCK.lockInterruptibly():此方式会等待,当未获得锁的线程调用.interrupt()会被中断等待,并抛出InterruptedException异常,否则会与lock()一样始终处于等待中,直到线程A释放锁。

- - 问什么说lock是可中断锁,sync是不可中断锁?

答:sync在被打断时,非阻塞的线程只会在线程中做一个标记,其他什么也不会做,阻塞式的线程则会抛出一个异常,并将被打断的状态改为false。
lock中,使用interruptibly()方法获取锁,是可以被其他线程使用interrept()方法打断的,类似于sync中打断阻塞式的线程,不同处在于lock中使用interruptibly()获取锁后,也会抛出异常

  • 公平锁(reentrantLock才有公平锁,sync没有)
    公平锁:线程会先检查等待队列中是否还有其他线程,如果有则进入等待队列,如果没有直接取获取锁
    非公平锁:不管等待队列中是否有线程,直接取获取锁
public class FairLock {
    // 将参数设置为true则为公平锁,但是也不能完全保证公平
    // 有可能一个线程执行完后,第二个线程还没有进入等待队列,第一个线程
    // 就又进入队列了
    final Lock lock = new ReentrantLock(true);

    public void getName() {
        for (int i = 0; i < 100; i++) {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "获得了锁");
            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        FairLock fl = new FairLock();
        new Thread(fl::getName,"t1").start();
        new Thread(fl::getName,"t2").start();
    }

}

countDownLatch(门闩)

  • 相当于一个计数器,可以用来等所有线程都执行完了之后做一些操作。
public class CountDown_01 {

    public static void main(String[] args) {
        Thread[] ts = new Thread[100];

        //new 一个countDownLatch
        CountDownLatch cd = new CountDownLatch(ts.length);
        for (int i=0;i<ts.length;i++){
            // 每个线程执行完之后都减一
            ts[i] = new Thread(()->cd.countDown());
        }

        for (int i=0;i<ts.length;i++){
            ts[i].start();
        }

        try {
            // 在这里拴住,阻塞,所有线程执行完了之后才可以往下走
            cd.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("所有的都执行完了");

    }

}

cyclicBarrier(循环栅栏)

  • 当线程数满足条件时,执行相应的操作
public class CylicBarrier_01 {


    public static void main(String[] args) {
        // 第一种方式,在await下面写操作
//        CyclicBarrier barrier = new CyclicBarrier(20);
        // 这种方式,当有20个线程了之后,走第二个参数
        CyclicBarrier barrier = new CyclicBarrier(20,()-> System.out.println("满了"));
        for (int i=0;i<100;i++){
            int finalI = i;
            new Thread(()-> {
                try {
                    barrier.await();
                    // 第一种方式不能用,是错误的
                    // System.out.println("满了"+ finalI);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }

}

cyclicBarrier进阶版phaser

  • 在线程的某个阶段做一些事情
  • 使用方法1.继承phaser类,重写onadvice方法2.详情在视频多线程与高并发(三)58分钟处

读写锁

  • 读锁也叫共享锁
  • 写锁也叫互斥锁
  • 读写锁主要是一个线程在读的时候另一个线程也可以进来读,写锁主要是在修改数据的时候,其他线程都不能进行读写,这样可以比互斥锁极大的提高效率
public class ReaderWriterLock_01 {
    // 互斥锁
    static Lock lock = new ReentrantLock();
    public static int value;
    // 读写锁
    static ReadWriteLock rw = new ReentrantReadWriteLock();
    // 读锁
    static Lock readLock = rw.readLock();
    // 写锁
    static Lock writer = rw.writeLock();
    // 读的方法
    public static void read(Lock lock){
        lock.lock();
        try {
            TimeUnit.SECONDS.sleep(1);
            System.out.println("read over");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    // 写的方法
    public static void writer(Lock lock,int value){
        lock.lock();
        try {
            int result = value;
            TimeUnit.SECONDS.sleep(1);
            System.out.println("writer over");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        Random random = new Random();
        // 传入读锁
        Runnable readThread = ()->ReaderWriterLock_01.read(readLock);
        // 传入写锁
        Runnable runnableWriter = ()->ReaderWriterLock_01.writer(writer,random.nextInt());
        for (int i=0;i<2;i++){
            new Thread(runnableWriter).start();
        }
        for (int i=0;i<18;i++){
            new Thread(readThread).start();
        }

    }

}

sermaphore (信号)----用来控制同时执行的线程数

public class Semaphore_01 {

    public static void main(String[] args) {
        // 允许线程同时执行的个数,这里写两个就表示可以同时又有两个线程执行
        // 如果写的是1,就需要一个线程执行完之后另一个线程才能执行
        Semaphore s = new Semaphore(2);

        new Thread(()-> {
            try {
                // 获得一个信号,上面给的参数就会减一个
                s.acquire();
                System.out.println("线程1执行了");
                TimeUnit.SECONDS.sleep(3);
                System.out.println("线程1又执行了");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                // 释放信号,释放后别的线程才能获取
                s.release();
            }
        }).start();

        new Thread(()-> {
            try {
                // 获得一个信号
                s.acquire();
                System.out.println("线程2执行了");
                TimeUnit.SECONDS.sleep(3);
                System.out.println("线程2又执行了");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                // 释放
                s.release();
            }
        }).start();
    }
}

exchanger(用于两个线程之间交换数据)

  • 交换数据的过程是阻塞的,比如一个线程调用了exchange方法,那么他就阻塞住了,需要有另一个线程也exchange一下,才可以继续执行

多线程与高并发(四)

LockSupport(阻塞线程)
  • LockSupport.park() 可以直接阻塞线程
  • LockSupport.unpark(要唤醒的线程对象) 可以直接唤醒线程
  • 注意事项:如果LockSupport.unpark在 LockSupport.park()之前执行,那么park将无效
LockSupport与wait,notify的区别?
  • wait和notify必须在同步代码块(sync)中使用,不然很有可能,一个线程在还没有wait之前,notity就已经执行了,导致该线程一直被阻塞。
  • notify 不能指定唤醒某一个线程
  • LockSupport不需要在sync中使用,可以直接使用,可以直接唤醒想要唤醒的线程
  • 注意:wait会释放锁,notify不释放锁
public class LockSupport_01 {

    public static void main(String[] args) {
       Thread t1 =  new Thread(()->{
            for (int i=0;i<10;i++){
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(i);
                if(i==5){
                    // 阻塞线程
                    LockSupport.park();
                }
            }
        });
        t1.start();
        try {
            TimeUnit.SECONDS.sleep(12);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        LockSupport.unpark(t1);
    }
}

练习题:实现一个容器,提供两个方法,add,size 写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个是,线程2给出提示并结束

  • 错误示范,这样并不能保证list中的数据被改变了之后立刻被其他线程发现,因为volatile并不能发现对象中成员变量的改变
public class practice {

    /* 实现一个容器,提供两个方法,add,size
     *  写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个是,线程2给出提示并结束
     */
    volatile static List list = new ArrayList();

    public static synchronized void add() {
        list.add(new Object());
    }

    public static synchronized Integer size() {
        return list.size();
    }

    public static void main(String[] args) {
        // 添加
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println(i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                add();
            }
        }).start();

        new Thread(()->{
            while (true){
            if(size()==5){
                System.out.println("5个了");
                break;
            }
        }}).start();
    }
}
  • 通过wait和notify来做真正的实现
  • 注意,notify不会释放锁,所以需要再notify后wait一下,释放锁,监视线程才能执行,并且另一个线程需要再notify一下,唤醒添加的线程
public class practice_01 {
    /* 实现一个容器,提供两个方法,add,size
     *  写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个是,线程2给出提示并结束
     */

    public static void main(String[] args) {
        List list = new ArrayList();
        final Object lock = new Object();
        // 先搞一个监视线程
        new Thread(()->{
            synchronized (lock){
                System.out.println("监视线程启动");
                if(list.size()!=5) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("监视线程结束");
                lock.notify();
            }
        },"t2").start();


        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(()->{
            synchronized (lock){
                for (int i = 0; i < 10; i++) {
                    System.out.println(i);
                    list.add(lock);
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    if(list.size()==5){
                        lock.notify(); // notyfy后不会释放锁,所以需要再wait一下,释放锁
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        },"t1").start();
    }

}
  • 用countdownLatch(门闩)来实现,监视线程在集合长度!=5时,拴住,添加线程在集合长度==5时拴住,并剪掉一个门闩,让监视线程得以执行
public class CountLatch_practice {
    /* 实现一个容器,提供两个方法,add,size
     *  写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个是,线程2给出提示并结束
     */
    public static void main(String[] args) {
        // 搞一个门闩
        CountDownLatch latch = new CountDownLatch(1);

        CountDownLatch latch1 = new CountDownLatch(1);
        // 搞一个list
        List list = new ArrayList();


        // 搞一个监视线程
        new Thread(()->{
            if(list.size()!=5){
                try {
                    // 拴住,不能走
                    latch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("给出提示,加了5个了");
                // 类似wait和notify的使用,让下面的线程继续执行
                latch1.countDown();
            }
        }).start();

        // 搞一个添加的线程
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                list.add(new Object());
                System.out.println("add"+i);

                if(list.size()==5){
                    // 剪掉一个门闩,上面的线程就可以执行了
                    latch.countDown();
                    // 需要用两个门闩来控制,不然输出的时机会有问题
                    try {
                        latch1.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }
}
  • 用LockSupport来实现
public class LockSupprot_practice {

    /* 实现一个容器,提供两个方法,add,size
     *  写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个是,线程2给出提示并结束
     */
    static Thread t1=null,t2=null;
    public static void main(String[] args) {
        List list = new ArrayList();

        t2 = new Thread(()->{
            // 监视线程
            // 上来就把自己停止
            LockSupport.park();
            System.out.println("监视线程启动了");
            System.out.println("见识到了");
            // 唤醒添加线程
            LockSupport.unpark(t1);
        });

        t1 = new Thread(()->{
            for (int i = 0; i < 10; i++) {
                list.add(new Object());
                System.out.println(i);
                if(list.size()==5){
                    // 唤醒监视线程
                    LockSupport.unpark(t2);
                    // 停止自己
                    LockSupport.park();
                }
            }
        });

        t2.start();
        t1.start();
    }
}
  • 作业小题:用两个线程顺序打印A,1,B,2----Z,26

练习题:写一个固定容量的同步容器,拥有put和get方法,以及getCount方法,能支持2个生产者线程以及10个消费者线程的阻塞调用

  • 第一种方式,使用sync,wait和notifyAll(有一个问题就是,所有阻塞的线程都都在同一个队列中的,当notifyAll时,会将生产和消费的线程全部唤醒加入到等待队列,有可能发生一种情况生产者线程唤醒后,又竞争得到了锁,有wait了一下,这样就浪费了资源)
public class Container<T> {

    // 写一个固定容量的同步容器,拥有put和get方法,以及getCount方法,能支持2个
    // 生产者线程以及10个消费者线程的阻塞调用,使用wait和notifyAll来实现
    final private List<T> lists = new ArrayList<T>();

    // 最多10个元素
    final private int MAX = 10;

    private int count = 0;
    // 生产者,如果容器中达到最大容量,停止生产
    public synchronized void put(T t){
        while (lists.size()==MAX){ // 这里用while,当线程被唤醒之后再判断一下是不是满的
             try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        lists.add(t);
        ++count;
        // 唤醒消费者线程
        this.notifyAll();
    }

    // 消费者,如果容器中没有了,就停止消费
    public synchronized T getCount(){
        while (lists.size()==0){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 移除第一个
        T t1 = lists.remove(0);
        -- count;
        // 唤醒生产者线程
        this.notifyAll();
        return t1;
    }

    public static void main(String[] args) {
        Container<String> container = new Container<>();
        // 启动消费者线程
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                for(int j=0;j<5;j++){
                    System.out.println(container.getCount());
                }
            },"c"+i).start();
        }

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 启动生产者线程
        for (int i = 0; i < 2; i++) {
            new Thread(()->{
                for (int j=0;j<25;j++){
                    container.put(Thread.currentThread().getName()+" "+j);
                }
            },"p"+i).start();
        }

    }

}
  • 使用lock锁,可以新建两个队列,让生产者停止自己的队列,唤醒消费者线程的队列。
public class ContainerLock<T> {

    // 写一个固定容量的同步容器,拥有put和get方法,以及getCount方法,能支持2个
    // 生产者线程以及10个消费者线程的阻塞调用,使用lock中的newCondition

    Lock lock = new ReentrantLock();
    // 新建一个等待队列
    Condition producer = lock.newCondition();
    // 再新建一个等待队列
    Condition consumer = lock.newCondition();

    LinkedList<T> list = new LinkedList<>();
    // 最多放10个
    final int MAX = 10;
    // 放了几个
    int count = 0;

    // 生产者方法
    public void put(T t) {
        try {
            lock.lock();
            // 当容器满了,停止生产,
            while (list.size() == MAX) {
                // 在生产者队列进行阻塞
                producer.await();
            }
            list.add(t);
            ++count;
            // 唤醒消费者队列
            consumer.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public T get() {
        T t = null;
                
        try {
            lock.lock();
            // 当容器里面空了,停止消费
            while (list.size() == 0) {
                // 在消费者队列中阻塞
                consumer.await();
            }
            // 移除第一个
            t = list.removeFirst();
            count--;
            // 唤醒生产者线程
            producer.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        return t;
    }

    public static void main(String[] args) {
        ContainerLock<String> c = new ContainerLock<>();
        // 10个消费者线程
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                for (int j=0;j<5;j++){
                    System.out.println(c.get()+j);
                }
            },"c"+i).start();
        }
        
        // 2个生产者线程
        for (int i = 0; i < 2; i++) {
            new Thread(()->{
                for (int j = 0; j < 25; j++) {
                    c.put(Thread.currentThread().getName()+j);
                }
            },"p"+i).start();
        }
    }
}

源码阅读原则

  1. 读源码很难!理解别人思路!需要数据结构基础,设计模式基础
  • 1.跑不起来不读(用debug跑起来,一层一层点进去)
  • 2.解决问题就好-目的性
  • 3.一条线索到底
  • 4.无关细节略过
  • 5.一般不读静态
  • 6.一般读动态方法
  • 需要画两种图:1.方法之间的调用图,具体见电脑桌面 2.类之间的类图。具体见电脑桌面

Lock源码解读(设计模式:模板模式templateMethod)

  • 主要使用的是aqs,里面的大概流程cas+volatile state。通过 volatile state 判断当前线程是否获取了锁,如果没获取,加到一个双向链表的等待队列中

AQS(AbstractQueuedSynchronizer)抽象的队列同步器-----原理

  • https://blog.csdn.net/u012881584/article/details/105886486?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param(详见这个链接,说的很清楚)
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b4adulvK-1606805531182)(8F63E637A8164A6CBDC01517F9BE6834)]
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qIEeKVNW-1606805531187)(D59B277FBB774617A82A4688A9CC60EF)]
  • reentrantLock,reentrantReadWriteLock,countDownLath,Semaphore,CylicBarrier都是基于AQS来实现的
  • AQS内部维护了一个volatile int state,和一个FIFO(先进先出)的双链表结构的线程等待队列(多线程竞争资源被阻塞时会进入此队列),通过CAS的方式进行锁的获取
  • AQS内部分为独占锁(reentrantLock),共享锁(Semaphore,countDownLatch等)
  • 由于AQS是一个抽象类,里面没有具体的实现,仅仅是抛出了异常,所以一般都由他的子类做具体的实现。
  • 以ReentrantLock来举例,使用的就是子类sync.lock()(这个是互斥锁的方法,sync这个静态内部类中还有其他的分享锁,释放锁的方法)。当同时又三个线程并发抢占锁,线程一抢占成功,线程二,三抢占失败。具体过程为:
  • 非公平锁的情况
  1. 三个线程都通过tryAcquire(int acquires)里面采用cas的方式进行锁的抢占,不论谁抢到了,都将state的状态改为1,并且设置对象独占锁线程为当前线程
  2. 如果state=0,就说明当前没有线程得到锁,则cas抢锁,抢到了则设置独占对象为当前线程,返回true。如果不为0,说明当前对象的锁已经被其他线程占有,接着判断占有锁的线程是否为当前线程,如果为当前线程则累加state的值,这就是可重入锁的具体实现,累加state值,释放锁的时候也需要依次递减state值。
  3. 如果即没抢到锁,得到锁的也不是当前线程,则执行addWaiter(Node node)(也就是将当前线程加入到一个双向链表结构的等待队列中,并挂起当前线程,等待其他线程释放锁来唤醒他),创建一个和当前线程绑定的node节点,node为双向链表结构。此时如果tail指针为空,通过一个for(;;)+cas的方式,一直循环直到把当前线程插入到双向链表尾部。这样的好处是可以代替sync锁住整个链表,采用cas(最后一个节点的方式),提高了效率
  4. addWaiter(Node node)—添加到队列尾部之后,如果他前面的节点是头节点(也就是已经获得锁的节点),就尝试以cas的方式去获取一下锁,获取到了直接返回,下一步阻塞当前线程节点,等待前置节点释放锁后来唤醒队列中的下一个节点
  • 公平锁的情况(公平锁自己又实现了一个tryAquire()
  1. 会先判断state值,如果不为0且获取锁的线程不是当前线程,直接返回false代表获取锁失败,被加入等待队列。如果是当前线程则可重入获取锁。
  2. 如果state=0则代表此时没有线程持有锁,执行hasQueuedPredecessors()判断AQS等待队列中是否有元素存在,如果存在其他等待线程,那么自己也会加入到等待队列尾部,做到真正的先来后到,有序加锁
VarHandle
  1. 普通属性原子操作
  2. 比反射速度快,直接操作二进制码
  • VarHandle可以获取任意变量的引用
  • 如果要原子性地增加某个字段的值,除了用atomic类,还可以用varHandel,以cas的方式进行相加
  • AQS中用的就是varHadle

ThreadLocal

  • Spring 的声明式事物用的ThreadLocal(比如有多个数据库连接,spring的声明式事物可以把他们合成一个事物,这样就需要保证每个连接都是一样的,所以采用ThreadLocal,直接从本地map来拿连接,而不是从连接池来拿链接)
  • 向一个对象中添加属性,保证只有添加的那个线程访问的到。其他线程访问不到
  • 原理(源码)----->当向ThreadLocal中set一个对象的时候,首先获取当前线程,在Thread类中有一个map,将set的对象放到当前线程的map集合中
  • ThreadLocal中的内存泄露问题
  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YzjIVWYn-1606805531190)(CC856C54AD2F4D92A472C600A0CF1E21)]
  1. 内存泄露是指有一块内存永远无法被回收
  2. 在threadLocal.set中,实际上是向threadlocal中的一个map中插入对量,map中的key为this,也就是ThreadLocal,这里的key采取的就是弱引用,这样的好处就是当new ThreadLocal()这个强引用消失时,key的弱引用也会被回收,这时map中的key就会变成null,key变成null之后,就会导致value永远都访问不到,依旧存在内存泄露,所以在每次使用完ThreadLocal后都remove一下
  3. 详情图见电脑桌面
public class ThreadLocal_01 {

    static ThreadLocal<Person> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        // 这个线程获取不到下面线程添加的对象
        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(threadLocal.get());
        }).start();

        // 这个线程添加数据,只有这个线程能获取到
        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Person person = new Person();
            person.name="ji";
            threadLocal.set(person);
            System.out.println(threadLocal.get().name+"t2");
        }).start();
    }




    static class Person{
        String name = "2";
    }
}

Java中的四种引用

  • 强弱软需
  1. 强引用----------->Object o = new Object(),只要存在当前引用,就不会被垃圾回收。如果o=null,则会被回收
  2. 软引用(可做缓存使用)----------> SoftReference<byte[]> m = new SoftReference(new byte[1024* 1024 *10])。当堆内存的大小够用时,即时调用System.gc()也不会被回收,当堆内存不够时,会自动回收软引用,回收后,m对象为null。
public class SoftRerfence_01 {

    public static void main(String[] args) {
        SoftReference<byte[]> m = new SoftReference<>(new byte[1024*1024*10]);
        System.out.println(m.get());
        System.gc();
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(m.get());
        //堆内存大小不够时,会自动回收软引用
        byte[] b = new byte[1024*1024*15];
        System.out.println(m.get());
    }
}
  1. 弱引用------------->只要遇到垃圾回收就会被回收(可以获取到里面的值)。弱引用中的问题在上文ThreadLocal中
  2. 虚引用------------->直接被垃圾回收回收,根本获取不到里面的值,当对象被回收时,可以通过队列检测到,然后清理堆外内存(操作系统的内存)

多线程与高并发第六节(容器部分)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rG6kIkGn-1606805531193)(CF6DBA6040FE4E6FA404BFC65B1662DC)]

  • 物理存储结构只有两种:1.数组-----2.链表
  • Vector和hashtable是最原始的版本,所有的方法都带锁的,后来有了hashmap和Collections.sycnMap(new hashMap),后来又有了ConcurrentHashMap
Map 相关
  • 向里面插入的效率上hashTable和Collections.sycnMap效率基本差不多,读取的时候ConcurrentHashMap效率最高,非常高
  • hashMap是用hash表来实现的,是没有排序的,treeMap使用红黑数来实现的,是排好序的,迭代的效率比较高
  • 只有concurrentHashMap,并没有concurrentTreeMap(在红黑树的结构下使用cas操作是非常复杂的),所有如果想要在高并发的情况下对map进行排序,需要使用ConcurrentSkipListMap()(通过跳表来实现)
  • 跳表的数据结构图。-----》解释一波,正常的链表在数据特别多时查询是非常慢的,但是使用跳表,在已有的链表上抽出一些重要的节点,再形成一个链表,依次类推,查询的时候就可以依次从上向下查询,提高查询速度
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j6QZkrrX-1606805531196)(FE260123AC0942F48F59E54281EF605D)]
List相关
  • Vector是线程安全的
  • 高并发情况用queue,queue是专门为高并发提供的,如果需要去重也可以用currentSet
  • List 中的==CopyOnWriteArrayList()==适用于读特别多,写比较少的情况 是线程安全的,写时复制,简单来说,读的时候不加锁,在写的时候加锁,加锁后获取当前数组和当前数组的长度,然后copy一份将数组长度加一,在复制后的集合中添加元素,添加完成后再讲引用指向新的集合
  • List 中的Collections.synchList(new ArrayList())
Queue相关
  • Queue-> ConcurrentLinkedDeque
  • queue.offer,queue.peek,queue.poll
public static void main(String[] args) {
        // queue里面的方法都是线程安全的
        Queue<String> queue = new ConcurrentLinkedDeque<>();

        for (int i = 0; i < 10; i++) {
            queue.offer("a"+i);// 相当于list中的add 区别是add填不进去了会抛异常,offer会给一个boolean的返回值
        }
        // peek 取出第一个元素但不会删掉值
        System.out.println(queue.peek());
        System.out.println(queue.size());
        // poll 取出第一个元素会删掉值
        System.out.println(queue.poll());
        System.out.println(queue.size());
    }
  • BlockingQueue–>LinkedBlockQueue无界的阻塞的队列(天生的实现了生产者消费者模型
  • bqueue.put() 如果放满了就会等待(链表是可以一直往里面放的,除非内存满了)
  • bqueue.take() 如果取不到了,就等待
  • BlockingQueue–>ArrayBlockingQueue有界的阻塞队列
public static void main(String[] args) throws InterruptedException {
        // 有界的阻塞队列
        BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(10);

        for (int i = 0; i < 10; i++) {
            blockingQueue.put("a"+i);
        }
        // 如果满了,就会阻塞住,等待消费者来消费
//        blockingQueue.put("满了");
        // blockingQueue.add("m");// 满了就会报异常
        //boolean m = blockingQueue.offer("m");// 成功与否会有一个返回值
    }
  • BlockingQueue–>DelayQueue可按照时间排序的阻塞队列(用作时间调度)
  1. 添加到delayQueue队列中的元素需要实现Delayed接口,重写里面的两个方法,一个定义时间排序的规则,一个定义获取时间的方法
  2. DelayQueue底层实现为PriorityQueue,这个队列直接就可以排序
public class DelayQueue_01 {

    static BlockingQueue<MyTack> tacks = new DelayQueue<>();
    // 必须实现delayed方法
    static class MyTack implements Delayed{

        String name;
        long runningTime;

        MyTack(String name,long runningTime){
            this.name = name;
            this.runningTime = runningTime;
        }

        @Override
        public long getDelay(TimeUnit unit) {
            // 将毫秒转成微秒
            return unit.convert(runningTime-System.currentTimeMillis(),TimeUnit.MICROSECONDS);
        }

        // 指定排序规则
        @Override
        public int compareTo(Delayed o) {
            if(getDelay(TimeUnit.MICROSECONDS)<o.getDelay(TimeUnit.MICROSECONDS))
                return -1;
            else if (getDelay(TimeUnit.MILLISECONDS)>o.getDelay(TimeUnit.MILLISECONDS))
                return 1;
            else return 0;
        }

        @Override
        public String toString() {
            return "MyTack{" +
                    "name='" + name + '\'' +
                    ", runningTime=" + runningTime +
                    '}';
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 获取当前系统时间,毫秒
        long now = System.currentTimeMillis();
        MyTack t1 = new MyTack("t1",now+1000);
        MyTack t2 = new MyTack("t2",now+2000);
        MyTack t3 = new MyTack("t3",now+1500);
        MyTack t4 = new MyTack("t4",now+2500);
        MyTack t5 = new MyTack("t5",now+500);

        tacks.put(t1);
        tacks.put(t2);
        tacks.put(t3);
        tacks.put(t4);
        tacks.put(t5);

        for (int i = 0; i < 5; i++) {
            System.out.println(tacks.take());
        }

    }

}
  • comparable使用方法
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bdod2z5E-1606805531200)(193AF583CCC34040A2A8F99AE2CDC4FF)]
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zwi33Lmj-1606805531202)(3FACBBBF802C49C5A2769316A4844E17)]
  • priorityQueue----DelayQueue底层使用的就是priorityQueue
public static void main(String[] args) {
        PriorityQueue<String> priorityQueue = new PriorityQueue<>();

        priorityQueue.offer("a");

        priorityQueue.offer("g");
        priorityQueue.offer("c");
        priorityQueue.offer("r");
        for (int i = 0; i < 5; i++) {
            System.out.println(priorityQueue.poll());
        }
    }
  • blockingQueue----》SynchronousQueue() 容量为0的queue,用于手对手的线程交换
public static void main(String[] args) throws InterruptedException {
        // 容量为0
        BlockingQueue blockingQueue = new SynchronousQueue<>();

        new Thread(()->{
            try {
                // 阻塞住,没有元素就一直等着
                System.out.println(blockingQueue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        // 这里用add,offer方法添加都会报错
        blockingQueue.put("aaa");
        System.out.println(blockingQueue.size());

    }
  • TransferQueue------->LinkedTransferQueue(),这个queue的主要区别是有一个transfer方法,这个方法必须添加完之后等到结果才会走
public static void main(String[] args) {
        TransferQueue<String> transferQueue = new LinkedTransferQueue<>();
        // 搞一个阻塞的消费者线程,拿不到就一直阻塞这
        new Thread(()->{
            try {
                System.out.println(transferQueue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        try {
            // transfer也是添加,但是添加完必须被消费,这个线程才会结束
            // put是添加完了,这个线程就结束了
            transferQueue.transfer("aaa");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
Map,List小结
  • 只有一个线程就用hashmap,ArrayList,linkedlist
  • 高并发情况,线程执行时间比较短,就用ConcurrentHashMap,ConcurrentLinkedQueue
  • 代码执行时间特别长,并发量不高,用syncHashMap,syncList
面试题:queue和List区别
  • queue主要是针对高并发的,这里面添加了对线程友好的api,比如offer(添加),peek(取出不删除),poll(取出删除)
  • 还有blockingQueue阻塞的队列,这里面还有put(阻塞的添加),take(阻塞的取出)

线程池

  • 5中状态
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fcPVVY8Z-1606805531204)(DA4C9B7E09304428A094F094C246EB0B)]
接口
  • Executor
  • ExecutorService
  • Callable ---->这个接口和runnable比较像,区别就是这个接口有返回值,比如通过这个线程计算一个数,就可以异步的返回计算的结果
 public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<String> callable = new Callable() {
            @Override
            public String call() throws Exception {
                return "哈哈哈";
            }
        };
        // 创建一个线程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        // 将执行任务放提交到线程池中,什么时候执行由线程池决定,将返回结果存在future中,异步的
        Future<String> submit = executorService.submit(callable);
        // 阻塞的从future中获取结果
        System.out.println(submit.get());

        executorService.shutdown();

    }
  • FutureTask---->这个接口可以将任务的执行与返回的结果融于一身,这个接口可以将执行完任务的返回值也放到future中。也就是说既是一个runnable也是一个future
public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<String> f = new FutureTask<>(()->{
            return "哈哈";
        });

        new Thread(f).start();

        System.out.println(f.get());// 阻塞获取futuretask里面的返回值
    }
  • CompletableFutrue----->可以对多任务进行集中管理,比如有三个任务需要三个任务都执行完,再获取结果,或者三个任务,只要有一个执行完就获取结果。或者对一个任务的结果进行处理。这里的每一步都是异步的
public class CompletableFuture_01 {

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        // 把这三个任务异步的加到future中
        CompletableFuture<Long> tb = CompletableFuture.supplyAsync(() -> getTaob());
        CompletableFuture<Long> jd = CompletableFuture.supplyAsync(() -> getJd());
        CompletableFuture<Long> pdd = CompletableFuture.supplyAsync(() -> getPdd());
        // 必须这三个任务都执行完了,才能够结束
//        CompletableFuture.allOf(tb,jd,pdd).join();
        // 有一个执行完了就可以结束
        CompletableFuture.anyOf(tb,jd,pdd).join();

        long end = System.currentTimeMillis();

        System.out.println(end-start);

//        CompletableFuture.supplyAsync(()->getTaob()).thenApply(String::valueOf).thenApply(str->"price"+str).thenAccept(System.out::println);
    }

    public static long getTaob() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return 1;
    }

    public static long getJd(){
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return 4;
    }

    public static long getPdd() {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return 8;
    }

}
两种线程池
  • ThreadPoolExecutor(所有的线程都用一个队列)
  • ForkJoinPool(分解,汇总线程池。每个线程都有一个队列)
    • 分解汇总的任务
    • 用很少的线程可以执行很多的任务(子任务)TPE做不到先执行子任务
    • cpu密集型

  • ThreadPoolExecutor中7个核心参数(背过)
    • corePoolSize 线程池中核心线程的个数
    • maximumPoolSize 线程池中最大线程数
    • keepAliveTime 线程池中无用的线程保持多场时间
    • TimeUnit 时间单位,秒,毫秒,微秒
    • BlockingQueue 可以放各种装任务的blockingqueue
    • ThreadFactory 线程工厂,定义线程的产生方式,阿里开发手册中提出必须指定线程的名字,不然回溯的时候很费劲的发现问题,java中也提供了一种默认的线程生产方式
    • RejectedExecutionHandler 拒绝策略,比如线程池中有两个核心线程,来了两个任务,把两个核心线程占了,再来任务时,新的任务加入到队列中,如果队列也满了,则新生产一个线程来处理队列之外的任务,如果达到线程池中的最大线程数,则执行拒绝策略
      • jdk默认提供了4种拒绝策略:
      • 1.AbortPolicy 抛异常
      • 2.DiscardPolicy 扔掉,不抛异常
      • 3.DiscardOldestPolicy 扔掉排队时间最长的
      • 4.CallerRunsPolicy 调用者处理任务
      • 还可以自定义策略,并且一般都会自定义策略,比如把订单消息保存到redis或者kafuka
public class ThreadPoolExecutor_01 {

    /**
     *
     */
    static class Task implements Runnable {
        private int i;

        public Task(int i) {
            this.i = i;
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "Task" + i);

            try {
                // 一直阻塞住当前线程
                System.in.read();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        @Override
        public String toString() {
            return "Task{" +
                    "i=" + i +
                    '}';
        }
    }

    public static void main(String[] args) {

        //创建一个线程池
        ThreadPoolExecutor tpe = new ThreadPoolExecutor(2, 4, 60,
                TimeUnit.SECONDS, new ArrayBlockingQueue(4),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardOldestPolicy());
        // 把执行器放到线程池中,也就是向线程池中添加了8个任务
        for (int i = 0; i < 8; i++) {
            tpe.execute(new Task(i));
        }
         System.out.println(tpe.getQueue());
        // 再放一个
        tpe.execute(new Task(100));

        System.out.println(tpe.getQueue());


        tpe.shutdown();
        


    }
}

Executors - 线程池的工厂

  1. SingleThreadExcutors----单线程的线程池

为什么会有单线程的线程池?
线程池有任务队列,直接new Thread()还需要自己进行任务队列的管理
线程池有生命周期管理

  • 线程池的底层实现就是new 了一个ThreadPoolExecutor(),核心数为1,最大线程数为1
ExecutorService s = Executors.newSingleThreadExecutor()
  1. CachedPool
  • 这个线程池内部实现核心数为0,最大线程为Inter.MAX_VALUE,队列用的是SynchronousQueue。
  • 也就是说这个线程池,来一个任务就会新建一个线程,这样也是不好的,任务不会堆积时用
  • 用在线程来的比较急,来一个就必须处理一个
  1. FixedThreadPool 固定线程数的线程池。
public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
  1. ScheduledThreadPoolExecutor 定时任务的线程池 DelayedWorkQueue–可以定时的队列
public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }
  1. 并发和并行
  • 并发指任务的提交
  • 并行值任务执行

ForkJoinPool系列

  • ForkJoinPool 可以把多个任务分波执行,比如1000个任务,分两波,或者4波,执行完了之后再汇总
  • workStealingPool(每个线程把自己队列中的任务执行完之后,去别的队列尾偷一个过来执行)

ParallemStream 并行流

  • 这个流底层使用的也是ForkJoinPool

JMH

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UxkW5Bmi-1606805531206)(1895243B131441AF939F1D0705325F31)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-egUfZkMP-1606805531207)(4923F6816FA14F3FA1E3B412BBCEE9E1)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9KM944s2-1606805531209)(3725033F993F4700B65D901C16364B39)]

public class PS {
    static List<Integer> list = new ArrayList<>();
    static {
        Random random = new Random();
        for (int i = 0; i < 1000; i++) {
            list.add(100000+random.nextInt(100000));
        }
    }

    static void foreach(){
        list.forEach(v->isPrime(v));
    }

    static void parallel(){
        list.parallelStream().forEach(PS::isPrime);
    }

    static boolean isPrime(int num){
        for (int i = 2; i <num/2 ; i++) {
            if(num%i==0)
                return false;
        }
        return true;
    }

}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gwKn9wra-1606805531211)(7DD04ED7BF054498AA5D2703947C14B9)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YrOl5vmf-1606805531213)(C6BB5D03621545D4BDE0D7E4D7E4C74E)]

public class PSTest {
    @Benchmark
    @Warmup(iterations = 1,time = 3)// 预热--jvm对多次运行的程序会有个优化,而不是运行class文件,所以需要提前预热
    @Fork(5)// 用多少线程去执行
    @BenchmarkMode(Mode.Throughput)// 基准测试模式  Throughput吞吐量,看这个方法每秒钟可以执行多少次
    @Measurement(iterations = 1,time = 3)// 调用这个方法多少次。一般调用很多次取平均值
    public void testForEach(){
        PS.foreach();
    }
}

Disruptor(内存里用于存储数据的高速队列)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HcihzgPt-1606805531214)(94FC5D04B5FE4569927C0E5B7C8658C2)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7L8Va8rY-1606805531216)(9C435AC91E81475A8684D23516EE417A)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vc9frfc0-1606805531219)(7212FAB7A79B493BA5D8E8F91B9E433E)]

  • 如果环形队列已经满了,但是消费者还没来消费,它是有策略的,常见的一种就是阻塞式的,如果还没被消费,阻塞等待8分钟,等待消费者线程来唤醒,消费过后才会被覆盖
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3rpAG3KU-1606805531221)(A83D1C317D7B4EC788E8B6F73991A69B)]
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值