深入JAVA并发编程(一):JAVA线程基础

JAVA线程基础

进程和线程

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,一个进程中至少有一个线程。

线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。

使用多线程而不是多进程去进行并发程序的设计,是因为线程间的切换和调度成本远小于进程。

JAVA中的线程

线程的状态

在这里插入图片描述

线程的优先级

java 中的线程优先级的范围是1~10,默认的优先级是5,10级最高。
可以通过setPriority方法设置。优先级高的线程分配时间片的数量要多于优先级低的线程,不同的操作系统线程规划会有差异,有的操作系统甚至忽略线程优先级,并不是很可靠。

线程的创建

JAVA中线程的创建方法有三种,我们分别进行讲解。

(1)继承Thread类并重写run方法。

  • 定义一个继承Thread类的子类,并重写该类的run()方法;
  • 创建Thread子类的实例,即创建了线程对象;
  • 调用该线程对象的start()方法启动线程。
public class FirstThreadDemo {

    public static class MyThread extends Thread{
        @Override
        public void run() {
            System.out.println("我是子线程");
        }
    }

    public static void main(String[] args)  {
        //创建线程
        MyThread myThread=new MyThread();
        //启动线程
        myThread.start();
        System.out.println("我是主线程");
    }
}

这种方法弊有一些弊端。JAVA是不支持多继承的,所以如果继承了Thread类,就不能再继承其他类,扩展性很差。而实现Runnable接口则可以很好的帮我们解决这个问题。

(2)实现Runnable接口创建线程

  • 定义Runnable接口的实现类,并重写该接口的run()方法;
  • 创建Runnable实现类的实例,并以此实例作为Thread的参数,该Thread对象才是真正的线程对象。
  • 调用start方法启动线程
public class TwoThreadDemo {

    public static class TwoThread implements Runnable{
        @Override
        public void run() {
            System.out.println("我是子线程");
        }
    }

    public static void main(String[] args)  {
        TwoThread twoThread=new TwoThread();
        Thread thread=new Thread(twoThread);
        thread.start();
    }
}

上面的两种方法都有一个缺点,当我们需要获取线程执行任务的返回值的时候怎么办?接下来的方法可以进行解决。

(3)通过Callable和Future创建线程

  • 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
  • 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
  • 使用FutureTask对象作为Thread对象的target创建并启动新线程。
  • 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
public class ThreeThreadDemo {

    public static class MyCaller implements Callable<String>{

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

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<String> futureTask=new FutureTask<>(new MyCaller());
        new  Thread(futureTask).start();
        String result=futureTask.get();
        System.out.println(result);
    }
}

线程的等待和通知

我们知道Object是所有类的父类,JAVA把所有类都需要的方法放到了Object类里面,其中就包含了线程等待和通知函数。

wait():当一个线程调用一个共享变量的wait方法时,该线程会被阻塞挂起,当发生以下几种情况时等待结束。

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

wait()方法需要事先获取目标对象的监视器锁,否则调用wait方法会抛出IllegalMonitorStateException异常。那么一个线程如何获取一个共享变量的监视器锁呢?

  • (1)执行synchronized同步代码块时,使用该目标对象作为参数。
  • (2)调用该目标对象的方法,并且该对象使用了synchronized修饰。

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

虽然虚假唤醒在应用实践中很少发生,但要预防这种情况的发生,做法就是不停地去测试该线程被唤醒的条件是否满足,不满足则继续等待。

我们通过一个简单的例子来加深理解。

如下例,queue为共享变量,生产者线程在调用queue的wait方法前,需要使用synchronized 拿到共享变量的监视器锁。如果当前队列满的话,就挂起当前线程,使用循环就是为了避免所说的虚假唤醒的问题。

假如生产者线程首先通过synchronized 获取到了queue上的锁,那么后续所有企图操作queue的线程将会在获取监视器锁的地方被阻塞挂起,然后生产者线程获取锁后发现队列已满会调用wait方法阻塞自己,然后释放queue的锁。然后其他线程获取锁后继续往下执行。

public class QueueDemo {

    public final static List<String> queue=new ArrayList<>(10);

    public static String i="1";

    //生产者线程
    public static class Producer implements Runnable{

        @Override
        public void run() {
            synchronized (queue){
				 //如果队列满则等待,释放锁
                while (queue.size()==10){
                    try {
                            queue.wait();
                    }catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                queue.add(i);
                queue.notify();
            }
        }
    }

    //消费者线程
    public static class Consumer implements Runnable{

        @Override
        public void run() {
            synchronized (queue){
                while (queue.size()==0){
                    try {
                        queue.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                for (int i=0;i<queue.size();i++){
                    System.out.println("第"+i+"个:"+queue.get(i));
                }
                queue.notify();
            }
        }
    }

    public static void main(String[] args) {
            new Thread(new Producer()).start();
            new Thread(new Consumer()).start();
    }


}

另外需要注意的是,当前线程调用共享变量的wait方法之后只会释放该共享变量上的锁,如果当前线程还持有其他共享变量的锁,是不会释放其他的锁的。如下例所示

public class FourDemo {
    private static volatile Object resourceA=new Object();
    private static volatile Object resourceB=new Object();

    public static void main(String[] args) {

    Thread threadA=new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                synchronized (resourceA) {
                    System.out.println("线程A获取到resourceA的锁");
                    synchronized (resourceB) {
                        System.out.println("线程A获取到resourceB的锁");
                        System.out.println("线程A释放resourceA的锁");
                        resourceA.wait();
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });

    Thread threadB=new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
                synchronized (resourceA) {
                    System.out.println("线程B获取到resourceA的锁");
                    synchronized (resourceB) {
                        System.out.println("线程B获取到resourceB的锁");
                        System.out.println("线程B释放resourceA的锁");
                        resourceA.wait();
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });

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

    }
}

在这里插入图片描述

我们再来看一个例子,当一个线程调用共享对象的wait方法被阻塞挂起后,如果其他线程中断了该线程,则该线程会抛出InterruptedException异常并返回,否则就会一直等待下去。

public class FiveDemo {
    static Object object=new Object();
    public static void main(String[] args) throws InterruptedException {
            Thread thread=new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println("子线程开始");
                        synchronized (object){
                            object.wait();
                        }
                        System.out.println("子线程结束");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });

            thread.start();
            Thread.sleep(1000);
            System.out.println("主线程开始");
            thread.interrupt();
            System.out.println("主线程结束");
    }
}

在这里插入图片描述

wait(long timeout):该方法多了一个超时参数。如果线程调用该方法挂起后,没有在指定时间内被唤醒,那么同样会结束等待。如果将timeout设置为0则和wait()方法效果一样,如果设置为负数则会抛出IllegalArgumentException异常。

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

notifyAll():该方法会唤醒所有在该共享变量上由于调用wait方法而被挂起的线程。

接下来我们看几个例子帮我们更好的理解。

public class SixDemo {
	//共享资源
    private static volatile Object redourceA=new Object();

    public static void main(String[] args) throws InterruptedException {
    	//创建A线程
        Thread threadA=new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (redourceA){
                    System.out.println("线程A获取到了监视器锁");
                    try {
                        System.out.println("线程A开始等待");
                        redourceA.wait();
                        System.out.println("线程A结束等待");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

	//创建B线程
    Thread threadB=new Thread(new Runnable() {
        @Override
        public void run() {
            synchronized (redourceA){
                System.out.println("线程B获取到了监视器锁");
                try {
                    System.out.println("线程B开始等待");
                    redourceA.wait();
                    System.out.println("线程B结束等待");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    });

	//创建C线程
    Thread threadC=new Thread(new Runnable() {
        @Override
        public void run() {
            synchronized (redourceA){
                System.out.println("线程C开始唤醒");
                redourceA.notify();
            }
        }
    });
	
    threadA.start();
    threadB.start();
    //休息一秒后再开启C线程
    Thread.sleep(1000);
    threadC.start();
}

}

在这里插入图片描述

上面的代码我们开启了三个线程,线程A和线程B调用了共享资源的wait方法,线程C则调用了notify方法,启动线程C之前调用了sleep让主线程休眠一秒,这样做的目的是让线程A和线程B都执行到wait方法,然后调用notify唤醒一个线程,从结果来看,线程B首先获取到了资源的监视器锁,调用wait方法挂起当前线程并释放获取到的锁,然后线程A获取到锁并调用wait方法挂起当前线程,这时候线程A和B都已经被阻塞了,线程C在主线程休眠结束后获取到锁然后调用了notify方法,然后notify方法随机激活了一个线程。

如果把notify方法换成notifyAll,则结果如下。

在这里插入图片描述

等待线程中止的方法

在我们实际的开发中,经常会遇到等待一些事情做完之后再继续往下执行,如果是单线程的话就很好处理,多线程的话Thread类为我们提供了一个join方法,该方法会一直等待线程中止后执行完毕。

我们来看一个简单的例子。

public class SevenDemo {
    public static void main(String[] args) throws InterruptedException {
    	//创建线程A
        Thread threadA=new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程A结束");
            }
        });
		//创建线程B
        Thread threadB=new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程B结束");
            }
        });

