多线程(Java版)

目录

   1.多线程的创建

       1.1基于继承Thread去创建线程

      1.2基于实现Runnable去创建线程

    1.3通过匿名内部类的方式创建线程

    1.4lambda表达式创建线程

2.深入了解Thread类以及常见的方法

   2.1Thread的构造方法

  2.2 Thread常见的几个属性

   2.3start()方法

   2.4线程的终止

2.4等待一个线程 --- join

  3.线程的状态

4.线程安全问题

   4.1“线程安全问题” ---- synchronized

  4.2内存可见性问题  ---- volatile

5.wait 和 notify


  在上一篇文章《进程和线程》中我们已经介绍了进程和线程的相关属性了,我们在这一篇文章进一步对线程进行学习和模拟实现多线程下的一些典型的模型(这里的时候把文章都是采用Java去实现,编译器为IDEA)。

   1.多线程的创建

       1.1基于继承Thread去创建线程

          我们在这里先去简单的实现一个线程,线程的目的就是可以无限循环的打印“hello thread”,  现在我先放实现的代码,对于代码的内容我等等会做进一步的解释。

//先写一个简单的hello world
class  MyThread extends Thread {
    @Override
    public void run() {
        //这里的run就相当于线程的入口方法
        //线程具体跑起来之后,要做啥事,都是通过这个run来描述的
        //System.out.println("hello world");
        while(true) {
            System.out.println("hello thread");
            try {
                //这里只能try catch,不能 throes
                //这里是父类方法的重写。对于父类的run方法来说,本身就没有throes XXX
                Thread.sleep(1000); //sleep类是Thread类的静态方法
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //和main的线程(主线程)并发执行,
            //hello thread 和 hello main交错打印
        }
    }
}

    在这上面的代码中我们先定义了一个MyThread的类,然后让这个类去继承Thread,可以很快的看出我定义的这个MyThread中好像就实现了 run()这个方法,对这个是对Thread中的run方法进行重写,当我对这个线程进行创建的时候,你要这个线程执行什么命令就在run里面描述出来,这里为了等等在执行的时候打印“hello thread”的速度慢一点就采用了Thread类中的另外一个方法sleep(有异常需要抛出或者解决,在这里只能采用try catch的方式来解决)。

   接下来我们在主线程(main)中去创建Mythread线程

public class Test {
    public static void main(String[] args) throws InterruptedException {
        MyThread myThread =new MyThread();
        myThread.start();//start就是在创建线程

    }
}

   在这里的创建线程的时候一定要使用类名.start()这样的写法,在后续的创建线程也是需要start()方法的。这里给出一个问题:“这里我为什么不能实例化对象之后,然后实例化的对象直接.run()去运行呢?”,这个问题我们等等去解决,我们先看如果是使用start()去创建线程的时候,该线程是否能按照我们最初的要求去执行指令。

    这里很显然是没有问题的,它会无限循环的执行打印“hello thread”的指令,那我们这里对main进行一些代码上的改变(main方法中的 throws InterruptedException 这个是对Thread.sleep抛出异常等等会使用到)。

public class Test {
    public static void main(String[] args) throws InterruptedException {
        MyThread myThread =new MyThread();
        myThread.start();//start就是在创建线程
        while(true) {
            System.out.println("hello main");
            Thread.sleep(1000);
        }

    }
}

    这里如果是我们之前没有学习多线程的话,加上我这里不是使用myThread.start()去创建线程而是使用myThread.run()去对对象点方法的话,按照代码的执行逻辑main中的while循环始终都不会被执行到(run方法中的while也是一个无限循环),这岂不是就变成了“串行执行”了,而我们学习多线程不就是为了提高代码执行的效率而去使用了“并发编程”的思路吗,这里先运行上面的代码去看一下运行的结果是啥样的。

   通过这段执行的结果我们不难看出这里好像是“hello thread” 和 “hello main”交错打印的(加上多线程的随机调度的特性,打印的顺序是不固定的),接着我们在使用thread.run()去替换掉thread.start()方法看看代码运行的效果有啥区别

