多线程——基础(生命周期、常用方法、优先级等)

8 篇文章 0 订阅
5 篇文章 0 订阅

概述

在Java中,程序的运行可以笼统地分为两种方式:同步异步
假设我们在某个方法里,前后有两个子方法:method1method2,现在想要执行完这两个子方法,按照同步、异步的方式,有以下两种执行过程:

  • 同步:先执行完成method1后,才会执行method2。即想要执行method2的话,必须先执行完成method1,且中途不能出错,否则程序就会终止,也就执行不到method2了。
    对于同步来说,程序就是线性的,根据代码编写的顺序自上而下执行。想要执行某一步,就必须经过前面的代码,无法跳跃。如果其中某一步报错了,后面的也就无法继续。如果前面耗费时间很长,后面的方法也只能是等待,等待前面的执行完。
  • 异步:将method1method2分别作为两个异步方法执行。这时候,这两个方法就是各自独立、互不干扰、可以同时执行的。即想要执行method2时,再也不需要关注method1是否执行完毕,是否发生报错了。
    对于异步,就是指脱离了程序的线性执行,可以在任何需要执行的时候执行的方式。异步的程序,互相独立、各自运行而不互相干扰。

本章讲的多线程,就是java中异步的具体体现。
我们平时说的程序,在系统上的呈现出来的概念就是进程。而线程,就是每个进程中的独立运行的任务。
虽然多线程是异步、可以同时执行的。但是,这里的“同时”,其实只是偷换概念,确切来说是指在用户察觉不到的情况下的“同时”,并不是真正意义上的同一时间进行。
因为在操作系统上,一个cpu同一时间,只能有一个程序在执行。所以多线程的“同时”,其实是这个cpu为每条线程分配时间片,在极短的时间内,根据各自的时间片,不断地进行程序切换运行,所达到的效果。切换的时间足够小,比如10ms可以对10条线程都切换一遍,这样用户就很难觉察得到这之间的时延,就产生了“同时”的感受。

注:
	现在计算机大多都是多核处理器了,也就意味着,多线程的性能得到极大的提升。
	如果计算机的内核数不小于程序的线程数,那每个cpu就不需要为多线程分配时间片,就可以直接单独处理一条线程。
	这样,多个cpu同时运作,每个cpu的线程也在同时运作,就能达到真正意义上的“同时”了。

简而言之,多线程能为我们的程序带来更多的创造性,是我们在开发中必不可少的一个技能。

平时对多线程这一块做了一些积累,现在放上来,分享、回顾、互相学习。

1、多线程的4种创建方式

  1. 继承Thread类,重写run()方法,调用start()方法开启线程。
		//1、继承Thread类,重写 run() 方法
		public class MyThread extends Thread{
			public void run(){
				//线程执行的内容
				System.out.println("mythread run");
			}
		}
		
		//2、在需要的地方调用 start() 方法执行
		MyThread t =new MyThread(); 
        t.start();
  1. 实现Runnable接口并编写run()方法。
		//1、实现 Runnable 接口,写 run() 方法
		public class MyRunnable implements Runnable{
			public void run(){
				//线程执行的内容
				System.out.println("myrunnable run");
			}
		}

		//2、新建Thread的时候,用runnable实现类来构建Thread
		Thread t = new Thread(runnable);
        t.start();
  1. 实现Callable接口并编写run()方法。
    CallableRunnable的不同之处是,有返回值。
    返回值为Future对象,在Future对象上调用get方法就可以获取Callable任务返回的Object了。
		//1、实现 Callable 接口,写 call() 方法
		public class MyCallable implements Callable {	
			public String call() {
				String str = "mycallable call";
				return str;
			}
		}
		
		//2、用Callable对象,构建FutureTask
    	FutureTask<String> futureTask = new FutureTask<String>(new MyCallable());
    	
    	//3、用FutureTask对象,构建Thread,并开启线程
        new Thread(futureTask).start();
        
		//4、等线程执行完成后,获取结果
		String str = futureTask.get();
  1. 使用线程池创建。
    后续介绍

2、线程锁机制简介