        threadA.start();
        threadB.start();
        //等待线程AB结束以后再继续执行
        threadA.join();
        threadB.join();
        System.out.println("等待所有线程结束以后我再执行");
    }
}

在这里插入图片描述

上面的代码启动了两个子线程,然后分别调用了join方法,那么主线程会在调用A线程的join方法时被阻塞,等A线程执行完毕后返回。然后调用B线程的join方法时同理。

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

public class EightDemo {
    public static void main(String[] args) throws InterruptedException {
    	//创建线程A
        Thread threadA=new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程A开始执行");
                while (true){
                }
            }
        });
		
		//获取主线程
        Thread mainThread=Thread.currentThread();
		
		//创建线程B
        Thread threadB=new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程B执行中断主线程");
                mainThread.interrupt();
            }
        });
        threadA.start();
        threadB.start();
        try {
            threadA.join();
        }catch (InterruptedException e){
            e.printStackTrace();
        }

    }
}

在这里插入图片描述

上面的代码,线程A执行死循环,然后主线程调用线程A的join方法阻塞自己等待线程A执行完毕,线程B调用主线程的interrupt方法中断主线程,此时主线程抛出InterruptedException异常返回。

线程睡眠的方法

Thread类中有一个静态的sleep方法,当一个执行中的线程调用了Thread的sleep方法后,调用线程会暂时让出指定时间的执行权,也就是在这期间不参与CPU的调度,但是该线程拥有的监视器资源,例如锁还是持有不让出的。指定的睡眠时间到了后该函数会正常返回,线程会重新参与CPU的调度,获取到CPU资源后就可以继续运行了。如果在睡眠期间其他线程调用了该线程的interrupt()方法中断了该线程,则该线程在调用sleep方法的地方抛出InterruptedException异常。

接下来我们来看一个例子,线程在睡眠时拥有的监视器资源不会被释放。

public class OneDemo {

    //创建一个独占锁
    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("线程A开始休眠");
                    Thread.sleep(10000);
                    System.out.println("线程A休眠结束");
                } 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("线程B开始休眠");
                    Thread.sleep(10000);
                    System.out.println("线程B休眠结束");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    lock.unlock();
                }
            }
        });

        threadA.start();
        threadB.start();
    }
}

在这里插入图片描述

上面的代码我们首先创建了一个独占锁,独占锁每一次只能被一个线程所持有,后面我们会讲到,先简单用一下。然后创建了两个线程,每个线程都会在内部先获取锁,然后休眠,最后释放锁。通过运行我们就可以观察到,无论是哪个线程先执行,然后在其休眠期间独占锁还是该线程本身拥有,而不会释放。

接下来我们来看一下,当一个线程处于休眠状态时,如果另一个线程中断了它,会不会在调用sleep处抛出异常。

public class TwoDemo {
    public static void main(String[] args) throws InterruptedException {
        //创建线程
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println("子线程开始休眠");
                    Thread.sleep(10000);
                    System.out.println("子线程休眠结束");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        thread.start();
        //主线程休眠2s
        Thread.sleep(2000);
        //主线程中断子线程
        thread.interrupt();
    }
}

在这里插入图片描述

子线程在睡眠期间,被主线程中断,所以子线程在调用sleep方法处抛出了异常。

让出cpu执行权的方法