    public static void main(String[] args) throws InterruptedException {
        MyThread myThread =new MyThread();
        //myThread.start();//start就是在创建线程
        myThread.run();
        while(true) {
            System.out.println("hello main");
            Thread.sleep(1000);
        }

    }

 

     这就是直接.run()的执行结果,并没有像上面一样代码是交错打印的,而是一味的去打印"hello thread",这就不是多线程的代码了,所以我们回到上面的问题, start()这个操作是对从底层去调用操作系统提供的“创建线程”的api,同时就会在操作系统的内核里去创建出对应的PCB给创建出并管理好 新的线程就参与调度,而myThread()也是可以去执行出run方法里面的指令,但是这本质上还是在主线程(main)中去执行的,并没有去创建出新的线程,所以当我们实例化好了一个线程的对象的时候一定要使用start()方法去创建线程。

    补充:Thread.sleep()方法会让线程进入(阻塞)状态(无法被操作系统调度),当这两个线程都唤醒之后,谁先调度谁后调度。可以视为是”随机“的(这里的“随机”调度的过程,也可以被称为“抢占式执行”)

      1.2基于实现Runnable去创建线程

         在多线程中除了上面的去继承Thread创建线程的方法外,还有是实现Runnable方法的,从字面上来看二者的区别好像蛮大的,实际上这里就只有在Java中继承和实现的定义不同,代码的实现是大差不差的,这里先上方法个大伙体验一下。

class MyRunnable implements Runnable {
    @Override
    //描述线程
    public void run() {
        while(true) {
            System.out.println("hello runnable");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

    本质就是定义的类去继承(implements)Runnable,然后再去实现run方法,但是在创建线程的时候操作会比较麻烦一点点,

 public static void main(String[] args) throws InterruptedException {
        MyRunnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        thread.start();//启动线程
        while (true) {
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }

  通过上面的代码不难看出比之前基于继承Thread去创建线程多了一步,那这里就有个问题了,既然达到的效果都是一样的,何必要去使用这种方法去创建线程呢? 基于实现Runnable看上去好像式把代码变得更复杂了,但是在Java中还存在这种写法肯定是有他的道理的,先让我们去看看上面的代码执行结果啥样先。

   代码执行结果很明显的了,可以正常的创建线程并且执行的,接下来就回到上面的问题了,把要完成的工作放到Runnable中,再让Runnable和Thread配合,1. 这样的写法把线程要执行的任务和线程本身进一步的解耦合 ;2.并发编程的方式,来完成某个工作的具体细节 而且 Runnable不仅仅只能搭配Thread去使用,还可以使用线程池的方式,就可以使用Runnable搭配线程池使用,使用协程(本篇文章不会涉及到)的方式,也可以使用Runnable搭配协程。

    所以Runnable在多线程这里的作用可以视为一个百搭怪,并且上面的基于继承Thread去创建线程和基于实现Runnable的写法都是比较繁琐的,因为都要事先去创建好一个类(这两其实都差不多,一样麻烦)。

    1.3通过匿名内部类的方式创建线程

      在学习Java中我们在类和对象那一块知识点接触到了“匿名内部类”,这个在之前的学习中好像没有体现啥优势区间(在优先级队列(PriorityQueue)中是有体现出来的),都是在多线程的创建这一快是可以很明显的减低创建线程的繁琐程度的,这里先上代码看看。

  //3.继承Thread,通过匿名内部类来创建线程
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("hello Thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };

        thread.start();
        while(true) {
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
 public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("hello Thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        t.start();
    }

   通过上面的代码,会发现好像有点点眼熟,在仔细一看会发现这不就是上面的基于继承Thread和实现Runnable去创建线程吗?对,就是基于上面两种方法搭配匿名内部类完成的线程创建,这里跟我们在给优先级队列加比较方法是差不多的(你可以在外面实现一个比较器在main中实例化给到优先级队列,也可以给优先级队列参数的时候给一个匿名内部类在里面实现比较器的代码),这里的第三种方法就是匿名内部类 + “基于继承Thread” 或者 “基于实现Runnable”去完成的,简化了额外去创建类的过程。

    1.4lambda表达式创建线程

      lambda表达式这里,博主对这个的定义不是特别熟悉,先上代码展示写法(具体的可以去其他博主那了解一下,由于篇幅原因就不做过多的解释啦)

 public static void main1(String[] args) {
        Thread thread = new Thread(() -> {
            while (true) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        thread.start();
    }

2.深入了解Thread类以及常见的方法

   2.1Thread的构造方法

 首当其冲的先让我们来看一下Thread的构造方法

  这里的重点不多,就是有一个命名线程名字的需要我们看一下,此时有的人就有疑问了,蛙趣原来我创建的线程还有名字的,这是肯定的不然到时候多线程运行的时候我们怎么去区别哪个是哪个线程的,这里要看到线程的名字需要一点点特殊的手法  先要找到你jdk在硬盘中的位置(idea里面可以找,不用一个文件一个文件的找)

   再去通过jdk的路径去找到里面的

 然后双击启动就 

 

选择你线程启动的项目名称

这里面的Thread-0就是我们正在运行的线程了,修改名称的话就是在这里可以体现出来的(自己去玩一下)。 

  2.2 Thread常见的几个属性
属性获取方法
ID

getID()

名称 getName()
状态getState()
优先级getPriority()
是否是后台线程isDaemon()
是否存活isAlive()
是否中断isInterrupted()

    ID:线程的身份标识(在jvm这里给线程设定的身份标识)

    名称:在上面的Thread构造方法中已经进行了介绍。

    状态:状态相关的内容在下文会有专门的地方进行描述

    优先级:线程被操作系统调度的优先级,设置/获取优先级,作用不是很大,线程的调度主要还是操作系统内核来负责的,而且操作系统调度的速度很快。

    后台线程(守护线程):后台线程是不会影响线程结束的

    前台线程:相比于后台线程,前台线程会影响进程结束,如果前台线程没有执行完,进程是不会结束的,也就是说一个线程前台线程都执行完指令了,就会退出,此时不管你后台线程有没有执行完都会跟着一起退出(我们在创建线程时 默认是创建前台线程的)。

    如果要将线程设置为后台线程可以通过Thread实例化的对象.setDaemon去完成,这里展示一下后台线程的代码运行

    可以很清楚的看到执行框,我后台线程里面的打印任务都没有执行就结束了(因为main方法里面没有什么要执行的指令了,创建好后台线程就没事了就直接退出) 

   是否存活:这里的存活指的是Thread对象对应的线程(系统内核)是否存活,Thread对象生命周期,并不是和系统中的线程完全一致的,一般来说都是Thread对象先创建好,然后手动调用start,内核才真正创建出线程,消亡的时候,可能是Thread对象,先结束了生命周期(没有引用指向这个对象),也可能是thread对象还在,内核中的线程把run执行完了就结束了

   2.3start()方法

    在之前多线程的创建的时候我们已经了解并使用了start()方法了,这里就不做过多的解释了

   2.4线程的终止

     在实现线程的终止我们在这有两种写法

      1).程序猿手动设置的标志位,来使run线程中的run尽快结束

      这里先上代码嗷

    //写作成员变量就不是触发变量捕获的逻辑了,而是“内部类访问外部类的成员”,本身就是欧克克的
    public  static  boolean isQuit = false;
    public static void main1(String[] args) throws InterruptedException {
        //final boolean  isQuit = false;
        Thread thread = new Thread(() -> {
            while(!isQuit) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();
        //主线程这里执行其他代码
        Thread.sleep(3000);
        //这个代码就是在修改前面设定的标识为
        isQuit = true;
        System.out.println("把 t 线程终止了");
    }

     这里可以看出我们是先在main方法的外部去创建一个全局变量,然后让它成为run方法里面的while循环条件,当我先把线程终止的时候就去修改isQuit(上述代码中的全局变量),就可以达到终止线程的操作了。 

     这个时候肯定又会有一个疑惑了,这里为什么一定要使用到全局变量呢,我在main方法里面定义一个变量给线程不可以吗?

     这个想法是错误的,这里的根本原因是我们在实例化对象的时候采用的使用lambda表达式的方式去实现的,lambda表达式的时机是很靠后的,这就导致,后续真正执行lambda的时候,在main方法里面定义的局部变量可能已经销毁了(这种情况是客观存在的),此时让lambda去访问一个已经被销毁的变量很明显是不合适的吧。

  我们先将上面的代码运行展示一下下

   lambda就引入了"变量捕获"这样的机制,lambda内部看起来是在直接访问外部的变量,其实本质上是把外面的变量给复制了一份到lambda里面(这样就可以解决刚才的生命周期的问题),但是变量捕获这里有个限制,要求捕获的变量得是一个final(至少看起来是final)

    public static void main1(String[] args) throws InterruptedException {
        final boolean  isQuit = false;
        Thread thread = new Thread(() -> {
            while(!isQuit) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();
        //主线程这里执行其他代码
        Thread.sleep(3000);
        //这个代码就是在修改前面设定的标识为
        //isQuit = true;  //这里就不能对isQuit进行修改了
        System.out.println("把 t 线程终止了");
    }
    public static void main1(String[] args) throws InterruptedException {
        boolean  isQuit = false;
        Thread thread = new Thread(() -> {
            while(!isQuit) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();
        //主线程这里执行其他代码
        Thread.sleep(3000);
        //这个代码就是在修改前面设定的标识为
        //isQuit = true;
        System.out.println("把 t 线程终止了");
    }

    上面的两段代码就可以在编译器上正常的运行的了,第二段代码只要你不对isQuit进行修改就是所谓的看上去像被final修饰的。

   这里如果这个变量想要进行修改,岂不是就不能进行变量捕获了?

   为啥Java中有这样设定呢?  Java是通过复制的方法来实现"变量捕获"的,如果外面的代码要对这个变量进行修改,就会出现一个情况:外面的变量被修改了,而里面的变量没变 --- 代码更容易出现歧义(不就寄了吗)---- 有些语言在这块的处理方式可能还会更加激进一点(比如js)

    2).直接使用Thread类里提供现成的标识位

    先上代码哈

public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()-> {
            //Thread.currentThread() 就是thread
            //但是lambda表达是在thread船舰
            while(!Thread.currentThread().isInterrupted()) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();//打印异常信息        
                }
            }
        });
        thread.start();
        Thread.sleep(3000);
        //把上述的标志位设置为true
        thread.interrupt();
    }

   有些人看到Thread.currentThread().isInterrupted()就有点蒙了,蛙趣这么能这么长的,其实这里是两个方法,Thread.currentThread()这个方法就是获取当前线程的对象,Thread提供的静态方法currentThread()在哪个线程调用这个方法就能获取到哪个线程的引用,由于这里实例化对象是通过匿名内部类的方式来实现的匿名内部类里面并不知道外面的thread到底是什么东西,所以就需要currentThread这个方法来指认一下下,然后再isInterrupted()(Thread里面提供的标志位)。

   然后下面的interrupt()方法就是把上面的标志位给设置为true,但这里会出现bug

  可以看到抛出错误的原因为java.lang.InterruptedException: sleep interrupted

  这里出现bug的原因是thread线程正在sleep()(阻塞状态)被interrupt唤醒了,一个线程可能是正在正常的运行,也可能是在sleep中,如果这个线程在sleep过程中,这里会出现个问题:是否需要将这个线程唤醒呢?? ----  还是得唤醒的。

  因此,线程正在sleep过程中,其他线程调用了interrupt方法,就会强制使sleep抛出一个异常,slepp也会被立即唤醒,但是sleep在被唤醒的同时系统会自动清楚标志位,给程序猿留下更多的操作空间,如果这里想让线程直接结束就在catch里面加一个break就欧克了,catch里面就是给我们进行收尾工作的(想干啥就干啥)

 //Thread 自带的标准位
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()-> {
            //Thread.currentThread() 就是thread
            //但是lambda表达是在thread船舰
            while(!Thread.currentThread().isInterrupted()) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();//打印异常信息,这里要是不想打印出异常信息就把这段代码删了就好了
                    break;
                }
            }
        });
        thread.start();
        Thread.sleep(3000);
        //把上述的标志位设置为true
        thread.interrupt();
    }

2.4等待一个线程 --- join

   多个线程是并发执行的,具体的执行过程都是由操作系统负责调度的,操作系统的调度线程的过程中是"随机的",无法确定线程执行的先后顺序,这里的随机性不是很好,不可控,在这里我们可以通过等待线程这样的规划去确定运行线程执行的顺序。

   假设现在有A和B两个线程,希望B先结束,A后结束,此时可以让A线程中调用B.join()的方法,此时B线程还没执行完的话,A线程如果先执行完的话就会一直在阻塞状态(死等)等到B线程结束了才会到A线程结束

   上代码看一下哈

public static void main(String[] args) {
        Thread B = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("hello B");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("B 结束了");
        });

        Thread A = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                System.out.println("hello A");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //先然B再A前先结束
            try {
                // 如果b此时还没执行完毕 B.join就会产生阻塞情况
                B.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("A 结束了");
        });
        A.start();
        B.start();
}

