美文网首页
一、多线程与线程安全

一、多线程与线程安全

作者: 小鱼你好 | 来源:发表于2022-03-16 21:21 被阅读0次

    一、多线程优势

    1,与进程相比线程切换开销更小同时在数据共享方面效率非常高。

    2,能够提升多核cup使用效率减少资源浪费。

    二、线程的状态

    1,New:新创建的状态。在线程被创建还没有调用start方法,在线程运行之前还有一些基础工作要做。

    2,Runnable:可运行状态。一旦调用start方法线程就进入可运行状态,其可以正在运行也可能没有运行,取决于操作系统给线程提供的运行时间。

    3,Blocked:阻塞状态。表示线程被锁阻塞,他暂时不活动。

    4,Waiting:等待状态。线程暂时不活动并且不运行任何代码,这消耗最少资源直到线程调度器重新激活它。

    5,Timed waiting:超时等待状态。和等待状态不同,它可以在指定时间自动返回。

    6,Terminated:终止状态。表示当前线程已经执行完毕,导致线程终止有两种情况:run方法执行完毕正常退出和因为没有捕获的异常而终止了run方法。

    三、线程的创建

    1,继承Thread类重写run方法。

    Thread本质是一个实现了Runnable接口的一个实例,使用时要注意调用start方法后不是立即执行而是使线程变成可运行状态(Runnable),什么时候运行多线程代码由操作系统决定。具体实现步骤:

    1,定义Thread的子类,并重写run方法,该run方法就代表了线程要完成的任务,run方法又被称为执行体

    2,创建Thread子类的实例,即线程的创建过程(New)

    3,调用线程的start方法启动线程使线程进入可运行状态(Runnable)

    2,实现Runnable接口并实现该接口的run方法

    具体步骤为:

    1,自定义类并实现Runnable接口,实现run方法

    2,创建Thread的实例,用实现Runnable接口的对象作为参数实例化该Thread对象

    3,调用Thread的start方法使线程进入可运行状态(Runnable)

    3,实现Callable接口重写call方法

    Callable接口是Executor框架中的功能类,类似于Runnable接口但比其功能更强大。Callable可以在任务结束后提供一个返回值且call方法可以抛出异常,运行Callable可以拿到一个Future对象(表示异步计算的结果)他提供了检查计算是否完成的方法。Future用来监视目标线程调用call方法的情况,通过调用Future的get方法已获取结果时当前线程会被阻塞直到call()返回结果。

    使用方法:

    1,创建实现Callable的类实例mCallable对象,重写call()方法里可以return执行完要返回的结果

    2,创建ExecutorService mExecutorService = Executors.newSingleThreadPool()创建线程池

    3,将mCallable对象作为参数传给线程池获取Future<String> future = mExecutorService.submit(mCallable)

    4,调用future.get()等待线程结束拿到返回结果

    四、理解中断

    1,线程在run()方法执行完毕或者在方法中出现没有捕获的异常时会终止。老版本可以在其他线程中调用当前线程的stop()方法来终止线程,此方法现在已经废弃。
    2,interrupt()方法可以用来请求中断线程,当interrupt()方法调用时线程的中断标识位将被置位,线程会不时检测中断标识位以判断当前线程是否应该中断。可以通过Thread.currentThread().isInterrupted()来获取线程是否被置位。
    3,可以调用Thread.interrupted()对中断标识位进行复位(将true设为false),但如果此时线程处于Blocked阻塞状态其无法检测中断状态,并且此时中断标识位为true则会在阻塞方法调用处抛出InterruptedException异常,且在抛出异常前会将标识位复位为false。
    4,InterruptException异常不要在底层代码进行捕获后不做处理可以做如下处理:,①,在catch中调用Thread.currentThread().interrupt()设置中断状态因为抛出异常时中断标识位会复位此时需要重新将中断标识位设为true,让外界通过判断Thread.currentThread().isInterrupted()来决定线程是否终止。②,更好的做法是不使用try直接抛出异常,让调用者去捕获这个异常做对应的处理。
    5,被中断线程不一定会终止,interrupt()方法只是为了引起线程注意,被中断的线程可以决定如何去响应中断,比较重要的线程是不会理会中断的,大部分情况线程中断会作为一个终止请求。lock.lockInterruptibly();拿到的锁可以中断执行,lock.lock()方法获取锁会阻塞。

    6,可以在Runnable实现类中添加变量来控制run()方法从而达到终止线程的目的

    public class MyRunnable implements Runnable{
        private volatile boolean on = true;
        @Override
        public void run() {
            while (on){
                System.out.println("");
            }
        }
        public void cancle(){
            on = false;
        }
    }
    

    五、同步

    1,重入锁与条件对象

    ReentrantLock表示锁能够支持一个线程对资源加锁使用方法为

    Lock mLock = new ReentrantLock();
    mLock.lock();
    try{
    ...
    }
    finally{
    mLock.unlock();
    }
    

    这一结构保证了同一时刻只能有一个线程进入临界区,即同一时刻只能有一个线程访问的代码区。一旦一个线程封锁了锁对象,其他任何线程都无法进入lock语句。使用finally进行解锁十分必要,如果临界区发生异常能够保证锁的释放从而不会阻塞其他线程。如果进入临界区时需要满足某个条件才能执行可以使用一个条件对象来管理已经获得锁但是却还不能执行的线程,条件对象也被称作条件变量。可以通过newCondition方法获取一个条件对象,用其调用await方法来使当前线程阻塞并放弃锁。当await方法调用后,线程会进入该条件的等待集并处以阻塞状态,直到另一个线程调用了同一个条件的mConditon.signalAll方法时就会激活因这一条件而阻塞的所有线程

    Lock mLock = new ReentrantLock();
    Condition mConditon = mLock.newCondition();
    mlock.lock();
    try{
    if(条件){
    //阻塞当前线程,放弃锁使其他线程获得锁进入临界区执行代码逻辑
    mConditon.await();
    }else{
    mConditon.signalAll();
    }
    }finally{
    mLock.unlock();
    }
    

    signalAll方法并不是立即激活等待线程,只是解除了他的阻塞状态,使其能够在当前线程退出临界区释放锁后通过竞争有机会获得锁从而重新进入临界区执行代码。signal方法则是随机解除某个线程的阻塞状态,如果解除的线程仍然满足临界区的阻塞条件则其还会进入阻塞状态。

    2,同步方法

    使用synchronize关键字声明方法使对象锁保护整个方法从而达到同步的效果,要调用该方法必须获得线程内部的对象锁。每一个对象内部都有一个内部锁,并且该锁有一个内部条件,由该锁管理那些试图进入synchronize方法的线程,由该锁中的条件来管理那些调用wait的线程。

    public synchronized void method(){
            if (条件){
                wait();
            }else {
                //notify();
                notifyAll();
            }
        }
    

    上面代码等价于前面提到的重入锁,wait方法相当于await,notify相当于condition.singal方法,notifyAll相当于condition.singalAll方法。
    使用同步代码块来实现同步效果:synchronize(obj)其中获得了obj锁,obj指的是一个对象例如:

    Object obj = new Object();
    synchronize (obj){
    ...
    }
    

    同步代码块一般不推荐使用,一般实现同步最好用java.util.concurrent包下提供的类,比如阻塞队列。

    3,volatile

    声明volatile关键字,编译器和虚拟机就知道volatile关键字的域可能被另一个线程并发访问

    java内存模型:

    java堆内存用来存储对象实例,堆内存是被所有线程所共享的运行时内存区域,因此存在可见性问题。局部变量、方法定义的参数不会在线程间共享则不存在可见性问题,也不受内存模型影响。
    java内存模型定义了线程和主存间的抽象关系:线程之间的共享变量存储在主存之中,每个线程都有一个私有的本地内存,本地内存中存储了该线程共享变量的副本。本地内存是java内存模型的抽象概念,并不真实存在,它涵盖了缓存、写缓冲区、寄存器等区域。java内存模型控制着线程间通信,它决定一个线程对主存共享变量的写入何时对另一个线程可见。

    线程A想与线程B通信需要如下步骤:
    一,线程A把线程A本地内存中更新过的共享变量刷新到主存当中。
    二,线程B到主存中读取线程A更新过的共享变量并在自己的本地内存中存入副本。
    由上可知对线程共享变量的修改不是直接修改主存中的变量,而是先修改自己本地内存中的共享变量值,之后再将新值刷新到主存中去供其他线程读取。

    原子性、可见性和有序性:

    原子性:

    对基本数据类型的变量读取和赋值操作是原子性,即这些操作不可中断,要么执行完毕要么不执行。

    x=0;
    y=x;
    x++;
    

    上述代码只有x=0为原子性操作,y=x涉及两个操作,读取x的值,写入工作内存,分开这两步都是原子操作。x++包括读取x的值对x的值+1,将新值写入工作内存一共三步,当一个语句包含多个操作时就不是原子性的。

    可见性:

    一个线程的修改结果,另一个线程能马上看到。volatile修饰的变量被修改后会立即更新到主存,而普通变量的值修改后不会立即更新到主存,也不能确定什么时候回更新到主存,当其他线程读取变量值时可能会读取到旧值从而无法保证可见性。

    有序性:

    java内存模型允许编译器和处理器对指令重排序,重排序不会影响单线程允行,但会影响多线程并发执行的正确性,可以通过使用volatile保证变量的同步,synchronize、lock来保证线程顺序执行同步代码,从而保证有序性。

    当一个共享变量被volatile修饰后表示一个线程修改了变量值后,变量的新值对其他线程立即可见,并且禁止使用重排序。重排序指编译器为了优化运行环境而采取的对指令进行重新排序的手段。分为编译期重排序、运行期重排序分别对应编译时环境和运行时环境。

    被volatile修饰的变量改变不保证原子性:

    如两个线程对volatile修饰的变量inc自增,因为自增过程不是原子性的,而是分成三步执行的,就可能存在a线程读取了inc值之后被阻塞,此时线程b读取inc做自增操作并将新值更新到主存,此时a线程继续对inc操作因为a线程开始已经读到了inc的值不会去主存中取新的inc值而是用的原来读取到的值,这种情况就导致了多线程操作inc异常的情况。

    被volatile修饰的变量保证有序性:

    volatile关键字禁止了重排序,因此当程序执行到volatile变量时,其前面的操作已经完全执行完毕,并且结果对后面的操作可见,指令优化时,在volatile变量前面的语句不会在其后执行,在其后执行的语句也不会到其前面执行

    volatile的使用:

    1,对变量的写操作不依赖当前值(volatile无法保证原子性)
    2,该变量没有包含在具有其他变量的不变式中。

    六、阻塞队列

    常用阻塞场景:

    1,当队列中没有数据的情况,消费端所有线程都会自动阻塞,直到有数据放入队列。
    2,当队列填满数据的情况,生产端所有线程都会自动阻塞,直到队列中有空位置时,线程被自动唤醒。

    BlockingQueue核心方法:

    offer(obj):如果可以的话将obj加人到BlockQueue队列,即队列可以容纳返回true否则false且不阻塞当前执行方法的线程。
    offer(E o,long timeout,TImeUnit unit):设定等待时间,在指定时间还未成功加入队列返回失败try处理。
    put(obj):将obj加入队列,如果队列没有空间则调用此方法的线程阻塞直到队列里面有空间。
    poll():取队列中首位的对象,取不到返回null。
    poll(long timeout,TimeUnit unit):取队列首位对象,若无数据在指定时间内有数据进入时立即取出否则返回失败try处理。
    take():取队列首位对象,若队列为空阻塞直到队列有数据进入。
    drainTo():一次取出所有数据。

    java中阻塞队列:

    1,ArrayBlockingQueue:

    数组实现的阻塞队列,先进先出,默认先阻塞的线程先访问,创建时可以指定公平访问

    ArrayBlockingQueue arrayQueue = new ArrayBlockingQueue (2000,true);
    
    2,LinkedBlockingQueue:

    链表阻塞队列,先进先出,内部有一个缓冲队列(链表构成),当生产者放入数据时队列会将从生产者拿到的数据先存入缓冲区,而生产者立即返回,只有当队列缓冲区容量达到最大时才会阻塞队列(可以初始化时设定)直到消费者消耗掉一定数量的数据后才会唤醒,并且生产和消费者采用了不同的锁,从而实现并发存取操作。初始化时一定要设定缓冲区大小默认integer.MAX_VALUE。

    3,PriorityBlockingQueue:

    一个支持优先级的无序队列,默认自然顺序升序排列,通过实现compareTo方法实现自己的排列规则,或初始化时指定Comparator排序,不保证同优先级元素顺序。

    4,DelayQueue:

    延时获取无阻塞队列,使用PriorityQueue实现,队列中元素必须实现Delayed接口。

    5,SynchronousQueue:

    不存储元素的阻塞队列,插入和移除相互等待,容量为0,不能peek。

    6,LinkedTransferQueue:

    链表结构无阻塞队列,实现了TransferQueue接口其重要的三个方法
    transfer(E e):若当前存在一个正在等待的消费线程则立即将元素传递给消费者,如果没有则将元素插入对尾并阻塞直到消费线程取走。
    tryTransfer(E e):若存在等待的消费线程,将元素直接交给消费线程,若不存在则返回false元素不加人队列不阻塞线程。
    tryTransfer(E e,long timeout,TimeUnit unit):若当前存在一个正在等待的消费线程则立即将元素传递给消费者,如果没有则将元素插入对尾并阻塞直到消费线程取走,若在time时间内有消费者取走元素返回true超时返回false。

    7,LinkedBlockingDeque:

    链表组成的双向阻塞队列。可以从队列的两端插入移出元素,在多线程入列时减少一半的竞争,提供了addFirst、addLast、offerFirst、offerLast、peekFirst、peekLast等方法,First表示插入获取或移出双端队列的第一个元素,Last表示插入获取或移出双端队列最后一个元素。

    七、线程池

    ThreadPoolExecutor构造方法:

    ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutorHandler handler)

    参数说明:

    corePoolSize:核心线程数,默认情况下线程为空,有任务才创建,当运行线程小于设定值会创建新线程,如果等于设定数则不再创建调用prestartAllcoreThread会提前启动设定数量的核心线程并等待任务。
    maximumPoolSize:允许最大线程数,小于此值会创建新线程执行新任务。
    keepAliveTime:非核心线程超时时间,设置allowCoreThreadTimeOut属性true时超时时间也会对核心线程生效。
    unit:设置超时时间单位。
    workQueue:任务队列,当前线程数大于设定的corePoolSize时会将任务加人队列,该队列为BlockingQueue类型阻塞队列。
    threadFactory:线程工厂,用于给每个创建的线程设定名字,一般不用设置。
    handler:饱和策略,当任务队列和线程池都饱和时的应对策略,默认AbordPolicy,表示无法处理新任务,抛出RejectedExecutionException,还有其他三种策略:
    CallerRunsPolicy:用调用者所在线程处理,提供简单的反馈控制机制,减缓新任务提交速度。
    DiscardPolicy:不能执行的任务,并将任务删除。
    DiscardPolicy:丢弃队列最近的任务,并执行当前任务。

    线程池处理流程:

    任务-》未到最大核心线程-》创建核心线程
    -------》达到最大核心线程-》进入任务队列-》队列已满-》是否达到最大线程数-》
    未达到创建非核心线程处理
    已达到执行饱和策略抛出RejectedExectutionException异常

    线程池种类:

    1,FixedThreadPool:

    只有核心线程,数量固定。队列使用LinkedBlockingQueue。

    2,CachedThreadPool:

    无核心线程,非核心线程不限。队列使用SynchronousQueue。

    3,SingleThreadExecutor:

    单线程线程池,队列使用LinkedBlockingQueue。

    4,ScheduledThreadPool:

    实现定时和周期任务的线程池,固定核心线程,最大线程数不限,队列使用DelayedWorkQueue。

    相关文章

      网友评论

          本文标题:一、多线程与线程安全

          本文链接:https://www.haomeiwen.com/subject/gcmpdrtx.html