并发编程基础一

本文围绕Java线程展开,介绍了线程和进程的概念,阐述了Java线程创建的三种方式及其优缺点。还详细讲解了线程的通知与等待机制,包括wait、notify等函数。此外,分析了线程上下文切换、死锁的产生原因及避免方法,最后介绍了守护线程和用户线程的区别及应用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.什么是线程?什么是进程?

进程是代码在数据集合上的一次运行活动,是系统资源调用和分配的基本单位。
线程则是进程的一个执行路径,一个进程中至少有一个线程,进程中多线程共享进程中的资源。
线程是CPU分配的基本单位。

2.程序计数器

程序计数器是块内存区域,用来记录线程当前要执行的指令地址。
那么为何要将程序计数器设计线程私有的呢?

前面说了线程是占用CPU执行的基本单位,而CPU一般是使用时间片轮转方式让线程轮询占用的,
所以当前线程CPU时间片用完后,要让出CPU,等下次轮到自己的时候再执行。
那么如何知道之前程序执行到哪里了呢?

其实程序计数器就是为了记录该线程让出CPU时的执行地址的,
待再次分配到时间片时线程就可以从自己私有的计数器指定地址继续执行。

另外需注意的是,如果执行的是native方法,那么pc计数器记录的是undefined地址,
只有执行的是Java代码时pc计数器记录的才是下一条指令的地址。
另外每个线程都有自己的栈资源,用于存储该线程的局部变量,
这些局部变量是该线程私有的,其他线程是访问不了的,
除此之外枝还用来存放线程的调用技帧。

3.堆

堆是一个进程中最大的一块内存,堆是被进程中的所有线程共享的,
是进程创建时分配的,堆里面主要存放使用new操作创建的对象实例。

4.方法区

方法区则用来存放NM加载的类、常量及静态变量等信息,也是线程共享的。

java线程创建的三种方式:

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

	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName()+"\tI am child Thread...");
	}
	
	
	
	public static void main(String[] args) {
		TestThread myThread=new TestThread();
		myThread.start();
	}

}

其实调用start方法后线程并没有马上执行而是处于就绪状态,这个就绪状态是指该线已经获取了除CPU资源外的其他资源,等待获取CPU资源后才会真正处于运行状态。一旦run方法执行完毕,该线程就处于终止状态。使用继承方式的好处是,在run()方法内获取当前线程直接使用this就可以了,无须使用Thread.currentThread()方法;不好的地方是Java不支持多继承,如果继承了Thread类,那么就不能再继承其他类。另外任务与代码没有分离,当多个线程执行一样的任务时需要多份任务代码,而Runable则没有这个限制。下面看实现Runnable接口的run方法方式。

public static class RunnableTask implements Runnable{

		@Override
		public void run() {
			System.out.println(Thread.currentThread().getName()+"\tI am child Thread...");
		}
		
	} 
	
	public static void main(String[] args) {
		RunnableTask task=new RunnableTask();
		new Thread(task).start();
		new Thread(task).start();
	}

如上面代码所示,两个线共用一个task代码逻辑,如果需要,可以给RunableTask添加参数进行任务区分。另外,RunableTask可以继承其他类。但是上面介绍的两种方式都一个缺点就是任务没有返回值。下面看最后一种,即使用FutureTask的方式。

public static class CallerTask implements Callable<String>{

		@Override
		public String call() throws Exception {
			return "hello!";
		}
	  
  }
	
	public static void main(String[] args) {

		// 创建异步任务
		FutureTask<String> future = new FutureTask<String>(new CallerTask());

		// 启动线程
		new Thread(future).start();

		// 等待任务执行完毕
		try {
			String result = future.get();
			System.out.println(result);
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (ExecutionException e) {
			e.printStackTrace();
		}

	}

上代码中的CallerTask类实现了Callable接口的call()方法。在main函数内首先创建了个FutrueTask对象(构造函数为CallerTask的实例),然后使用创建的FutrueTask对象作为任务创建了一个线程并且启动它,最后通过futureTask.get()等待任务执行完毕并返回结果。

小结:使用继承方式的好处是方便传参,你可以在子类里面添加成员变量通过set方法设置参数或者通过构造函数进行传递,而如果使用Runnable方式,则只能使用主线程里面被声明为final的变量。不好的地方是Java不支持多继承,如继承了Thread类,那么子类不能再继承其他类,而Runable则没有这个限制。前两种方式都没办法拿到任务的返回结果,但是Futuretask方式可以。

1.3线程的通知与等待

        Java中的Object类是所有类的父类,鉴于继承机制,
        Java把所有类都需要的方法放到了Object类里面,
        其中就包含本节要讲的通知与等待系列函数。
        1.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()方法的实例,        首先通过同步块获取obj上面的监视器锁,然后在while循环内调    用obj的wait()方法。下面从一个简单的生产者和消费者例子来加深理解。
  如下面代码所示,其中queue为共享变量,生产者线程在调用queue的wait()方法前,使用synchronized关键宇拿到了共享变量queue的监视器锁,所以调用wait()方法才不会抛出lliegalMonitorStateException异常。如果当前队列没有空闲容量则会调用queued的wait()方法挂起当前线程,这里使用循环就是为了避免上面说的虚假唤醒问题。假如当前线程被虚假唤醒了,但是队列还是没有空余容量, 那么当前线程还是会调用wait()方法把自己挂起。

    //生产线程
        synchronized(queue)
        {
            //消费队列满,则等待队列空闲 
            while (queue.size() == MAX_SIZE) {
                try {
        //挂起当前线程,并释放通过同步块获取的 queue上的锁,
         //让消费者线程可以获取该锁,然后获取队列里面的元素 
                    queue.wait();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }

            // 空闲则生成元素 , 并通知消费者线程
            queue.add(ele);
            queue.notifyAll();
  }
		//消费者线程
		synchronized (queue) { 
			// 消费队列为空 
			while (queue.size() == 0) { 
			try {
			//挂起当前线程,并释放通过同步块获取的 queue上的锁, 让生产者线程可以获取该锁,将生产元素放入队 
              queue.wait();
			} catch (Exception ex ) {
				ex . printStackTrace() ; 
			}
			//消费元素 ,并通知唤醒生产者线程 
			queue.take(); 
			queue.notifyAll();
		}
}

上代码中假如生产者线程A先通过synchronized获取到了queue上的锁,那么后续所有企图生产元素的线程和消费线程将会在获取该监视器锁的地方被阻塞挂起。线程A获取锁后发现当前队列己满会调用queue.wait()法阻塞自己,然后释放获取的queue上的锁,这里考虑下为何要释放该锁?如果不释放,由于其他生产者线程和所有消费者线程都己经被阻塞挂起,而线程也被挂起这就处于了死锁状态。这里线程A挂起自己后释放共享变量上的锁,就是为了打破死锁必要条件之一的持有并等待原则。关于死锁后面的章节会讲。线程A释放锁后,其他生产者程和所有消费者线程中会有一个线程获取queue上的锁进而进入同步块,这就打破了死锁状态。另外需要注意的是,当前线程调用共享变量的wait()方法后只会释放当前共享变量上的锁,如果当前线程还持有其他共享变量的锁,则这些锁是不会被释放的。下面来看一个例子。

public class WaitNotifyTest {
    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() {
            @Override
            public void run() {
                try {
                    synchronized (resourceA) {
                        System.out.println("threadA get resourceA lock");

                        synchronized (resourceB) {
                            System.out.println("threadA get resourcesB lock");
                            System.out.println("threadA release resourceA lock");

                            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("threadB get resourceA lock");
                        System.out.println("threadB try get resourceB lock ...");

                        synchronized (resourceB) {
                            System.out.println("threadB get resourcesB lock");
                            System.out.println("threadB release resourceA lock");
                            resourceA.wait();
                        }
                    }
                }catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

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

        threadA.join();
        threadB.join();

        System.out.println("main over");
    }
}

上代码中,在main函数里面启动了线程A和线程B,为了让线程A先获取到锁,这里让线程B先休眠了l秒,线程A先后获取到共享变量resourceA共享变量resourceB上的锁,然后调用了resourceAwait()方法阻塞自己,阻塞自己后线程A释放掉获取的resourceA上的锁。

线程B休眠结束后会首先尝试获取resourceA上的锁,如果当时线程A还没有调用wait()方法释放锁,那么线程B会被阻塞,当线程A释放了resourceA上的锁后,线程B就会获取到resourceA上的锁,然后尝试获取resourceB上的锁。由于线程A调用的是resourceAwait()方法,所以线程A挂起自己后并没有释放获取到的resourceB上的锁,所以线程B尝获取resourceB上的锁时会被阻塞。这就证明了当线程调用共享对象的wait()方法时,当前线程只会释放当前共享对象,当前线程持有的其他共享对象的监视器锁并不会被释放。

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

public class WaitNotifyInterupt {
	static Object obj = new Object();
	
	public static void main(String[] args) throws InterruptedException {
		Thread thread=new Thread(new Runnable() {
			
			@Override
			public void run() {
				synchronized (obj) {
					try {
						System.out.println("-----begin-----------");
						obj.wait();
						System.out.println("-----end----------");
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		});
		thread.start();
		thread.sleep(1000);
		System.out.println("thread_Interrupt start...");
		thread.interrupt();
		System.out.println("thread_Interrupt end...");
	}
}

在如上代码中,threadA调用共享对象objwait()方法后阻塞挂起了自己,然后主线程在休眠1秒后中断了threadA线程,中断后threadAobj.wait()处抛出java.lang.InterruptedException异常而返回并终止。

2.wait(longtimeout)函数该方法相比wait()方法多了一个超时参数,它的不同之处在于,如果一个线程调用共享对象的该方法挂起后,没有在指定的timeoutms时间内被其他线程调用该共享变量的notify()或者notifyAll()方法唤醒,那么该函数还是会因为超时而返回。如果将timeout设置为0则和wait方法效果一样,因为在wait方法内部就是调用了wait(0)。需要注意的是,如果在调用该函数时,传递了一个负的timeout则会抛出IllegalArgumentException异常。

3.wait(longtimeout,intnanos)函数在其内部调用的是wait(longtimeout)函数,如下代码只有在nanos>O时才使参数timout递增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);
    }

4.notify()函数

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

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

5.notifyAll()函数

不同于在共享变量上调用notify()函数会唤醒被阻塞到该共享变量上的一个线程,notifyAll()方法则会唤醒所有在该共享变量上由于调用wait系列方法而被挂起的线程。下面举一个例子来说明notify()和notifyAll()方法的具体含义及一些需要注意的地方,代码如下。

public class WaitNotifyAllTest {

    //创建资源
    private static volatile Object resourceA = new Object();

    public static void main(String[] args) throws InterruptedException{

        //创建线程 threadA
        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 (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

        //创建线程 threadB
        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {

                //获取 resourceB 的资源监视器
                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 (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

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

        //启动线程
        threadA.start();
        threadB.start();
        Thread.sleep(1000);
        threadC.start();

        //等待线程结束
        threadA.join();
        threadB.join();
        threadC.join();

        System.out.println("main over");

    }
}

上代码开启了三个线程,其中线程A和线程B分别调用了共享资源resourceA的wait()方法,线程C则调用了nofity()方法。这里启动线程C前首先调用sleep方法让主程休眠ls,这样做的目的是让线程A和线程B全部执行到调用wait方法后再调用线程C的notify方法。这个例子试图在线程A和线程B因调用共享资源resourceA的wait()方法而被阻塞后,让线程C再调用resourceA的notify()方法,从而唤醒线程A和线程B。但是从执行结果来看,只有一个线程A被唤醒,线程B有被唤醒:

从输出结果可知线程调度器这次先调度了线程A占用CPU来运行,线程A首先取resourceA上面的锁,然后调用resourceA的wait()方法挂起当前线程并释放获取到的锁,然后线程B获取到resourceA上的锁井调用resourceA的wait()方法,此时线程B被阻塞挂起并释放了resourceA上的锁,到这里线程A和钱程B被放到了resourceA的阻塞集合里面。线程C休眠结束后在共享资源resourceA上调用了notify()法,这会激活resourceA的塞集合里面的一个线程,这里激活了线程A,所以线程A调用的wait()方法返回了,线程A执行完毕。而线程B还处于阻塞状态。如果把线程C调用的notify()方法改为调用notifyAll()方法,则执行结果如下。

输入结果可知线程A和线程B被挂起后,线程C调notifyAll()法会唤醒resourceA的等待集合里面的所有线程,这里线程A和线程B都会被唤醒,只是线程B先获取到resourceA上的锁,然后从wait()方法返回。线程B执行完毕后,线程A又获取了resourceA上的锁,然后从wait()方法返回。线程A执行完毕后主线程返回,然后打印输出。

一个需要注意的地方是,在共享变量上调用notifyAll()方法只会唤醒调用这个方法前调用了wait系列函数而被放入共享变量等待集合里面的线程。如果调用notifyAll()方法后一个线程调用了该共享变量的wait()方法而被放入阻塞集合,则线程是不会被唤醒的。尝试把主线程里面休眠ls的代码注释掉,再运行程序会有一定概率输出下面的结果。

就是在线程B调用共享变量的wait()方法前线程C调用了共享变量的notifyAll方法,这样只有线程A被唤醒,而线程B并没有被唤醒,还是处于阻塞状态。

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

在项目实践中经常会遇到一个场景,就是需要等待某几件事情完成后才能继续往下执行,比如多个线程加载资源,需要等待多个线程全部加载完毕再汇总处理。Thread类中有一个join方法就可以做这个事情,前面介绍的等待通知方法是Object类中的方法,而join方法则是Thread类直接提供的。join是无参且返回值为void的方法。下面来看一个简单的例子。

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

        Thread threadTwo = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException 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!");

    }
wait all child thread over!
child threadOne over!
child threadTwo over!
all child thread over!

另外,线程调用线程B的join方法后会被阻塞,当其他线程调用了线程A的interrupt()方法中断了线程A时,线程A会抛出InterruptedException异常而返回。下面通过一个子来加深解。

public class JoinInterruptedExceptionTest {
	public static void main(String[] args) throws InterruptedException{

        //线程 One
        Thread threadOne = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("threadOne begin run!");
                for(;;){
                }
            }
        });

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

        //线程 two
        Thread threadTwo = new Thread(new Runnable() {
            @Override
            public void run() {
                try{
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //中断主线程
                mainThread.interrupt();
            }
        });

        //启动子线程
        threadOne.start();

        //延迟 1s 启动线程
        threadTwo.start();


        //等待线程 One 执行结束
        try{
           threadOne.join();
        }catch (InterruptedException e){
            System.out.println("main thread:" + e);
        }
    }
}
threadOne begin run!
main thread:java.lang.InterruptedException

上代码在threadOne线程里面执行死循环,主线程调用threadOne的join方法阻塞自己等待线程threadOne执行完毕,待threadTwo休眠ls后会调用主线程的interrupt()方法设置主线程的中断标志,从结果看在主线程中的threadOne.join()处会抛出InterruptedException异常。这里需要注意的是,在threadTwo里面调用的是主线程的interrupt()方法,而不是线程threadOne的。

1.5让线程睡眠的sleep方法

Thread类中有一个静态的sleep方法,当一个执行中的线程调用了Thread的sleep方法后,调用线程会暂时让出指定时间的执行权,也就是在这期间不参与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("child threadA is in sleep");

                    Thread.sleep(10000);

                    System.out.println("child threadA is in  awaked");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        });

        //创建线程A
        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 (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    //释放锁
                    lock.unlock();
                }
            }
        });

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

上代码首先创建了一个独占锁,然后创建了两个线程,每个线程在内部先获取锁,然后睡眠,睡眠结束后会释放锁。首先,无论你执行多少遍上面的代码都是线程A先输出或者线程B先输出,不会出现线程和线程B交叉输出的情况。从执行结果来看,线程A先获取了锁,那么线程A会先输出一行,然后调用sleep方法让自己睡眠10s,在线程A睡眠的这10s内那个独占锁lock还是线程A自己持有,线程B会一直阻塞直到线程A醒来后执行nlock释放锁。下面再来看一下,当一个线程处于睡眠状态时,如果另外一个线程中断了它会不会在调用sleep方法处抛出异常。

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 (InterruptedException e) {
	                    e.printStackTrace();
	                }
	            }
	        });

	        //启动线程
	        thread.start();

	        //主线程休眠 2s
	        Thread.sleep(2000);

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

线程在睡眠期间,主线程中断了它,所以子线程在调用sleep方法处抛出了InterruptedException常。另外需要注意的是,如果在调用Thread.sleep(longmillis时为millis参数传递了一个负数,则会抛出IllegalArgumentException异常,如下所示。

1.6让出CPU执行权的yield方法

Thread类中有一个静态的yield方法,当一个线程调用yield方法时,实际就是在暗示线程调度器当前线程请求让出自己的CPU使用,但是线程调度器可以无条件忽略这个暗示。我们知道操作系统是为每个线程分配一个时间片来占有CPU的,正常情况下当一个线程把分配给自己的时间片使用完后,线程调度器才会进行下一轮的线程调度,而当一个线程调用了Thread类的静态方法yield时,是在告诉线程调度器自己占有的时间片中还没有使用完的部分自己不想使用了,这暗示线程调度器现在就可以进行下一轮的线程调度。

当一个线程调yield方法时,当前线程会让出CPU使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,当然也有可能会调度到刚刚让出CPU的那个线程来获取CPU执行权。下面举一个例子来加深对yield方法的理解。

public class YieldTest implements Runnable {
    public 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");
                //当前线程让出 CPU 执行权,放弃时间片,进行下一轮调度
                Thread.yield();
            }
        }

        System.out.println(Thread.currentThread() + "is over");
    }

    public static void main(String[] args) {
        new YieldTest();
        new YieldTest();
        new YieldTest();
    }
}

如上代码开启了三个线程,每个线程的功能都样,都是在for循环中执行5次打印。运行多次后,上面的结果是出现次数最多。解开Thread.yield()注释再执行,结果如下:

 

从结果可知,Thread.yield()方法生效了,三个线程分别在i=O时调用了Thread.yield()方法,所以三个线程自己的两行输出没有在-起,因为输出了第一行后当前线程让出了CPU执行权。

一般很少使用这个方法,在调试或者测试时这个方法或许可以帮助复现由于并发竞争条件导致的问题,其在设计并发控制时或许会有用途,后面在讲解java.util.concurrent.locks包里面的锁时会看到该方法的使用。

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

1.7线程中断

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。

与islnterrupted不同的是,该方法如果发现当前线程被中断,则会清除中断标志,并且该方法是static方法,可以通过Thread类直接调用。另外从下面的代码可以知道,在interrupted()内部是获取当前调用线程的中断标志而不是调用interrupted()方法的例对象的中断标志。

public static boolean interrupted(){

//清除中断标志

return currentThread().isinterrupted(true);

}

下面看一个线程使用Interrupted优雅退出的经典例子,代码如下。

    public void run(){
        try{
           //线程退出条件
            while(!Thread.currentThread().isInterrupted() && more work to do){
                //domorework;
            }catch(InterruptedExceptione){
                //thread was interrupted during sleep or wait
            }finally{
                    //cleanup,if required
                }
            }
        }