Thread类中有一个静态的yield方法,当一个线程调用yield方法时,实际就是在暗示线程调度器当前线程请求让出自己的CPU使用权,但是线程调度器可以无条件忽略该暗示。当一个线程调用yield方法时,当前线程会让出cpu使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,当然也有可能获取到刚才让出cpu使用权的那个线程。

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==0){
                System.out.println(Thread.currentThread()+"让出线程");
                Thread.yield();
            }
        }
        System.out.println(Thread.currentThread()+"结束");
    }
	
	//启动三个线程
    public static void main(String[] args) {
        new YieldTest();
        new YieldTest();
        new YieldTest();
    }
}

在这里插入图片描述

从结果可知,yield方法生效了,三个线程分别在i=0时调用了yield方法,然后线程就让出了自己的执行权。

线程中断

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

  • void interrupt():中断线程。例如:当线程A运行时,线程B可以调用线程A的interrupt()方法来设置线程A的中断标志为true并立即返回。设置标志仅仅是设置标志,线程A实际并没有被中断,它会继续向下执行。如果线程A调用了wait、join、sleep等方法被阻塞挂起,这时候线程B调用线程A的interrupt()方法,线程A会在调用这些方法的位置抛出InterruptedException异常。

  • boolean isInterrupted(boolean ClearInterrupted): 检测当前线程是否被中断,是返回true,否返回false。里面的参数传入true时表示清除中断标志,false为不清除。

  • boolean isInterrupted():检测当前线程是否被中断,是返回true,否返回false。其实方法内部默认调用的是上面的方法,传入了false参数。

  • boolean interrupted():检测当前线程是否被中断,是返回true,否返回false。与isInterrupted不同的地方在于,该方法如果发现当前线程被中断,则会清除中断标志,并且该方法是static方法,可以直接调用。其实方法内部是调用了上面的方法,但是传入了true参数清除标志。并且看该方法源码可知,该方法获取的是当前调用线程的中断标志而不是调用该方法的对象的中断标志。

我们通过一个例子来理解一下isInterrupted和interrupted的区别。

public class ThreeDemo {
    public static void main(String[] args) {
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){

                }
            }
        });

        thread.start();
        //设置子线程的中断标志
        thread.interrupt();
		//获取中断标志
        System.out.println("中断标志为:"+thread.isInterrupted());
        //获取并重置中断标志
        System.out.println("中断标志为:"+thread.interrupted());
        System.out.println("中断标志为:"+Thread.interrupted());
        System.out.println("中断标志为:"+thread.isInterrupted());
    }
}

在这里插入图片描述

看上面的代码可知,我们中断了子线程,然后获取子线程的中断标志为true。但是别忘了interrupted获取的是当前线程的中断标志,所以其实是返回的主线程的中断标志,所以为false。

我们修改一下代码

public class FourDemo {

    public static void main(String[] args) {
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
            	//当中断标志为true时跳出循环,并重置中断标志
                while (!Thread.currentThread().interrupted()){

                }
                System.out.println("中断标志为"+Thread.currentThread().isInterrupted());
            }
        });

        thread.start();
        //中断子线程
        thread.interrupt();
    }
}

在这里插入图片描述

线程上下文切换

在多线程编程中,线程个数一般都大于CPU个数,每个CPU的核心同一时刻只能被一个线程使用,为了让用户感觉多个线程是在同时执行的,CPU资源的分配采用了时间片轮转的策略,也就是给每个线程分配一个时间片,线程在时间片内占用CPU执行任务,当前线程使用完时间片后,就会处于就绪状态并让出CPU让其他线程使用,切换线程上下文时需要保存当前线程的执行现场,当再次执行时可以根据保存的执行现场继续向下执行。 任务从保存到再加载的过程就是一次上下文切换。

上下文切换会带来线程调度的开销,使性能下降,所以我们要减少上下文的切换,方法有无锁并发编程、使用最少线程、CAS算法等。

死锁

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象。
死锁的产生必须将具备以下四个条件。

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

接下来我们来看下造成死锁的一个例子。

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

    public static void main(String[] args) {
        //创建线程A
        Thread threadA=new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resourceA){
                    System.out.println("A线程获取到了resourceA");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("A线程准备获取resourceB");
                    synchronized (resourceB){
                        System.out.println("A线程获取到了resourceB");
                    }
                }
            }
        });

        //创建线程B
        Thread threadB=new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resourceB){
                    System.out.println("B线程获取到了resourceB");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("B线程准备获取resourceA");
                    synchronized (resourceA){
                        System.out.println("B线程获取到了resourceA");
                    }
                }
            }
        });

        threadA.start();
        threadB.start();
    }
}

