JUC并发编程(一)---- 线程详解

并发编程多线程基础

什么是线程与进程

  • 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。所有的应用程序都需要进入到内存中执行。临时存储RAM。

  • 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

    简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程

操作系统在分配资源时是把资源分配给进程的,但是CPU资源比较特殊,它是被分配到线程的,因为真正要占用CPU运行的是线程,所以也说线程是CPU分配的基本单位。

在Java中,当我们启动main函数时其实就启动了一个JVM的进程,而main函数所在的线程就是这个进程中的一个线程,也称主线程。

进程和线程的关系如下图所示:
在这里插入图片描述
一个进程中有多个线程,多个线程共享进程的堆和方法区资源,每个线程有自己的程序计数器和栈区域。

  • 程序计数器是一块内存区域,用来记录线程当前要执行的指令地址。简单来说,当前线程CPU时间片用完后,要让出CPU,这时候就要用程序计数器记录该线程让出CPU时的执行地址,等到该线程再次执行时直接从程序计数器指定地址继续执行。
  • 每个线程都有自己的栈资源,用于存储该线程的局部变量,这些局部变量是私有的,其他线程是访问不了的。
  • 堆是一个进程中最大的一块内存,堆是被进程中的所有线程共享的。堆里面主要存放使用new操作创建的对象实例。
  • 方法区则用来存放JVM加载的类、常量及静态变量等信息,也是线程共享的。

线程创建与运行

Java中有三种线程创建方式,分别为实现Runnable接口的run方法,继承Thread类并重写run的方法,使用FutureTask方式。

1.继承Thread类

public class ThreadTest {

    //继承Thread类并重写run方法
    public  static  class  MyThread extends  Thread{
        @Override
        public void run() {
            System.out.println("我是一个线程");
        }
    }

    public static void main(String[] args) {
        //创建线程
        MyThread myThread = new MyThread();
        //启动线程
        myThread.run();
    }
}

注意:其实调用start方法后线程并没有马上执行,只有等待获取CPU资源后才会真正处于运行状态。一旦run方法结束,该线程处于终止状态。

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

2.实现Runnable接口

public class ThreadTest1 {
    public  static  class  RunableTask implements  Runnable{

        @Override
        public void run() {
            System.out.println("我是一个线程");
        }
    }

    public static void main(String[] args) {
        RunableTask task = new RunableTask();
        new Thread(task).start();
        new Thread(task).start();
    }
}

两个线程共用一个task代码逻辑,解决了任务与代码分离,并且RunableTask可以继承其他类。

缺点:任务没有返回值。

3.FutureTask的方式

public class ThreadTest2 {
    //创建任务类,类似Runable
    public  static  class  CallerTask implements Callable<String>{

        @Override
        public String call() throws Exception {
            return "Hello Java";
        }
    }

    public static void main(String[] args) {
        //创建异步任务
        FutureTask<String> futureTask = new FutureTask<>(new CallerTask());
        //启动线程
        new Thread(futureTask).start();
        try {
            //等待任务执行完毕,并返回结果
            String result = futureTask.get();
            System.out.println(result);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

线程通知与等待

wait()函数

当一个线程调用一个共享变量的wait()方法,该调用线程会被阻塞挂起,直到出现下面几种情况才继续执行:

  • 其他线程调用了该共享对象的notify()或者notifyAll()方法。
  • 其他线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回。

另外需要注意的是,如果调用wait()方法的线程没有事先获取该对象的监视器锁,则调用wait()方法时调用线程会抛出IllegalMonitorStateException异常。

一个线程如何才能获取一个共享变量的监视器锁呢?

  1. 执行synchronized同步代码块时,使用该共享变量作为参数
synchronized(共享变量){
	//doSomething
}
  1. 调用该共享变量的方法,并且该方法使用了synchronized修饰。
synchronized void add(int a,int b){
	//doSomething
}

需要注意的是:一个线程可以从挂起状态变为可以运行状态(也就是被唤醒),即使该线程没有被其他线程调用notify()、notifyAll()方法进行通知,或者被中断,或者等待超时,这就是所谓的虚假唤醒。

解决办法:可以不停的去测试该线程唤醒的条件是否满足,不满足则继续等待。

synchronized(obj){
	while(条件不满足){
		obj.wait();
	}
}

下面以一个简单的生产者和消费者来举例说明:

public class ThreadTest3 {
    //生产者与消费组的共享变量
    public  static Queue<String> queue = new ArrayDeque<>();
    public  static  int MAX_SIZE = 10;
    public static void main(String[] args) {


           Thread thread1 = new Thread(new Runnable() {
               @Override
               public void run() {
                   //生产线程
                   synchronized (queue){
                       //消费队列满,则进行等待队列
                       while (queue.size() == MAX_SIZE){
                           try {
                               //挂起当前线程,并释放通过同步块获取的queue上的锁,
                               //让消费者线程可以获取该锁,然后获取队列里面的元素进行消费。
                               queue.wait();
                           }catch (Exception e){
                               e.printStackTrace();
                           }
                       }

                       //空闲则生成元素,并通知消费者线程。
                       queue.add("123");
                       System.out.println("生产者生成了123");
                       queue.notify();
                   }

               }
           });
           thread1.start();
            //消费线程
            Thread thread2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (queue){
                        //消费队列为空
                        while (queue.size()==0){
                            try {
                                //挂起当前线程,并释放通过同步块获取的queue上的锁,
                                //让生产者线程可以获取该锁,将生成元素放入队列
                                queue.wait();
                            }catch (Exception e){
                                e.printStackTrace();
                            }
                        }
                        //消费元素,并通知唤醒生产者线程
                        System.out.println(queue.poll());
                        queue.notify();
                    }
                }
            });
            thread2.start();
        }
}

在上面代码中,生产者线程A首先通过synchronized获取到了queue上的锁,那么消费者线程在获取该监视器锁的地方被阻塞挂起。线程A获取锁后发现当前队列已满会调用queue.wait()方法阻塞自己,然后释放获取的queue上的锁。如果不释放,就会处于死锁状态,因为消费者线程已经被阻塞挂起,需要有人去唤醒。
释放锁相当于生产者已经生产好数据,通知消费者去消费。

另外需要注意的是,当前线程调用共享变量的wait()方法后只会释放当前共享变量上的锁,如果当前线程还持有其他共享变量的锁,则这些锁是不会被释放的。

public class ThreadTest4 {
    //创建资源
    private  static  volatile  Object resourceA = new Object();
    private  static  volatile  Object resourceB = new Object();

    public static void main(String[] args) throws InterruptedException {
        //创建线程
        Thread threadA = new Thread(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                try {
                    //获取resourceA共享资源的监视器
                    synchronized (resourceA) {
                        System.out.println("threadA get resourceA lock");

                        //获取resourceB共享资源的监视器锁
                        synchronized (resourceB) {
                            System.out.println("threadA get resourceB lock");

                            //线程A阻塞,并释放获取到的resourceA的锁
                            System.out.println("threadA release resourceA lock");
                            resourceA.wait();
                        }
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        });

        //创建线程
        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    //休眠1s
                    Thread.sleep(1000);
                    //获取resourceA共享资源的监视器
                    synchronized (resourceA) {
                        System.out.println("threadB get resourceA lock");
                        System.out.println("threadB get resourceB lock...");


                        //获取resourceB共享资源的监视器
                        synchronized (resourceB) {
                            System.out.println("threadB get resourceB lock");

                            //线程B阻塞,并释放获取到的resourceA的锁
                            System.out.println("threadB release resourceA lock");
                            resourceA.wait();
                        }
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        });

        //启动线程
        threadA.start();
        threadB.start();
        //等待两个线程结束
        threadA.join();
        threadB.join();
        System.out.println("main over");
    }
}

在这里插入图片描述
如上代码,为了让线程A先获取到锁,让线程B休眠了1秒,线程A获取到了resourceA和resourceB上的锁,然后调用了resourceA的wait()方法阻塞自己,并释放了resourceA上的锁。

