• 中文
    • English
  • 注册
  • 查看作者
  • 一. 并发编程线程基础

    一.  线程和进程

    1.  什么是进程

    进程是代码在数据集合上的一次运行活动,是系统除CPU外进行资源分配和调度的基本单位(CPU资源是直接分配到线程的,线程是CPU分配的基本单位),进程包括线程。

    2.  什么是线程

    线程是进程的一个实体,一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源

    3.  在Java中,启动main函数就相当于启动了一个JVM进程,main函数所在的线程称为主线程

    4.  一个进程中的线程共享该进程的堆和方法区的资源,堆是一个进程中最大的一块内存,主要存放使用new操作创建的对象实例。方法区用来存放JVM加载的类、常量、及静态变量等信息。

    5.  每个线程有自己的程序计数器和栈区域,程序计数器用于记录当前线程要执行的指令地址,栈用来存储该线程的局部变量,该局部变量无法被其他线程访问。

    二.  线程创建与运行

    创建线程一共有三种方式

    方式一:继承Thread类

    public class ThreadTest {
        public static class MyThread extends Thread{
            @Override
            public void run() {
                System.out.println("I am a child thread");
            }
        }
    
        public static void main(String[] args) {
            //1.  创建线程
            MyThread myThread = new MyThread();
            //2.  启动线程
            myThread.start();
        }
    }

    需要注意的是,当第一步创建了Thread对象后,线程并没有被执行,直到调用了start方法,该线程才被启动执行,调用start方法后,该线程处于就绪状态(已经获得了除CPU外其他资源),当该线程获取了CPU资源后才真正处于运行状态,执行完run方法,线程处于终止状态。

    方式二:实现Runnable接口

    public class ThreadTest {
    
        public static void main(String[] args) {
            //1.  创建线程
            RunnableTask task = new RunnableTask();
            //2.  启动线程
            new Thread(task).start();
            new Thread(task).start();
        }
    
        public static class RunnableTask implements Runnable {
    
            @Override
            public void run() {
                System.out.println("I am a child thread");
            }
        }
    }

    前两种方式任务都没有返回值,可以FutureTask的方式来获取任务的返回值

    方式三:FutureTask

    public class ThreadTest {
    
        public static void main(String[] args) {
            //1. 创建异步任务:使用FutrueTask对象作为任务,创建了一个进程
            FutureTask<String> futureTask = new FutureTask<>(new CallerTask());
            //2. 启动线程
            new Thread(futureTask).start();
            try {
            //3. 等待任务执行完毕,并返回结果
                String result = futureTask.get();
                System.out.println(result);//输出:zhangjia
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        //创建任务类,类似Runnable
        public static class CallerTask implements Callable<String> {
    
            @Override
            public String call() throws Exception {
                return "ZhangJia";
            }
        }
    }

    三种方式的优缺点:

    • 继承Thread类

      • 优点:在run()方法中可以直接使用this获取当前线程,方便传参,可以在子类里添加成员变量,通过set方法设置参数或者通过构造方法传递

      • 缺点:java不支持多继承,任务和代码没有分离,当多个线程执行一样的任务时需要多份任务代码

    • 实现Runnable接口

      • 优点:可以继承其他类,多个线程可以共用一份代码

      • 缺点:没有返回值,只能使用主线程里面被声明为final的变量

    • FutureTask:

      • 优点:可以拿到任务的返回值

    三.  线程的通知与等待

    在讨论线程的通知与等待之前,想起之前面试的时候,面试官一直在问各种多线程相关的问题,其中有一个问题印象深刻:你用过Object类中的哪些方法?菜鸡的我只说出了equals,toString,getClass,hashCode之类常见的方法,其实面试官想要的答案是wait、notify、notifyAll,接下来我们就从这几个函数入手,来讲解一下线程的通知与等待

    1. wait()函数

    一个线程可以通过调用一个共享变量的wait()方法将自己挂起,直到发生以下事情之一才返回:

    • 其他线程调用了该共享对象的notify() 方法或者notifyAll() 方法

    • 其他线程(或者该线程自己调用interrupt)调用了该线程的interrupt()方法导致该线程抛出InterruptedException异常返回

    另外如果调用wait方法的线程事先没有获取该独享的监视器锁,那么会抛出IllegalMonitorStateException异常,可以通过下面两种方法来获取该对象的监视器锁来避免该异常

    //方法一
    synchronized (共享变量){
        //防止虚假唤醒
        while(条件不满足) {
            共享变量.wait(); 
        }
    }
    //方法二
    synchronized void add(int a, int b){
        //doSomething
    }

    需要注意的是,如果一个线程持有多个共享变量的锁,那么调用wait方法后只会释放当前共享变量的锁,举个例子来理解一下这句话:

    package io.zhangjia.threads.test.Test;
    
    import org.apache.ibatis.annotations.Param;
    
    /**
     * @Author : ZhangJia
     * @Date : 2019/12/4 21:45
     * @Description : 
     */
    public class Test2 {
        private static volatile Object a = new Object();
        //volatile关键字能够保证代码的有序性
        private static volatile Object b = new Object();
    
        public static void main(String[] args) {
            new Thread(new ThreadA()).start();
            new Thread(new ThreadB()).start();
        }
    
        public static class ThreadA implements Runnable {
    
            @Override
            public void run() {
                try {
                    synchronized (a) {
                        System.out.println("线程A获得了共享变量a的监视器锁");
    
                        synchronized (b) {
                            System.out.println("线程A获得了共享变量b的监视器锁");
    
                            System.out.println("线程A阻塞,并释放获取到的共享变量a的锁");
                            a.wait();
    
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
    
        public static class ThreadB implements Runnable {
    
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                    synchronized (a) {
                        System.out.println("线程B获取到共享变量a的锁啦");
                        System.out.println("我是线程B,我一直在尝试获取共享变量b的锁,就是没获取到");
    
                        synchronized (b) {
                            System.out.println("线程B获取到共享变量b的锁啦");
                            System.out.println("线程B阻塞,并释放获取到的共享变量a的锁");
                            a.wait();
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
            }
        }
    }
    输出:
    线程A获得了共享变量a的监视器锁
    线程A获得了共享变量b的监视器锁
    线程A阻塞,并释放获取到的共享变量a的锁
    线程B获取到共享变量a的锁啦
    我是线程B,我一直在尝试获取共享变量b的锁,就是没获取到

    上例中,线程A一开始拥有变量a和变量b的锁,通过调用a的wait方法后,只有a的锁被释放了,b的锁并没有被释放掉,所以线程B无法获取到b变量的锁。

    2. wait(long tiemout)函数

    timeout意为超时参数,单位是毫秒(ms),比如wait(1000)意为在1秒内如果没被其他线程调用notify()或者notifyAll()方法唤醒,还是会因为超时返回。

    3.  wait(long timeout,int nanos)函数

    timeout单位是毫秒,nanos单位是纳秒,1毫秒 = 1000 微秒 = 1000 000 纳秒 ,该函数的主要作用是为了能够更加精确的控制等待时间

    4. notify()函数

    该方法用户唤醒被挂起的线程。注意阻塞和挂起的区别,阻塞是被动的,挂起是主动的。

    另外如果一个共享变量上有多个线程被挂起了,那么具体唤醒哪个等待线程是随机的,被唤醒的线程依旧需要先获取共享对象的锁才可以返回。

    notify函数和wait函数类似,如果当前线程没有获取到共享变量的监视器锁就调用notify方法,也会抛出IllegalMonitorStateException异常。

    5. notifyAll()函数

    当一个线程通过共享变量调用notifyAll()函数时,该方法会唤醒在此之前所有通过该共享变量调用了wait系列函数的线程。

    举一个例子来理解:

    package io.zhangjia.threads.test.Test;
    
    /**
     * @Author : ZhangJia
     * @Date : 2019/12/15 19:30
     * @Description : 
     */
    public class NotifyTest {
        private static volatile Object a = new Object();
    
        public static void main(String[] args) throws InterruptedException {
            Thread threadA = new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (a) {
                        System.out.println("线程A获得了共享变量a的监视器锁");
                        try {
                            System.out.println("线程A开始等待");
                            a.wait();
                            System.out.println("线程A结束等待");
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
    
                    }
                }
            });
    
            Thread threadB = new Thread(new Runnable() {
                @Override
                public void run() {
                   synchronized (a) {
                       System.out.println("线程B获得了共享变量a的监视器锁");
                       try {
                           System.out.println("线程B开始等待");
                           a.wait();
                           System.out.println("线程B结束等待");
                       } catch (InterruptedException e) {
                           e.printStackTrace();
                       }
                   }
                }
            });
    
            Thread threadC = new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (a) {
                        System.out.println("线程c开始唤醒");
                        a.notify();
                    }
                }
            });
    
            threadA.start();
            threadB.start();
            Thread.sleep(1000); //主线程先休眠1s,为了让线程A和线程B都执行完wait方法后再调线程c的notify方法
            threadC.start();
    
            //等待线程结束
            threadA.join();
            threadB.join();
            threadC.join();
            System.out.println("主线程结束");
        }
    }
    //输出:
    线程A获得了共享变量a的监视器锁
    线程A开始等待
    线程B获得了共享变量a的监视器锁
    线程B开始等待
    线程c开始唤醒
    线程A结束等待

    通过上例可以看到,当线程A和线程B分别通过a变量调用wait方法将自身挂起后,线程c调用notify方法,随机唤醒了一个线程,如果将线程c中的 a.notify();修改为 a.notifyAll();则结果为:

    线程A获得了共享变量a的监视器锁
    线程A开始等待
    线程B获得了共享变量a的监视器锁
    线程B开始等待
    线程c开始唤醒
    线程B结束等待
    线程A结束等待
    主线程结束

    四.  等待线程执行终止的Join方法

    未完待续…

    最近一次更新:19.12.15

    山东省·日照市
  • 0
  • 0
  • 0
  • 1.2k
  • zjmarina

    请登录之后再进行评论

    登录
    单栏布局 侧栏位置: