《Java后端知识体系》系列之并发编程基础

想了想还是从基础开始整理并发编程的知识吧,实在是太多了!!!

并发编程基础

  • 基础概念:

    • 线程与进程

      • 进程:进程是系统进行资源分配和调度的基本单位,平时我们在电脑上启动的一个程序就是一个进程。
      • 线程:线程是操作系统进行调度的最小单位
      • 关系:一个进程可以启动一个或多个线程,进程中所有的线程会共享进程中的内存空间,每个线程都有自己的程序计数器栈区域,进程中的栈资源用来存储该线程的局部变量,这些局部变量是线程私有
      • 线程与进程的关系如图所示:
        在这里插入图片描述
    • 线程的创建

      • Java中有三种线程创建方式,分别是实现Rannable接口的run方法、继承Thread类重写run方法、实现Callable接口

      • 继承Thread
        继承Thread类的实现代码如下:

        	/**
        	 * @author admin
        	 */
        	public class ThreadTest {
        		//继承Thread类并重写run方法
        	    public static class MyThread extends Thread{
        	        @Override
        	        public void run(){
        	            System.out.println("a thread");
        	        }
        	    }
        	
        	    public static void main(String[] args) {
        	    	//创建线程
        	        MyThread myThread = new MyThread();
        	        //启动线程
        	        myThread.start();
        	    }
        	}
        
        
        • 在以上代码中MyThread继承Thread类,并重写了run方法。在main函数中创建MyThread实例,然后调用MyThread的start()方法启动该线程。但是我们要知道在创建MyThread对象后线程并没有立即启动执行,需要调用start方法后才启动线程。

        • 调用start方法之后线程并没有马上执行而是线程处于就绪状态,等待获取到CPU资源之后才真正处于运行状态

        • run方法执行完毕,线程就处于终止状态

        • 使用继承Thread类的好处是,在run方法内获取线程时直接调用this就可以了,无需通过Thread.currentThread()方法;不好的地方是Java不允许多继承,如果继承了Thread类就无法继承其它类。并且任务与代码没有分离,当多个线程执行一样的任务时需要多份任务代码。

      • 实现Rannable

        实现Runnable接口代码如下:

            public static class RunnableTask implements Runnable{
                @Override
                public void run() {
                    System.out.println("a runnable thread");
                }
            }
            public static void main(String[] args) {
                RunnableTask runnableTask = new RunnableTask();
                new Thread(runnableTask).start();
                new Thread(runnableTask).start();
            }
        
        • 在以上代码中,两个线程公用了一个runnableTask代码逻辑,并且也可以根据需要给RunnableTask添加参数进行区分(重载)。另外RunnableTask可以继承其它类。但是继承Thread类和实现Runnable接口的方式都是没有返回值的。
      • 实现Callable接口
        实现Callable接口方式:

        		//创建任务类
        public static class CallTask implements Callable<String>{
        
            @Override
            public String call() throws Exception {
                return "hello callAble";
            }
        }
        
        public static void main(String[] args) throws InterruptedException {
            //创建异步任务
            FutureTask<String> futureTask = new FutureTask<>(new CallTask());
            //启动线程
            new Thread(futureTask).start();
            try {
                //等待任务执行结束并返回结果
                String result = futureTask.get();
                System.out.println(result);
            }catch (ExecutionException e){
                e.printStackTrace();
            }
        }
        
        • 在以上代码中实现Callable接口的call()方法,在main函数中创建一个FutureTask对象(构造函数为CallTask的实例),然后使用创建的FutureTask对象作为任务创建一个线程并启动它,最后通过futureTask.get()等待任务执行完毕并返回结果。
      • 总结:

        • 使用继承Thread类的好处是方便传参数,可以在子类中添加成员变量,通过set方法设置参数或者构造函数进行传递;如果使用实现Runnable接口的方式,则只能使用主线程里面被声明为final的变量,但是前两种都没有返回结果,但是Callable接口可以实现获取返回结果。
    • 线程等待和通知

      • Object级别

        • wait:当一个线程调用共享变量的wait()方法时,该调用线程会被阻塞挂起

          • 直到发生一下几件事才返回:(1)其它线程调用该共享对象的notify()或者notifyAll()方法,(2)其它线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回。

          • 另外,如果调用wait()方法的线程没有事先获取对象的监视器锁,则调用wait()方法时调用线程会抛出IllegalMonitorStateException异常。一个线程获取该共享变量的监视器锁的方式有两种:1、执行synchronized同步代码块,使用该共享变量作为参数

            synchronized(共享变量){
            	//doSomething
            }
            

            2、调用该共享变量的方法,并且该方法使用了synchronized修饰

            synchronized void add(int a,int b){
            	//doSomething
            }
            
          • 另外需要注意的是,一个线程可以从挂起状态变为可以运行状态,即使该线程没有被其它线程调用notify()、notifyAll()方法进行唤醒,或者被中断等,这就是所谓的虚假唤醒。虽然虚假唤醒很少发生,但是要防患于未然,做法就是不停的测试该线程被唤醒的条件是否满足,不满足就继续等待,也就是说在一个循环调用中调用wait()方法进行防范。退出的条件是满足了唤醒该线程的条件。

            synchronized(obj){
            	while(条件不满足){
            		obj.wait();
            		}
            }
            
          • 线程挂起的方法也存在一个wait(long timeout)的方法,如果一个线程调用共享对象的wait(long timeout)方法后,如果没有在指定的timeout ms时间内被其它线程调用该共享变量的notify()和notifyAll()方法唤醒,那么该方法会因为超时而返回。如果将timeout设置为0,则效果和wait()方法效果一样。

        • notify

          • 一个线程调用共享对象的notify()方法后,会唤醒一个在共享变量上调用wait系列方法后被挂起的线程。一个共享变量上可能有多个线程在等待,具体唤醒哪个线程是随机的。
          • 此外被唤醒的线程并不会马上从wait方法返回并执行,它必须在获取了共享对象的监视器锁后才可以返回,也就是唤醒它的线程释放了共享变量的监视器锁之后,被唤醒的线程也不一定会直接获取到共享变量的监视器锁,因为该线程还需要和其它线程竞争该锁(所以说synchronized锁并不是公平的),只有竞争到共享变量的监视器锁(synchronized锁)之后才可以继续执行。
        • notifyAll

          • 不同于notify()会唤醒被阻塞到该共享变量的一个线程,notifytAll()会唤醒所有的该共享变量上调用wait系列方法被挂起的线程。

            //创建共享变量
            private static volatile Object object = new Object();
            
            public static void main(String[] args) throws InterruptedException {
                //创建线程A
                Thread threadA = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        synchronized (object) {
                            try {
                                System.out.println("threadA start wait");
                                object.wait();
                                System.out.println("threadA wait end");
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                });
                //创建线程B
                Thread threadB = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        synchronized (object) {
                            try {
                                System.out.println("threadB start wait");
                                object.wait();
                                System.out.println("threadB wait end");
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                });
                //创建线程C
                Thread threadC = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        synchronized (object) {
                            System.out.println("threadC start notifyAll");
                            object.notifyAll();
                            System.out.println("threadC notifyAll end");
                        }
                    }
                });
                threadA.start();
                threadB.start();
                Thread.sleep(1000);
                threadC.start();
                //等待线程结束
                threadA.join();
                threadB.join();
                threadC.join();
            }
            

            在这里插入图片描述
            如上代码中我们创建两个线程并都执行wait()方法挂起线程,然后沉睡一秒之后执行线程C执行notifyAll(),这样线程A和线程B被同时唤醒,因此线程A和线程B需要竞争共享变量object的监视器锁,谁先获取到监视器锁谁就先执行。如上的执行结果中我们可以看到线程A先获取到了监视器锁然后执行了后续的操作。

      • Thread级别

        • sleep:让线程睡眠的方法

          • 当一个执行中的线程执行了sleep()方法后,该线程会暂时让出CPU的执行权,不参与CPU的调度,但是该线程仍然持有监视器锁(也就是不会释放锁)。当指定的睡眠时间到了之后该函数会正常返回,线程就处于就绪状态,重新参与CPU调度,获取到CPU资源之后就可以继续运行。如果在睡眠期间其它线程调用了睡眠中的线程的interrupt()方法中断该线程,那么该线程会在调用sleep()的地方抛出IntermptedException异常并返回。

            public class SleepTest {
                //创建一个独占锁
                private static final Lock lock = new ReentrantLock();
            
                public static void main(String[] args) throws InterruptedException {
                    //创建线程A
                    Thread threadA = new Thread(new Runnable() {
                        @Override
                        public void run() {
                            //获取独占锁
                            lock.lock();
                            try {
                                System.out.println("threadA start sleep");
                                Thread.sleep(10000);
                                System.out.println("threadA end sleep");
                            }catch (InterruptedException e){
                                e.printStackTrace();
                            }finally {
                                //释放锁
                                lock.unlock();
                            }
                        }
                    });
                    //创建线程B
                    Thread threadB = new Thread(new Runnable() {
                        @Override
                        public void run() {
                            //获取独占锁
                            lock.lock();
                            try {
                                System.out.println("threadB start sleep");
                                Thread.sleep(10000);
                                System.out.println("threadB end sleep");
                            }catch (InterruptedException e){
                                e.printStackTrace();
                            }finally {
                                //释放锁
                                lock.unlock();
                            }
                        }
                    });
                    //启动线程
                    threadA.start();
                    threadB.start();
                }
            }
            

            在这里插入图片描述

          • 以上代码中创建两个线程,threadA和threadB,并且两个线程都休眠10秒,并且启动两个线程,这样的情况不论执行多少次都会是threadA先执行,因为threadA睡眠时并不会释放锁,依然持有独占锁资源,所以最后睡眠结束时依然是先持有锁的线程先执行。

          • 另外如果在sleep(long millis)中millis参数传递了一个负数,则会抛出IllegalArgumentException异常。

        • join:等待线程执行终止的方法

          • 当多个线程加载资源时,需要等待所有的线程执行完毕之后再做汇总处理的情况时,就需要一个方法来控制这些线程的执行,控制所有线程执行完毕才释放线程,此时就需要用到join()方法。
              public static void main(String[] args) throws InterruptedException {
                  Thread threadA = new Thread(new Runnable() {
                      @Override
                      public void run() {
                          try {
                              Thread.sleep(1000);
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
                          System.out.println("threadA over");
          
                      }
                  });
                  Thread threadB = new Thread(new Runnable() {
                      @Override
                      public void run() {
                          try {
                              Thread.sleep(1000);
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
                          System.out.println("threadB over");
                      }
                  });
                  //启动子线程
                  threadA.start();
                  threadB.start();
                  //等待子线程执行完毕返回
                  threadA.join();
                  threadB.join();
              }
          
          • 在以上代码中启动了两个线程,分别调用了join()方法,那么当主线程执行到threadA.join()时会被阻塞,等待threadA执行完毕之后返回,因此主线程执行到join()方法时会被阻塞,等待threadA的线程执行成功后才能继续执行。同理threadB也是如此,主线程执行到threadB时也会被阻塞,等待threadB执行成功后才返回。
          • 同时threadA调用threadB的join()方法时也会被阻塞,当其它线程调用threadA的interrupt()方法中断threadA时,threadA会抛出InterruptedException异常并返回。
          • join()方法阻塞的是当前线程,比如主线程中执行threadA.join(),阻塞的是主线程。而不是threadA线程。
        • yield:让出CPU执行权的方法

          • 当一个线程调用yield()方法时,就是表明当前线程请求让出自己的CPU使用权,但是线程调度器可以忽视这个请求。

          • 操作系统中线程的调度是按照时间片进行分配CPU执行权的,当一个线程使用完自己的时间片之后,线程调度器才会进行下一轮的线程调度,而当一个线程调用了yield()方法时,是告诉线程调度器自己的时间片还没用完但是不想用了,线程调度器可以进行下一轮线程调度了。

            public class YieldTest implements Runnable {
                YieldTest(){
                    //创建并启动线程
                    Thread thread = new Thread(this);
                    thread.start();
                }
            
            
                @Override
                public void run() {
                    for (int i = 0; i < 5; i++) {
                        //当i=0时让出CPU执行权,放弃时间片,进行下一轮调度
                        if (i%5==0){
                            System.out.println(Thread.currentThread()+"yield");
                            //当前线程让出CPU执行权,放弃时间片,进行下一轮调度
                            Thread.yield();
                        }
                    }
                    System.out.println(Thread.currentThread()+"is over");
            
                }
                public static void main(String[] args) {
                    new YieldTest();
                    new YieldTest();
                    new YieldTest();
                }
            }
            

            在这里插入图片描述

          • 以上代码中启动了三个线程并且分别在i=0 时候调用了Thread.yield()方法,所以三个线程中的输出语句并没有连在一起,因为输出第一行后当前线程就让出了CPU执行权,其它线程先使用CPU调用了其它方法。

        • 总结:sleep()与yield()的区别在于,当线程调用sleep()方法时线程会被阻塞挂起到指定的时间,这期间线程调度器并不会调度该线程。而使用yield()方法时,线程知识让出自己的时间片,并没有被阻塞挂起,而是处于就绪状态,在线程调度器进行下一次调度时仍然参与线程的竞争调度,并且有可能调度到该线程。

    • 线程中断

      • void interrupt():中断线程,当线程A正在运行时,线程B可以调用线程A的interrupt()的方法来设置线程A的中断标志为true并立即返回。设置标志仅仅是设置标志,线程A并没有被中断,它会继续执行。但是若线程A调用了wait()、join()、yield()方法被阻塞挂起时,此时线程B调用线程A的interrupt()方法,线程A会在调用这些方法的地方抛出InterruptedException异常而返回。

      • boolean isInterrupted():检测当前线程是否被中断,如果是则返回true,否则返回false。

      • boolean interrupted():检测当前线程是否被中断,如果是则返回true否则返回false。与isInterrupted()不同的是该方法如果发现当前线程被中断则会清除中断标志,并且该方法是static方法,可以直接通过Thread类调用,并且interrupted()内部是获取当前调用线程的中断标志,而不是调用interrupted()方法的实例对象的中断标志。

            public static boolean interrupted() {
                return currentThread().isInterrupted(true);
            }
        

        例子如下:

        public class InterruptedTest {
            public static void main(String[] args) throws InterruptedException {
                Thread thread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        for (;;){
        
                        }
                    }
                });
                //启动线程
                thread.start();
                //设置中断标志
                thread.interrupt();
                //获取中断标志
                System.out.println("isInterrupted:"+thread.isInterrupted());
                //获取中断标志并重置(此时获取的是主线程的中断标志)
                System.out.println("isInterrupted:"+ Thread.interrupted());
                //获取中断标志
                System.out.println("isInterrupted:"+ thread.isInterrupted());
                thread.join();
            }
        }
        

        在第二个获取中断标志并重置的地方,此时获取的中断标志其实是主线程的中断标志,

    • 死锁

      • 概念:死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在没有外力的情况下,这些线程会相互等待而无法执行下去。
      • 产生条件
        • 互斥:线程对已经获取到的资源会排斥其它线程的使用,也就是该资源同时只能由一个线程占用。

        • 循环等待:发生死锁时,是由一个线程请求资源的环形的链造成的。也就是t0等待t1的资源,t1等待t2资源…tn等待t0的资源。

        • 不可剥夺:线程获得资源之后就不可被其它线程抢占,除非自己使用完成。

        • 占用且等待:一个线程持有了一个资源但是又请求其它资源,而其它资源正在被其它线程占用,因此当前线程就会阻塞。

          public class DeadLockTest {
              //创建资源
              private static Object objectA = new Object();
              private static Object objectB = new Object();
              public static void main(String[] args) {
                  //创建线程A
                  Thread threadA = new Thread(new Runnable() {
                      @Override
                      public void run() {
                          synchronized (objectA){
                              System.out.println(Thread.currentThread()+"get ObjectA");
                              try {
                                  Thread.sleep(1000);
                              } catch (InterruptedException e) {
                                  e.printStackTrace();
                              }
                              System.out.println(Thread.currentThread()+"wait get ObjectB");
                              synchronized (objectB){
                                  System.out.println(Thread.currentThread()+"get ObjectB");
                              }
                          }
                      }
                  });
                  //创建线程A
                  Thread threadB = new Thread(new Runnable() {
                      @Override
                      public void run() {
                          synchronized (objectB){
                              System.out.println(Thread.currentThread()+"get ObjectB");
                              try {
                                  Thread.sleep(1000);
                              } catch (InterruptedException e) {
                                  e.printStackTrace();
                              }
                              System.out.println(Thread.currentThread()+"wait get ObjectA");
                              synchronized (objectA){
                                  System.out.println(Thread.currentThread()+"get ObjectA");
                              }
                          }
                      }
                  });
                  threadA.start();
                  threadB.start();
              }
          }
          
          

          在以上代码中线程A获取到objectA资源,线程B获取到了objectB资源。线程A休眠结束后会企图获取objectB资源,但是objectB资源正在被线程B持有,所以线程A会被阻塞而等待,而线程B休眠结束后企图获取objectA资源,objectA正在被线程A持有,所以线程A与线程B就陷入相互等待中,也就产生了死锁。

      • 避免死锁
        • 打破至少一个造成死锁的条件,但是目前只有占有和等待以及循环等待是可以打破的。
        • 造成死锁的原因其实和申请资源的顺序有关,使用资源申请的有序性原则就可以避免死锁。
    • Deamon守护线程

      • Java中的线程分为两类,分别为deamon线程和user线程(用户线程),JVM启动时会调用main函数,main函数所在的线程就是守护线程,其实JVM内部还启动了好多守护线程,比如垃圾回收线程

      • 守护线程与用户线程的区别为当最后一个用户线程结束时,JVM就会正常退出,而不管当前是否还有守护线程,也就是说守护线程不影响JVM的退出。因此只要有一个用户线程没有结束,正常情况下JVM就不会退出。

        创建一个守护线程如下:

        public class DaemonTest {
            public static void main(String[] args) {
                Thread daemonThread = new Thread(new Runnable() {
                    @Override
                    public void run() {
        
                    }
                });
                daemonThread.setDaemon(true);
                daemonThread.start();
            }
        }
        
        

        只需要设置线程的daemon参数为true即可。

      • 当用户线程都执行结束之后,JVM会执行一个叫做DestroyJava VM的线程,该线程会等待所有用户线程结束后终止JVM进程。

      • 总结:如果你希望在主线程结束后 JVM进程马上结束,那么在创建线程时可以将其设置为守护线程,如果你希望在主线程结束之后子线程继续工作,等子线程结束之后再结束JVM进程,那么就将子线程设置为用户线程。

    • ThreadLocal

      • 概念:多线程访问同一个共享变量时容易出现并发问题,特别是多个线程需要对一个共享变量写入时, 为了保证线程安全,在访问共享变量时需要进行适当的同步。

      • 同步的措施一般是加锁,但是使用加锁的方式增加了性能的损耗,因此可以使用ThreadLocal来实现。

      • ThreadLocal是JDK提供的,提供了线程本地变量,也就是如果创建了一个TreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。因此当多个线程操作这个ThreadLocal变量时,其实操作的是自己本地内存里面的变量,从而避免了线程安全问题。

        public class ThreadLocalTest {
            //创建ThreadLocal变量
            static ThreadLocal<String> threadLocal = new ThreadLocal<>();
            //print函数
            static void print(String string){
                //打印当前线程本地内存中的threadLocal
                System.out.println(string+":"+threadLocal.get());
                //清除当前线程本地内存中的threadLocal
        //        threadLocal.remove();
            }
        
            public static void main(String[] args) {
                //创建线程
                Thread threadA = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        //设置线程A中本地变量threadLocal的值
                        threadLocal.set("threadA");
                        //调用打印函数
                        print("threadA");
                        System.out.println("threadA remove "+threadLocal.get());
                    }
                });
                //创建线程
                Thread threadB = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        //设置线程B中本地变量threadLocal的值
                        threadLocal.set("threadB");
                        //调用打印函数
                        print("threadB");
                        System.out.println("threadB remove "+threadLocal.get());
                    }
                });
                threadA.start();
                threadB.start();
            }
        }
        

        在这里插入图片描述
        执行threadLocal.remove()之后结果
        在这里插入图片描述

      • 以上代码中创建了一个共享变量threadLocal,两个线程,线程A和线程B,当我们在线程A中对共享变量设置值时,并不会影响线程B中threadLocal的值,线程中通过set方法设置threadLocal的值,其实设置的是线程中本地内存中的一个副本,这个副本线程B是访问不了的。同时通过get获取的是当前线程本地内存中的值。

      • 实现原理
        ThreadLocal的结构在这里插入图片描述

      • 通过图中我们可以看到Thread类中有一个threadLocalsinheritableThreadLocals,它们都是ThreadLocalMap类型的变量,而ThreadLocalMap是一个定制化的HashMap。在默认情况下每个线程中的threadLocals和inheritableThreadLocals都为null,当线程第一次调用set或get时才会创建它们。

      • 每个线程的本地变量并不存在ThreadLocal实例里面,而是存放在线程中的threadLocals变量中,因此ThreadLocal就是一个工具壳,通过set将值放入线程中的threadLocals中,通过get将值从threadLocals中取出来,也可以通过调用remove将当前线程的threadLocals中的值删除。

      • 那么为什么threadLocals为什么被设置成map结构,因为一个线程可能关联多个ThreadLocal变量

      • 每个线程内部都有一个threadLocals的成员变量,该变量类型为HashMap,key为ThreadLocal实例对象的引用value则是需要设置的每个线程的本地变量都存放在线程自己的内存变量threadLocals中,如果线程不死亡,那么该变量会一直存在,因此会造成内存溢出,因此使用完毕应当调用remove删除threadLoccals中的变量。

        从源码中我们也可以看到是通过threadLocals来实现的

            public void set(T value) {
            	//获取当前线程
                Thread t = Thread.currentThread();
                调用getMap,当前线程作为key,去查找对应的线程变量
                ThreadLocalMap map = getMap(t);
                //如果map不为空,则调用set方法,key为当前ThreadLocal的实例对象引用,value是传递的值。
                if (map != null)
                    map.set(this, value);
                else
                	//如果是第一次则调用
                    createMap(t, value);
            }
            ThreadLocalMap getMap(Thread t) {
            	//在getMap中获取的是当前线程的threadLocals
                return t.threadLocals;
            }
        

        另一个类lnheritableThreadLocal类是为了解决子线程可以访问父线程中设置的本地变量

        public class InheritableThreadLocal<T> extends ThreadLocal<T> {
            /**
           
             *
             * @param parentValue the parent thread's value
             * @return the child thread's initial value
             */
            protected T childValue(T parentValue) {
                return parentValue;
            }
        
            /**
             * Get the map associated with a ThreadLocal.
             *
             * @param t the current thread
             */
            ThreadLocalMap getMap(Thread t) {
               return t.inheritableThreadLocals;
            }
        
            /**
             * Create the map associated with a ThreadLocal.
             *
             * @param t the current thread
             * @param firstValue value for the initial entry of the table.
             */
            void createMap(Thread t, T firstValue) {
                t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
            }
        }
        
        • InheritableThreadLocal继承ThreadLocal,并重写三个方法。因此当调用set方法时,创建的是当前线程的inheritableThreadLocals变量的实例而不再是threadLocals,调用get方法时获取当前线程内部的map变量时,获取的是inheritableThreadLocals而不再是threadLocals。因此在InheritableThreadLocal的世界里,变量inheritableThreadLocals替代了threadLocals。

整理了三天的并发编程的知识,也不算整理就是看着并发编程的文章抄的,也算给自己加深学习吧!

该知识来自Java并发编程!

依然是会敲代码的汤姆猫!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章