注意: 线程A并没有释放resourceB上的锁。

这时候线程B就获取到了resourceA的锁,由于线程A没有释放resourceB的锁,线程B是获取不到resourceB的锁。

当一个线程调用共享对象的wait()方法被阻塞挂起后,如果其他线程中断了该线程,则该线程会抛出InterruptedException异常并返回。

public class ThreadTest5 {
    static  Object obj = new Object();

    public static void main(String[] args) throws InterruptedException {
        //创建线程
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println("--begin---");
                    //阻塞当前线程
                    synchronized (obj){
                        obj.wait();
                    }
                    System.out.println("--end---");
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        });

        threadA.start();
        Thread.sleep(1000);
        System.out.println("begin interrupt threadA");
        threadA.interrupt();
        System.out.println("end interrupt threadB");
    }
}
wait(long timeout) 函数

该方法相比wait()方法多了一个超时参数。如果一个线程调用共享对象的该方法挂起后,没有在指定的timeout ms时间内被其他线程调用该共享变量的notify()或者notifyAll()方法唤醒。那么该线程会在timeout ms后自动苏醒。

注意: 如果timeout设置为0和wait方法效果一样,并且timeout不能为负数,否则会抛出IllegalArgumentException异常。

public class ThreadTest5 {
    static  Object obj = new Object();

    public static void main(String[] args) throws InterruptedException {
        //创建线程
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println("--begin---");
                    //阻塞当前线程
                    synchronized (obj){
                        obj.wait(2000);
                    }
                    System.out.println("--end---");
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        });
        threadA.start();
    }

该线程阻塞2s后,会自己苏醒执行下面的语句。
在这里插入图片描述

wait(long timeout, int nanos) 函数

其内部调用的还是wait(long timeout) 函数,只有在nanos>0时才使参数timeout递增1,其源码如下:

public final void wait(long timeout, int nanos) throws InterruptedException {
        if (timeout < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos > 0) {
            timeout++;
        }
        wait(timeout);
    }
notity()函数

一个线程调用共享对象的notify()方法后,会唤醒一个在该共享变量上调用wait系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。

此外,被唤醒的线程不能马上从wait方法返回并继续执行,它必须在获取了共享对象的监视器锁后才可以返回,也就是唤醒它的线程释放了共享变量上的监视器锁后,被唤醒的线程也不一定会获取到共享对象的监视器锁,这是因为该线程还需要和其他线程一起竞争该锁,只有该线程竞争到了共享变量的监视器锁后才可以继续执行。

类似wait系列方法,只有当前线程获取到了共享变量的监视器锁后,才可以调用共享变量的notify()方法,否则会抛出IllegalMonitorStateException异常。

notifyAll()函数

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

代码演练:

public class ThreadTest6 {

    private  static  volatile  Object resourceA = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                //获取resourceA共享资源的监视器锁
                synchronized (resourceA){
                    System.out.println("threadA get resourceA lock");
                    try {
                        System.out.println("threadA begin wait");
                        resourceA.wait();
                        System.out.println("threadA end wait");
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
            }
        });

        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                //获取resourceA共享资源的监视器锁
                synchronized (resourceA){
                    System.out.println("threadB get resourceA lock");
                    try {
                        System.out.println("threadB begin wait");
                        resourceA.wait();
                        System.out.println("threadB end wait");
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
            }
        });

        Thread threadC = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resourceA){
                    System.out.println("threadC begin notify");
                    resourceA.notify();
                }
            }
        });

        threadA.start();
        threadB.start();
        threadC.start();

        //等待线程结束
        threadA.join();
        threadB.join();
        threadC.join();
        System.out.println("main over");
    }
}

输出结果如下:
在这里插入图片描述
线程C只唤醒了线程A,如果线程C调用的是notifyAll()方法,则线程A和线程B都会被唤醒,输出结果如下:
在这里插入图片描述
线程C休眠1s的目的是让线程A和线程B全部执行到wait方法后再调用线程C的notify方法。

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

