并发编程之线程基础

目录

前言

进程与线程

并行与并发

并发三要素

Java线程基础

创建线程

线程的状态

 线程状态转换

常见方法

start和run

sleep和yield

join

interrupt

wait notify

sleep和wait比较

Park&Unpark

park于wait的比较

两阶段终止

守护线程

同步模式之保护性暂停

异步模式之生产者/消费者

 JMM

可见性

有序性

volatile原理

原子整数

原子引用

原子数组

unsafe对象

不可变类

JUC工具类

Semaphore

CountDownLatch(倒计时锁)

CyclicBarrier(循环栅栏)


前言

进程与线程

  • 进程:当一个程序被执行,从磁盘将代码加载到内存中就开启了一个进程;进程可以看成是程序的实例,有的程序能开几个进程(记事本,绘图...)有的只能开一个(网易音乐,360..)

  • 线程:一个进程有多个线程;线程是一个指令流,将指令流中的指令交给CPU去执行;Java中线程是最小的调度单位,进程是最小的资源分配单位;进程是不活动的只作为线程的容器

  • 比较:进行时相互独立的,线程是共存在一个进程中的可以共享资源;进程间通信需要使用IPC(同一计算机)或者HTTP(不同计算机);

并行与并发

  • 并发(concurrent):是同一时间应对(dealing with)多件事情的能力

    • 例如单核CPU同一时刻只能执行一个线程,此时有多个线程就会在多个线程中来回切换,只是时间很短所以微观上是串行的宏观上则是并行的

  • 并行(parallel):是同一时间做(doing)多件事情的能力

    • 例如多核CPU此时同一时刻就可以执行多个线程,但是一般情况下线程数量都是大于CPU核数的,因此会并行并发同存

应用

  • 异步调用:即不需要等待结果就可以执行下一行代码,就是异步调用(Async)

    • 同步调用:需要等待上一步的结果才能执行下一行代码,即让多个线程步调一致

  • 提高效率:充分利用多核CPU的优势提高效率

并发三要素

  • 可见性:由于CPU缓存引起,典型的多个线程对共享资源进行操作,这时会发生执行线程A的CPU将共享资源加载到自己的缓存中并进行了操作,而这时执行线程B的CPU并未及时看到A的操作因此读到的依旧是为更改的共享资源

  • 原子性:由于分时复用导致,典型的转账问题,如果不能做到一起成功或者一起失败就有可能导致A账户扣了钱B账户却没有收到

  • 有序性:重排序导致,由于执行执行程序时为了提高性能编译器和执行器会对指令进行重排序

Java线程基础

创建线程

  • 继承Thread类重写run方法,Thread类也实现了Runnable接口

  • 实现Runnable接口

  • 实现Callable接口

    public static void main(String[] args) {
        Thread t = new Thread(() -> System.out.println(Thread.currentThread().getName()+"running"),"r1");
        t.start();//没有start的run方法就是一个普通的方法
    }
  • 实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。

运行

  • 每个线程都有自己独立的栈互不干扰,线程启动就会分配一个属于他自己的栈内存;这些独立的栈中运行这属于线程私有的各类方法(栈帧)

  • 线程上下文切换:因为一些原因导致CPU不再执行当前线程,转而执行另一个线程;例如:线程的cpu时间片使用完,垃圾回收导致STW,有更高优先级的线程,或者线程自己调用了sleep,yield等

    • 在线程切换时操作系统需要记录当前线程的状态,并且恢复另一个线程的状态(程序计数器,局部变量表等等);频繁的切换回导致性能降低

线程的状态

  • 操作系统来区分有五种:新建;可运行;运行;阻塞;终止

  • Java(Thred.start())来区分六种:新建(New);可运行(Runnable);阻塞(Blocked);无限期等待(Wating);等待(Timed_Waiting);死亡(Terminated)

    • 新建就是new出来的一个线程还没有调用start的时候

    • 其中Java中的Runnable包含了操作系统中的可运行和运行以及阻塞

    • 阻塞状态比如线程A和线程B需要竞争同一把锁,而线程A拿着锁B就会进入阻塞

    • Wating状态就是无限期的等待;Timed_Waiting就是有时限的等待

    • 死亡就是线程结束了

 线程状态转换

常见方法