    这里可以看出A线程中的任务早早就完成了,可是A线程中的结束是打印的内容却在B的后面,这就是A线程在执行完任务后进入了“阻塞”状态,等到B线程完成以后才从阻塞状态中出来。

   阻塞 --》让代码暂时不继续执行了(该线程暂时不去cpu上参与调度了) 

   在之前讲的Thread类中提供的方法sleep()也是将线程进入阻塞状态,但不是死等,这里是死等,“死等”这种操作对应计算机来说并不是很合适(我怎么你要执行到猴年马月嘞),所以join这里提供了有带时间的版本

   这里要使用join进行等待的话,就尽量使用带有时间版本的,只要最迟等待时间还没被唤醒就不等了。当然join和前面的sleep一样也是可以被interrupt强制唤醒的 这里还有我们后面要讲到的wait。

  3.线程的状态

    在多线程中的状态有以下几种:

     NEW: 安排了工作, 还未开始行动
     RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.
     BLOCKED: 这几个都表示排队等着其他事情
     WAITING: 这几个都表示排队等着其他事情
     TIMED_WAITING: 这几个都表示排队等着其他事情
     TERMINATED: 工作完成了

    接下来我来一一说明

    1.NEW:安排了工作,还未开始行动

       Thread对象已经创建好了,都是没有调用start --- 就实例化好了对象,但是没有创建对应的线程