在项目实践中经常会遇到一个场景,就是需要等待某几件事情完成后才继续往下执行。比如多个线程加载资源,需要等待多个线程全部加载完毕再汇总处理。

代码演示:

public class ThreadTest7 {
    public static void main(String[] args) throws InterruptedException {
        Thread threadOne = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                }catch (Exception e){
                    e.printStackTrace();
                }
                System.out.println("child threadOne over");
            }
        });

        Thread threadTwo = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                }catch (Exception e){
                    e.printStackTrace();
                }
                System.out.println("child threadTwo over");
            }
        });
        //启动子线程
        threadOne.start();
        threadTwo.start();
        System.out.println("wait all child thread over!");

        //等待子线程执行完毕,返回
        threadOne.join();
        threadTwo.join();
        System.out.println("all child thread over!");
    }
}

执行效果如下所示:
在这里插入图片描述

如果将线程的join()方法注释,效果如下:
在这里插入图片描述

另外,主线程调用线程A的join方法后会被阻塞,其他线程调用了主线程的interrupt()方法中断了主线程时,主线程会抛出InterruptedException异常而返回。

public class ThreadTest8 {
    public static void main(String[] args) {
        //线程A
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("threadA begin run!");
                for(;;){

                }
            }
        });

        //获取主线程
        final Thread mainThread = Thread.currentThread();

        //线程B
        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                //休眠1s
                try {
                    Thread.sleep(1000);
                }catch (Exception e){
                    e.printStackTrace();
                }
                //中断主线程
                mainThread.interrupt();
            }
        });

        //启动子线程
        threadA.start();
        threadB.start();
        try {
            //等待线程A执行结束
            threadA.join();
        }catch (Exception e){
            System.out.println("main thread:" +e);
        }
    }
}

让线程睡眠的sleep方法

当一个执行中的线程调用了Thread的sleep方法后,调用线程会暂时让出指定时间的执行权,也就是在这期间不参与CPU的调度,但是该线程所拥有的监视器资源,比如锁还是持有的。指定的睡眠时间到了后,继续参与CPU的调度,获取到CPU资源后继续运行。

public class SleepTest {

    //创建一个独占锁
    private  static  final Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        //创建线程A
        Thread  threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                //获取独占锁
                lock.lock();
                try {
                    System.out.println("child threadA is in sleep");
                    Thread.sleep(10000);
                    System.out.println("child threadA is in awaked");
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    //释放锁
                    lock.unlock();
                }
            }
        });

        //创建线程B
        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                //获取独占锁
                lock.lock();
                try {
                    System.out.println("child threadB is in sleep");
                    Thread.sleep(10000);
                    System.out.println("child threadB is in awaked");
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    lock.unlock();
                }
            }
        });
        //启动线程
        threadA.start();
        threadB.start();
    }
}

在这里插入图片描述
无论执行多少遍上面的代码都是线程A先输出或者线程B先输出,不会出现线程A和线程B交叉输出的情况。因为在线程A睡眠的10s内独占锁lock还是线程A自己持有,线程B会一直阻塞直到线程A醒来后执行unlock释放锁。

如果在睡眠期间其他线程调用了该线程的interrupt()方法中断了该线程,则该线程会在调用sleep方法的地方抛出InterruptedException异常而返回。

public class SleepTest1 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println("child thread is in sleep");
                    Thread.sleep(10000);
                    System.out.println("child thread is in awaked");
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        });
        //启动线程
        thread.start();
        //主线程休眠2s
        Thread.sleep(2000);
        //主线程中断子线程
        thread.interrupt();
    }
}

在这里插入图片描述
注意:调用Thread.sleep(long millis)时,millis不能为负数,不然会抛出IllegalArgumentException异常。

让出CPU执行权的yield方法

Thread类中有一个静态的yield方法,当一个线程调用yield方法时,当前线程会告诉线程调用器我不想使用CPU了,并且让出CPU使用权。

代码演练:

public class YieldTest  implements  Runnable{
    YieldTest(){
        //创建并启动线程
        Thread t = new Thread(this);
        t.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();
    }
}