start和run

  • start是用来启动线程的,而run方法只是一个普通的方法,如果直接调用run就起不到多线程或者异步的效果了

  • start只能被调用一次,如果多次调用就会报出非法的线程状态异常

  • 在start前是NEW状态,start后就变成了RUNNING就可以被cpu调用了

sleep和yield

  • sleep:会让当前线程从running状态进入Timed Waiting状态,sleep可以传入一个参数单位是毫秒即睡眠时间

  • 由于可能会被其他线程使用interrupt方法唤醒所以会抛出异常InterruptedException需要捕获

  • 睡眠结束后并不会马上得到执行

    • sleep适用于无需锁的同步场景,例如后台服务可能会使用到while(true)来执行一些代码,如果不使用sleep就会产生空转的现象把CPU全部占满导致其他程序无法使用,这时只需要sleep()就可以避免

  • yield:让出,让位;在调用yield方法后该线程会让出当前的cpu让线程从Running状态进入Runnable(就绪)状态,但不一定会让成功

  • 具体能不能礼让成功还要看cpu;类似的还有一个线程优先级(最小1最大10默认5)设置后会提醒CPU优先执行优先级高的,但仅仅是提醒而已

join

  • 实现多线程的同步调用,例如线程A正在执行,线程B需要线程A执行完以后的结果,那么就需要join(A.join)来等待A线程执行结束以实现线程的同步;如果调用的多个线程的join方法所需等待的时间取决于耗时最长的线程

  • join(long n)可以传入参数设置超时值,即如果等待n毫秒后还无结果就不等了执行下一步;如果设置的等待时间超过了线程的运行时间,在线程结束的时候就会结束等待

  • 使用的是保护性暂停模式实现

interrupt

  • 打断阻塞状态的线程(sleep wait join);在被打断后会抛异常然后把打断标记置为false

  • 打断正在运行的线程;在被打断后自己选择是否停止,通过isInterrupted获取打断标记

  • isInterrupted和interrupted的区别就是后者会在判断后将打断标记清除

wait notify

  • Owner的线程发现自己有条件不满足,调用了wait方法,于是进入了WaitSet进入阻塞状态(Waiting)

  • blocked和waiting都是阻塞状态,全都不占用时间片;只是阻塞原因不同,waiting是已经获得锁又调用了wait方法放弃了锁,blocked是在等待竞争锁资源

  • blocked会在锁释放时被唤醒进入锁的抢夺;waiting会在Owner线程调用notify或者notifyAll方法时被唤醒然后进入blocked状态之后再重新竞争锁

  • wait,notify,notify all都是object的方法wait还有一个带参数的方法,即设置等待时间,如果时间到了就醒了

    • wait使用时最好使用while来判断调价是否成立,这样可以有效避免虚假唤醒(即如果使用if判断可能会发生唤醒了线程条件依然不满足导致线程没有执行代码就结束了);有多个线程在使用锁时时使用notify all来唤醒因为notify是随机唤醒一个无法保证正确的唤醒

sleep和wait比较

  1. sleep是Thread的方法,wait是object的方法

  2. sleep随时都能用,而wait必须配合synchronized才可以使用

  3. sleep阻塞时不会释放锁资源,wait会释放锁资源

Park&Unpark

  • LockSupport类中方法;使用park方法会让线程进入Wait状态,之后使用Unpark(park的线程)唤醒park的线程

park于wait的比较

  • wait和notify,notify all必须使用synchronized配合使用;而park不需要

  • park的unpark可以精准的唤醒某个线程,而notify只能随机唤醒一个notify all会全部唤醒

  • unpark可以再park之前调用也可以在之后调用都会唤醒线程;而notify只能在wait之后调用否则就唤不醒了

两阶段终止

  • 使用isInterrupted方法进行判断是否被打断,如果在阻塞状态被打断就在catch块重新调用一次interrupt方法,这样打断标记就是true了就可以优雅的关闭了;如果是正常执行代码期间被打断了那么就会自己优雅的退出了线程状态转换

守护线程

  • 默认情况下只要还有Java线程在执行,Java进程就不会结束;而守护线程是在其他非守护线程结束之后就会结束(setDaemon(true)默认是false);典型的应用比如GC垃圾回收线程

同步模式之保护性暂停

  • 即 Guarded Suspension,用在一个线程等待另一个线程的执行结果,要点:

  • 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject;如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者);且保护性暂停中生产结果的线程和使用结果的线程要一一对应

  • JDK 中,join 的实现、Future 的实现,采用的就是此模式;因为要等待另一方的结果,因此归类到同步模式