下面看一根据中断标志判断线程是否终止的例子:

 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 interrupted thread");
		thread.interrupt();
		
		//等待子线程执行完毕
		thread.join();
		System.out.println("main is over");
		
		
	}

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

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

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 2000s");
					Thread.sleep(2000000);
					System.out.println("ThreadOne awaking");
				} catch (InterruptedException 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");

    }

在如上代码中,threadOne线程休眠了2000s,在正常情况下该线程需要等到2000s后才会被唤醒,但是本例通过调用threadOne.interrupt()方法打断了该线程的休眠,该线程会在调用sleep方法处抛出InterruptedException异常后返回。下面再通过一个例子来了解interrupted()与islnterrupted()方法的不同之处。

	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:" + Thread.interrupted());

        //获取中断标志
        System.out.println("isInterrupted:" + threadOne.isInterrupted());

        threadOne.join();
        System.out.println("main is over");
    }

第一行输出true这个大家应该都可以想到,但是下面三行为何是false、false、true呢,不应该是true、false、false吗?如果你有这个疑问,则说明你对这两个函数的区别还是不太清楚。上面我们介绍了在interruptd()方法内部是获取当前线程的中断状态,这里虽然调用了threadOne的interrupted()方法,但是获取的是主线程的中断标志,因为主线程是当前线程。threadOne.interrupted()和Thread.interrupted()方法的作用是一样的,目的都是获取当前线程的中断标志。修改上面的例子为如下:

public static void main(String[] args) throws InterruptedException{
        Thread threadOne = new Thread(new Runnable() {
            @Override
            public void run() {
                //中断标志位true时会退出循环,并且清除中断标志
            	while (!Thread.currentThread().interrupted()) {
					
				}
            	System.out.println("ThreadOne is interrupted :"+Thread.currentThread().isInterrupted());
            }
        });

        //启动线程
        threadOne.start();

        //设置中断标志
        threadOne.interrupt();

        threadOne.join();
        System.out.println("main thread is over");
    }

输出结果可知,调用interrupted()方法后中断标志被清除了。

1.8  理解线程上下文切换

在多线程编程中,线程个数一都大于CPU个数,而每个CPU同一时刻只能被一个线程使用,为了让用户感觉多个线程是在同时执行的,CPU资源的分配采用了时间片轮转的策略,也就是给每个线程分配一个时间片,线程在时间片内占用CPU执行任务。当前线程使用完时间片后,就会处于就绪状态并让出CPU让其他线程占用,这就是上下文切换,从当前线程的上下文切换到了其他线程。那么就有一个问题,让出CPU的线程等下次轮到自己占有CPU时如何知道自己之前运行到哪里了?所以在切换线程上下文时需要保存当前线程的执行现场,当再次执行时根据保存的执行现场信息恢复执行现场。

线程上下文切换时机有:

当前线程的CPU时间片使用完处于就绪状态时,

当前线程被其他线程中断时。

1.9 线程死锁

1.9.1 什么是线程死锁

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

在图1-2中,线程A己经持有了资源2,它同时还想申请资源l,线程B已经持有了资源l它同时还想申请资源2,所以线程l和线程2就因为相互等待对方已经持有的资源,而进入了死锁状态。那么为什么会产生死锁呢?

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

·互斥条件:线程对己经获取到的资源进行排它性使用,即该资源同时只由一个线程占用。如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占有资源的线程释放该资源。

·请求并持有条件:指一个线程己经持有了至少一个资源,但又提出了新的资源请求,而新资源己被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己己经获取的资源。

·不可剥夺条件:指线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才由自己释放该资源。

·环路待条件:指在发生死锁时,必然存在一个线程→资源的环形链,即线程集合{TO,T1,T2,…,Tn}中的TO正在等待一个Tl占用的资源,Tl正在等待T2占用的资源,……Tn正在等待己被TO占用的资源。下面通过一个例子来说明线程死锁:

public class DeadLockTest {

    public static Object resourceA = new Object();
    public 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(Thread.currentThread() + "get resourceA");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread() + "waiting get resourceB");
                    synchronized (resourceB){
                        System.out.println(Thread.currentThread() + "get resourceB");
                    }
                }
            }
        });

        //创建线程B
        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();
    }

}

上面分析代码和结果:

