Java多线程基础总结

37 篇文章 0 订阅
4 篇文章 0 订阅

背景

   Java采用多线程方式实现并行计算,当然并行计算也可以采用多进程方式实现,但是进程切换耗费比较高。而且进程间是隔离的,进程间通信机制比较麻烦,最后JVM本身在操作系统中就一个进程,由它再启动一个进程不太合适,所以Java采用多线程方式实现并行计算。

   Java从诞生之初,多线程就围绕的是Runnable接口和Thread类展开的。它的底层采用的是c的p线程方式,而且由于多线程的复杂性,p线程的很多概念知识被延伸到了Java层面,这对Java开发者来说算是一个不幸的消息。但是由于多线程的复杂性,这种延伸不得不有。


1.实例化和启动

1.1传统启动方式

Runnable接口

   Runnable接口是Java被设计在线程体中运行某项任务的接口。通过实现Runnable接口的run方法,在run方法中执行线程代码,实现某项任务。《Java编程思想》的作者曾在书中提到认为Runnable这个接口的命名很不准确,叫Task比较合适。而实现了该接口的类,表示该类可以并行的运行run方法,也就是run的语句执行并不是连续的,很可能在t1时间执行第一条,t3时间执行第二条。这里面也就带来了原子操作、原语、同步、互斥、协同等问题。


Thread类

   Thread是Java中的线程类,通过实例化Thread和调用start方法可以启动线程。Thread是java.lang包下面的一个类,它本身实现了Runnable接口。所以线程的第二种实例化和启动写法可以继承Thread类,并重写Run方法,Threa类的run()方法的源码如下:

public void run() {
         if(target != null) {
             target.run();
         }
}

    Thread有很多重载的构造方法。可以指定线程的名字,Runnable对象,所属线程组等信息。一般只指定Runnable对象就够了。Java线程的基本用法基本上就是对Thread类里面一些方法的用法和注意事项的介绍。

 

2.JDK1.5 后新的实例化和启动方法

   JDK 1.5是Java比较重要的一个版本,有很多改进和提高,其中JDK 1.5就有志于改进Java线程的一些内容,并因此引入了java.util.concurrent包。通过该包下面的Executors及相关类,可以获得一个Java标准的线程池,而不采用其他的线程框架。

      JDK 1.5之后Java目标尽量减少直接操作Thread类,所以用concurrent包后很多时候我们常见到的Thread方法调用,Synchronized关键字都可以省略。


2.1 Executor相关

   Executors有三个主要的static方法,这三个方法采用策略模式,封装了线程池的管理和操作实现,统一返回ExecutorService接口实现对象。

newCachedThreadPool()
该方法会创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。
 
newFixedThreadPool()
创建一个可重用固定线程数的线程池,以共享的LinkedBlockQueue方式来运行这些线程。
 
newSingleThreadExecutor()
创建一个使用单个 worker 线程的 Executor,以LinkedBlockQueue方式来运行该线程。

用法:

ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new MyThread());
 
exec.shutdown();


   调用execute可以将runnable对象加入到线程队列中,executorService的根据自身的实现启动线程运行任务。shutdown方法执行后表示线程池不再接受新的任务。

  

2.2 ThreadFactory

   上面实例化线程池的时候,可以传入一个实现了ThreadFactory接口的对象来自定义线程属性,如批量创建守护线程。ThreadFactory这种工程模式,表明Java线程的设计其实就是完成某些并发任务,而Executor可以执行一组相似的任务,所以这些Thread可以通过一个工厂产出。

ExecutorService exec =Executors.newCachedThreadPool(new ThreadFactory(){
         ThreadnewThread(Runnable r){
         Thread t = new Thread(r);
         t.setDaemon(true);
}
});
for(int i=0;i++;i<5)
exec.execute(new MyThread());
 
exec.shutdown();

 