异步模式之生产者/消费者

  • 不同于保护性暂停在于不粗要生产结果的线程和消费结果的线程一一对应

  • 它的实现要借助一个消息队列,这个队列可以平衡生产者和消费者之间的资源;让生产者和消费者只专注于自己的事

  • 消息队列是有容量限制的,当达到容量时不会再放入数据,新来的的数据会阻塞;当消息队列已经空了时不会再消耗JDK中的各种阻塞队列就是采用的这种模式

 JMM

  • java内存模型,是一个抽象的概念,主要是将主存(共有变量)和工作内存(线程私有,局部变量),方便我们操作

  • 主要体现三个方面:原子性(例如使用synchronized),可见性(避免CPU缓存),有序性(避免CPU指令优化影响)

可见性

 static boolean run = true;
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
           while (run){
               //....这里如果写了syso也会保证可见性,因为println放使用了synchronized
           }
        });
        
        Thread.sleep(1000);
        run=false;//在主线程设为false后while并不会停止;因为JIT会把run放入t线程的工作内存中不会每次都去读取主存中的run值
    }
   上面的代码就导致了可见性问题;解决方法就是加上修饰符volatie,这样线程每次都是去主存中读取而不是在工作内存中读取了,保证了可见性,但是也降低了效率
  • volatile可以修饰成员变量或者静态成员变量以避免线程从自己的工作缓存获取值,而是去主存获取

  • volatile只能保证可见性,适用于一个线程写其他线程都是读的场景

有序性

  • 在不改变结果的前提下,将指令的各个阶段(取指令,指令译码,执行指令,内存访问,数据写回)通过重排序和组合实现指令并行,可以提高效率

  • 怎么禁止呢 巧了还是volatile可以避免指令重排序;原理就是加了一个写屏障这样被加了volatile的变量前面的代码就不会排到它后面了

  • volatile只能保证本线程的有序性保证不了线程间的那是CPU时间片决定的

volatile原理

  • volatile的底层原理是内存屏障;对加了volatile的变量的写指令加的是写屏障,对读指令加的是读屏障

  • 那么如何保证可见性能:写屏障前的所有代码都会同步到主存中;读屏障后的变量都会读取主存中的值而不再是工作内存中的值

  • 有序性:在操作volatile修饰的变量时会在后面加一个写屏障,写屏障前的代码不会出现在写屏障后面;读取时反之,在读取volatile修饰的变量前加入一个读屏障,后面的代码就不会出现在变量之前

  • synchronized既能保证原子性又能保证可见性和有序性,但这一切都时基于原子性的基础上,由于synchronized每次释放锁都会把共享内容放入主存中所以就避免了可见性,而有序性是因为synchronized执行的代码只有一个线程,单线程不管怎么重排序都是不会有影响的,但若把锁的一部分放在synchronized外部就无法保证了,也就是说synchronized并不能组织重排序,例如dcl问题

原子整数

  • 常见的原子类有:AtomicInteger,AtomicBoolean,AtomicLong

private AtomicInteger balance;
balance = new AtomicInteger(100)

常用API

方法作用
public final int get()获取 AtomicInteger 的值
public final int getAndIncrement()以原子方式将当前值加 1,返回的是自增前的值 i++
public final int incrementAndGet()以原子方式将当前值加 1,返回的是自增后的值 ++i
public final int getAndSet(int value)以原子方式设置为 newValue 的值,返回旧值
public final int addAndGet(int data)以原子方式将输入的数值与实例中的值相加并返回
就看方法名就是了get在前面就是返回旧值Get在后面就是返回处理后的值
public final int updateAndGet(IntUnaryOperator updateFunction)传入的参数是一个函数式接口,可以是加减乘除任意运算

原子引用

  • AtomicReference、AtomicStampedReference、AtomicMarkableReference

  • 我们需要保护的共享变量并不是只有基本类型,例如想保护String或者是BigDecimal这些类型时就需要使用到原子引用的类了