Thread-0是线程A,Thread-1是线程B,代码首先创建了两个资源,并创建了两个线程。从输出结果可以知道,线程调度器先调度了线程A,也就是把CPU资源分配了线程A,线程A使用synchronized(resourceA)方法获取到了resourceA的监视器锁,然后调用sleep数休眠ls休眠ls是为了保证线程A在获取resourceB对应的锁前让线程B抢占到CPU,获取到资源resourceB上的锁。线程A调用sleep方法后线程B会执行synchronized (resourceB)方法,这代表线程B获取到了resourceB对象监视器锁资源,然后调用sleep函数休眠ls。好了,到了这里线程A获取到了resourceA资源,线程B获取到了resourceB资源。线程A休眠结束后会企图获取resourceB资源,而resourceB资源被线程B所持有,所以线程A会被阻塞而等待。而同时线程B休眠结束后会企图获取resourceA资源,而resourceA源己经被线程A持有,所以线程A和程B就陷入了相互等待的状态,也就产生了死锁。下面谈谈本例是如满足死锁的四个条件的。

首先,resourceA和rsourceB都是互斥资源,当线程A调用synchronized(resourceA)方法获取到resourceA上的监视器锁并释放前,线程B再调用synchronized(resourceA)方法尝试获取该资源会被阻塞,只有线程A主动释放该锁,线程B才能获得,这满足了资源互斥条件。

 

线程A首先通过synchronized(resourceA)方法获取到resourceA上的监视器锁资源,然后通过synchronized (resourceB)方法等待获取resourceB上的监视器锁资源,这就构成了请求并持有条件。

线程A在获取resourceA上的监视器锁资源后,该资源不会被线程B掠夺走,只有线程A自己主动释放reourceA资源时,它才会放弃对该资源的持有权,这构成了资源的不可剥夺条件。

线程A持有objectA资源并等待获取objectB资源,而线程B持有objectB资源并等待objectA资源,这构成了环路等待条件。所以线程A和线程就进入了死锁状态。

1.9.2如何避免线程死锁

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

 

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

      Thread threadB = 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 resourceB");
                synchronized (resourceB){
                    System.out.println(Thread.currentThread() + "get resourceB");
                }
            }
      
        }
      });

上代码让在线程B中获取资源的顺序和在线程A中获取资源的顺序保持一致,其实资源分配有序性就是指,假如线程A和线程B都需要资源1,2,3,...,n时,对资源进行排序,线程A和线程B只有在获取了资源n-1时才能去获取资源n我们可以简单分析一下为何资源的有序分配会避免死锁,比如上面的代码,假如线程A和线程B同时执行到了synchronized(resourceA),只有一个线程可以获取到resourceA上的监视器锁,假如线程A获取到了,那么线程B就会被阻塞而不会再去获取资源B,线程A获取到resourceA的监视器锁后会去申请resourceB的监视器锁资源,这时候线程A是可以获取到的,线程A获取到resourceB资源并使用后会放弃对资源resourceB的持有,然后再释放对resourceA的持有,释放resourceA后线程B才会被从阻塞状态变为激活状态。所以资源的有序性破坏了资源的请求并持有条件和环路等待条件,因此避免了死锁。

1.10 守护线程和用户线程

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

public static void main(String[] args) {
		Thread demonThread=new Thread(new Runnable() {
			public void run() {
				
			}
		});
		demonThread.setDaemon(true);
		demonThread.start();
	}

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

	public static void main(String[] args) {
		Thread thread=new Thread(new Runnable() {
			public void run() {
				for (;;) {
					
				}
			}
		});
		thread.start();
		System.out.println("main thread is over");
	}
main thread is over

如上代码在main线程中创建了一个thread线程,在thread线程里面是一个无限循环。从运行代码的结果看,main线程已经运行结束了,那么JVM进程己经退出了吗?在IDE的输出结果右上侧的红色方块说明,JVM进程并没有退出。另,在mac上执行jps会输如下:

这个结果说明了当父线程结束后,子线程还是可以继续存在的,也就是子线程的生命周期并不受父线程的影响。这也说明了在用户线程还存在的情况下JVM进程并不会终止。那么我们把上面的t h r e a d线程设置为守护线程后,再来运行看看会有什么结果:

//设置为守护线程thread.setDaemo口(true);

//启动子线程thread.start();

输出结果如下:

启动线程前将线程设置为守护线程,执行后的输出结果显示,JVM进程己经终止了,执行ps-eaf|grepjava也看不到NM进程了。