为了使后文通畅,更易理解,先简单介绍下锁机制,后续再补充完整这一块。
多线程在一般情况下,是异步运行,互不干扰的。但有时候线程会使用到公共的资源,由于线程又是不同步的,多个线程使用了同个资源,势必就会造成资源的不同步。即:A线程和B线程获取了同一个资源,此时A线程对资源进行写操作,B线程进行读操作。等A线程写操作完成,B线程读到的还是一开始获取到的资源,并不是A线程更新后的新的资源,这就造成了脏读。
类似的情况还有很多,只要是对共享内容有操作(如对公共资源的读写),多线程的异步就会出现问题。
因为共享内容只有一份,而多线程又是各自独立异步的。所以要解决问题,就需要加入一定的机制。使多线程在操作共享内容的时候是同步的,其他时候是异步的。
所以锁机制就产生了。
为每个共享资源对象配置一个锁对象。想访问它的话,必须先取得锁,访问完成后,必须释放锁。
由于每个资源对应的锁只有一个。这样,每个线程在访问这个资源对象的时候,同一时间就只有一个线程能获取到锁访问到资源,而其他线程必须等待这个锁释放出来后,才能争取到这个锁,才能访问到这个资源。
这样就可以控制同一时间访问共享资源的线程只有一个,保证了资源的操作是同步的。
一个简单的使用锁的例子,加深下印象:

	try {
		synchronized(classA){//1、获取锁 classA对象
			//2、执行逻辑。。。
			//3、执行完成后,自动释放锁 classA对象
		}
	} catch (Exception e) {
	}

3、多线程的生命周期

线程创建并启动后,并不是马上就开始执行。
就如前面说的,一个cpu同一时间只能处理一个线程。所以线程启动后,需要等待cpu分配时间给他,线程才能执行。这其中有多个线程,就需要排队、等待分配时间执行、线程之间的切换,等等。
整理一下以上的逻辑,就有了线程的生命周期。
在一个完整的线程生命周期里,需要经过:新建—>就绪—>运行—>阻塞—>死亡 一共5个状态。

  1. 新建:就是new了一个Thread对象,此时还未执行start()方法.
  2. 就绪:当Thread对象执行了start()方法后,线程就处于了就绪状态。即告诉 JVM “我已经准备好了,随时可以来分配时间片给我,让我执行”。此时 JVM 就会为它创建一些基本的调用前的准备,如创建方法调用栈、程序计数器等,等待调用运。
  3. 运行:处于就绪状态的线程分配到了cpu的时间片,得到了cpu的调用,开始运行其run()方法(或call()方法),此时就是处于了运行状态。
  4. 阻塞:当线程对象遇到了某些情况后,放弃了cpu当前的使用权,退出来让给其他线程对象执行。此时本线程对象就处于了阻塞状态。处于阻塞状态的线程,需要等待某个时机(具体看导致阻塞的原因),重新进入就绪状态,等待cpu的调用,而不是直接恢复执行状态。(就好比在银行窗口办事时,中途有事离开了,回来之后就要重新排队等待叫号了)
    造成阻塞的情况,可以粗略分为三种:
    1. 等待阻塞:调用了该线程的wait()方法,线程进入等待队列。
    2. 同步阻塞:在线程用到某个共享资源,获取其锁的时候,锁被其他线程对象占用,则本线程进入阻塞状态,等待其他线程释放锁,直到获取到锁为止。
    3. 其他阻塞:调用sleep()方法或join()方法,或者发出来I/O请求的时候,线程会处于阻塞状态。
  5. 死亡:在线程执行结束后,就进入了死亡状态。结束的具体情况分为三种:
    1. 正常结束:run()call()方法执行完毕,线程正常结束。(占用的锁会释放)
    2. 异常结束:线程抛出了异常,导致异常结束。
    3. 调用stop:调用了线程的stop()方法,结束了线程的执行。(已过时的方法,线程不安全,不推荐)


线程是JVM级别的,所以只有当JVM退出了,未执行完的线程才会真正退出。
如果是在类似Tomcat容器中运行有多线程的程序,就算关闭了Tomcat,还未执行完的线程依旧是活跃的。

4、常用方法—运行

  • start():启动线程的方法,将线程由新建状态改为就绪状态,正式进入等待队列,等待cpu调用线程执行。
    :线程对象新建完成后,并不会自动开启,需要手动调用start()方法,线程才会正式启动,否则线程对象就仅仅只是一个普通的对象而已。
  • run():线程要执行的具体内容的方法。cpu调用线程,执行的就是线程的run()方法。run()方法执行完毕,线程就结束。
    :不需要手动调用run()方法(手动调用就成了普通方法调用,不是线程了),这是当线程被cpu调用时,cpu要执行的线程内容,由系统自动调用。只需要在run()方法里写上我们需要线程执行的操作即可。

