美文网首页从CRUD到高软
线程基础、线程之间的共享和协作

线程基础、线程之间的共享和协作

作者: 冰封陈韶 | 来源:发表于2020-06-01 18:22 被阅读0次

    什么是线程?

    在说线程之前,先说说进程。那么什么是进程?进程是程序运行的环境,是操作系统分配资源的最小单元,每个进程都是独立的,其中包含的资源有CPU、内存空间、磁盘IO。而线程就是在一个进程内部的,共享当前进程的所有资源,是CPU调度和分配的最小基本单元。

    了解了线程和进程的含义之后,我们在电脑上运行程序,就是启动一个进程,那为什么我们可以同时启动很多程序而系统不卡呢?原因在于CPU时间片轮转机制,CPU为每个进程分配了时间片,如果某个进程在时间片结束时还在运行,那么CPU将被剥夺并分配给另一个进程;如果某个进程在时间片结束前阻塞或者结束,那么CPU就会立即切换。CPU的时间片轮转机制以及上下文切换,在用户没有感知的情况下完成了看上去是同时运行的程序。

    什么是并行和并发?

    并行:假如有两个饮水机,两个人同时接水,并行是同时做一件事
    并发:假如有一个饮水机,两个人交替接水,并发是交替去做,并发不能脱离时间,不然没有意义,一般是在一定时间内研究并发

    多线程程序的优缺点

    优点

    1.可以充分利用操作系统资源
    以前是一个线程执行,使用多线程后,可以提高CPU利用率,减少CPU空闲时间,提高并发量

    2.加快响应用户的时间
    以web项目为例,多开一个子域名,浏览器就多开一个线程去执行

    3.可以使代码模块化、异步化、简单化
    可以将相同功能代码模块化,将不是实时功能代码异步化,代码结构层次清晰明了

    缺点

    1.线程之间的安全问题
    线程之间是共享进程所有资源的,所以在共享变量的操作有线程安全问题

    2.线程之间的死锁问题
    A线程持有a的锁,等待b的锁;B线程持有b的锁,等待a的锁,造成循环等待,形成死锁。解决这一类问题,可以考虑在A线程持有a锁,在一定时间内如果一直获取不到b的
    锁,就主动释放a的锁。死锁还有很多种,这只是其中一种,具体的可参考百度百科[https://zhidao.baidu.com/question/1448029152492656860.html]

    3.线程过多导致的当机问题
    线程创建过多,不及时回收,容易造成资源浪费以及可能引发当机问题,合理创建线程,如果可以,多使用线程池技术

    下面来说说JAVA线程

    JAVA是支持多线程的语言,创建线程的方式有两种,一种是继承Thread类 ,X extend Thread;一种是实现Runnable接口,X implement Runnable。Thread是JAVA针对线程的抽象,Runnable是JAVA针对任务的抽象,这两个类都有一个run()方法,重写这个方法来实现业务逻辑。

    JAVA线程运行图 :

    线程.png

    依据上图,我们可以看到,线程新建的时候,只是新建,当调用start()方法的时候,线程进入就绪状态,不是运行状态。在就绪状态,调用join()方法,可以将别的线程加入,让线程按照顺序执行。在运行期,调用sleep()方法,线程进入阻塞状态;sleep()时间到期,线程重新进入就绪状态;调用wait()方法,线程进入阻塞状态,但是这时候要唤醒线程需要调用 notify()、notifyAll()方法才可以。调用yield()方法,线程会让出CPU控制器,重新进入就绪状态。在阻塞状态,如果调用interrupt()方法,线程也会进入就绪状态。运行期间,如果run()方法执行完成,或者调用stop()方法(不推荐使用),setDemon()方法,会让线程进入结束状态。

    yield()方法
    线程出让自己的CPU时间片给操作系统,和其他线程一起去争夺CPU的时间片,但是不释放自己持有的资源。

    setDemon()方法
    设置成守护线程,守护线程是一种支持性线程,用于程序中后台调度与支持性工作,例如gc;守护线程与用户线程不同,守护线程在用户线程结束之时,立即结束,具体要看CPU分配的时间片。一般业务代码使用不到守护线程

    stop()、interrupt()方法区别:

    stop()是强制关闭线程,不释放线程内部持有的资源,例如锁,有引发死锁风险
    interrupt()是给某一线程添加一个中断标志位,该线程不一定理会,其可以通过isInterrupt()方法来响应。

    线程校验中断调用方法有两个:
    isInterrupt()
    Thread.interrupted()
    其中,第二个方法会在校验中断标志位之后,清除标志位,置为false

    最好不要自己实现interrupt()方法,因为如果调用阻塞方法sleep(),wait()的时候,线程是不会检测状态的,但是阻塞方法内部都抛出InterruptedException(),其对中断标志位是有响应的;sleep()方法在抛出InterruptedException之后,会将中断标志位置为false;因为如果不改变中断标志位,那么在阻塞之后,线程的资源得不到释放就被中断了,容易导致线程安全问题。

    线程启动的start()方法,和重写的run()方法有何区别?

    调用start()方法,将当前线程与操作系统进程挂钩。只能调用一次,如果第二次还调用,则会抛出异常;run()方法只是一个普通方法,与线程无关,只是线程业务逻辑的实现而已,本身可以被任意调用;

    synchronized锁

    synchronized是java关键字,当它修饰方法时,锁定的是调用这个方法的对象,修饰静态方法或者类时,锁定的是Class对象,所以synchronized锁定的是对象,任何非对象的数据,都不能使用synchronized锁,否则没有锁的效果;

    volatile关键字

    volatile可以保证共享变量的可见性,一被修改,其他线程可以立即看到。虽然同样进程里面的线程是共享所有资源的,但是JAVA里面还是进行了处理,所以如果需要可见性,就要使用volatile关键字。

    ThreadLocal

    ThreadLocal.png

    ThreadLocal包含一个ThreadLocalMap内部类,而每个线程里面都存储了一个ThreadLocal.ThreadLocalMap属性,ThreadLocalMap里面包含一个Entry内部类,Entry实现了WeakReference<ThreadLocal<?>>,而我们使用ThreadLocal时候,经常使用的是ThreadLocal.get(),set(T value),remove()方法;
    下面看看ThreadLocal.get()方法源码

    public T get() {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null) {
                ThreadLocalMap.Entry e = map.getEntry(this);
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    T result = (T)e.value;
                    return result;
                }
            }
            return setInitialValue();
        }
    

    1:拿到当前线程;
    2:从线程中获取ThreadLocalMap;
    3:在ThreadLocalMap的Entry对象里面获取数据;

    再看看ThreadLocal.set()方法源码

    public void set(T value) {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null)
                map.set(this, value);
            else
                createMap(t, value);
        }
    

    前两步和上面相同;
    3:如果当前ThreadLocalMap存在,则替换
    4:不存在就创建

    使用ThreadLocal的好处是ThreadLocal是线程本地变量,每个线程独一份,不会受到多线程的影响,实现物理隔离,但是不保证数据一致性(也就是说不能使用ThreadLocal来操作共享变量,否则线程不安全)

    强引用:Object o = new Object(),我们new出来的对象o,就是强引用。
    软引用:当java虚拟机内存不足引发gc时,软引用会被打上标记并被回收,如果内存充足,则不会;而强引用就算内存不足引发gc时,也不会被回收,除非将强引用置为null(栈里面没有指向堆的引用的时候),或者等程序自动结束。
    弱引用:当发生gc时,弱引用一定会被回收。
    虚引用:最弱的引用,可以用来做为对象是否存活的监控。

    坏处是 可能引发内存泄露,ThreadLocalMap存的是Entry,Entry实现了一个弱引用ThreadLocal类。如果Entry实现强引用的ThreadLocal类,则ThreadLocal百分之百引发内存泄露,因为Entry持有的不仅仅是ThreadLocal对象,还持有Object 对象,所以内存泄露是肯定的。但是为什么说可能呢?因为ThreadLocal.get(),set()方法里面都有内存回收机制,但不是每次都执行,所以可能导致内存泄露,所以我们在使用完ThreadLocal之后,一定要用ThreadLocal.remove()方法清除value值,防止可能引起的内存泄露。

    线程之间的协作

    等待和通知
    wait() notify() notifyAll()

    这里有个编程范式:

    synchronized(obj){
        while(条件不满足){
            obj.wait();
        }
    //TODO
    }
    synchronized(obj){
        obj.notify();
        //obj.notifyAll();
    }
    

    这里有个要点:wait()方法执行的时候,会自动释放锁资源;
    notify(),notifyAll()是当synchronized方法执行完成,才会释放锁资源

    等待超时模式实现一个线程池

    //线程池
    LinkedList<Object> pool = new LinkedList<Object>();
    //获取资源
    public Object getObj(long mills) throws InterruptedException{
        synchronized(pool){
         //永不超时
          if(mills <=0){
             while(pool.isEmpty()){
                    pool.wait();
            }
          return pool.removeFirst();
          }else{
          long future = System.currentTimeMillis() + mills;
          long remain = mills;
          while(pool.isEmpty() && remain > 0){
               pool.wait(remain);
               //这里需要计算时间,防止超时(因为线程可能还是没有抢到资源)
               remain = future - System.currentTimeMillis();
          }
          Object obj = null;
          if(!pool.isEmpty()){
              obj = pool.removeFirst();
          }
          //可能返回的是null
          return obj;
        }
     }
    }
    
    //释放资源
    public void releaseObj(Object obj) throws InterruptedException{
        if(obj != null){
          synchronized(pool){
            pool.addLast(obj);
            pool.notifyAll();
        }
      }
    }
    

    上面的代码中使用了 wait()范式,notifyAll()方法;
    下面来介绍一下wait(),notifyAll(),notify(),sleep(),yield()方法对锁的影响
    wait:线程在执行到wait方法的时候,主动释放锁资源,直到被notify,notifyAll唤醒之后,先去抢锁资源,然后执行wait方法后面的内容;
    notify、notifyAll:线程执行到这两个方法时,不会释放锁资源,直到synchronized包含的内容走完,才会释放锁资源,所以建议将这两个方法写在synchronized内容的最后面;
    sleep:线程进入阻塞状态,不会释放锁资源
    yield:线程主动出让CPU控制权,然后和其他线程一起去争抢CPU的时间片,不会释放其持有的资源,包括锁。

    相关文章

      网友评论

        本文标题:线程基础、线程之间的共享和协作

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