在这个例子中,main函数是唯一的用户线程,thread线程是守护线程,当main线程运行结束后,JVM发现当前己经没有用户线程了,就会终止JVM进程。由于这里的守护线程执行的任务是一个死循环,这也说明了如果当前进程中不存在用户线程,但是还存在正在执行任务的守护线程,则JVM不等守护线程运行完毕就会结束JVM进程。main线程运行结束后,jvM会自动启动一个叫作DestroyJavaVM的线程,该线程会待所有用户线程结束后终止jvM进程。下面通过简单的jvM代码来证明这个结论。

翻看JVM的代码,能够发现,最终会调用到JavaMain这个C函数。

			int  JNICALL
			JavaMain(void  *  args){
			...
			//执行Java中的ma工n函数
			(*env)->CallStaticVoidMethod(env,mainClass,  mainID ,  mainArgs) ;
			//main函数返回值
			ret  =(*env)->ExceptionOccurred(env)==NULL?0:1;
		    //等待所有非守护线程结束,然后销毁JVM进程LEAVE();
		    }
			//LEAVE是C语言里面的一个宏定义,具体定义如下。
			#define  LEAVE  () \
			do  ( \
			  if  ((vm)->DetachCurrentThread(vm ) !=JNI_OK ) { \
						JLI_ReportErrorMessage(JVM  ERROR2); \
						ret  =  l ; \
						}\
		         if( JNI_TRUE){ \
		        	 (*vm)->DestroyJavaVM(vm);  \
		        	 return  ret;  \
		        	 }\
		         }  while(JNI_FALSE)
		    
           

 该宏的用是创建一个名为DestroyJavaVM的线程,用来等待所有用户线程结束。


 在Tomcat的NIO实现NioEndpoint中会开启一组接受线程来接受用户的连接请求,
 以及一组处理线程负责具体处理用户请求,那么这些线程是用户线程还是守护线程呢?
 下面我们看一下NioEndpoint的startlntemal方法。

	public void startInternal() throws Exception {
		if (!this.running) {
			this.running = true;
			this.paused = false;
			this.processorCache = new SynchronizedStack(128, this.socketProperties.getProcessorCache());
			this.eventCache = new SynchronizedStack(128, this.socketProperties.getEventCache());
			this.nioChannels = new SynchronizedStack(128, this.socketProperties.getBufferPool());
			if (this.getExecutor() == null) {
				this.createExecutor();
			}

			this.initializeConnectionLatch();
                      //创建处理线程
			this.pollers = new Poller[this.getPollerThreadCount()];

			for (int i = 0; i < this.pollers.length; ++i) {
				this.pollers[i] = new Poller(this);
				Thread pollerThread = new Thread(this.pollers[i], this.getName() + "-ClientPoller-" + i);
				pollerThread.setPriority(this.threadPriority);
                          //声明为守护线程
				pollerThread.setDaemon(true);
                         //启动接受线程
				pollerThread.start();
			}

			this.startAcceptorThreads();
		}

	}

			       protected final void startAcceptorThreads() {
			         int count = getAcceptorThreadCount();
			        acceptors = new ArrayList<>(count);

			        for (int i = 0; i < count; i++) {
			            Acceptor<U> acceptor = new Acceptor<>(this);
			            String threadName = getName() + "-Acceptor-" + i;
			            acceptor.setThreadName(threadName);
			            acceptors.add(acceptor);
			            Thread t = new Thread(acceptor, threadName);
			            t.setPriority(getAcceptorThreadPriority());
			            t.setDaemon(getDaemon()); //设置是否为守护线程,默认为守护线程
			            t.start();
			        }
			    }

private boolean daemon = true;
    public void setDaemon(boolean b) { daemon = b; }
    public boolean getDaemon() { return daemon; }

在如上代码中,在默认情况下,接受线程和处理线程都是守护线程,这意味着当tomcat收到shutdown令后并且没有其他用户线存在的情况下tomcat进程会马上消亡,而不会等待处理线程处理完当前的请求。

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

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值