    2.RUNNABLE:就绪状态

       就绪状态可以理解成两种情况:1)线程正在CPU上运行,2)线程正在排队,随时都可以去CPU上执行

   3.BLOCKED:因为锁产生的阻塞(下文会进行说明)

   4.WAITED:因为调用wait产生的阻塞

   5.TIMED_WAITING:因为sleep产生的阻塞,如果是使用带时间版本的join也是TIMED_WAITING

   6.TERMINATED:不带时间版本的join就是TERMINATED,线程已经结束,但是Thread对象还在

 这里我们可以去画个图去描述上述状态之间的关系

   一条主线,三个分支 如果先要查询当前线程状态的话就可以去使用之前讲过的Thread类中的getClass()方法。

4.线程安全问题

   4.1“线程安全问题” ---- synchronized

    在这里我们假设一种情况,就是我们先基于继承Thread的方式去创建好两个线程,然后我run方法里面是对一个成员变量进行加1,我两个线程同时启动去调用用run方法(每个线程都去调用50000次)当所有线程都执行完的话,按照常理来这个成员变量一个是比初始的时候多了100000的,我们先实现上述的代码先

class Counter{
    public int count = 0;
    public void increase() {
        count++;
    }
}
public class demo12 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread t2 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println(counter.count);

    }
}

  此时我们运行一下代码,看一下count的值到底是不是100000

   这里你不管运行多少次不不可能得到100000的,这里就出现了bug,在多线程下,发现由于多线程执行,导致的bug,统称“线程安全问题”

   那我们来分析一下为什么会出现这样的问题, 

   我们先来看一下count++这个操作涉及到了几个步骤:1.把内存中的数据,加载到CPU的寄存器中(load);2.把寄存器中的数据进行+1(add);3.那寄存器中的数据写回到内存中(save),这里还记得操作系统的调度是随机性的吗,问题就出现在这里了

 如果系统在调度的过程中刚刚好是这种顺序的话,两个线程的指令刚刚好是在一起的话,对count的加1是可以正常进行的(两个线程都对count成功加了一次) 

  那如果是这种顺序呢 ---- 两个线程对数据进行读的指令的指令刚好紧紧挨着,或者一个线程的save指令还没被调度到,系统就调度了另外一个线程的load指令,这样的结果就会导致我save操作的时候t1线程和t2线程都对同一个数据进行了加一操作,导致返回到内存中的值是一样的(实际上只加了一次),现在只是两个线程并且就只有两个完整的操作,那换到多个线程或者t1执行了两个完整的指令后,但是在你t1执行之前我t2拿到了数据,然后等你t1两个完成的操作都完成之后我t2在把剩下的指令都完成了,岂不是前面t1的操作就没意义了(在实际情况下还会更加复杂一点)

    1)根本原因:多个线程之间的调度顺序是“随机的”,操作系统使用“抢占式”执行的策略来调度线程 ---- 万恶之源,在加上和单线程不同的是,多线程下,代码的执行顺序,产生了更多的变化.以往只需要考虑代码在一个固定的顺序下执行,执行正确即可.现在则要考虑多线程下,N 种执行顺序下,代码执行结果都得正确

    2)多个线程同时修改同一个变量,容易产生线程安全问题,如果是以下情况是不会出现线程安全问题的:1.一个线程修改一个变量;2.多个线程读取同一个变量;3.多个线程修改多个变量(自己修自己的);所以说从本质上来说是代码结构的问题

    3).进行的修改不是“原子的”,如果修改操作能够按照原子方式来完成。此时也不会有线程安全问题

   4)内存可见性 引起的线程安全问题(后面才涉及到)

   5)指令重排序,引起的线程安全问题 (后面)

  这里我们要解决方法主要采取的思路是把count++这一个操作变成“原子性”的,这里“天降猛男”---synchronized

   synchronized 就是我们一直有提到的锁操作,加锁-->就相当于把一组操作,给打包成了一个“原子”的操作,代码中的锁就是让多个线程同一时刻,只能有一个线程可以使用里面的变量

   这里加锁有两种方式

   1.直接对可能会出现线程安全问题的方法加锁