输出结果如下:
在这里插入图片描述
解开Thread.yield()注释在执行,结果如下:
在这里插入图片描述
三个线程的两行输出没有在一起,因为输出了第一行后当前线程让出了CPU执行权。

总结:sleep与yield方法的区别在于:

  • 线程调用sleep方法时调用线程会被阻塞挂起指定的时间,在这期间线程调度器不会去调度该线程。
  • 调用yield方法时,线程只是让出自己剩余的时间片,并没有被阻塞挂起,而是处于就绪状态,线程调度器下一次调度时就有可能调度到当前线程执行。

线程中断

Java中的线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的执行,而是被中断的线程根据中断状态自行处理。

  • void interrupt()方法:中断线程。 例如:当线程A运行时,线程B可以调用线程A的interrupt()方法来设置线程A的中断标志为true并立即返回。设置标志仅仅是设置标志,线程A实际并没有被中断,它会继续往下执行。如果线程A因为调用了wait系列函数、join方法或者sleep方法而被阻塞挂起,这时候若线程B调用线程A的interrupt()方法,线程A会在调用这些方法的地方抛出InterruptedException异常而返回。
  • boolean isInterrupted()方法:检测当前线程是否被中断,如果是返回true,否则返回false。
public boolean isInterrupted(){
	//传递false,说明不清除中断标志
	return isInterrupted(false);
}
  • boolean interrupted()方法:检测当前线程是否被中断,是返回true,否则返回false。与isInterrupted不同的是,该方法如果发现当前线程被中断,则会清除中断标志。 在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() {
                //如果当前线程被在中断则退出循环
                while (!Thread.currentThread().isInterrupted()){}
                System.out.println(Thread.currentThread()+"hello");
            }
        });

        //启动子线程
        thread.start();
        //主线程休眠1s,以便中断前让子线程输出
        Thread.sleep(1000);
        //中断子线程
        System.out.println("main thread interrupt thread");
        thread.interrupt();
        //等待子线程执行完毕
        thread.join();
        System.out.println("main is over");
    }
}

子线程thread通过检查当前线程中断标志来控制是否退出循环,主线程在休眠1s后调用threada的interrupt()方法设置了中断标志。

下面再来看一种情况。当线程为了等待一些特定条件的到来时,一般会调用sleep函数、wait系列函数或者join()函数来阻塞挂起当前线程。比如一个线程调用了sleep(3000),那么该线程会被阻塞,直到3s后才会从阻塞状态变为激活状态。但是有可能在3s内条件已被满足,如果一直等到3s后再返回有点浪费时间,这时候就可以调用该线程的interrupt()方法,强制sleep方法抛出InterruptedException异常而返回,线程恢复到激活状态。代码如下:

public class InterruptedTest1 {
    public static void main(String[] args) throws InterruptedException {
        Thread threadOne =new Thread(new Runnable() {
            @Override
            public void run() {
               try {
                   System.out.println("threadOne begin sleep for 2000 seconds");
                   Thread.sleep(2000000);
                   System.out.println("threadOne awaking");
               }catch (Exception e){
                   System.out.println("threadOne is interrupted while sleeping");
                   return;
               }
                System.out.println("threadOne-leaving normally");
            }
        });

        //启动线程
        threadOne.start();
        //确保子线程进入休眠状态
        Thread.sleep(1000);
        //打断子线程的休眠,让子线程从sleep函数返回
        threadOne.interrupt();
        //等到子线程执行完毕
        threadOne.join();
        System.out.println("main thread is over");
    }
}

输出结果如下:
在这里插入图片描述
下面再通过一个列子来了解interrupted()与isInterrupted()方法的不同之处。

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

    }
}

输出结果如下:
在这里插入图片描述
至于为什么是true、false、false、true呢? 上面已经介绍过interrupted()方法内部是获取当前线程的中断状态,这里虽然调用了threadOne的interrupted()方法,但是获取的是主线程的中断标志,因为主线程是当前线程,这时候主线程并没有中断,所以返回false。