5、常用方法—线程休眠、等待与唤醒、让步

  • sleep()线程休眠Thread类的静态方法,设置指定的时间,然后在这个指定时间内,暂停当前线程的执行,让出cpu给其他线程使用。当指定时间到了之后,线程又自动恢复执行。
    • 调用sleep()的时候,线程的锁不会释放。
    • 只能是休眠当前运行中线程(this.currentThread()),即使是其他线程t调用t.sleep(),也只会休眠当前线程。
  • wait()线程等待Object类的方法,调用后暂停当前线程的执行。可设置暂停时间,也可不设置。
    注:
    • 设置了时间,指定时间后会回到队列排队等待。
    • 不设置时间,需要调用notify()唤醒方法,才能重新回到队列中。
    • wait()是与锁相关的,在调用wait()之前,必须获得该对象的对象级别锁,即只能在同步方法或者同步代码块中使用wait()
    • 调用wait()后,当前线程的对象锁会释放。
    • 调用不同对象的wait(),只会释放该对象自身的锁。譬如:线程A当前有a、b两个不同对象锁。此时调用a.wait()只会释放a的锁,b锁不会被释放。反之,调用b.wait()只会释放b锁,a锁不会释放。
  • notify()线程唤醒Object类的方法,调用后唤醒在该对象上等待的线程,恢复为就绪状态。
    • 调用notify()前,也必须获得该对象的对象锁。
    • 调用notify(),也一样需要在同步方法或同步代码块中调用。
    • 调用notify()后,需要等到同步方法或同步代码块执行完毕了,才发挥notify()唤醒作用。
    • 如果调用notify()的对象上有多个线程在等待,则随机唤醒一个。
    • 为什么说对象上有多个线程在等待,出现这种情况,正如上面说的,调用notify()需要先获取对象锁,而一个锁,可能就有多个线程在等待,所以这里才这么说。同时,也说明,调用了notify()后,恢复的不一定就是前面对应调用了wait()的线程,也可能是其他也在等待这同个对象锁的线程。
    • 调用notify()后唤醒的线程执行完毕释放锁之后,如果还有其他还在等待的线程,那它们是不会接收到通知的,只会继续等待,直到另一个notify(),或notifyAll()被调用。
  • notifyALL()线程唤醒。类似notify(),都是唤醒该对象锁等待中的线程。区别是,notifyAll()唤醒的是在此对象上等待的所有线程。
  • yield()线程让步,即当前线程退出cpu的占用,回到就绪状态,重新排队等待cpu调用。
    • 调用yield()后回到就绪状态,意味着随时可能再次被cpu调用到。所以就可能出现一退出cpu占用后,立马又被cpu调用回去的情况。
    • 调用yield(),不会释放锁。

举个例子,加深印象:

public class TestThreadWaitNotify {
	