class Counter{
    public int count = 0;
    synchronized public void increase() {
        count++;
    }
}

  2.针对方法里面可能会出现线程安全的操作进行加锁

class Counter{
    public int count = 0;
    public void increase() {
        synchronized (this) {
            count++;
        }
    }
}

  这里先不讨论这两种加锁有什么区别,我们先看看加锁后代码会方式啥变化

    如果对count++进行加锁后,当t1调用了increase方法时候就会对里面的数据进行加锁,当t2也想尝试对increase进行加锁的时候就会进入阻塞状态,这个阻塞会持续到t1把锁释放后t2才能进行加锁成功(也是要看系统调度到谁的 有可能t1连续别调度到几次),此处的t2的阻塞等待,就会把t2的针对count++ 操作,推迟到后面完成,直到t1完成了count++ ,t2才能够真正进行count++。

   把“穿插执行”变成了“串行执行”。

我们先来看一下这样改过之后的代码执行结果

     加锁的串行并不是整体的串行,这里的i不是同一个变量,不涉及线程安全问题,所以两个线程有一部分代码是串行执行,有一部分代码是并发执行的 ---- 仍然比存粹的串行执行,效率要高的,能并发的地方就并发,总比没有强的。

     synchronized Java提供的加锁方式,搭配代码块来完成的,1.进入代码块就加锁;2.出来代码块就解锁,synchronized只要出了代码块无论是因为return还是抛异常,都能保证顺利解锁。

   synchronized 进行加锁解锁,其实是以“对象”为维度进行展开的,加锁是为了“互斥”的使用资源,但是加锁是以对象进行加锁,这就是我们上述的第二种写方法了 ----- synchronized针对某个特定的对象进行加锁,当 synchronized 直接修饰方法, 此时就相当于是针对 this 加锁(修饰方法相当于上述代码的简化写法)[不存在所谓的“同步方法”这样的概念]