修改上面的例子如下:

public class InterruptedTest2 {
    public static void main(String[] args) throws InterruptedException {
        Thread threadOne = new Thread(new Runnable() {
            @Override
            public void run() {
            	System.out.println("threadOne isInterrupted:"+Thread.currentThread().isInterrupted());
                //中断标志位true时会退出循环,并且清除中断标志
                while (!Thread.currentThread().interrupted()){}
                System.out.println("threadOne isInterrupted:"+Thread.currentThread().isInterrupted());
            }
        });
        //启动线程
        threadOne.start();
        //设置中断标志
        threadOne.interrupt();
        threadOne.join();
        System.out.println("main thread is over");

    }
}

输出结果如下:
在这里插入图片描述
可以发现,调用interrupted()方法后中断标志被清除了。

线程上下文切换

在多线程编程中,线程个数一般都大于CPU个数,而每个CPU同一时刻只能被一个线程使用,为了让用户感觉多个线程是在同时执行的,CPU的资源的分配采用了时间片轮转的策略,也就是给每个线程分配一个时间片,线程在时间片内占用CPU执行任务。当前线程使用完时间片后,就会处于就绪状态并让出CPU让其他线程占用,这就是上下文切换,从当前线程的上下文切换到了其他线程。

那么就有一个问题,让出CPU的线程等下次轮到自己占有CPU时如何知道自己之前运行到哪里了?所以在切换线程上下文时需要保存当前线程的执行现场,当再次执行时根据保存的执行现场信息恢复执行现场。

线程上下文切换时机有:当前线程的CPU时间片使用完处于就绪状态时,当前线程被其他线程中断时。

线程死锁

什么是线程死锁

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。

在这里插入图片描述
在上图中,线程A已经持有了资源2,它同时还想申请资源1,线程B已经持有了资源1,它同时还想申请资源2,所以线程1和线程2就因为相互等待对方已经持有的资源,而进入了死锁状态。

为什么会产生死锁? 学过操作系统的朋友应该都知道,死锁的产生必须具备以下四个条件。

  • 互斥条件:指线程对已经获取到的资源进行排它性使用,即该资源同时只由一个线程占用。如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占有资源的线程是否释放该资源。
  • 请求并持有条件:指一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取的资源。
  • 不可剥夺条件:指线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才由自己释放该资源。
  • 环路等待条件:指在发生死锁时,必然存在一个线程—资源的环形链,即线程集合{T0, T1, T2, …, Tn}中的T0正在等待一个T1占用的资源,T1正在等待T2占用的资源,……Tn正在等待已被T0占用的资源。

下面将通过一个例子来说明线程死锁:

public class DeadLockTest2 {
    //创建资源
    private  static  Object resourceA = new Object();
    private  static  Object resourceB = new Object();

    public static void main(String[] args) {
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resourceA){
                    System.out.println(Thread.currentThread()+"get ResourceA");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread()+"waiting get sourceB");
                    synchronized (resourceB){
                        System.out.println(Thread.currentThread()+"get resourceB");
                    }
                }
            }
        });

        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resourceB){
                    System.out.println(Thread.currentThread()+"get ResourceB");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread()+"waiting get resourceA");
                    synchronized (resourceA){
                        System.out.println(Thread.currentThread()+"get resourceA");
                    }
                }
            }
        });

        //启动线程
        threadA.start();
        threadB.start();
    }
}

在这里插入图片描述
线程A休眠结束后会企图获取resourceB资源,而resourceB资源被线程B所持有,所有线程A会被阻塞等待。线程B也是如此,所以线程A和线程B就陷入了相互等待的状态,也就产生了死锁。

