美文网首页
进程与线程:线程基础、线程之间的共享和协作

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

作者: 壹元伍角叁分 | 来源:发表于2021-06-07 09:04 被阅读0次

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

    1.1基础概念

    1.1.1 什么是进程和线程

    进程是程序运行资源分配的最小单位;
    线程是 CPU 调度的最小单位,必须依赖于进程而存在

    1.1.2 CPU 核心数和线程数的关系

    核心数、线程数:目前主流 CPU 都是多核的。增加核心数目就是为了增加线程数,因为操作系统是通过线程来执行任务的,一般情况下它们是 1:1 对应关系,也就是说四核 CPU一般拥有四个线程。但 Intel 引入超线程技术后,使核心数与线程数形成 1:2 的关系。

    1.1.3 时间片轮转机制

    1.1.4 并行和并发

    举个例子,如果有条高速公路 A 上面并排有 8 条车道,那么最大的并行车辆就是 8 辆。此条高速公路 A 同时并排行走的车辆小于等于 8 辆的时候,车辆就可以并行运行。CPU 也是这个原理,一个 CPU 相当于一个高速公路 A,核心数或者线程数就相当于并排可以通行的车道;而多个 CPU 就相当于并排有多条高速公路,而每个高速公路并排有多个车道。
    当谈论并发的时候一定要加个单位时间,也就是说单位时间内并发量是多少。离开了单位时间其实是没有意义的。

    综合来说:
    并行:指应用能够同时执行不同的任务。例:吃饭的时候可以边吃饭边打电话,这两件事情可以同时执行。
    并发:指应用能够交替执行不同的任务,比如单 CPU 核心下执行多线程。并非是同时执行多个任务,如果你开两个线程执行,就是在你几乎不可能察觉到的速度不断去切换这两个任务,已达到“同时执行效果”,其实并不是的,只是计算机的速度太快,我们无法察觉到而已。

    两者区别:一个是同时执行,一个是交替执行。

    1.1.5 高并发编程的意义、好处和注意事项

    (1)充分利用 CPU 的资源
    (2)加快响应用户的时间
    (3)可以使你的代码模块化,异步化,简单化

    1.1.6 多线程程序需要注意事项

    (1)线程之间的安全性

    同一个进程中的多线程,资源是共享的,也就是可以访问同一个内存地址的变量。如果多个线程同时执行写操作,则需要考虑线程同步,否则会影响线程安全。

    (2)线程之间的死锁:

    形成死锁的条件,缺一不可:

    1、多操作者(M>=2个)情况下,争夺多个资源(N>=2个,且N<=M)才会发生这种情况。很明显,单线程自然不会有死锁;
    2、争夺资源的顺序不对,如果争夺资源的顺序是一样的,也不会产生死锁;
    3、争夺者拿到资源不放手。

    学术化的定义

    死锁的发生必须具备以下四个必要条件。
    1)互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
    2)请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
    3)不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
    4)环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。

    危害

    1、线程不工作了,但是整个程序还是活着的;
    2、没有任何的异常信息可以供我们检查;
    3、一旦程序发生了发生了死锁,是没有任何的办法恢复的,只能重启程序,对正式已发布程序来说,这是个很严重的问题。

    解决方案

    关键是保证拿锁的顺序一致
    两种解决方式
    1、内部通过顺序比较,确定拿锁的顺序;
    2、采用尝试拿锁的机制。

    为解决线程之间的安全性,引入了java中的锁机制,而不小心产生的java线程死锁的多线程问题,因为不同线程都在等待那些根本不可能被释放的锁,从而导致所有的工作都无法完成。

    假设有两个线程分别代表两个饥饿的人,他们必须共享刀叉并轮流吃饭,都需要获得刀和叉才能进行下一个操作。而线程A获取到了刀,而线程B获取到了叉,那线程A、B都会进入到阻塞状态,等待获取对方拥有的锁。

    ①引入lock,解决死锁问题

    public class TryLock {
        private static Lock No13 = new ReentrantLock();//第一个锁
        private static Lock No14 = new ReentrantLock();//第二个锁
    
        //先尝试拿No13 锁,再尝试拿No14锁,No14锁没拿到,连同No13 锁一起释放掉
        private static void fisrtToSecond() throws InterruptedException {
            String threadName = Thread.currentThread().getName();
            Random r = new Random();
            while(true){
                if(No13.tryLock()){
                    System.out.println(threadName +" get 13");
                    try{
                        if(No14.tryLock()){
                            try{
                                System.out.println(threadName +" get 14");
                                System.out.println("fisrtToSecond do work------------");
                                break;
                            }finally{
                                No14.unlock();
                            }
                        }
                    }finally {
                        No13.unlock();
                    }
    
                }
                //Thread.sleep(r.nextInt(3));
            }
        }
    
        //先尝试拿No14锁,再尝试拿No13锁,No13锁没拿到,连同No14锁一起释放掉
        private static void SecondToFisrt() throws InterruptedException {
            String threadName = Thread.currentThread().getName();
            Random r = new Random();
            while(true){
                if(No14.tryLock()){
                    System.out.println(threadName +" get 14");
                    try{
                        if(No13.tryLock()){
                            try{
                                System.out.println(threadName +" get 13");
                                System.out.println("SecondToFisrt do work------------");
                                break;
                            }finally{
                                No13.unlock();
                            }
                        }
                    }finally {
                        No14.unlock();
                    }
    
                }
                //Thread.sleep(r.nextInt(3));
            }
        }
    
        private static class TestThread extends Thread{
            private String name;
    
            public TestThread(String name) {
                this.name = name;
            }
    
            public void run(){
                Thread.currentThread().setName(name);
                try {
                    SecondToFisrt();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        public static void main(String[] args) {
            Thread.currentThread().setName("TestDeadLock");
            TestThread testThread = new TestThread("SubTestThread");
            testThread.start();
            try {
                fisrtToSecond();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    

    ②保证拿锁的顺序一致

    public class NormalDeadLock {
    
        private static Object apple = new Object();//第一个锁
        private static Object orange = new Object();//第二个锁
    
        //第一个拿锁的方法
        private static void thread1Do() throws InterruptedException {
            String threadName = Thread.currentThread().getName();
            synchronized (apple){
                System.out.println(threadName+" get apple");
                Thread.sleep(100);
                synchronized (orange){
                    System.out.println(threadName+" get orange");
                }
            }
        }
    
        //第二个拿锁的方法
        private static void thread2Do() throws InterruptedException {
            String threadName = Thread.currentThread().getName();
            synchronized (apple){
                System.out.println(threadName+" get apple");
                Thread.sleep(100);
                synchronized (orange){
                    System.out.println(threadName+" get orange");
                }
            }
        }
    
        //子线程
        private static class Thread2 extends Thread{
            private String name;
    
            public Thread2(String name) {
                this.name = name;
            }
    
            @Override
            public void run() {
                Thread.currentThread().setName(name);
                try {
                    thread2Do();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            //主线程
            Thread.currentThread().setName("Thread1");
            Thread2 thread2 = new Thread2("Thread2");
            thread2.start();
            thread1Do();
        }
    }
    

    活锁

    两个线程在尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生同一个线程总是拿到同一把锁,在尝试拿另一把锁时因为拿不到,而将本来已经持有的锁释放的过程。
    解决办法:每个线程休眠随机数,错开拿锁的时间

    线程饥饿

    低优先级的线程,总是拿不到执行时间

    (3)线程太多了会将服务器资源耗尽形成死机当机

    线程数太多有可能造成系统创建大量线程而导致消耗完系统内存以及cpu的过度切换,造成系统死机。解决的办法是使用线程池。

    1.2 java中的线程

    1.2.1 线程的启动与中止

    启动

    启动线程的方式有两种:
    thread源码中有注释写明(There are two ways to create a new thread of execution.)
    1、X extends Thread,然后 X.start
    2、X implements Runnable;然后交给 Thread 运行

    Thread 和 Runnable 的区别
    Thread 才是 Java 里对线程的唯一抽象,Runnable 只是对任务(业务逻辑)
    的抽象。Thread 可以接受任意一个 Runnable 的实例并执行。

    中止

    (1)线程自然终止:run方法执行完成,获取是抛出了一个未处理的异常而导致线程提前结束

    (2)top:暂停(suspend())、恢复(resume())和停止(stop()),这些api是过时的,不建议使用。原因是,以suspend()方法为例,在调用后,线程不会释放已经占有的资源,而是占着资源进入到睡眠状态,这样就容易引发死锁问题。同样stop()方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。正是suspend()、resume()、stop()方法带来的副作用,这些方法才会被注明不建议使用的过时方法。

    (3)中断

    安全的中止,则是其他线程通过调用某个线程A的interrupt()方法对其进行中断操作,中断操作好比通知此线程A停止工作,但线程A可以完全不理会这种中断请求,因而调用interrupt()方法的线程不一定会立即停止工作。一般需要线程通过检查自身的中断位,isInterrupted()方法判断线程是否被通知中断。

    还有一个方法,Thread.interrupted(),同样可以用来判断线程是否被通知中断,此方法和isInterrupted()方法的不同之处在于,调用此方法后,会同时将中断标识改成false。

    不建议自定义一个取消标志位来中止线程的运行。因为run方法里有阻塞调用时会无法很快检测到取消标志,线程必须从阻塞调用返回后,才会检查这个取消标志。这种情况下,使用中断会更好,因为:一、一般的阻塞方法,如sleep等本身就支持中断检查;二、检查中断位的状态和检查取消标志位没什么区别,用中断位的状态还可以避免声明取消标志位,减少资源的消耗。

    1.3 java中的线程的认识

    1.3.1 run()和start()方法

    Thread类是java里对线程概念的抽象。可以这样理解,我们通过new Thread()其实只是new出了一个Thread的实例,还没有操作系统中真正的线程挂起钩来,只有执行了start()方法,才实现了真正意义上的启动线程。

    start(),方法只允许调用一次,多次调用会抛出异常。调用start()方法会,并不会立即执行该线程的run()方法,而是进入到可执行状态,等待cpu分配。

    run(),可以被单独调用,但单独调用的话,还是在调用此方法的线程中执行。

    1.3.2 其他线程相关方法

    (1)yield()方法:让出cpu,将线程状态从运行状态转到可运行状态,不会释放锁

    执行yield()方法的线程进入到可运行状态后,等待cpu的再次轮转。与其他线程的被执行的几率是一样的。

    (2)join方法:
    获得执行权,将指定的线程加入当前线程,可以将两个交替执行的线程合并为顺序执行。比如在线程B中调用了线程A的jon()方法,知道线程A执行完毕后,线程B才开始继续执行。

    1.3.3 线程的优先级

    通过priority变量来控制优先级,一般是在1~10之间,通过setPriority(int)方法来修改优先级,默认优先级是5。

    1.3.4 守护线程

    用户线程和守护线程(守护线程在用户线程之后结束)
    守护线程的finally方法不一定会执行,完全看操作系统的调度;用户线程的finally一定会执行。

    线程的共享和协作(线程不安全)

    synchronized:机制锁

    锁的是某个具体的对象
    代码块、方法上加锁

    类锁:本质上也是对象锁,只是对象比较特殊,锁的是每一个类在虚拟机中生成的class文件。

    如果两个线程的锁对象不一样,那这两个线程是并行的。

    Volatile:最轻量的同步机制

    在主线程中修改了某个成员变量的数据,如果需要子线程能够感知到,在成员变量添加volatile关键字。

    不能替代synchronized,只保证读的准确性,不能保证写的准确性,适用于一写多读的场景。

    ThreadLocal:为每一个线程提供了变量的副本,只访问自己的数据,实现了线程的隔离

    ThreadLocal获取到当前线程所维护的ThreadLocalMap,Thread类中有一个ThreadLocalMap的成员变量,ThreadLocalMap是每一个线程所独有的。ThreadLocalMap中有个entry数组 ,维护着当前线程所创建的多个ThreadLocal对象。

    public class ThreadLocal<T> {
       public void set(T value) {
         Thread t = Thread.currentThread();
         ThreadLocalMap map = getMap(t);
         if (map != null)
             map.set(this, value);
         else
             createMap(t, value);
       }
    
        ThreadLocalMap getMap(Thread t) {
            return t.threadLocals;
        }
    }
    
    class Thread implements Runnable {
          ThreadLocal.ThreadLocalMap threadLocals = null;
    }
    
    static class ThreadLocalMap {
          private Entry[] table;
    }
    
    static class Entry extends WeakReference<ThreadLocal<?>> {
          /** The value associated with this ThreadLocal. */
          Object value;
    
          Entry(ThreadLocal<?> k, Object v) {
             super(k);//key
             value = v;//value,每个线程独有的副本
           }
     }
    

    使用不当会引发内存泄漏

    强引用:代码中普遍存在,直接new出来的对象,垃圾收集器永远不会回收掉被引用的对象实例。

    软引用:被SoftRefence引用,要发生内存溢出了,如果回收了,发现还是不够,才进行回收。

    弱引用:被WeakReference引用,只要发生了垃圾回收,弱引用所指在堆上的实例就一定会被回收。

    虚引用:最弱的一种引用关系。唯一目的就是能在这个对象实例被收集器回收时收到一个系统通知。

    key 使用强引用:引用 ThreadLocal 的对象被回收了,但是 ThreadLocalMap 还持有 ThreadLocal 的强引用,如果没有手动删除,ThreadLocal 的对象实例不会被回收,导致 Entry 内存泄漏。

    key 使用弱引用:引用的 ThreadLocal 的对象被回收了,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用,即使没有手动删除,ThreadLocal 的对象实例也会被回收。value 在下一次 ThreadLocalMap 调用 set,get,remove 都有机会被回收。

    由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果都没有手动删除对应 key,都会导致内存泄漏,但是使用弱引用可以多一层保障。

    因此,ThreadLocal 内存泄漏的根源是:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除对应 key 就会导致内存泄漏,而不是因为弱引用。

    总结:

    JVM 利用设置 ThreadLocalMap 的 Key 为弱引用,来避免内存泄露。

    JVM 利用调用 remove、get、set 方法的时候,回收弱引用

    当 ThreadLocal 存储很多 Key 为 null 的 Entry 的时候,而不再去调用 remove、 get、set 方法,那么将导致内存泄漏。

    使用线程池+ ThreadLocal 时要小心,因为这种情况下,线程是一直在不断的重复运行的,从而也就造成了 value 可能造成累积的情况。

    图片 1.png

    错误使用 ThreadLocal 导致线程不安全

    ThreadLocalMap 中保存的其实是对象的一个引用,而指向的是同一个对象。这样的话,当有其他线程对这个引用指向的对象实例做修改时,其实也同时影响了所有的线程持有的对象引用所指向的同一个对象实例。

    等待和通知

    wait()和notify()/notifyAll()方法是object中的方法

    必须在synchronized关键字中使用,一旦线程调用wait()方法,进入休眠状态前,会先释放锁。而调用notify()/notifyAll()方法并不会释放锁,需要完成synchronized中的全部代码,才会释放锁

    notify 和 notifyAll的区别

    尽可能用 notifyAll(),谨慎使用 notify(),因为 notify()只会唤醒一个线程,我们无法确保被唤醒的这个线程一定就是我们需要唤醒的线程。

    调用yield()、sleep()、wait()、notify()方法对锁有和影响?

    yield()方法,释放cpu的执行权,并不会释放锁;
    sleep()方法,不会释放当前线程持有的锁;
    wait()方法,释放当前线程所持有的锁,当被唤醒的时候,去竞争锁,拿到锁后,才去执行wait方法后面的方法;
    notify()/notifyAll()方法,也不会释放锁,需要同步代码块中代码执行完,所以一般notify()/notifyAll()方法都会放在同步代码块的最后一行。

    推荐书籍:

    1、Java核心技术 卷1 基础知识(无基础看)
    2、Java并发编程实战(比较晦涩,有基础看)

    相关文章

      网友评论

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

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