public void increase() {
        synchronized (this) {
            count++;
        }
    }

  如果是两个线程对同一个对象进行加锁就会出现锁冲突/锁竞争

      如果是两个线程针对的不同对象加锁 -- 不会产生竞争,也不存在阻塞等待一类操作。所以如果要是两个线程针对不同的对象加锁,此时,就不会有阻寒等待 也就不会让两个线程按照串行的方式进行 count++,也就仍然会存在线程安全问题

//线程安全问题  ---  解决方法加锁进行“互斥”
class Counter{
    //在这里定义一个object类给increase2进行加锁
    private Object object = new Object();

    public int count = 0;
    public void increase1() {
        synchronized (this) {
            count++;
        }
    }

    public void increase2() {
        synchronized (object) {
            count++;
        }
    }

}
public class demo12 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase1();//针对this进行加锁
            }
        });
        Thread t2 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                counter.increase2();//针对object进行加锁
            }
        });
        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println(counter.count);

    }
}

补充:加锁的对象可以是任何类,在这里我实例化了一个Object类的对线给increase2进行加锁

  看一下代码运行的结果

  如果我把increase2中的加锁对象换成this,在看一下代码执行结果

 所以,具体是针对哪个对象加锁并不重要,重要的是,两个线程是不是针对同一个对象进行加锁

    锁(synchronized)的唯一规则:如果两个(或者多个)线程针对同一个对象进行加锁,就会出现锁竞争/锁冲突(一个线程加锁成功,其他线程加锁进入阻塞状态)。

     补充:加锁的时候还可以针对类对象的.class文件进行加锁(懒的额外去弄锁对象的时候可以采用类对象的.class文件进行加锁)。

  4.2内存可见性问题  ---- volatile

       照样的先假设场景,假设我有两个线程 和 一个全局变量,线程1依据这个全局变量的值进行while循环,线程2为等操作系统调度到2的时候,2有权利去修改这个全局变量的值从而去影响线程1是否要继续循环,这里上实现代码

