多线程初阶知识要点总结

一.基本概念

      1).进程

              进程的管理

                对于操作系统来说,管理进程主要分为两个部分:

                        1.描述:一个进程可以用一个类(或者结构体)来描述,类中存储了进程的一些特征(优先级,状态,记账信息,上下文等等),我们称之为PCB(进程控制块).

                        2.管理:用数据结构,将若干个PCB在操作系统内部中整理起来.简单来说,操作系统在内部使用双向链表串联PCB.

               进程的虚拟地址空间

                    在很多进程同时执行的时候,为了防止一个进程非法访问其他进程的内存而导致进程崩溃.我们引入了"虚拟地址空间"的概念.

                     如图:

                    这个机制使得每个进程即使访问虚拟地址空间内相同的地址,这个地址在经过MMU处理之后,都会对应真实内存上的一份独一无二的空间. 

                    简单来说,上述机制体现出了进程间的"隔离性".即一个进程运行正常与否,一般不会影响到另一个进程.

                     注:为了实现进程间的通信,操作系统在隔离性的基础上引入了"进程间通信"的概念.进程间通信的方法很多,但其原理是一致的:引入一个公共媒介,使得进程可以通过访问公共媒介,间接的与其他进程交流.

        2).线程

                1.引入线程的原因

                        要想讨论这个问题,首先要先明确进程创建(或销毁)的步骤:

                                第一步:创建PCB

                                第二步:分配系统资源(尤其是内存资源)

                                第三步:将创建好的PCB加入操作系统的双向列表中

                        以上这三步中,第二步最为耗时.因此在频繁创建和销毁进程的情况下,系统的效率就会受到很大的影响.因此我们引入了"轻量级进程"-----线程.

               2.线程的基本概念

                        1.线程本质上是"执行流",每个线程按照自己的顺序执行自己的代码,多个线程之间"同时"执行自己的代码,彼此之间通常来说不互相影响.

                        2.之所以将线程称为"轻量级进程",是因为操作系统允许一个进程中包含多个线程(操作系统以PCB为单位),一个进程内的若干线程共享同一份系统资源.这也就意味着,当我们重新创建线程时,不需要重新给其分配资源,只需要复用当前进程内已有的资源即可,从而就省去了上述创建步骤中最耗时的第二步.

                        3.多线程编程能提高运行效率的前提是系统有足够的CPU资源,当所开线程已经吃满CPU资源后,再增加线程数不仅无法提高程序的运行效率,反而会影响操作系统的调度,降低程序的运行效率.

二.要点整理

        1.进程和线程的辨析

                1).进程是包含线程的,一个进程内部可以有多个线程.

                2).每个进程有自己的虚拟地址空间(内存资源)和文件描述符表(文件资源),进程之间的资源是独立的;而同一个进程的多个线程之间共享上述这些资源.

                3).进程是操作系统中资源分配的基本单位;线程是操作系统中调度执行的基本单位.

                4).对于多个进程来说,其中一个崩溃一般不会影响其余进程;而同一进程的多个线程中,一个线程崩溃一般会把整个进程一起带走.

        2.Thread中run()和start()的辨析

                Thread.run()仅会在当前线程中执行Thread线程中的内容,如图:

public class test {
    public static void main(String[] args)  {
        Thread t = new Thread(() -> {
           while(true){
               System.out.println("该线程在死循环");
               try {
                   Thread.sleep(5000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        t.run();

        System.out.println("主线程是否能被执行");
    }
}

                如图代码的执行结果如下:

                 可见主线程阻塞在了上面的死循环中.

                而Thread.start()会在操作系统中创建新的线程,在新的线程中执行Thread线程中的内容,如图:

public class test {
    public static void main(String[] args)  {
        Thread t = new Thread(() -> {
           while(true){
               System.out.println("该线程在死循环");
               try {
                   Thread.sleep(5000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        t.start();

        System.out.println("主线程是否能被执行");
    }
}

                        如图代码的执行结果为:

                         可见主线程的执行并没有受到影响.

        3.创建线程

                1.继承Thread类,重写run()方法

                        1).额外实现线程类,继承自Thread

class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("自实现类继承Thread");
    }
}
public class test {
    public static void main(String[] args) {
        Thread t = new MyThread();
        t.start();
    }
}

                         2).使用匿名内部类

public class test {
    public static void main(String[] args) {
        Thread t = new Thread(){
            @Override
            public void run() {
                System.out.println("使用匿名内部类");
            }
        };
        t.start();
    }
}

                2.实现runnable接口,重写run()方法

public class test {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("实现runnable接口");
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();
    }
}

                3.使用lambda表达式

public class test {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println("使用lambda表达式");
        });
        t.start();
    }
}

                4.基于Callable与FutureTask创建线程

                        Callable相比于Runnable,可以传一个返回值,该返回值用FutureTask.get()获取.

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                System.out.println("利用Callable创建线程");
                return 0;
            }
        };
        FutureTask<Integer> futureTask = new FutureTask<Integer>(callable);
        Thread t = new Thread(futureTask);
        t.start();

        System.out.println(futureTask.get());
    }
}

                5.基于线程池创建线程

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class test {
    public static void main(String[] args)  {
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        for (int i = 0; i < 100; i++) {
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("提交任务");
                }
            });
        }
    }
}

               如图为创建10个线程的线程池.

                其中FixedThreadPool是创建固定数量的线程池.java标准库中还有其余两个常用实现,分别为:CachedThreadPool:线程数根据任务动态调整的线程池 和 SingleThreadExecutor:仅单线程执行的线程池.

        4.join()方法

                join()方法可以控制多个线程之间的结束顺序.若一个线程调用了另一个线程的join()方法,其在代码执行至join()时会阻塞,直到另一个线程执行结束.示例代码如图:

public class test {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            try {
                Thread.sleep(2000);
                System.out.println("该线程执行结束");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t.start();
        t.join();
        System.out.println("主线程内容");
    }
}

                  执行结果如下:

                 可见主线程在线程t执行完之前,一直阻塞在t.join()部分.

        5.守护线程

                线程分为前台线程和后台线程.进程结束的前提是所有前台线程执行完,否则就会阻塞等待.而一个线程被创建出来时默认是前台线程(比如main线程).我们可以通过setDaemon()方法将一个前台线程设置成后台线程.(注意:该操作要在start()方法之前调用)代码示例如图:

public class test {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while(true){
                System.out.println("死循环");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.setDaemon(true);
        t.start();

        System.out.println("主线程结束");
    }
}

                        运行结果如下:

                         可见程序会直接因为主线程(唯一的前台线程)结束而结束,而不去考虑后台线程是否结束.

        6.线程中断

                首先介绍Thread.currentThread()方法,该方法是Thread类中的一个静态方法,可以返回调用该方法的线程的对象.如线程1调用它,就会返回线程1的Thread对象.

                线程内部提供了一个标志位(可以简单的理解成一个布尔值),便于程序员操作线程的执行情况,我们可以通过interrupt()方法来修改标志位,通过isInterrupted()方法来获取标志位,标志位为true表示线程应该中断.

                其中,interrupt()方法的执行逻辑如下:

                        1).如果调用interrupt方法时线程正在阻塞,此时interrupt方法会控制线程中阻塞的部分抛出异常,而不修改内置的标志位.

                        2).如果调用interrupt方法时线程没有阻塞,那么此时interrupt方法就会修改内置的标志位.

                示例代码如下:

public class test {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
           while(!Thread.currentThread().isInterrupted()){
               try {
                   Thread.sleep(1000);
                   System.out.println("线程死循环");
               } catch (InterruptedException e) {
                   System.out.println("线程中断");
                   break;
               }
           }
        });
        t.start();

        Thread.sleep(5000);
        System.out.println("主线程控制线程退出");
        t.interrupt();
    }
}

             代码的执行结果如下:

                 可见在主线程调用interrupt()方法时,线程t处于阻塞状态,因此interrupt()方法让阻塞部分抛出异常,该异常被捕获到,从而执行catch部分的内容.

                7.线程状态

                                在java中,线程有如下几种状态:

                                        1).NEW状态.该状态为线程对象被创建出来了但没有调用start方法(即没有在操作系统内部创建出线程).

                                        2).RUNNABLE状态.该状态为线程已经准备好工作了(在就绪队列中但没有被CPU调度)或正常工作(在CPU上执行).

                                        3).TIME_WAITING状态.在RUNNABLE状态下调用sleep()方法即会进入这种状态.为阻塞状态的一种.时间到了之后由操作系统负责唤醒.

                                        4).WATING状态.在RUNNABLE状态下调用wait()方法,即会进入这种状态.为阻塞状态的一种.需要其他线程调用notify()来唤醒.

                                        5).BLOCKER状态.在RUNNABLE状态下,尝试获取已经锁上的锁(synchronize),就会进入这种状态,为阻塞状态的一种.在其他线程释放锁之后,由操作系统负责唤醒.

                                        6).TERMINATED状态.在RUNNABLE状态下,如果线程内的代码全部执行完,就会进入该状态,表示线程已经结束运行.

                8.yield()方法

                                该方法让调用者暂时放弃CPU,重新在就绪队列里排队,其作用相当于sleep(0).

                9.线程安全

                                我们先来展示线程不安全的典型情况--------两个线程同时修改一个变量:

public class test {
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count的值为" + count);
    }
}

                    运行结果如下:

                    以上只是多次运算结果中的一次,结果并不是预期中的10w.

                原因分析

                        产生上述结果的原因是:自增操作并不是原子化的.一个自增操作其实对应着三个机器指令,具体如下:

                                1).从内存中读取数据到CPU中(变量存储在内存中)-----load.

                                2).在CPU寄存器中,完成加法运算-----add.

                                3)把CPU寄存器中的结果写回到内存中-----save.

                        而操作系统对于线程的调度是随机的,一个线程可能刚执行完add指令,就换成另一个线程执行load指令了.这会导致后来的线程并没有读取到先前线程add之后的值,加法操作的运算结果就出错了.

                        为了解决这个问题,我们需要把自增操作打个包,使其变成原子化操作.此时我们就引入了synchronize加锁操作.

                解决问题

                        用synchronize将自增操作包起来即可有效解决线程不安全问题.这是因为一个线程通过synchronize对锁对象加锁后,其他线程再想尝试获取锁就会失败,从而阻塞.直到获取到锁的线程执行完save指令将锁释放,这保证了一次加法操作的独立性和完整性.代码如下:

public class test {
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            synchronized (test.class){
                for (int i = 0; i < 50000; i++) {
                    count++;
                }
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized (test.class){
                for (int i = 0; i < 50000; i++) {
                    count++;
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count的值为" + count);
    }
}

                        我们以当前test类作为类对象,对自增操作加锁.自增操作原子化之后,就可以准确的得到10w了.

                其他导致线程不安全的情况

                        除了多个线程同时修改一个变量之外,还有一些情况也会导致线程不安全问题.

                                1).内存可见性问题

                                        考虑如下情形:有线程1和线程2.其中线程1一直在循环重复两个操作:从内存中读取某个数据 和 使用该数据进行运算;而线程2每过较长一段时间会修改一次这个数据.

                                        由于从内存中读取数据到CPU所花费的时间远远多于利用该数据进行操作所花费的时间(后者仅在CPU上执行).所以当操作系统发现程序在不断快速(此处一定要注意这个限制)从内存中重复读取一个没有修改的数据时,操作系统会将线程1的操作优化成直接从CPU的寄存器里读.这就会导致线程2对那个数据的修改,对于线程1来说是不可见的,这就导致线程安全问题.代码示例如下:

public class test {
    public static boolean flag = false;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while(!flag){
            }
        });
        Thread t2 = new Thread(() -> {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag = true;
        });
        t1.start();
        t2.start();
    }
}

                                        预期情况下,上述程序会在5s后结束.然而实际运行时,线程t1始终在死循环中,并没有感知到flag的变化.这就是内存可见性问题.

                                        为此,java引入了volatile关键词.被volatile修饰的关键字,操作系统不会对其进行上述优化.

                                2).指令重排序问题

                                        考虑如下代码:

Object object = new Object();

                                       这行代码被转化为机器指令后被分成3步:

                                                1.创建内存空间

                                                2.往这个内存空间上构造一个对象

                                                3.将这个内存的引用赋值给object

                                        其中1无疑是最先被执行的,但是2和3的顺序在操作系统的优化下,执行顺序可能会发生变化.这时可能会出现一种情况:其他线程在上述指令以1->3->2的顺序执行时,在第3步之后且第2步之前尝试获取object,此时object实际上是一个无效对象,这就出现了线程安全问题.

                总结

                        线程安全问题的本质是操作系统的随机调度/抢占式执行.

                10.wait()与notify()

                                wait()和notify()主要用于控制多个线程间的运行顺序.

                                对于调用wait()的线程来说,有如下的执行逻辑:

                                        1).将锁释放并进入阻塞状态

                                        2).等待其他线程调用notify()

                                        3).当通知到达后,就会被唤醒,并重新尝试获取锁.

                                可见线程调用wait()的前提是得先获取到锁,因此得配合synchronize使用(在synchronize内部).

                                给出一个示例:

public class test {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            synchronized (test.class){
                for (int i = 0; i < 10; i++) {
                    if(i == 5){
                        try {
                            test.class.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println(i);
                }
            }
        });
        t1.start();
        Thread t2 = new Thread(() -> {
           synchronized (test.class){
               System.out.println("获取到锁并睡眠");
               try {
                   Thread.sleep(2000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               System.out.println("唤醒其他线程");
               test.class.notify();
           }
        });
        t2.start();
    }
}

                                            运行结果:

                            

                                        可见线程t1的运行在调用wait()方法之后中止了.

                                        这里需要注意的是,线程调用sleep()方法并不会将锁释放,而是会将CPU让出,给其他不存在锁竞争关系的线程.

                11.单例模式

                                饿汉模式-----运行即创建                                     

class SingletonHungry{
    private static SingletonHungry singletonHungry = new SingletonHungry();

    public static SingletonHungry getSingletonHungry() {
        return singletonHungry;
    }

    private SingletonHungry(){}
}

                                     由于饿汉模式下,程序一运行就会创建类对象.所以在多线程获取类对象时,只涉及变量的读取,因此这种情况下不存在线程安全问题.

                                懒汉模式-----用时再创建

                                        这种情况比较特殊,有且仅有类对象被创建时,才涉及到线程安全问题(即多线程同时读写一个变量).在类对象已经被创建出来之后,懒汉模式退化成饿汉模式.因此我们需要套一层特判,避免因无意义的获取锁而造成程序的性能降低.示例如下:

class SingletonLazy{
    private static SingletonLazy singletonLazy = null;

    public static SingletonLazy getSingletonLazy() {

        if(singletonLazy == null){
            synchronized (SingletonLazy.class){
                if(singletonLazy == null){
                    singletonLazy = new SingletonLazy();
                }
            }
        }
        return singletonLazy;
    }
    private SingletonLazy(){}
}

                                        此处第一个if判断的目的是避免除创建对象那次之外,其余情况下的无意义的获取锁的行为;第二个if判断的目的是保证多线程读写下的线程安全.

                                       单例模式的内存分布

                 12.阻塞队列-----BlockingQueue

                                阻塞队列相比于原始的Queue,保证了线程安全(即多个线程同时操作一个队列时,不会出现bug).此外,当阻塞队列为空时,尝试出队列会阻塞;当阻塞队列满时,尝试加入元素也会阻塞.

                                手动实现阻塞队列:

class MyBlockingQueue{
    private int[] items = new int[1000];
    private int size = 0;
    private int head = 0;
    private int tail = 0;

    public void put(int value) throws InterruptedException {
        synchronized (this){
            while(size == items.length){
                this.wait();
            }
            items[tail] = value;
            tail++;

            if(tail == items.length){
                tail = 0;
            }
            size++;
            this.notify();
        }
    }