在这里插入图片描述

上面的代码中首先创建了两个资源,然后创建了两个线程。从输出的结果可知,线程调度器首先调用了线程A,线程A获取到了resourceA的监视器锁,然后休眠1s,这里休眠的原因是保证线程B先抢到resourceB的监视器锁,第二条输出说明线程B成功的获取到了resourceB的监视器锁,然后休眠。到这里,线程A获取到了resourceA的锁,线程B获取到了resourceB的锁,线程A在休眠结束后会尝试获取resourceB的锁,而resourceB的锁已经被线程B占有,所以线程A会被阻塞等待,线程B也会在休眠结束后尝试获取resourceA的锁,而resourceA的锁已经被线程A占有,所以线程B也会被阻塞等待,两个线程就陷入了相互等待的状态,产生了死锁。

如何避免死锁

要想避免死锁,只需要破坏掉至少一个构造死锁的必要条件即可,但是我们学习操作系统时知道,目前只有请求并持有条件和环路等待条件是可以被破坏的。

(1)造成死锁的原因和申请资源的顺序有很大关系,使用资源申请的有序性原则就可以避免死锁,就是说当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生,如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。我们来改一下上面线程B的代码,将获取锁的顺序换一下。

资源的有序性其实是破坏了资源的请求并持有条件和环路等待条件,因此避免了死锁。

//创建线程B
        Thread threadB=new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resourceA){
                    System.out.println("B线程获取到了resourceA");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("B线程准备获取resourceB");
                    synchronized (resourceB){
                        System.out.println("B线程获取到了resourceB");
                    }
                }
            }
        });

在这里插入图片描述

(2)使用定时锁,给锁指定一个超时时限

守护线程和用户线程

java中的线程分为两类,分别为daemon线程(守护线程)和user线程(用户线程)。在JVM启动时会调用main函数,main函数所在的线程就是一个用户线程,其实在JVM内部同时还启动了好多守护线程,比如垃圾回收线程。那么守护线程和用户线程有什么区别呢?区别之一就是当最后一个非守护线程结束时,JVM会正常退出,而不管当前是否有守护线程,也就是说守护线程是否结束并不影响JVM的退出。

创建守护线程

		Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {

            }
        });
		//设置守护线程
        thread.setDaemon(true);
        thread.start();

我们来通过例子来理解用户线程和守护线程的区别。

public class TenDemo {

    public static void main(String[] args) {
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){

                }
            }
        });
        thread.start();
        System.out.println("主线程结束");
    }
}

在这里插入图片描述

我们发现主线程虽然结束了,但是JVM进程并没有退出,子线程还在继续运行。这个结果同时也说明了子线程的生命周期并不受父线程的影响。我们来把子线程设置成守护线程。

在这里插入图片描述

然后可以看到JVM进程已经退出。这个例子中主线程是用户线程,子线程是守护线程,当JVM发现当前已经没有用户线程,即使守护线程还在执行任务,JVM进程会照样结束。

ThreadLocal

多线程访问同一个共享变量时特别容易出现并发问题,特别是多个线程需要对一个共享变量进行写入时,为了保证线程安全,一般使用者在访问共享变量时需要适当的加锁。

当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本,从而避免了线程安全问题。

我们先来看一个ThreadLocal使用案例

public class ThreadLocalDemo {

	//创建ThreadLocal变量
    public static ThreadLocal<String> threadLocal=new ThreadLocal<>();
	
	
    public static void main(String[] args) {
   		 //创建A线程
        Thread threadA=new Thread(new Runnable() {
            @Override
            public void run() {
                threadLocal.set("我是A线程");
                System.out.println("A线程threadlocal值为:"+threadLocal.get());
                threadLocal.remove();
                System.out.println("A线程threadlocal值为:"+threadLocal.get());

            }
        });

		//创建B线程
        Thread threadB=new Thread(new Runnable() {
            @Override
            public void run() {
                threadLocal.set("我是B线程");
                System.out.println("B线程threadlocal值为:"+threadLocal.get());
                threadLocal.remove();
                System.out.println("B线程threadlocal值为:"+threadLocal.get());
            }
        });

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

    }
}