	//测试service类
	public static class TestService{
		//测试wait
		public void testWait(Object lock) {
			try {
				synchronized (lock) {
					System.out.println(new Date() + " |  begin wait() ThreadName:"+Thread.currentThread().getName());

					lock.wait();
					
					System.out.println(new Date() + " |  end wait() ThreadName:"+Thread.currentThread().getName());
				}
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		
		//测试notify
		public void testNotify(Object lock) {
			try {
				synchronized (lock) {
					System.out.println(new Date() + " |  begin notify() ThreadName:"+Thread.currentThread().getName());
					
					lock.notify();
					Thread.sleep(3000); // 休眠三秒,以展示需要执行完同步块内容,才真正执行notify唤醒的效果
					
					System.out.println(new Date() + " |  end notify() ThreadName:"+Thread.currentThread().getName());
				}
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		
		//测试notifyAll
		public void testNotifyAll(Object lock) {
			try {
				synchronized (lock) {
					System.out.println(new Date() + " |  begin notifyAll() ThreadName:"+Thread.currentThread().getName());
					
					lock.notifyAll();
					Thread.sleep(3000);// 休眠三秒,以展示需要执行完同步块内容,才真正执行notifyAll唤醒的效果
					
					System.out.println(new Date() + " |  end notifyAll() ThreadName:"+Thread.currentThread().getName());
				}
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

	//调用wait()的线程
	public static class ThreadWait extends Thread{
	
		private Object lock;
		public ThreadWait (Object lock) {
			super();
			this.lock = lock;
		}
		
		public void run() {
			TestService service = new TestService();
			service.testWait(lock);
		}
	 }
 
 	//调用notify()的线程
	public static class ThreadNotify extends Thread{
	
		private Object lock;
		public ThreadNotify (Object lock) {
			super();
			this.lock = lock;
		}
		
		public void run() {
			try {
				sleep(3000); //休眠3秒,为了在wait线程全部调用完再调用到
				TestService service = new TestService();
				service.testNotify(lock);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	 }
	
	//调用notifyAll()的线程
	public static class ThreadNotifyAll extends Thread{
		
		private Object lock;
		public ThreadNotifyAll (Object lock) {
			super();
			this.lock = lock;
		}
		
		public void run() {
			try {
				sleep(8000); //休眠8秒,使其最后才被调用到
				TestService service = new TestService();
				service.testNotifyAll(lock);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

	public static void main(String[] args) {
		//随便新建一个Object作为对象锁
		Object lock = new Object();
		
		//创建四个线程,轮流 获取资源->等待->释放资源
		Thread wait1 = new ThreadWait(lock);
		wait1.setName("wait-1");
		wait1.start();
		Thread wait2 = new ThreadWait(lock);
		wait2.setName("wait-2");
		wait2.start();
		Thread wait3 = new ThreadWait(lock);
		wait3.setName("wait-3");
		wait3.start();
		Thread wait4 = new ThreadWait(lock);
		wait4.setName("wait-4");
		wait4.start();
		
		//唤醒一个等待中的线程
		Thread notify1 = new ThreadNotify(lock);
		notify1.setName("notify-1");
		notify1.start();
		
		//再唤醒一个等待中的线程
		Thread notify2 = new ThreadNotify(lock);
		notify2.setName("notify-2");
		notify2.start();
		
		//唤醒剩余两个等待中的线程
		Thread notify3 = new ThreadNotifyAll(lock);
		notify3.setName("notify-3");
		notify3.start();

	}
}

测试结果为:
Tue May 26 09:26:50 CST 2020 |  begin wait() ThreadName:wait-1
Tue May 26 09:26:50 CST 2020 |  begin wait() ThreadName:wait-2
Tue May 26 09:26:50 CST 2020 |  begin wait() ThreadName:wait-3
Tue May 26 09:26:50 CST 2020 |  begin wait() ThreadName:wait-4
Tue May 26 09:26:53 CST 2020 |  begin notify() ThreadName:notify-1
Tue May 26 09:26:56 CST 2020 |  end notify() ThreadName:notify-1
Tue May 26 09:26:56 CST 2020 |  end wait() ThreadName:wait-1
Tue May 26 09:26:56 CST 2020 |  begin notify() ThreadName:notify-2
Tue May 26 09:26:59 CST 2020 |  end notify() ThreadName:notify-2
Tue May 26 09:26:59 CST 2020 |  end wait() ThreadName:wait-2
Tue May 26 09:26:59 CST 2020 |  begin notifyAll() ThreadName:notify-3
Tue May 26 09:27:02 CST 2020 |  end notifyAll() ThreadName:notify-3
Tue May 26 09:27:02 CST 2020 |  end wait() ThreadName:wait-4
Tue May 26 09:27:02 CST 2020 |  end wait() ThreadName:wait-3


过程解析:
1、由于ThreadNotify、ThreadNotifyAll线程执行了sleep()方法,导致线程休眠,所以首先是4个ThreadWait线程抢占了cpu,先执行;
2、这4个ThreadWait线程,轮流 获取了锁,然后调用了wait()释放了锁,进入等待。
   释放了的锁,又被下一个ThreadWait线程获取到,循环往复,直到最后一个ThreadWait线程(wait-4)调用wait()释放了锁,进入等待队列;
33秒后,所有ThreadNotify线程休眠结束,线程[notify-1] 获取到由线程[wait-4]释放的锁;
4、获取到锁的线程[notify-1]执行完synchronized同步块里面的代码(notify()后休眠了3秒),释放了锁;
5、由线程[notify-1]释放的锁随机唤醒了一个ThreadWait线程(wait-1),线程[wait-1]获取到锁,继续执行剩下的代码。
   此时还有3个ThreadWait线程(wait-2、wait-3、wait-4)还在等待中;
6、上面获取到锁的线程[wait-1]执行完同步块代码,释放了锁。此时第二个ThreadNotify线程(notify-2)获取到锁,执行了notify()方法。
   (因为此时其他三个ThreadWait线程还在等待,ThreadNotifyAll线程(notify-3)还在休眠,所以是ThreadNotify线程(notify-2)获取到锁);
7、由线程[notify-2]释放的锁,又随机唤醒了一个ThreadWait线程(wait-2),线程[wait-2]获取到锁,继续执行剩下的代码。
   此时还有2个ThreadWait线程(wait-3、wait-4)还在等待中;
8、线程[wait-2]执行完同步代码块后释放了锁,锁被线程[notify-3]获取到,执行notifyAll()方法,释放了锁,唤醒所有其他等待中的线程;
9、剩下的两个等待中的ThreadWait线程(wait-3、wait-4),其中线程[wait-4]先获取到锁,执行完其同步代码块,释放锁;
10、最后一个ThreadWait线程(wait-3)获取到锁,执行完同步代码块,释放了锁;
11、所有线程执行完毕,程序结束

6、常用方法—线程中断

  • interrupt()线程中断。参考JDK中的说明,简单的理解就是:interrupt()方法实际上只是通知线程,修改线程的中断状态标识为true,并不会直接中止该线程。具体做什么事情由写代码的人决定(通常我们会结束该线程)。
    • 如果线程在阻塞状态时(调用了wait()sleep()join()等引起阻塞的方法),调用了它的interrupt()方法,则其中断状态将被清除(即置为false),还将收到一个InterruptedException异常。
    • 如果线程被阻塞在一个Selector选择器中,那么通过interrupt()中断它时,线程的中断标记会被设置为true,并且它会立即从选择操作中返回。
    • 中断一个“已终止的线程”不会产生任何操作。
  • isInterrupted():调用指定线程的isInterrupted()方法,可以获取线程的中断状态标志。
  • interrupted():调用静态方法Thread.interrupted(),会先返回当前线程的中断状态,然后再清除当前线程的中断状态(即置为false)。

综上所述,多线程的中断机制,其实就是修改中断标记的机制,相关的有以下三个方法:

  • interrupt(),设置当前中断标记为true
  • isInterrupted(),检测当前的中断标记。
  • interrupted(),检测当前的中断标记,然后重置中断标记为false

举个例子,加深印象:

/**
 * 主类:
 * 1、创建启动线程
 * 2、让线程运行3秒钟
 * 3、中断线程
 **/
public class InterruptTest {
	public static void main(String[] args) {
		//1、创建、启动线程
		Thread1 thread1 = new Thread1();
		thread1.start();
		try {
			//2、挂起主线程3秒,执行thread1
			Thread.sleep(3000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		//3、thread1执行中断
		thread1.interrupt();
		System.out.println("thread1 执行中断");
	}
}

/**
 * 线程类:
 * 调用 isInterrupted() 判断中断状态
 * 根据中断状态,自定义编码结束线程
 **/
public static class Thread1 extends Thread{
	public void run() {
		while (true){
			if(isInterrupted()){
				System.out.println(new Date() + " |  当前线程 isInterrupted()为  " + isInterrupted() + " 结束当前线程");
				break;
			} else {
				System.out.println(new Date() + " |  当前线程 isInterrupted()=为  " + isInterrupted() + " 继续执行当前线程");
			}
		}
	}
 }

测试结果为:
Tue May 26 10:31:34 CST 2020 |  当前线程 isInterrupted()false 继续执行当前线程
...
...
Tue May 26 10:31:35 CST 2020 |  当前线程 isInterrupted()false 继续执行当前线程
Tue May 26 10:31:35 CST 2020 |  当前线程 isInterrupted()true 结束当前线程
thread1 执行中断

过程解析:
1、创建、启动Thread1线程;
2、主线程调用sleep(1000)休眠了1秒,让出cpu,执行Thread1线程;
3、这1秒内,Thread1线程一直在判断其是否中断;
41秒后,随机轮到了主线程执行,主线程调用了thread1.interrupt() 中断thread1线程;
5、随机切换到了thread1线程执行,判断是否中断的时候,确认中断了,程序执行break,线程结束;
6、切换回主线程,调用完最后一句代码,程序结束。

7、常用方法—等待线程结束

场景:线程A的业务逻辑,有一部分需要在线程B执行完成后,才能继续执行。这时候,就需要我们在线程A执行过程中,在需要线程B执行的时候,进行阻塞,切换执行线程B,等待线程B执行完成后,再恢复就绪状态。
根据前面介绍的方法,可以通过wait()notify(),来对线程A调用等待,线程B完成后调用唤醒。但现在介绍一个更简便的方式,就是使用join()

  • join():等待其他线程结束。在当前线程中调用其他线程tjoin()方法(t.join())后,当前线程阻塞,等线程t结束了,当前线程才恢复就绪状态。
    • join()内部是使用wait()来实现的,所以join()会释放锁。
  • join(long):long参数为设定指定的等待时间。
    • 当指定时间后,join()线程还未结束,则还是按指定时间,原来的线程结束阻塞,进入就绪状态,join()线程状态不变。
    • 在指定时间内,join()线程提前结束,则原来的线程提前进入就绪状态。

举个例子:

public class TestThreadJoin {
   	public static class Thread1 extends Thread{
   		public void run() {
   			try {
   				sleep(1000); // 休眠1秒,意在让出cpu,让主线程先执行join
   				
   				System.out.println(new Date() + " | join Thread1 ");
   				sleep(2000);
   				System.out.println(new Date() + " | end Thread1  ");
   			} catch (Exception e) {
   				e.printStackTrace();
   			}
   		}
   	 }
   	
   
   	public static void main(String[] args) {
   		try {
   			System.out.println(new Date() + " | main Thread start ");
   			
   			Thread thread1 = new Thread1();
   			thread1.start();
   			thread1.join();
   			
   			System.out.println(new Date() + " | main Thread end ");
   		} catch (InterruptedException e) {
   			e.printStackTrace();
   		}
   	}
   	
}

测试结果:
Tue May 26 17:59:50 CST 2020 | main Thread start 
Tue May 26 17:59:51 CST 2020 | join Thread1 
Tue May 26 17:59:53 CST 2020 | end Thread1  
Tue May 26 17:59:53 CST 2020 | main Thread end 

执行解析:
1、主线程创建、启动Thread1线程;
2、Thread1线程 run() 里调用 sleep() ,意在让出cpu,由主线程先执行 thread1.join() 方法;
3、主线程执行 thread1.join() 方法,cpu切换到thread1线程;
4、thread1线程执行完毕;
5、cpu切换回主线程,执行余下代码;
6、程序结束。

注:
如果thread1线程不一开始就调用sleep()休眠、让出cpu,则可能在主线程调用join()之前,thread1线程就已经执行,甚至执行完成了。
此时程序不会报错,逻辑还是正确的。
因为调用thread1.join()后,系统检测到thread1正在执行,就只是省去了将thread1切换到运行状态而已。
如果调用join()时,thread1已经执行结束,则马上返回到主线程。
如果调用join()时,thread1还未结束,则按照原本的逻辑继续执行。

8、常用方法—获取、设置

  • currentThread():获取当前运行中的线程,Thread类静态方法,返回当前线程的Thread对象。
  • isAlive():判断线程是否处于活动状态,返回为boolean
  • activeCount():返回当前活跃的线程数量,Thread类静态方法,返回为int
    • 当前活跃的线程,包括了休眠中、等待中的所有线程。
  • getId(): 获取线程唯一标识,返回long
  • getState():获取线程的状态,返回Thread.State(线程状态枚举类)。
  • setName(String):设置线程名。
  • getName():获取线程名,返回String
  • setPriority(int):设置线程优先级。
  • getPriority():获取线程优先级,返回int
  • setDaemon(boolean):设置线程为守护线程。
  • isDaemon():判断线程是否为守护线程,返回boolean

9、线程优先级

  • 线程可以划分优先级,优先级更高的线程可以获得更多的cpu资源,即cpu优先执行优先级更高的线程。
  • 优先级具有随机性。优先级更高的线程优先执行,是包含着随机性的,即不一定完全按照优先级执行完线程。因为优先级更高,也只是获得的cpu时间片更多而已。本质上,还是需要cpu切换时间片执行的。
  • 优先级等级分为1-10,其中10的优先级最高。
    调用方法setPriority()可以设置。
    如果设置范围不在1-10内,会报错。
  • 优先级具有继承性。被启动的线程,拥有与启动它的线程一样的优先级。譬如:线程A启动线程B,则线程B拥有与线程A一样的优先级。
  • 优先级等级差距越大,效果越明显。
    因为等级差距越大,获得的cpu资源差距就越大,cpu调用的频率就差得越多,优先级高的执行的机会就越多,低的执行的机会就越少。

10、守护线程

  • 多线程可以分为两种:用户线程、守护线程。
  • 一般我们创建的都是用户线程。想要创建守护线程,可以在线程start()前,调用setDaemon(true)方法将其设置为守护线程。
  • 守护线程,顾名思义,可以看作是守护其他线程的线程。也因此,当其他线程都结束后,守护线程没有了守护对象,就会自行结束。
    所以,守护线程基本与其他非守护线程一样,只有一点不同,就是当系统中不存在非守护线程的时候,守护线程会自动销毁。
  • 典型的例子就是垃圾回收线程,当程序中有其他线程运行的时候,就会产生垃圾,垃圾回收线程就会一直存在。当程序中没有其他线程在运行了,也就不会产生垃圾,垃圾回收线程也就无垃圾可回收,就会自动结束了。
  • 守护线程的优先级较低。
  • 守护线程的子线程,也是守护线程。(类似于优先级的继承性)

11、线程的状态

根据线程不同的运行情况,会有不同的状态,这些状态存在于Thread.State枚举类中,分别为:

  • NEW:创建完,尚未启动的线程,处于这种状态。
  • RUNNABLE:正在JVM中执行的线程,处于这种状态。
  • BLOCKED:受阻塞并等待锁的线程,处于这种状态。
  • WAITING:无限期地等待另一个线程来执行某一特定操作的线程,处于这种状态。
  • TIMED WAITING:等待另一个线程来执行取决于指定等待时间的操作的线程,处于这种状态。(相比WAITING状态,这里指有指定的等待时间)
  • TERMINATED:已退出的线程,处于这种状态。

线程在实际运行过程中,线程状态的改变主要由JVM的调度和各个线程方法的调用引起,线程的状态切换过程具体如下图:
线程状态的切换

12、线程本地变量

多线程要共享一个变量,可以通过public static来定义变量,这样在每个线程中都可以使用到这个变量(需要特别注意线程安全)。
但如果每个线程要共享一个变量,又要保持各自对该变量的修改,public static就满足不了了,因为它一旦被改动,所以用到它的地方都会随之改变。所以此时就需要用到线程的本地变量了。

  • ThreadLocal:线程本地变量,它可以隔离每个线程的共享变量。即复制变量到每个线程里,每个线程里调用的都是复制后的独立的副本,不会被其他线程影响。
    • get():获取当前值
    • set():设置当前值
    • remove():移除当前值
    • 修改默认值:线程本地变量默认值是null。如果要修改默认值,使得每个使用到这个变量的线程能读取到这个默认值,就需要创建一个类,来继承ThreadLocal,并重写其initialValue()方法,在return上返回默认值。
      如:
		//ThreadLocal中initialVal方法源码
		protected T initialValue() {
	        return null;
	    }
		
		//我们可以新建一个类来继承,并重写initialVal方法,修改默认值
		public class ThreadLocalMyVal extends ThreadLocal{
			protected Object initialValue() {
		        return "新的默认值";
		    }
		}
  • InheritableThreadLocal:父线程传递到子线程的本地变量,是ThreadLocal的子类。如:在主线程创建并启动子线程A,此时主线程调用InheritableThreadLocal进行赋值,子线程A可以通过InheritableThreadLocal获取到父线程赋的值,二者调用的是同一个InheritableThreadLocal。即只对非父子关系的线程进行隔离。

举个例子,加深印象:

//定义共享变量
public class ThreadLocalVal {
	public static ThreadLocal tl = new ThreadLocal();
	public static String s;
}

//创建3个线程,各自在内部调用共享变量,修改值,并打印
public class TestThreadLocalTest {
	public static class Thread1 extends Thread{
		public void run() {
			//修改ThreadLocal值,并打印
			try {
				for(int i = 0; i < 5; i++) {
					System.out.println("ThreadName:" + this.getName() + " --"+ThreadLocalVal.tl.get()+"-ThreadLocal");
					sleep(100);
					ThreadLocalVal.tl.set(this.getName()+"-"+i);
					
					System.out.println("ThreadName:" + this.getName() + " --"+ThreadLocalVal.s+"-String");
					sleep(100);
					ThreadLocalVal.s = this.getName()+"-"+i;
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	 }

	public static void main(String[] args) {
		//创建三个线程,相互抢占cpu,看共享变量情况
		Thread thread1 = new Thread1();
		thread1.setName("t1");
		thread1.start();
		Thread thread2 = new Thread1();
		thread2.setName("t2");
		thread2.start();
		Thread thread3 = new Thread1();
		thread3.setName("t3");
		thread3.start();
	}
}

测试结果:
ThreadName:t3 --null-ThreadLocal
ThreadName:t2 --null-ThreadLocal
ThreadName:t1 --null-ThreadLocal
ThreadName:t2 --null-String
ThreadName:t3 --null-String
ThreadName:t1 --null-String
ThreadName:t3 --t3-0-ThreadLocal
ThreadName:t2 --t2-0-ThreadLocal
ThreadName:t1 --t1-0-ThreadLocal
ThreadName:t3 --t1-0-String
ThreadName:t2 --t1-0-String
ThreadName:t1 --t1-0-String
ThreadName:t3 --t3-1-ThreadLocal
ThreadName:t2 --t2-1-ThreadLocal
ThreadName:t1 --t1-1-ThreadLocal
ThreadName:t2 --t1-1-String
ThreadName:t3 --t1-1-String
ThreadName:t1 --t1-1-String
ThreadName:t3 --t3-2-ThreadLocal
ThreadName:t2 --t2-2-ThreadLocal
ThreadName:t1 --t1-2-ThreadLocal
ThreadName:t3 --t1-2-String
ThreadName:t2 --t1-2-String
ThreadName:t1 --t1-2-String
ThreadName:t2 --t2-3-ThreadLocal
ThreadName:t3 --t3-3-ThreadLocal
ThreadName:t1 --t1-3-ThreadLocal
ThreadName:t3 --t1-3-String
ThreadName:t2 --t1-3-String
ThreadName:t1 --t1-3-String

执行解析:
1、分别开启了3个线程,这时3个线程会抢占cpu,交错执行;
2、每个线程都不断地修改两个共享变量的值;
3、一开始日志的输入都是null,证明ThreadLocal默认值为null;
4、仔细观察日志可以发现,普通共享变量(Sting),3个线程每次打印日志的时候,都是返回一样的值。证明这时3个线程调用的都是同一个变量;
5、线程的本地变量(ThreadLocal),3个线程每次调用的,都是返回与各自线程名对应的值。证明每次线程调用的都是各自的本地变量。

13、并发集合

多线程中,使用共享数据,会引发同步问题。所以就必须加锁。
除了手动加锁,java中还有一些集合类,已经帮我们做好了相应的锁逻辑。
常用的集合中,HashMap非线程安全,HashTable是线程安全,但是HashTable每次是锁住整张表来让线程独占,效率低。
ConcurrentHashMap则不仅是线程安全的,同时还提高了效率。
ConcurrentHashMap的实现原理:
ConcurrentHashMap内部由Segment(段)组成,默认有16个,每个Segment的数据结构类似于HashMap
ConcurrentHashMap加入新的内容或者修改,不需要对整个ConcurrentHashMap加锁,而只需根据内容的hashCode,计算出对应的Segment,对该Segment加锁即可。
这样,在多线程操作的时候,每个线程都需要先获取Segment的锁,就可以保证同步的安全性。
同时,要是每个线程对应的Segment不是同一个,又可以实现真正的并发,就算有些是同个的,也大大降低了多个线程等待同个锁的概率,提高了效率。

14、线程内异常的传递

多线程中的产生异常,可以使用UncaughtExceptionHandler类进行捕获。

  • setUncaughtExceptionHandler():设置指定线程的异常处理器。
  • setDefaultUncaughtExceptionHandler():设置所有线程默认的异常处理器。

设置异常处理器的时候,需要新建类,实现UncaughtExceptionHandler异常处理器接口,实现接口的uncaughtException方法。
举个例子,加深印象:

//让线程报错,调用两种方式来捕获
public class TestThreadError {
	public static class Thread1 extends Thread{
		public void run() {
			//报空指针异常
			String a = null;
			System.out.println(a.toString());
		}
	 }

	public static void main(String[] args) {
		//设置默认的异常捕获器
		Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
			public void uncaughtException(Thread t, Throwable e) {
				System.out.println(t.getName() + " -- Default");
				e.printStackTrace();
			}
		});
		
		Thread1 thread1 = new Thread1();
		thread1.setName("t1");
		//设置线程t1自身的异常捕获器
		thread1.setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
			public void uncaughtException(Thread t, Throwable e) {
				System.out.println(t.getName() + " -- current");
				e.printStackTrace();
			}
		});
		thread1.start();
		
		//不设置捕获器,使用默认的
		Thread1 thread2 = new Thread1();
		thread2.setName("t2");
		thread2.start();
	}
}

测试结果:
t1 -- current
t2 -- Default
java.lang.NullPointerException
	at testApi.TestThreadError$Thread1.run(TestThreadError.java:9)
java.lang.NullPointerException
	at testApi.TestThreadError$Thread1.run(TestThreadError.java:9)

执行解析:
1、先设置了默认异常处理器,然后创建线程[t1]的时候,再给[t1]设置自己的异常处理器,线程[t2]不设置自己的异常处理器;
2、线程[t1]执行,报错,调用了自身的异常处理器捕获了异常;
3、线程[t2]执行,报错,调用了默认的异常处理器捕获了异常;
4、通过以上测试也可以知道,默认先调用线程自身的异常处理器,没有了才调用公共的。

:使用单例的SimpleDateFormat在多线程中极易出现日期转换出错,所以平时使用需谨慎虑多线程情况。
(解决方法可以是各个线程各自创建SimpleDateFormat实例,也可以使用ThreadLocal绑定SimpleDateFormat

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值