private AtomicReference<指定类型> balance;
balance = new AtomicReference<>("ava");
  • 使用AtomicReference会导致ABA问题(例如一个线程要将A-C;在此期间有一个线程将A-B,另一个线程将B-A那么此A已经非彼A了);解决方法就是使用AtomicStampedReference类,这个类加入了版本号这样就能分清楚是哪个A了

  • AtomicStampedReference这个类使用的是一个int数字加一的方法进行版本号管理,那么有时候我们并不关心被更改了多少,而时关系有没有被更改过,这时可以使用AtomicMarkableReference类,这个类会传入一个Boolean类型的值来判断是否被更改过

原子数组

  • 原子数组类:AtomicIntegerArrayAtomicLongArrayAtomicReferenceArray

  • 由于普通数组是无法保证线程安全的,所以提供了原子数组来操作API都类似

unsafe对象

  • Unsafe 对象提供了非常底层的,操作内存、线程的方法,Unsafe 对象不能直接调用,只能通过反射获得

  • 可以发现AtomicInteger以及其他的原子类底层都使用的是Unsafe类

public static void main(String[] args) {
    MyAtomicInteger atomicInteger = new MyAtomicInteger(10);
    if (atomicInteger.compareAndSwap(20)) {
        System.out.println(atomicInteger.getValue());
    }
}
​
class MyAtomicInteger {
    private static final Unsafe UNSAFE;
    private static final long VALUE_OFFSET;
    private volatile int value;
​
    static {
        try {
            // 需要反射获取
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            UNSAFE = (Unsafe) theUnsafe.get(null);
            // 获取 value 属性的内存地址,value 属性指向该地址,直接设置该地址的值可以修改 value 的值
            VALUE_OFFSET = UNSAFE.objectFieldOffset(
                           MyAtomicInteger.class.getDeclaredField("value"));
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
            throw new RuntimeException();
        }
    }
​
    public MyAtomicInteger(int value) {
        this.value = value;
    }
    public int getValue() {
        return value;
    }
​
    public boolean compareAndSwap(int update) {
        while (true) {
            int prev = this.value;
            int next = update;
            //                          当前对象  内存偏移量    期望值 更新值
            if (UNSAFE.compareAndSwapInt(this, VALUE_OFFSET, prev, update)) {
                System.out.println("CAS成功");
                return true;
            }
        }
    }
}

不可变类

  • final变量会在赋值后加入写屏障,保证了写屏障前不会重排序,和可见性;读取时添加了读屏障保证

  • 所以final说不可变并且线程安全;类似于String类是final的,它的subString方法其实是采用了保护性拷贝来创建一个新对象

  • 但是这些不可变类只能保证某一个方法不会发生多线程问题,但无法保证多个方法的组合不会发生问题

  • 不可变类是指有成员有状态但状态在创建后就是不可变的;而还有一种无状态直接连成员变量都没了,那自然也就是安全的了

JUC工具类

Semaphore

  • Semaphore是信号量,用来限制同时访问共享资源的线程上限,共享资源有多个,也允许被多个线程访问,但是对线程上限有要求

  //参数为3就代表最多来三个线程,还可以加一个Boolean类型的参数表示公平锁与非公平锁
        Semaphore semaphore = new Semaphore(3);
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                try {
                    semaphore.acquire();//上锁
                    System.out.println("begin....");
                    Thread.sleep(1000);
                    System.out.println("end....");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    semaphore.release();//释放
                }
            }).start();
        }
  • semaphore的原理就是在创建的时候将int参数设为AQS的state值;在使用acquire方法时将state值减一并且CASState修改state值当修改到小于0(state最多=0)时就会进入AQS的park阻塞了

  • release方法中会去获取state的值(占满的时候时0),然后进行修改(1);之后执行doReleaseShared方法,判断等待状态是否为-1,之后唤醒阻塞队列中的结点,之后被唤醒的线程会去再次修改state成功之后就可以了

CountDownLatch(倒计时锁)

  • 进行线程间同步协作,等待所有线程完成倒计时;类似于游戏中等待所有玩家准备好才能开始

  • 构造参数的值为等待的数值,也就是说如果有三个线程那么参数就应该时3,每有一个线程结束任务就会让数值减一知道为0

  • await等待计数归零,countDown用来让计数减一;

CyclicBarrier(循环栅栏)

  • 和CountDownLatch功能差不多,但是它是可以重用的(计数值)CountDownLatch是不能重置的

  • 它的原理是await的时候会把计数减一,一直减到0结束;最后汇总的时候可以在构造方法中的Runnable函数式接口汇总

  • 线程池中的数量要和计数值数量一致才能达到效果

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值