在这里插入图片描述

ThreadLocal详解

首先在Thread类中有一个threadLocals变量,类型为ThreadLocal.ThreadLocalMap,ThreadLocalMap是ThreadLocal类中一个定制化的HashMap。

 ThreadLocal.ThreadLocalMap threadLocals = null;

默认情况下,每个线程中的threadLocals变量都为null。只有当前线程第一次调用ThreadLocal的set或get方法时才会创建。也就是说,其实每个线程的本地变量并不放在ThreadLocal实例里面,而是存放在每个线程的threadLocals变量里。ThreadLocal只是一个工具,它通过set方法把值放入调用线程的threadLocals变量里面存放,通过get方法从当前线程的threadLocals变量中取出。如果调用线程一直不结束,那么这个变量就会一直存放在threadLocals变量里面。当不需要使用本地变量时,可以通过ThreadLocal的remove方法进行移除。为什么threadLocals要设计成Map类型呢?是因为每个线程可以关联多个ThreadLocal变量。

(1)set():首先获取当前线程,然后将当前线程作为key,去查找对应的threadLocals变量,查到则直接向Map中设置,没查到则需要先创建threadLocals变量再设置,threadLocals变量是HashMap类型的,key就是当前的ThreadLocal的实例对象引用。

	public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

(2)get():首先获取当前线程,然后获取当前线程的threadLocals变量,如果不为null,则取出value返回。如果为null,则去初始化threadLocals变量。

	public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

(3)remove():从threadLocals变量中获取当前线程对应的value,如果不为null,则移除。

	public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

总结:每个线程内部有一个threadLocals变量,变量类型为特殊的HashMap。其中key为我们定义的ThreadLocal变量的引用,value则为我们设置的值,如果线程不结束,那么本地变量会一直存在,可能会造成内存溢出,因此要记得remove。

InheritableThreadLocal

我们知道ThreadLocal变量被主线程设置后,子线程中也是获取不到的,而InheritableThreadLocal就是为了解决这个问题,让子线程能够获取到父线程中设置的ThreadLocal值。我们先来看下InheritableThreadLocal源码。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {

    protected T childValue(T parentValue) {
        return parentValue;
    }

   
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

   
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

看代码可知InheritableThreadLocal继承了ThreadLocal。我们看到它重写了createMap方法,那现在第一次调用set方法时,创建的则是Thread类中的inheritableThreadLocals变量。通过第二个getMap方法也可知,当调用get方法获取变量时,获取的也是inheritableThreadLocals。其实也就是使用InheritableThreadLocal时,inheritableThreadLocals变量代替了threadLocals变量。

那么这个类是如何实现让子线程访问父线程的本地变量的呢?我们需要看一下Thread类的构造函数。

	public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }


	private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        
		....... 
		.......
		//获取当前线程
        Thread parent = currentThread();
        .......
        .......
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        
        this.stackSize = stackSize;

        
        tid = nextThreadID();
    }

我们可以看到,在Thread类的构造方法中调用了init方法,init方法中会获取当前线程(父线程),然后会判断inheritThreadLocals是否为空,这里不为空,调用了createInheritedMap方法。

	static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
    }

可以看到,在这个方法内部,使用父线程的inheritThreadLocals 作为参数构建了一个新的ThreadLocalMap返回赋值给子线程的inheritThreadLocals 。我们来看下ThreadLocalMap的构造函数做了什么。

	private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }

可以看到,该构造方法中把父线程的inheritThreadLocals 变量复制到新的对象中。并且调用了InheritableThreadLocal类的childValue方法。

总结:InheritableThreadLocal通过重写getMap和createMap让本地变量保存到了inheritThreadLocals 变量里面,线程在通过InheritableThreadLocal 类set或者get设置变量时,会创建当前线程的inheritThreadLocals 变量。当父线程创建子线程时,构造函数会把父线程的inheritThreadLocals里面的值复制一份保存到子线程的inheritThreadLocals变量里面。

  • 6
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值