接下来谈谈上面是如何满足死锁的四个条件的:

  • 首先,resourceA和resouceB都是互斥资源,当线程A调用Synchronized(resourceA)方法获取到resoucreA上的监视器锁并释放前,线程B再调用synchronized(resoucreA)方法尝试获取该资源会被阻塞,只有线程A主动释放该锁,线程B才能获得,这满足了资源互斥条件。
  • 线程A首先通过synchronized(resourceA)方法获取到resourceA上的监视器锁资源,然后通过synchronized(resourceB)方法等待获取resourceB上的监视器锁资源,这就构成了请求并持有条件。
  • 线程A在获取resourceA上的监视器锁资源后,该资源不会被线程B掠夺走,只有线程A自己主动释放resourceA资源时,它才会放弃对该资源的持有权,这构成了资源的不可剥夺条件。
  • 线程A持有objectA资源并等待获取objectB资源,而线程B持有objectB资源并等待objectA资源,这构成了环路等待条件。所以线程A和线程B就进入了死锁状态。
如何避免线程死锁

要想避免死锁,只需要破坏掉至少一个构造死锁的必要条件即可,目前只有请求并持有和环路等待条件是可以被破坏的。

造成死锁的原因其实和申请资源的顺序有很大关系,使用资源申请的有序性原则就可以避免死锁。 对上面线程B的代码进行如下修改:

Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resourceA){
                    System.out.println(Thread.currentThread()+"get ResourceB");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread()+"waiting get resourceA");
                    synchronized (resourceB){
                        System.out.println(Thread.currentThread()+"get resourceA");
                    }
                }
            }
        });

输出结果如下:
在这里插入图片描述
上述代码让在线程B中获取资源的顺序和在线程A中获取资源的顺序保持一致。其实资源分配有序性就是指,假如线程A和线程B都需要资源1,2,3, …, n时,对资源进行排序,线程A和线程B只有在获取了资源n-1时才能去获取资源n。

为何资源的有序分配会避免死锁?
比如上面的代码,假如线程A和线程B同时执行到了synchronized(resourceA),只有一个线程可以获取到resouceA上的监视器锁,假如线程A获取到,那么线程B就会被阻塞而不会再去获取资源B,线程A获取到resouceA的监视器锁后会去申请resourceB的监视器锁,这时候线程A是可以获取到的,线程A获取到resoucreB资源并使用后会放弃对资源resourceB的持有,然后再释放对resourceA的持有,释放resourceA后线程B才会从阻塞状态变为激活状态。所以资源的有序性破坏了资源的请求并持有条件和环路等待条件,因此避免了死锁。

守护线程与用户线程

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

守护线程和用户线程的区别:守护线程是否结束并不影响JVM的退出,但是只要有一个用户线程还没结束,正常情况下JVM就不会退出。

在Java中创建一个守护线程:

public class DaemonThreadTest {
    public static void main(String[] args) {
        Thread deamonThread = new Thread(new Runnable() {
            @Override
            public void run() {

            }
        });

        //设置为守护线程  只需要设置线程的daemon参数为true即可
        deamonThread.setDaemon(true);
        deamonThread.start();
    }
}

通过一个例子来理解用户线程与守护线程的区别。首先看下面的代码:

public class DaemonThreadTest1 {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for(;;){}
            }
        });

        //启动子线程
        thread.start();
        System.out.println("main thread is over");
    }
}

输出结果如下:
在这里插入图片描述
从运行代码的结果看,main线程已经运行结束了。但是在IDE的输出结果红色方块说明,JVM进程并没有退出。

这个结果说明了当父线程结束后,子线程还是可以继续存在的,也就是子线程的生命周期并不受父线程的影响。这也说明了在用户线程还存在的情况下JVM进程并不会终止。

现在我们将上面的trhead线程设置为守护线程后,再来看看运行结果:
在这里插入图片描述
执行后的输出结果表示:JVM进程已经终止了。 JVM不会等待守护线程运行完毕就会结束JVM进程。

注意:main线程运行结束后,JVM会自动启动一个叫作DestroyJavaVM的线程,该线程会等待所有用户线程结束后终止JVM进程。

总结:

  • 如果你希望在主线程结束后JVM进程马上结束,那么在创建线程时可以将其设置为守护线程。
  • 如果你希望在主线程结束后子线程继续工作,等子线程结束后再让JVM进程结束,那么就将子线程设置为用户线程。
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值