2.3带返回值的任务:

   多线程(并发计算)的一个好处是,我们可以把耗时的计算放到后台,这样就可以创建有响应不会假死的UI界面。Runnable接口的run方法为void方法,不带有任何返回值,对于一些计算需求不是很方便。JDK 1.5之后,可以实现带有返回值的Thread。

   想要让任务带有返回值,那么我们的类就要实现Callable<V>接口,这是一个泛型接口,可以认为这个接口是Runnable的一个平级兄弟接口。只不过Runnable接口的run方法不能抛出受检查的异常,不带有返回值,但是这个接口可以带有返回值,可以跑出异常。该接口规定了一个泛型方法:

V call() throws Exception;

    可以在这个方法中抛出受检查的异常和返回值。返回值被Future<T>对象包围,这么做可以保证我们在多线程环境下获取到计算完毕的值。因为如果直接返回一个变量的地址,那么我们无法确定这个变量是否被计算完毕或者说call方法是否执行完毕,通过Future可以由JVM来帮我们确保这项工作。并且Future还有更深的用法,比如中断线程池中的单个线程。

 

3.Thread类的一些其他操作

   Thread类还提供了Sleep、setPriority、setDaemon、join等操作,这些种操作都是针对线程的,因此他们都属于Thread里面的方法。而且这些操作不会涉及到线程锁,因此他们的操作一般都是安全的。

 

3.1休眠Sleep

   Thread类的静态方法,让线程休眠一段时间,是一个native方法,单位是毫秒。会抛出InterruptedException异常,需要捕捉,该异常在Thread中断中再解释。比如一般我们都会在Demo中通过Sleep方法模拟一些耗时操作,让线程等待一段时间,让Thread慢下来以便重现一些现象。

 

public class A extends Thread{
    
    public void run(){
     try {
<span style="white-space:pre">	</span>TimeUnit.SECONDS.sleep(2);
     } catch (InterruptedException e) {
     }
    }
  
}

3.2设置优先级,setPriority

   Thread类的对象方法,设置线程优先级,优先级越高的可以被尽可能多的轮询。JDK规定了10个优先级,但是JVM于多数操作系统都不能很好的映射,而且要注意优先级只是一种建议,并不是低优先级的一定在高优先级的Thread完成后才能执行。

   线程的优先级最终是由JVM映射到OS层面实现的,但是不同OS的优先级数不一样,少的比如Windows也就7个而且不固定,Solairs有2的31次方个优先级。所以我们在设置的时候一般设置为Thread类的三个常量MAX_PRIORITY/NORM_PRIORITY/MIN_PRIORITY三个级别就够了。

 

<span style="white-space:pre">	</span>public class B implements Runnable {


<span style="white-space:pre">	</span>public static void main(String[] args) {
<span style="white-space:pre">		</span>ExecutorService exec = Executors
<span style="white-space:pre">				</span>.newCachedThreadPool(new DaemonThreadFactory());
<span style="white-space:pre">		</span>for (int i = 0; i < 10; i++)
<span style="white-space:pre">			</span>exec.execute(new DaemonFromFactory());
<span style="white-space:pre">	</span>}


<span style="white-space:pre">	</span>@Override
<span style="white-space:pre">	</span>public void run() {
<span style="white-space:pre">		</span>Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
<span style="white-space:pre">	</span>}
}

3.3让步yield

   Thread类的静态方法,native方法,表示让出当前CPU时间,但是让步操作仅仅是一个建议,具体会不会让出CPU还要由JVM调度决定。这个方法可以在测试和演示时加快线程切换频率,让一些问题更快的发生。需要注意的是让步操作不会操作锁,也就是说,如果让步操作中发生在同步块儿内,线程让出CPU但是不会让出对象锁。

 

public class C implements Runnable {

	public static void main(String[] args) {
		ExecutorService exec = Executors
				.newCachedThreadPool(new DaemonThreadFactory());
		for (int i = 0; i < 10; i++)
			exec.execute(new DaemonFromFactory());
	}

	@Override
	public void run() {
		Thread.yield();
		synchronized (this) {
			Thread.yield();
		}
	}
}