//内存可见性
public class demo13 {
    private static  int isQuit = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while(isQuit == 0) {
                ;
            }
            System.out.println("t1 执行结束");
        });

        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入 isQuit的值:");
            isQuit = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

  按照我最初的设定当操作系统调度到线程2的时候,且当2将isQuit修改成一个不为0的数时,线程1就会停止执行while循环并且打印“t1 执行结束”,那让我们来看一下这段代码的运行结果

    这里就会发现,哎这么我都输入不为1的数个isQuit了,t1线程怎么还没结束的,这里就是内存可见性引起的问题。

    根本原因:编译器优化。本质上是靠代码,智能的对你写的代码进行分析判断进行调整. 这个调整过程大部分情况下都是 ok,都能保证逻辑不变但是,如果遇到多线程了,此时的优化可能就会出现差错!!!

    此时,编译器/jvm就发现,这个逻辑中代码要反复的,快速的读取同一个内存的值,并且这个值没次读出来还是一样的(你不知道系统什么是调度到线程2,更不知道线程2什么是希望线程1停下来),此时编译器就做出了一个大胆的决定,直接把load(读取)操作优化掉了,只是在第一次执行load,后续的操作直接拿寄存器中存储的数据(第一次load读到的数据)进行比较,所以我们在t2线程中对isQuit进行了修改,编译器无法准确的判定出t2线程待敌会不会执行(啥时候执行),编译器误判了,因此t1线程就无法感知到t2的修改,就出现bug了 --- 引起了内存可见性问题。

    这个时候有一位“天降猛男”  ----- volatile

      把volatile用来修饰一个变量之后,编译器就明白,这个操作时“易变”的,就不能按照之前的方式来处理读操作了(编译器就会禁止在这一块的优化)。

     我们把volatile给isQuit加上看一下执行的结果        

    此时的代码执行起来就是我们最初想要实现的效果了,volatile在多线程中不止有这个作用,它还能避免指令重排序的效果(后面再讲多线程模型的时候会讲到)

