主要讲java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述、线程池等等。在这之前,首先让我们来了解下在操作系统中进程和线程的区别:
进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1--n个线程。(进程是资源分配的最小单位)
线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位)
线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。
多进程是指操作系统能同时运行多个任务(程序)。
多线程是指在同一程序中有多个顺序流在执行。
在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个jVM实习在就是在操作系统中启动了一个进程
(一)多线程的实现方式
多线程的实现方式有三种:1、继承Thread类,重写run()方法;2、实现Runnable接口,实现run()方法;3、实现Callable()接口,实现call()方法
一个类如果实现了Runnable接口或者继承了Thread类,那么它就是一个多线程类,如果是要实现多线程,还需要重写run()方法,所以run() 方法是多线程的入口。
但是在启动多线程的时候,不是从run()方法开始的,而是从start()开始的 理由是:当执行多线程的时候,每一个线程会抢占资源,而操作系统会为其分配资源,在start()方法中不仅执行了多线程的代码,除此还调用了一个start0()方法,该方法的声明是native,在Java语言中用一种技术叫做JNI,即JavaNativeInterface,该技术特点是使用Java调用本机操作系统提供的函数,但是有一个缺点是不能离开特定的操作系统,如果线程需要执行,必须有操作系统去分配资源,所以此操作主要是JVM根据不同的操作系统来实现的
如果多线程是通过实现Runnable接口来实现的,那么与通过继承Thread来实现有一个区别,那就是多线程的启动方式——必须是通过start()来启动,但是Runnable接口只有一个方法,并没有start()方法,所以在启动多线程的时候必须调用Thread类的一个构造方法——Thread(Runnable target),该构造方法得到了Runnable接口的一个实现,于是就可以调用Thread类的start()方法了。
多线程的两种实现方式的区别:
1.Thread是Runnable接口的子类,实现Runnable接口的方式解决了Java单继承的局限
2.Runnable接口实现多线程比继承Thread类更加能描述数据共享的概念
3、线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类
注意:通过实现Runnable接口解决了Java单继承的局限,所以不管其他的区别联系是什么,这一点就决定了多线程最好是通过实现Runnable接口的方式
1、继承Thread类实现多线程
继承Thread类的方法尽管被我列为一种多线程实现方式,但Thread本质上也是实现了Runnable接口的一个实例,它代表一个线程的实例,并且,启动线程的唯一方法就是通过Thread类的start()实例方法。start()方 法是一个native方法,它将启动一个新线程,并执行run()方法。这种方式实现多线程很简单,通过自己的类直接extend Thread,并复写run()方法,就可以启动新线程并执行自己定义的run()方法。例如:
1 public class MyThread extends Thread { 2 public void run() { 3 System.out.println("MyThread.run()"); 4 } 5 }
在合适的地方启动线程:
1 MyThread myThread1 = new MyThread(); 2 MyThread myThread2 = new MyThread(); 3 myThread1.start(); 4 myThread2.start();
2、实现Runnable接口,实现多线程
如果自己的类已经extends另一个类,就无法直接extends Thread,此时,必须实现一个Runnable接口,如下:
1 public class MyThread extends OtherClass implements Runnable { 2 public void run() { 3 System.out.println("MyThread.run()"); 4 } 5 }
为了启动MyThread,需要首先实例化一个Thread,并传入自己的MyThread实例:
1 MyThread myThread = new MyThread(); 2 Thread thread = new Thread(myThread); //调用Thread类的一个构造方法——Thread(Runnable target) 3 thread.start();
事实上,当传入一个Runnable target参数给Thread后,Thread的run()方法就会调用target.run(),参考JDK源代码:
1 public void run() { 2 if (target != null) { 3 target.run(); 4 } 5 }
3、使用ExecutorService、Callable、Future实现有返回结果的多线程
ExecutorService、Callable、Future这个对象实际上都是属于Executor框架中的功能类。想要详细了解Executor框架的可以访问http://www.javaeye.com/topic/366591 ,这里面对该框架做了很详细的解释。返回结果的线程是在JDK1.5中引入的新特征,确实很实用,有了这种特征我就不需要再为了得到返回值而大费周折了,而且即便实现了也可能漏洞百出。
可返回值的任务必须实现Callable接口,类似的,无返回值的任务必须Runnable接口。执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了,再结合线程池接口ExecutorService就可以实现传说中有返回结果的多线程了。下面提供了一个完整的有返回结果的多线程测试例子,在JDK1.5下验证过没问题可以直接使用。代码如下:
1 import java.util.concurrent.*; 2 import java.util.Date; 3 import java.util.List; 4 import java.util.ArrayList; 5 6 /** 7 * 有返回值的线程 8 */ 9 @SuppressWarnings("unchecked") 10 public class Test { 11 public static void main(String[] args) throws ExecutionException, 12 InterruptedException { 13 System.out.println("----程序开始运行----"); 14 Date date1 = new Date(); 15 16 int taskSize = 5; 17 // 创建一个线程池 18 ExecutorService pool = Executors.newFixedThreadPool(taskSize); 19 // 创建多个有返回值的任务 20 List<Future> list = new ArrayList<Future>(); 21 for (int i = 0; i < taskSize; i++) { 22 Callable c = new MyCallable(i + " "); 23 // 执行任务并获取Future对象 24 Future f = pool.submit(c); 25 // System.out.println(">>>" + f.get().toString()); 26 list.add(f); 27 } 28 // 关闭线程池 29 pool.shutdown(); 30 31 // 获取所有并发任务的运行结果 32 for (Future f : list) { 33 // 从Future对象上获取任务的返回值,并输出到控制台 34 System.out.println(">>>" + f.get().toString()); 35 } 36 37 Date date2 = new Date(); 38 System.out.println("----程序结束运行----,程序运行时间【" 39 + (date2.getTime() - date1.getTime()) + "毫秒】"); 40 } 41 } 42 43 class MyCallable implements Callable<Object> { 44 private String taskNum; 45 46 MyCallable(String taskNum) { 47 this.taskNum = taskNum; 48 } 49 50 public Object call() throws Exception { 51 System.out.println(">>>" + taskNum + "任务启动"); 52 Date dateTmp1 = new Date(); 53 Thread.sleep(1000); 54 Date dateTmp2 = new Date(); 55 long time = dateTmp2.getTime() - dateTmp1.getTime(); 56 System.out.println(">>>" + taskNum + "任务终止"); 57 return taskNum + "任务返回运行结果,当前任务时间【" + time + "毫秒】"; 58 } 59 }
代码说明:
上述代码中Executors类,提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口。
public static ExecutorService newFixedThreadPool(int nThreads) //创建固定数目线程的线程池。
public static ExecutorService newCachedThreadPool() //创建一个可缓存的线程池,调用execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从 缓存中移除那些已有 60 秒钟未被使用的线程。
public static ExecutorService newSingleThreadExecutor() //创建一个单线程化的Executor。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) //创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。
ExecutoreService提供了submit()方法,传递一个Callable,或Runnable,返回Future。如果Executor后台线程池还没有完成Callable的计算,这调用返回Future对象的get()方法,会阻塞直到计算完成。
(二)线程的状态转换
(三)线程调度
1、调整线程优先级:Java线程有优先级,优先级高的线程会获得较多的运行机会。Java线程的优先级用整数表示,取值范围是1~10,Thread类有以下三个静态常量:
static int MAX_PRIORITY //线程可以具有的最高优先级,取值为10。 static int MIN_PRIORITY //线程可以具有的最低优先级,取值为1。 static int NORM_PRIORITY //分配给线程的默认优先级,取值为5。
单单在概念上理解清楚了还不够,需要在实际的例子中进行测试才能更好的理解。对Object.wait(),Object.notify()的应用最经典的例子,应该是三线程打印ABC的问题了吧,这是一道比较经典的面试题,题目要求如下:
建立三个线程,A线程打印10次A,B线程打印10次B,C线程打印10次C,要求线程同时运行,交替打印10次ABC。这个问题用Object的wait(),notify()就可以很方便的解决。代码如下:
1 /** 2 * wait用法 3 * @author DreamSea 4 * @time 2015.3.9 5 */ 6 package com.multithread.wait; 7 public class MyThreadPrinter2 implements Runnable { 8 9 private String name; 10 private Object prev; 11 private Object self; 12 13 private MyThreadPrinter2(String name, Object prev, Object self) { 14 this.name = name; 15 this.prev = prev; 16 this.self = self; 17 } 18 19 @Override 20 public void run() { 21 int count = 10; 22 while (count > 0) { 23 synchronized (prev) { 24 synchronized (self) { 25 System.out.print(name); 26 count--; 27 28 self.notify(); 29 } 30 try { 31 prev.wait(); 32 } catch (InterruptedException e) { 33 e.printStackTrace(); 34 } 35 } 36 37 } 38 } 39 40 public static void main(String[] args) throws Exception { 41 Object a = new Object(); 42 Object b = new Object(); 43 Object c = new Object(); 44 MyThreadPrinter2 pa = new MyThreadPrinter2("A", c, a); 45 MyThreadPrinter2 pb = new MyThreadPrinter2("B", a, b); 46 MyThreadPrinter2 pc = new MyThreadPrinter2("C", b, c); 47 48 49 new Thread(pa).start(); 50 Thread.sleep(100); //确保按顺序A、B、C执行 51 new Thread(pb).start(); 52 Thread.sleep(100); 53 new Thread(pc).start(); 54 Thread.sleep(100); 55 } 56 }
输出结果:
ABCABCABCABCABCABCABCABCABCABC
先来解释一下其整体思路,从大的方向上来讲,该问题为三线程间的同步唤醒操作,主要的目的就是ThreadA->ThreadB->ThreadC->ThreadA循环执行三个线程。为了控制线程执行的顺序,那么就必须要确定唤醒、等待的顺序,所以每一个线程必须同时持有两个对象锁,才能继续执行。一个对象锁是prev,就是前一个线程所持有的对象锁。还有一个就是自身对象锁。主要的思想就是,为了控制执行的顺序,必须要先持有prev锁,也就前一个线程要释放自身对象锁,再去申请自身对象锁,两者兼备时打印,之后首先调用self.notify()释放自身对象锁,唤醒下一个等待线程,再调用prev.wait()释放prev对象锁,终止当前线程,等待循环结束后再次被唤醒。运行上述代码,可以发现三个线程循环打印ABC,共10次。程序运行的主要过程就是A线程最先运行,持有C,A对象锁,后释放A,C锁,唤醒B。线程B等待A锁,再申请B锁,后打印B,再释放B,A锁,唤醒C,线程C等待B锁,再申请C锁,后打印C,再释放C,B锁,唤醒A。看起来似乎没什么问题,但如果你仔细想一下,就会发现有问题,就是初始条件,三个线程按照A,B,C的顺序来启动,按照前面的思考,A唤醒B,B唤醒C,C再唤醒A。但是这种假设依赖于JVM中线程调度、执行的顺序。
1. Thread类的方法:sleep(),yield()等
Object的方法:wait()和notify()等
2. 每个对象都有一个锁来控制同步访问。Synchronized关键字可以和对象的锁交互,来实现线程的同步。
sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
3. wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
所以sleep()和wait()方法的最大区别是:
sleep()睡眠时,保持对象锁,仍然占有该锁;
而wait()睡眠时,释放对象锁。
但是wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException(但不建议使用该方法)。
(四)常见的线程名词解释
线程类的一些常用方法:
sleep(): 强迫一个线程睡眠N毫秒。
isAlive(): 判断一个线程是否存活。
join(): 等待线程终止。
activeCount(): 程序中活跃的线程数。
enumerate(): 枚举程序中的线程。
currentThread(): 得到当前线程。
isDaemon(): 一个线程是否为守护线程。
setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束)
setName(): 为线程设置一个名称。
wait(): 强迫一个线程等待。
notify(): 通知一个线程继续运行。
setPriority(): 设置一个线程的优先级。
(五)线程同步
1、synchronized关键字的作用域有二种:
1)是某个对象实例内,synchronized aMethod(){}可以防止多个线程同时访问这个对象的synchronized方法(如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法)。这时,不同的对象实例的synchronized方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法;
2)是某个类的范围,synchronized static aStaticMethod{}防止多个线程同时访问这个类中的synchronized static 方法。它可以对类的所有对象实例起作用。
2、除了方法前用synchronized关键字,synchronized关键字还可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。用法是: synchronized(this){/*区块*/},它的作用域是当前对象;
3、synchronized关键字是不能继承的,也就是说,基类的方法synchronized f(){} 在继承类中并不自动是synchronized f(){},而是变成了f(){}。继承类需要你显式的指定它的某个方法为synchronized方法;
Java对多线程的支持与同步机制深受大家的喜爱,似乎看起来使用了synchronized关键字就可以轻松地解决多线程共享数据同步问题。到底如何?――还得对synchronized关键字的作用进行深入了解才可定论。
总的说来,synchronized关键字可以作为函数的修饰符,也可作为函数内的语句,也就是平时说的同步方法和同步语句块。如果再细的分类,synchronized可作用于instance变量、object reference(对象引用)、static函数和class literals(类名称字面常量)身上。
在进一步阐述之前,我们需要明确几点:
A.无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁――而且同步方法很可能还会被其他线程的对象访问。
B.每个对象只有一个锁(lock)与之相关联。
C.实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
(六)线程数据传递
1、通过构造方法传递数据
在创建线程时,必须要建立一个Thread类的或其子类的实例。因此,我们不难想到在调用start方法之前通过线程类的构造方法将数据传入线程。并将传入的数据使用类变量保存起来,以便线程使用(其实就是在run方法中使用)。下面的代码演示了如何通过构造方法来传递数据:
1 2 package mythread; 3 public class MyThread1 extends Thread 4 { 5 private String name; 6 public MyThread1(String name) 7 { 8 this.name = name; 9 } 10 public void run() 11 { 12 System.out.println("hello " + name); 13 } 14 public static void main(String[] args) 15 { 16 Thread thread = new MyThread1("world"); 17 thread.start(); 18 } 19 }
2、通过变量和方法传递数据
向对象中传入数据一般有两次机会,第一次机会是在建立对象时通过构造方法将数据传入,另外一次机会就是在类中定义一系列的public的方法或变量(也可称之为字段)。然后在建立完对象后,通过对象实例逐个赋值。下面的代码是对MyThread1类的改版,使用了一个setName方法来设置 name变量:
1 2 package mythread; 3 public class MyThread2 implements Runnable 4 { 5 private String name; 6 public void setName(String name) 7 { 8 this.name = name; 9 } 10 public void run() 11 { 12 System.out.println("hello " + name); 13 } 14 public static void main(String[] args) 15 { 16 MyThread2 myThread = new MyThread2(); 17 myThread.setName("world"); 18 Thread thread = new Thread(myThread); 19 thread.start(); 20 } 21 }
3、通过回调函数传递数据
上面讨论的两种向线程中传递数据的方法是最常用的。但这两种方法都是main方法中主动将数据传入线程类的。这对于线程来说,是被动接收这些数据的。然而,在有些应用中需要在线程运行的过程中动态地获取数据,如在下面代码的run方法中产生了3个随机数,然后通过Work类的process方法求这三个随机数的和,并通过Data类的value将结果返回。从这个例子可以看出,在返回value之前,必须要得到三个随机数。也就是说,这个 value是无法事先就传入线程类的。
1 2 package mythread; 3 class Data 4 { 5 public int value = 0; 6 } 7 class Work 8 { 9 public void process(Data data, Integer numbers) 10 { 11 for (int n : numbers) 12 { 13 data.value += n; 14 } 15 } 16 } 17 public class MyThread3 extends Thread 18 { 19 private Work work; 20 public MyThread3(Work work) 21 { 22 this.work = work; 23 } 24 public void run() 25 { 26 java.util.Random random = new java.util.Random(); 27 Data data = new Data(); 28 int n1 = random.nextInt(1000); 29 int n2 = random.nextInt(2000); 30 int n3 = random.nextInt(3000); 31 work.process(data, n1, n2, n3); // 使用回调函数 32 System.out.println(String.valueOf(n1) + "+" + String.valueOf(n2) + "+" 33 + String.valueOf(n3) + "=" + data.value); 34 } 35 public static void main(String[] args) 36 { 37 Thread thread = new MyThread3(new Work()); 38 thread.start(); 39 } 40 }