3.4获取当前线程currentThread

   Thread类的静态方法,native方法,返回当前的线程对象。通过这个方法,可以在线程中获取自身的一些属性,状态等,比如演示或做日志时,经常用Thread.currentThread.getName()输出当前Thread的名字,方便观察。

 

3.5后台线程setDaemon

   Thread对象方法,final方法不可覆盖。后台线程或叫守护线程,是指程序运行的时候后台提供的一种通用服务线程,而且这种线程不是程序不可或缺的一部分,当所有非后台线程结束时,程序终止,它们也就自动被终止了,而且从后台线中程构造启动的线程都默认是后台线程(也就是Thread会继承创建它的Thread的一些属性)

 

public class D implements Runnable {

	public static void main(String[] args) {
		ExecutorService exec = Executors
				.newCachedThreadPool(new DaemonThreadFactory());
		for (int i = 0; i < 10; i++)
			exec.execute(new DaemonFromFactory());
	}

	@Override
	public void run() {
		Thread.currentThread().setDaemon(true);
	}

}

3.6加入join

   Thread对象方法,final方法,而且是同步的,抛出InterruptedExecption异常。join允许让一个线程加入到另一个线程中,但是要注意join的时候是将谁(Thread t1)加入到谁(Threadt2),因为join的意思是将当前线程(t2)挂起,直到目标线程(t1)结束才恢复当前线程(t2)

join有多个重载的方法,调用时可以加上超时参数,单位是毫秒,这样在超时时间内,join总可以返回,而且join可以被打断。

 

public class E extends Thread {

	private double num = 0;

	public static void main(String[] args) {
		E e = new E();
		Thread f = new F(e);
		f.start();
		e.start();
	}

	@Override
	public void run() {
		System.out.println("计算E");
		for (int i = 0; i < 10; i++) {
			num = Math.PI * Math.E + num;
		}
		System.out.println("E计算完成" + num);
	}

	public synchronized double getNum() {
		return num;
	}

}

class F extends Thread {

	private E e = null;

	public F(E e) {
		this.e = e;
	}

	@Override
	public void run() {
		try {
			System.out.println("执行F");
			e.join();
			System.out.println("E执行完毕" + e.getNum());
		} catch (InterruptedException e) {
			System.out.print("被打断");
		}
	}
}

3.7是否存活isAlive

   Thread对象方法,final方法,native方法,通过它可以判断一个Thread是否存活。但是要注意,Thread死亡是从run退出,也就是run执行完毕或执行了return语句。中断并不代表线程死亡,或者即使任务正确处理了中断,Thread应该结束了,但是也不要立即判断Thread对象状态。比如,下面的如果不Sleep,两次输出都会是true。


import java.util.concurrent.TimeUnit;
public classTestAlive {
    public static void main(String[] args) throws InterruptedException {
        TMt = new TM();
        t.start();
        System.out.println(t.isAlive());
        TimeUnit.SECONDS.sleep(1);
        t.interrupt();
        //TimeUnit.SECONDS.sleep(1);
        System.out.println(t.isAlive());
    }
 
}
 
class TM extendsThread {
    public void run() {
        while (true) {
            // System.out.println("存活……");
            if (Thread.interrupted()){
                return;
            }
        }
    }
 
}


 

3.8其他

   其他的Thread方法,Interrupt和isInterrupted会在Thread中断中总结,剩下的stop、resume、suspend等有些是JDK废弃的了,有些是与ThreadGroup有关的。废弃方法知道就行,没必要浪费精力,ThreadGroup是一次失败的尝试,不值得在浪费精力学习了。

 

4.线程组:

   线程组是Java一次不成功的尝试,而且JDK也一直在有意的慢慢的遗忘、淡化它,可以不学习。Java线程组是承诺升级理论的现实写照:“继续错误的代价由别人来承担,而承认错误的代价由自己承担”。因此Java一直没有官方表明线程组是好还是坏。

 

