一、Java中线程实现
Java 中实现多线程的代码有三种方式,一种是继承 Thread 类,另一种是实现 Runnable 接口,在JDK1.5之后还有一个 Callable 接口,Runnable 接口方式利于资源共享的处理,Callable 接口的实现方式可以获取线程的返回值。
1. 方法1——继承 Thread 类
Thread 类是在 java.lang 包中定义的。一个类只要继承了 Thread 类就称为多线程操作类。在 Thread 的子类中必须明确覆写 Thread 类中的 run() 方法,此方法为线程主体。线程类定义如下:
class 类名 extends Thread { 属性... 方法... public void run() { 线程主体 } }
启动线程是调用 Thread 类的 start() 方法,而不是 run() 方法。若直接调用 run() 方法就是一个普通方法调用,而不是多线程。并且 start() 方法只能调用一次,因为 start() 方法中有一个调用计数,多次调用会 throw new IllegalThreadStateException() 异常。
例子:
class MyThread extends Thread { private String name; public MyThread(String name) { this.name = name; } public void run() { for (int i = 0; i < 10; i++) { System.out.println("name: " + name + " i=" + i); } } } public class ThreadDemo { public static void main(String args[]) { MyThread mt1 = new MyThread("mt1"); MyThread mt2 = new MyThread("mt2"); //mt1.run(); //简单的方法调用 //mt2.run(); mt1.start(); mt2.start(); //mt2.start(); //触发IllegalThreadStateException异常 } }
2. 方法2——实现 Runnable 接口
Java 中也可以通过实现 Runnable 接口的方式实现多线程,此接口定义为:
public interface Runnable { public void run(); }
使用 Runnable 接口实现多线程的格式:
class 类名 implements Runnable { 属性... 方法... public void run() { 线程主体 } }
Runnable 接口实际上还是依靠 Thread 实现多线程启动的,可以看 Thread 类的定义就知道使用方法了:
public class Thread extends Object implements Runnable { private Runnable target; public Thread(Runnable target, String name) { init(null, target, name, 0); } private void init(ThreadGroup g, Runnable target, String name, long stackSize) { ... this.target = target; ... } public void run() { if (target != null) { target.run(); } } }
如果传了 Runnable 类型的参数,最终执行的就是 Runnable 参数的 run() 方法。
举例1:
class MyThread implements Runnable { private String name; public MyThread(String name) { this.name = name; } public void run() { for (int i = 0; i < 10; i++) { System.out.println("name: " + name + " i=" + i); } } } public class ThreadDemo { public static void main(String args[]) { MyThread mt1 = new MyThread("mt1"); MyThread mt2 = new MyThread("mt2"); Thread t1 = new Thread(mt1); //传腹泻run()方法的Runnable的子类 Thread t2 = new Thread(mt2); t1.start(); t2.start(); } }
通过 Runnable 接口实现多线程比起通过实现 Thread 类实现多线程的优势是便于多个同类对象资源共享时的处理。因为后者的运行的 run() 方法是来自参数对象的,因此多个线程传同一个参数对象的话其属性就只有一份资源。而前者需要定义多个对象然后调用其 run() 方法实现多线程,由于是多个对象,其属性就是多个资源了。开发过程中建议使用 Runnable 接口的实现方式实现多线程。
3. 方法3——利用 Callable 接口
通过 Runnable 接口实现的多线程会出现 run() 方法不能返回操作结果的问题,为了解决此问题,JDK1.5开始提供了一个新的接口 java.util.concurrent.Callable,定义如下:
public interface Callable<V> { public V call() throws Exception; }
call() 方法在执行完后可以返回一个具体类型的数据。但是 Thread 类中没有定义任何构造方法来接收 Callable 接口对象实现对象,这导致多线程的启动又遇到了问题,JDK1.5之后开始提供一个 java.util.concurrent.FutureTask<V> 类来解决这个问题,其定义:
public class FutureTask<V> extends Object implements RunnableFuture<V>
FutureTask 实现了 RunnableFuture 接口,而后者又同时实现了 Future 和 Runnable 接口。如果想要接收线程执行的返回结果,调用 Future 接口中的 get() 方法即可。FutureTask 类常用方法如下:
public FutureTask(Callable<V> callable); //构造函数,接收 Callable 接口对象实例 public FutureTask(Runnable runnable, V result); //接收 Runnable 接口实例,并指定返回结果类型 public V get() throws InterruptedException, ExecutionException; //取得线程的执行结果,由 Future 接口定义
FutureTask 是 Runnable 接口的子类,并且其构造函数可以接收 Callable 实例,因此依然可以利用 Thread 类来实现多线程的启动。若想获取线程执行结果,则利用 Future 接口中的 get() 方法。
例子:
import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; import java.util.concurrent.ExecutionException; class MyThread implements Callable<String> { private int ticket = 5; //@override public String call() throws Exception { for (int i = 0; i < 10; i++) { if (ticket > 0) { System.out.println("ticket left: " + ticket--); } } return "sold out"; } } public class ThreadDemo { public static void main(String args[]) { MyThread mt1 = new MyThread(); MyThread mt2 = new MyThread(); FutureTask<String> task1 = new FutureTask<String>(mt1); FutureTask<String> task2 = new FutureTask<String>(mt2); new Thread(task1).start(); new Thread(task2).start(); try { System.out.println("task1 return: " + task1.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } try { System.out.println("task2 return: " + task2.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }
Runnable 接口是 Java 最早提供也是使用最广泛的,平时建议通过使用 Runnable 接口的方式实现多线程。
二、线程操作相关方法
1. Thread 类中的主要方法
在 Java 实现多线程的程序中,虽然 Thread 类实现了 Runnable 接口,但是操作线程的主要方法并不在 Runnable 接口中,而是在 Thread 类中,下面列出 Thread 类中的主要方法:
public Thread(Runnable target) //构造方法,通过 Runnable 接口子类对象实例化 Thread 对象 public Thread(Runnable target, String name) //构造方法,通过 Runnable 接口子类对象实例化 Thread 对象,并设置子线程名称 public Thread(String name) //构造方法,实例化对象并设置子线程名称 public static Thread currentThread() //返回目前正在执行的线程,静态方法,可以直接 Thread.currentThread()进行调用。 public final String getName() //返回线程名称 public final void setName(String name) //设定线程名称 public final int getPriority() //返回线程优先级 public final void setPriority(int newPriority) //设置线程优先级 public boolean isInterrupted() //判断目前线程是否被中断,如果是返回true,否则返回false public final boolean isAlive() //判断线程是否在活动,如果是返回true,否则返回false public final void join() throws InterruptedException //等待线程死亡 public final synchronized void join(long millis) throws InterruptedException //等待 millis ms后,线程死亡 ###### public void run() //线程函数主体 public static void sleep(long millis) throws InterruptedException //使目前正在执行的线程休眠 millis ms public void start() //开始执行新线程 public String toString() //返回代表线程的字符串 public static void yield() //将目前正在执行的线程暂停,允许其他线程执行 public final void setDaemon(boolean on) //将一个线程设置为后台运行
2. Thread 类中的方法使用
(1) getName/setName
线程名称一般是启动前设置,但是也允许为已经运行的线程设置名字,允许两个 Thread 对象有相同的名字。如果没有设置线程的名字,系统会自动为其分配,格式为 Thread-X,X是数字,从0开始。
class MyThread implements Runnable { public void run() { for (int i = 0; i < 3; i++) { System.out.println(Thread.currentThread().getName() + " running " + i); //为啥直接使用getName()和Thread.getName()都报错 } while(true); } } public class ThreadDemo { public static void main(String args[]) { MyThread mt1 = new MyThread(); new Thread(mt1).start(); MyThread mt2 = new MyThread(); new Thread(mt2).start(); MyThread mt3 = new MyThread(); new Thread(mt3, "mt3").start(); mt1.run(); //直接调用run()也打印出了main,说明main也是一个线程 } } /* # java ThreadDemo Thread-0 running 0 Thread-0 running 1 Thread-0 running 2 mt3 running 0 mt3 running 1 mt3 running 2 main running 0 main running 1 main running 2 Thread-1 running 0 Thread-1 running 1 Thread-1 running 2 */
而直接继承 Thread 类是可以直接调用的,Runnable 接口继承 Thread 类,这里 MyThread 类实现 Runnable 接口,与直接继承 Thread 类有何区别?
class MyThread extends Thread { public void run() { for (int i = 0; i < 100; i++) { try { Thread.sleep(1); } catch(Exception e) {} System.out.println("name: " + getName()); if (i > 50) { while(true); } } } } public class ThreadDemo { public static void main(String args[]) { MyThread mt1 = new MyThread(); mt1.start(); try { Thread.sleep(10); } catch(Exception e) {} mt1.setName("Hello"); } }
这些线程的名字只是 Java 层的,cat /proc/<pid>/task/<tid>/comm 全部显示为java,此例中/proc/<pid>/task/下有17个线程,名字全为java。就算是调用了 setName() 也不会改变 cat 出来的名字。
(2) isAlive()判断线程是否启动
class MyThread implements Runnable { public void run() { System.out.println(Thread.currentThread().getName() + " running"); while(true); } } public class ThreadDemo { public static void main(String args[]) { MyThread mt1 = new MyThread(); Thread t1 = new Thread(mt1); System.out.println("isAlive: " + t1.isAlive()); t1.start(); System.out.println("isAlive: " + t1.isAlive()); } } /* # java ThreadDemo isAlive: false isAlive: true Thread-0 running */
注意,主线程先执行完,但是其它线程不会受到任何影响,也不会随着主线程的结束而结束。和C不同!
(3) sleep() 线程的休眠
class MyThread implements Runnable { public void run() { while(true) { System.out.println(Thread.currentThread().getName() + " running"); } } } public class ThreadDemo { public static void main(String args[]) { MyThread mt1 = new MyThread(); Thread t1 = new Thread(mt1); t1.setDaemon(true); t1.start(); } }
但是没有设置成功,程序执行后直接退出。
(3) setPriority()/getPriority() 线程的优先级
class MyThread implements Runnable { public void run() { for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " running " + i); } while(true); } } public class ThreadDemo { public static void main(String args[]) { MyThread mt1 = new MyThread(); Thread t1 = new Thread(mt1, "MIN_P"); t1.setPriority(Thread.MIN_PRIORITY); t1.start(); System.out.println(t1.getName() + " priority: " + t1.getPriority()); MyThread mt2 = new MyThread(); Thread t2 = new Thread(mt2,"MAX_P"); t2.setPriority(Thread.MAX_PRIORITY); t2.start(); System.out.println(t2.getName() + " priority: " + t2.getPriority()); MyThread mt3 = new MyThread(); Thread t3 = new Thread(mt3, "NOR_P"); t3.setPriority(Thread.NORM_PRIORITY); t3.start(); System.out.println(t3.getName() + " priority: " + t3.getPriority()); } } # java ThreadDemo MIN_P priority: 1 ... MAX_P priority: 10 ... NOR_P priority: 5 ...
top看CPU占用率为300%,的确是三个核被占满了,但是cat /proc/<pid>/task/<tid>/sched,所有线程的优先级还是120,看来又是Java 虚拟机自己封装了优先级,对操作系统是不可见的。
(4) yield()线程礼让
class MyThread extends Thread { public void run() { for (int i = 0; i < 100; i++) { System.out.println(getName() + " running " + i); if (i == 50) { System.out.println(getName() + " yield"); yield(); } } } } public class ThreadDemo { public static void main(String args[]) { MyThread mt1 = new MyThread(); mt1.start(); MyThread mt2 = new MyThread(); mt2.start(); } }
yield() 对应内核的实现机制是将此任务设置为 ignore buddy,只是选中它运行时只跳过一次,若是下次任务切换再次选中,就继续运行了,所以上面测试用例yield()后下次选可能还是选自己。
(5) interrupt()中断线程
class MyThread extends Thread { public void run() { for (int i = 0; i < 100; i++) { System.out.println(getName() + " running " + i); try { sleep(100); } catch(Exception e) { System.out.println(getName() + " get exception and return"); return; } } } } public class ThreadDemo { public static void main(String args[]) { MyThread mt1 = new MyThread(); mt1.start(); try { Thread.sleep(2000); } catch(Exception e) { } mt1.interrupt(); } } /* # java ThreadDemo Thread-0 running 0 ... Thread-0 running 19 Thread-0 get exception and return */
可以看出 interrupt() 就是使自己受到一个异常。若 run() 中没有调用 sleep 并进行 catch 异常,线程是不会响应 interrupt() 调用的,正常执行完毕。
三、线程同步互斥问题
1. synchronized 关键字
临界区可以通过 同步代码块 或 同步方法 两种方式完成。代码块就是使用 {} 括起来的一段代码,根据其位置和声明的不同,又分为普通代码块、构造块、静态块 3种。若代码块上加 synchronized 关键字就称为同步代码块。
同步代码块:
//同步代码块: synchronized(同步对象) { ... } //同步方法: synchronized 返回值类型 方法名(参数列表) { ... }
Java 中定义方法的完整格式:
访问权限{public/default/protected/private}[final][static][synchronized] 返回值类型 方法名称(参数列表)[throws Exception1, Exception2] { 函数体 }
2. 等待与唤醒
Object类是所有类的父类,此类中有以下方法是对多线程进行支持的,notify()只唤醒一个,notifyAll()唤醒所有等待线程。
public final void wait() throws InterruptedException //线程等待 public final void wait(long timeout) throws InterruptedException //线程等待,可指定最长等待时间,单位ms public final void wait(long timeout, int nanos) throws InterruptedException //线程等待,可指定最长等待多少ms和ns public final void notify() //唤醒一个等待线程 public final void notifyAll() //唤醒全部等待线程
3. 一个生产者和消费者的例子
class Info { private static boolean flag = true; //true can produce private String content; public synchronized String get() { if (flag) { try { super.wait(); //Object's, the same as wait() } catch(Exception e) {} } else { flag = true; super.notify(); //Object's, the same as notify() } return this.content; } public synchronized void set(String content) { if (flag) { flag = false; this.content = content; System.out.println("set: " + this.content); super.notify(); } else { try { super.wait(); } catch(Exception e) {} } } } class MyThread implements Runnable { private boolean role; private Info info; public MyThread() { this.info = new Info(); } public void run() { if ("Provider".equals(Thread.currentThread().getName())) { role = true; } else { role = false; } if (role) { for (int i = 0; i < 100; i++) { System.out.println("get: " + info.get()); } } else { for (int i = 0; i < 100; i++) { info.set("I am " + i); } } } } public class ThreadDemo { public static void main(String args[]) { MyThread mt = new MyThread(); new Thread(mt, "Consumer").start(); new Thread(mt, "Provider").start(); } } /* ... set: I am 94 get: I am 94 set: I am 96 get: I am 96 set: I am 98 get: I am 98 //为啥都是偶数,丢一个数据呢? */
首先要保证两个线程共享 Info 实例对象才行,这样使用实现 Runnable 方式来实现线程好一些。
四、线程的生命周期
1. 线程中的 suspend() resume() stop() 方法已经被标记为 @Deprecated 注释,不建议使用。
2. 可以通过自己实现一个 stop() 然后在 run() 调用来实现 stop 线程。