    public int take() throws InterruptedException {
        int ret = 0;
        synchronized (this){
            while(size == 0){
                this.wait();
            }
            ret = items[head++];
            if(head == items.length){
                head = 0;
            }
            size--;
            this.notify();
        }
        return ret;
    }
}

                                阻塞队列与普通队列一样,是一个循环队列.使用双向的wait()-notify()机制来保证线程安全.

                                在代码的put()和take()方法中设置while循环的原因是:在wait()之前,毫无疑问需要判断一次.但是在被唤醒之后,由于操作系统的随机调度,不能保证当前队列的情况就是非空/非满(比如刚put的元素又被其他线程取走了,此时队列仍可能是空),因此我们需要循环判断,保证线程安全.

                13.定时器

                        Java内部有封装好的定时器,这里我们基于多线程自己实现一个,具体如下:

import java.util.concurrent.PriorityBlockingQueue;

class MyTask implements Runnable,Comparable<MyTask>{
    private Runnable command;
    private long time;

    public MyTask(Runnable command,long after){
        this.command=command;
        this.time=System.currentTimeMillis()+after;
    }
    public long getTime() {
        return time;
    }

    @Override
    public void run() {
        this.command.run();
    }

    @Override
    public int compareTo(MyTask o) {
        return (int) (this.time-o.time);
    }
    //实现compareTo接口
}

class MyTimer{
    private Object locker = new Object();//锁对象
    public PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();//优先阻塞队列

    public void schedule(Runnable command,long after){
        MyTask myTask = new MyTask(command,after);

        synchronized (locker){
            queue.put(myTask);
            locker.notify();
        }
    }

    public MyTimer(){
        Thread t1 = new Thread(() -> {
            while (true){
                synchronized (locker){
                    try {
                        MyTask target = queue.take();
                        if (target.getTime()>(System.currentTimeMillis())){
                            queue.put(target);
                            locker.wait(target.getTime()-System.currentTimeMillis());
                        }else {
                            target.run();
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        t1.start();
    }
}

                               一些要点:

                                        1.MyTask类需要实现CompareTo接口,不然无法作为PriorityBlockingQueue的类型参数.

                                        2.wait()操作务必需要和put()操作同在被加锁的代码块内.考虑如下情形:当前时间为10:00,此时从队列中取出任务,发现该任务应在11:00时执行.然后我们应将该任务放回队列中然后wait()1小时.然而,在put()操作和wait()之间,由于线程的随机调度,此时有一个其他线程向队列中添加一个应在10:30执行的任务.

                                        如果wait()操作和put()操作不同在被锁的代码块内,则有可能执行完put(11:00)这个操作后,调度至执行put(10:30)和locker.notify().此时的notify()在wait()之前.这就出现了无效notify().程序仍然会wait()一个小时,10:30这个任务将不会被执行.从而产生线程安全问题.

                                        至于notify()要不要和put()操作同在被锁代码块内,笔者认为不是必须的.只需要保证notify()被加锁即可,因为问题的关键在于notify()与wait()的先后顺序.

                14.线程池

                                前面在线程的创建部分我们已经提及了Java内置的线程池,此处我们利用多线程手动实现一个线程池.代码如下:

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

class MyThreadPool{
    private BlockingQueue<Runnable> queue = new LinkedBlockingQueue();

    public void submit(Runnable task) throws InterruptedException {
        queue.put(task);
    }

    public MyThreadPool(int n){
        for (int i = 0; i < n; i++) {
            Thread thread = new Thread(() -> {
               while(true){
                   try {
                       Runnable runnable = queue.take();
                       runnable.run();
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }
            });
            thread.start();
        }
    }
}

public class test4 {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool myThreadPool = new MyThreadPool(10);
        for (int i = 0; i < 100; i++) {
            myThreadPool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("线程池"+System.currentTimeMillis());
                }
            });
        }
    }
}

                                一些要点:

                                        1.我们采用阻塞队列来存储提交的任务,使用LinkedBlockingQueue或者ArrayBlockingQueue均可.

                                        2.在线程池内部我们根据参数创建对应数量线程,然后令每个线程都循环从队列中读取任务并执行.

                                               

                        

                

                

                                        

    

        

                

             

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值