5.异常捕捉:

   线程是很特殊的,因此我们不能捕获从线程中逃逸出来的异常,只能在线程内处理。但是如果代码中抛出了一个RuntimeException,这个报错会抛给控制台。JDK5之后可以使用Executor创建线程池,然后给它添加一个异常处理器,来捕获线程抛出的任何异常。

   PS:不能捕捉异常的意思是,我们在线程代码外,无法用try-catch方式捕获run抛出的任何异常。也就是下面的代码是不对的。

try {
    Threadm =new Thread(new Runnable() {
        public void run() {
            throw newRuntimeException();
        }
    });
    m.start();
} catch (Exceptione) {
    System.out.println("捕捉到了异常");
}


 

Thread.UncaughtExceptionHandler接口

     这个接口是JDK1.5之后出现的接口,Thread的内部接口。把该接口的实例对象设置给线程对象(t.setUncaughtExceptionHandler)或者设置为全局默认的异常处理器(Thread.setDefalutUncaughtException)即可捕捉Thread中抛出的异常。

    PS:这里讨论的异常,是我们的任务代码抛出的异常,不应把线程的中断异常也包括进来。

publicclassUncatchExceptionThread extends Thread {
    publicvoid run() {
        throw newRuntimeException("出错了!");
    }
 
    publicstatic void main(String[]args) {
        UncatchExceptionThreadt =new UncatchExceptionThread();
        Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler(){
            public void uncaughtException(Threadt, Throwablee) {
                System.out.println("发生了异常:" + e.getLocalizedMessage());
            }
        });
        try {
            ExecutorServiceexec = Executors.newCachedThreadPool();
            exec.execute(t);//这种异常更友好!
            // t.start();
        }catch(Exceptione) {
            System.out.println("捕捉了异常" + e);
        }
    }
}

6.TimeUnit

      TimeUnit是JDK 1.5新增的一个枚举类,位于java.util.concurrent包下。该枚举的主要意图是将程序猿从毫秒的“暴政”中解脱出来。在JDK 1.5之前,如果我们想基于时间单位做一些事情,比如Sleep 3秒,那么我们需要计算3等于多少毫秒,虽然这个计算并不复杂,但是有了TimeUnit,我们可以直接调用TimeUnit的SECONDS的Sleep传入long值,TimeUnit的相关方法自动帮我们转换时间单位。

     使用TimeUnit类能让代码更清晰易读,毕竟我们读代码的时间可能比写代码的时间要长。

	@Override
	public void run() {
		try {
			// 传统的时间写法
			long mills = 3 * 1000;// 休眠3秒,一秒等于1000毫秒
			Thread.sleep(mills);
			// 用TimeUnit休眠
			TimeUnit.SECONDS.sleep(3);// 休眠3秒

		} catch (InterruptedException e) {
			System.out.print("被打断");
		}
	}

总结:

      Java的多线程是Java实现并发的基础,多线程编程本身不是什么新鲜的,也不是Java特有的,但是Java语言本身支持多线程,这比C等语言编写多线程代码要容易的多,而且Java本身努力消除OS层面的线程差异。Java的多线程基本语法都很简单,比较困难的是基本概念。如果有计算机操作系统进程调度管理等方面的知识的话这些学习理解起来就比较容易了。

     Java多线程的基础是首先要理解Thread的各个方法、概念,明白Thread的四种状态,状态之间的转换。能够合理的利用Thread类提供的各种方法完成一些事情。理解了Java多线程基础,之后才能更好的研究Java多线程之间的同步、协同机制。对于JDK 1.5提供的concurrent包,先明白基础之后再研究这个包比较妥当。并发专家很多都建议先使用Java传统的做法,等有特殊场景或者需要性能优化的时候再用concurrent包相关的工具替换自己的实现。比较concurrent包里面提供的很多东西都太高级了,而且多线程性能调优本身就应根据实际场景甚至是机器、JVM版本来调试。



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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值