5.wait 和 notify

    内存可见性和加锁,是描述线程安全问题的典型情况和处理方式,wait和notify也是多线程编程中的重要工具使用来协调线程顺序的,由于线程的执行顺序是受操作系统的调度影响(随机性),这里我们为了让它有一点点执行顺序,我们就会通过让某个线程先进入阻塞(随眠)状态来调控线程的执行顺序,  

     wait 等待 ,notify 通知这两个是搭配使用的,使用wait和notify之前一定要先对某个对象进行加锁,任何在对这个加锁的对象进行 被加锁对象.wait() 才能顺利的进入阻塞状态,在使用notify的时候,如果wait 和 notify 在不同的线程中,那么先要放入notify去解锁前面那个因为wait进入阻塞状态的线程,这改线程也要针对前面线程加锁的对象进行加锁。

    这里上一个完整的代码展示一下

//wait 和 notify
public class demo14 {
    //使用这个锁对象进行加锁
    private static Object object = new Object();
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            while(true) {
                synchronized (object) {
                    System.out.println("t1 wait 开始");
                    try {
                        object.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("t1 wait 结束");
                }
            }
        });
        t1.start();

        Thread t2 = new Thread(() ->{
            while (true) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (object) {
                    System.out.println("t2 notify 开始");
                    object.notify();
                    System.out.println("t2 notify 结束");
                }
            }
        });
        t2.start();
    }

}

   这里t2线程肯定是在t1线程后执行的,当t1打印完“t1 wait 开始”的时候,t1线程就因为wait进入的阻塞状态,然后一直等到操作系统调用t2线程时,t2线程进行了notify操作,t1才从阻塞状态中被唤醒,等t2运行完之后,t1也将后续的操作继续执行

    看到运行结果中給框起来的执行结果就可以更加清晰的知道这两线程的执行顺序了

    这里有几个注意事项:

        1.要想让notify能够唤醒wait 就需要确保wait和notify都是使用同一个对象调用

         2.wait和notify都需要放到synchronized之内的  虽然notify不涉及“解锁操作”但是Java也强制要求notify要放到synchronized中

         3.3.如果进行notify的时候,另一个线程并没用处于wait状态(阻塞),此时,notify相当于“空打一炮”,不会有任何副作用(t2线程相当于辅助t1线程)

   补充:线程可能有多个比如,可以有 N 个线程进行 wait, 一个线程负责 notifynotify 操作只会唤醒一个线程. 具体是唤醒了哪个线程? 是随机的。notifyAll唤醒全部处于waiting中的线程,但是不推荐使用notifyAll(notifyAll有点违背了wait的初心了),多个线程wait时,就想唤醒某个指定的线程,就可以让不同的线程,使用不同的对象来进行wait 想唤醒谁,就可以使用对应的对象来notify。

       wait也有一个带有超时时间的版本和join类似(尽量少用死等的,给自己都个底),因此协调多个线程之间的执行顺序,还是优先考虑使用wait notify

     好了,多线程的知识到这就差不多讲解完成了,下一篇博客我去简述一些多线程的代码案例,如果你觉得这篇文章简述的不错,或者是在哪个知识点有帮助到你(给个赞呗),如果本篇文章在哪里有简述错误的地方欢迎在评论区指出,博主回去看的

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值