美文网首页
并发基础知识扫盲

并发基础知识扫盲

作者: Kip_Salens | 来源:发表于2019-07-22 00:28 被阅读0次
    thread_content.png

    1. 进程和线程

    提到并发,首先需要了解下进程和线程。

    1.1 进程

    进程,可以理解为就是一个应用程序,如当我们听音乐时,开启的程序就是一个进程;当我们听着音乐,写着代码,这个时候就开启了两个程序,有两个进程在运行。此时,相当于 CPU 在同时处理两个任务,属于并发,对用户来说,两个进程就像在同时运行一样。现代的多核处理器,确实可以做到多个进程并行。一个进程开启,操作系统会为这个进程分配独立的资源,不同的进程之间相互隔离,所以进程是资源分配的基本单元,进程间的隔离,会使得进程在运行时,就像该进程占据着全部的资源。进程拥有自己独立的虚拟空间,包括从文件加载的程序代码,程序数据,用户栈和运行时堆等。正是因为进程间是隔离的,但是在进程进行切换时,需要保存当前进程的上下文,这部分工作由操作系统维护的进程控制块(PCB)来完成,维护进程编号,进程状态,程序计数器,寄存器,CPU 调度信息,内存管理,打开的文件列表等,当切换回来时,再恢复这些数据继续执行。以前的单核 CPU,一个时间点只能执行一个任务,由于切换的过程很快,所以用户感觉像是在同时运行多个任务。

    thread_process.png

    1.2 线程

    线程是 CPU 调度的最小单元,一个进程至少有一个线程,当然,也可以有多个线程,现在的应用程序基本都是多线程的。那么为什么需要多个线程,多个线程有哪些好处呢?一个进程可能需要多个子任务,如用户点击界面上的一个按钮,点击后,需要进行网络请求,网络请求可能需要 10s,那么用户点击后如果在 10s后才有反应,那么对用户来说,可能是程序出了问题。所以在用户点击后,可以在一个线程中显示一个提示界面或者加载界面,同时用另外一个线程去执行网络请求,请求完成后,再切换到另一个线程显示界面,告知用户。这样开启多个线程能够大大提高效率,进程让操作系统的并发性成为可能,而线程让进程的内部并发成为可能。

    一个进程的不同线程共享进程的资源,线程也有自己的寄存器和堆栈,属于线程独有,其他线程不能访问。

    thread_definition.png

    2.线程同步

    同步的问题也是来源于线程之间的共享变量,如果不同线程bu操作共享变量也就不会出现数据安全性问题。这里的变量只是指实例变量,静态变量,并不包括局部变量,因为局部变量是线程私有的,并不存在共享。

    Java 中出现数据安全性问题,也是由于 Java 内存模型导致的,每条线程都有自己的工作内存,工作内存当中会有共享变量的副本。线程只能对自己工作内存的当中的共享变量副本进行操作,不能直接操作主内存的共享变量。不同线程之间无法直接操作对方的工作内存的变量,只能通过主线程来协助完成。由于缓存的存在,一方面带来了性能上的提升,CPU 不用每次都去内存中取数据,可以从缓存中获取数据;另一方面,缓存也造成了数据不同步的问题,一个线程在工作内存中拿到的数据,可能是一个旧值,此时主内存中的数据可能已经被其他线程修改过。

    为了保证数据的安全性问题,需要满足 3 个条件:原子性,可见性,有序性。

    thread_model_java.png

    3.线程状态

    thread_state.png

    (1) 初始(NEW):新创建了一个线程对象,但还没有调用 start() 方法。

    (2) 可运行状态/就绪(RUNNABLE):线程对象创建后,其他线程调用了该对象的 start() 方法。该状态的线程位于可运行线程池中,变得可运行,等待获取 CPU 的使用权。

    (3) 运行状态(Running):就绪状态的线程获取了 CPU,执行程序代码。

    (4) 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃 CPU 使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

    • 等待阻塞:运行的线程执行 wait() 方法,JVM 会把该线程放入等待池中。(wait 会释放持有的锁)

    • 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池中。

    • 其他阻塞:运行的线程执行 sleep() 或 join() 方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep() 状态超时、join() 等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。(注意,sleep 是不会释放持有的锁)

    (5) 终止(TERMINATED):表示该线程已经执行完毕。

    4.基本用法

    4.1 创建

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

    public class TheadTest {
    
        private static class MyThread extends Thread {
            @Override
            public void run() {
                int count = 0;
                while (count < 5) {
                    System.out.println(Thread.currentThread().getName() + " - " + count);
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    count++;
                }
            }
        }
        
        public static void main(String[] args) {
            Thread myThread = new MyThread();
            myThread.start();
        }
    }
    

    (2) 实现 Runnable 接口,实现 run 方法

    public class ThreadRunnable {
    
        private static class MyRunnable implements Runnable{
    
            @Override
            public void run() {
                int count = 0;
                while (count < 5) {
                    System.out.println(Thread.currentThread().getName() + " - " + count);
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    count++;
                }
            }
        }
    
        public static void main(String[] args) {
            Thread thread = new Thread(new MyRunnable());
            thread.start();
        }
    }
    
    

    (3) 实现 Callable 接口

    Callable 是 jdk 1.5 版本之后增加的任务接口,实现 call 方法,并且能够从线程返回结果,但是 Callable 只能通过线程池来提交执行。

    public class ThreadCallable {
    
        private static class MyCallable implements Callable<String> {
    
            @Override
            public String call() throws Exception {
                int count = 0;
                while (count < 5) {
                    System.out.println(Thread.currentThread().getName() + " - " + count);
                    Thread.sleep(1000);
                    count++;
                }
                return "Thread is finished,the result is Hello World!";
            }
        }
    
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            ExecutorService service = Executors.newCachedThreadPool();
            Future<String> future = service.submit(new MyCallable());
            while (!future.isDone()) {
                System.out.println("waiting ------");
                Thread.sleep(500);
            }
            System.out.println(future.get());
            service.shutdown();
        }
    }
    

    (3) 通过 FutureTask 创建任务

    FutureTask 也是 jdk 1.5 版本之后增加的任务类,除了能够获取线程返回的结果,还能够对线程进行更多的操作,如取消,查询线程状态,结果等。FutureTask 既可以用线程池提交执行,也可以使用 Thread 来开启。

        private static class MyCallable implements Callable<String> {
    
            @Override
            public String call() throws Exception {
                int count = 0;
                while (count < 5) {
                    System.out.println(Thread.currentThread().getName() + " - " + count);
                    Thread.sleep(1000);
                    count++;
                }
                return "Thread is finished,the result is Hello World!";
            }
        }
    
    
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            // 1.线程池方式提交
            ExecutorService service = Executors.newCachedThreadPool();
            FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
            service.submit(futureTask);
            service.shutdown();
        }
    

    通过普通线程开启 FutureTask 任务

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 2.FutureTask 支持线程方式
        FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
        Thread thread = new Thread(futureTask);
        thread.start();
        while (!futureTask.isDone()) {
            System.out.println("waiting ------");
            Thread.sleep(500);
        }
        System.out.println(futureTask.get());
    }
    

    实现 Runnable 接口比继承 Thread 类所具有的优势:

    • 适合多个相同的程序代码的线程去处理同一个资源

    • 可以避免 java 中的单继承的限制

    • 增加程序的健壮性,代码可以被多个线程共享,代码和数据独立

    • 线程池只能放入实现 Runable 或 Callable 类线程,不能直接放入继承 Thread 的类

    4.2 基本操作

    (1) start : 这个不用多讲,上面例子已经提到了,线程创建之后开启线程,此时线程处于就绪状态,等待操作系统分配时间片,获取到时间片就可以运行

    (2) setPriority 设置线程优先级:Java线程有优先级,优先级高的线程会获得较多的运行机,但并不表明一定能够运行,只是获取时间片的概率高。

    Java线程的优先级用整数表示,取值范围是 1~10,Thread类有以下三个静态常量:

    • static int MAX_PRIORITY 线程可以具有的最高优先级,取值为10。

    • static int MIN_PRIORITY 线程可以具有的最低优先级,取值为1。

    • static int NORM_PRIORITY 分配给线程的默认优先级,取值为5。

    每个线程都有默认的优先级。主线程的默认优先级为 Thread.NORM_PRIORITY。
    线程的优先级有继承关系,比如 A 线程中创建了 B 线程,那么 B 将和 A 具有相同的优先级。
    JVM 提供了 10 个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用 Thread 类有以下三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式。

    (3) setDaemon 设置后台线程

    守护线程和用户线程的区别在于:守护线程依赖于创建它的线程,而用户线程则不依赖。如果在 main 线程中创建了一个守护线程,当 main 方法运行完毕之后,守护线程也会随着消亡。而用户线程则不会,用户线程会一直运行直到其运行完毕。在JVM中,像垃圾收集器线程就是守护线程。

    此外,守护线程中创建的线程也是守护线程。

    public class ThreadDaemon {
    
        private static class PrintRunnable implements Runnable {
    
            private String name;
    
            PrintRunnable(String name) {
                this.name = name;
            }
    
            @Override
            public void run() {
                int count = 0;
                System.out.println(name + " - start - isDaemon - " + Thread.currentThread().isDaemon()¬);
                while (true) {
                    System.out.println(name + " - " + count);
                    count++;
                    try {
                        Thread.sleep(1500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
        public static void main(String[] args) {
            Thread daemon = new Thread(new PrintRunnable("daemon"));
            daemon.setDaemon(true);
            daemon.start();
            int count = 5;
            while (count > 0) {
                System.out.println(Thread.currentThread().getName() + " - " + count);
                count--;
                try {
                    Thread.sleep(1500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    

    守护线程没有运行完,随着主线程退出而退出。

    (4) yield,线程礼让。

    调用 yield 方法会让当前线程交出 CPU 权限,让 CPU 去执行其他的线程。它跟 sleep 方法类似,同样不会释放锁。但是 yield 不能控制具体的交出 CPU 的时间,另外,yield 方法只能让拥有相同优先级的线程有获取 CPU 执行时间的机会。

    注意,调用 yield 方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取时间片,这一点是和 sleep 方法不一样的。

    public class ThreadYield {
    
        private static class PrintRunnable implements Runnable {
    
            private String name;
    
            PrintRunnable(String name) {
                this.name = name;
            }
    
            @Override
            public void run() {
                int count = 100;
                System.out.println(name + " - start");
                while (count > 0) {
                    if ("yield".equals(name)) {
                        Thread.yield();
                    }
                    System.out.println(name + " - " + count);
                    count--;
                }
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            Thread yield = new Thread(new PrintRunnable("yield"));
            Thread normal = new Thread(new PrintRunnable("normal"));
            yield.setPriority(Thread.MAX_PRIORITY);
            yield.start();
            normal.start();
            Thread.sleep(2000);
        }
    }
    

    从运行结果看出 yield 方法并不一定立即交出 CPU 使用权,另外,yield 方法增加其他线程获取时间片的机会,并不意味着调用 yield 的线程就获取不到时间片。

    yield - start
    normal - start
    yield - 100
    normal - 100
    yield - 99
    normal - 99
    yield - 98
    normal - 98
    yield - 97
    normal - 97
    yield - 96
    normal - 96
    normal - 95
    normal - 94
    yield - 95
    normal - 93
    yield - 94
    ...
    

    (5) join

    join 方法有三个重载函数:

    join()
    join(long millis)     //参数为毫秒
    join(long millis,int nanoseconds)    //第一参数为毫秒,第二个参数为纳秒
    

    调用 Thread 的 join 方法,表明会等待指定的线程执行完毕,或者等待这个 线程执行一段时间后再开始执行。下面例子中 A 线程会等待 B 线程执行结束后再继续执行。如果执行时间,表明会等待一段时间后继续执行。

    public class ThreadJoin {
    
        private static class PrintRunnable implements Runnable {
    
            private String name;
            private Thread joinThread;
    
            public PrintRunnable(Thread joiner, String name) {
                this.name = name;
                this.joinThread = joiner;
            }
    
            public PrintRunnable(String name) {
                this.name = name;
            }
    
            @Override
            public void run() {
                int count = 5;
                System.out.println(name + " - start");
                while (count > 0) {
                    if (joinThread != null) {
                        try {
                            joinThread.join();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println(name + " - " + count);
                    count--;
                }
            }
        }
    
        public static void main(String[] args) {
            Thread B = new Thread(new PrintRunnable("thread-B"));
            Thread A = new Thread(new PrintRunnable(B, "thread-A"));
            A.start();
            B.start();
        }
    }
    

    (6) sleep 线程睡眠。

    sleep方法有两个重载版本:

    sleep(long millis)     //参数为毫秒
     
    sleep(long millis,int nanoseconds)   //第一参数为毫秒,第二个参数为纳秒
    

    sleep 相当于让线程睡眠,交出 CPU,让 CPU 去执行其他的任务。

    有一点要非常注意,sleep 方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用 sleep 方法,其他线程也无法访问这个对象。

    public class ThreadSleep {
    
        private static int i = 10;
        private static final Object object = new Object();
    
        public static void main(String[] args) throws IOException {
            MyThread thread1 = new MyThread();
            MyThread thread2 = new MyThread();
            thread1.start();
            thread2.start();
        }
        
        private static class MyThread extends Thread{
            @Override
            public void run() {
                synchronized (object) {
                    i++;
                    System.out.println("i:"+i);
                    try {
                        System.out.println("线程"+Thread.currentThread().getName()+"进入睡眠状态");
                        Thread.sleep(10000);
                    } catch (InterruptedException e) {
                        // TODO: handle exception
                    }
                    System.out.println("线程"+Thread.currentThread().getName()+"睡眠结束");
                    i++;
                    System.out.println("i:"+i);
                }
            }
        }
    }
    
    

    (7) 线程中断,interrupt 、isInterrupted、interrupted

    这里需要注意,调用 interrupt 方法相当于将中断标志位置为 true,并不是直接中断线程,isInterrupted 是用来判断中断标志位的,interrupted 同样也是用来判断中断标志位的,但是多一个操作,会将中断标志位置为 false。

    interrupt 方法调用后,如果线程处于运行状态,仅仅表现为中断标识位为 true,并不能中断线程,这时需要自己通过 isInterrupted 或者 interrupted 判断来中断线程;如果线程处于阻塞状态,会抛出 InterruptedException 异常,此时线程会继续执行,如果是循环执行的线程,需要中断的话,需要在异常出现时自己做中断处理。

    下面来看下中断线程的几种方法:

    public class ThreadInterruption {
    
        private static class Interruption1 implements Runnable {
    
            @Override
            public void run() {
                int count = 0;
                try {
                    while (!Thread.currentThread().isInterrupted()) {
                        Thread.sleep(500);
                        count++;
                        System.out.println(Thread.currentThread().getName() + " --- " + count + " ---");
                    }
                    // 如果不睡眠,会走这里正常结束
                    System.out.println(Thread.currentThread().getName() + " is interrupted normal");
                } catch (InterruptedException e) {
                    // 睡眠,会走这里,报异常
                    e.printStackTrace();
                    System.out.println(Thread.currentThread().getName() + " is interrupted abnormal!");
                }
            }
        }
    
        private static class Interruption2 implements Runnable {
    
            @Override
            public void run() {
                int count = 0;
                while (!Thread.currentThread().isInterrupted()) {
                    count++;
                    System.out.println(Thread.currentThread().getName() + " --- " + count + " ---");
                    // 如果不加睡眠,可以中断
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        System.out.println(Thread.currentThread().getName() + " is interrupted abnormal!");
                        e.printStackTrace();
                        // 加睡眠,会走异常,需要在这里处理,结束线程,否则线程继续执行
                        break;
                    }
                }
    
            }
        }
    
        private static class Interruption3 implements Runnable {
    
            private volatile boolean isStop = false;
    
            public void setStop(boolean stop) {
                isStop = stop;
            }
    
            @Override
            public void run() {
                int count = 0;
                while (!isStop) {
                    count++;
                    System.out.println(Thread.currentThread().getName() + " --- " + count + " ---");
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName() + " is stopped normal");
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            Thread thread1 = new Thread(new Interruption1());
            Thread thread2 = new Thread(new Interruption2());
            Interruption3 target = new Interruption3();
            Thread thread3 = new Thread(target);
            thread1.start();
            thread2.start();
            thread3.start();
    
            Thread.sleep(2000);
            thread1.interrupt();
            Thread.sleep(2000);
            thread2.interrupt();
            Thread.sleep(2000);
            target.setStop(true);
        }
    }
    
    

    Interruption1 中 try-catch 包裹 while 循环的外面,线程阻塞时被中断,抛出异常,线程会结束;如果线程在非阻塞时中断,通过线程的中断状态来结束 while 循环,正常结束线程。

    Interruption2 中 while 循环包裹 try-catch 的外面,线程阻塞时被中断,抛出异常,此时如果不加 break 或者 return 之类的操作,线程会继续执行;如果线程在非阻塞时中断,通过线程的中断状态来结束 while 循环,正常结束线程。

    Interruption3 中自己设置一个变量来控制线程的中断。

    (8) wait 、notif、notifyAll

    • 调用某个对象的 wait() 方法能让当前线程阻塞,并且当前线程必须拥有此对象的 monitor(即锁),所以调用 wait() 方法必须在同步块或者同步方法中进行(synchronized 块或者 synchronized 方法)。

    • 调用某个对象的 notify() 方法能够唤醒一个正在等待这个对象的 monitor 的线程,如果有多个线程都在等待这个对象的 monitor,则只能唤醒其中一个线程;

    • 调用 notifyAll() 方法能够唤醒所有正在等待这个对象的 monitor 的线程;

    调用某个对象的 wait() 方法,相当于让当前线程交出此对象的 monitor,然后进入等待状态,等待后续再次获得此对象的锁(Thread 类中的 sleep 方法使当前线程暂停执行一段时间,从而让其他线程有机会继续执行,但它并不释放对象锁);

    notify() 方法能够唤醒一个正在等待该对象的 monitor 的线程,当有多个线程都在等待该对象的 monitor 的话,则只能唤醒其中一个线程,具体唤醒哪个线程则不得而知。

    下面演示一个线程的小算法,通过两个线程实现交替打印奇偶数。

    public class ThreadOddEven {
    
        private static class OddEvenRunnable implements Runnable {
            private final Object object = new Object();
            private int count = 0;
    
            @Override
            public void run() {
                while (!Thread.currentThread().isInterrupted()) {
                    synchronized (object) {
                        System.out.println(Thread.currentThread().getName() + " --- " + count++);
                        try {
                            Thread.sleep(300);
                            object.notifyAll();
                            object.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            break;
                        }
                    }
                }
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            OddEvenRunnable oddEvenRunnable = new OddEvenRunnable();
            Thread even = new Thread(oddEvenRunnable);
            Thread odd = new Thread(oddEvenRunnable);
            even.start();
            odd.start();
    
            Thread.sleep(5000);
    
            even.interrupt();
            odd.interrupt();
        }
    }
    

    4.3 同步

    同步这部分涉及的内容较多,后面单独来讲,基础的主要就是 synchronized、lock 的使用以及 volatile 关键字。

    参考

    Java多线程学习

    Java并发编程:线程间协作的两种方式:wait、notify、notifyAll和Condition

    多线程详解(2)——不得不知的几个概念

    相关文章

      网友评论

          本文标题:并发基础知识扫盲

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