Java 并发——多线程基础
Thead类与Runnable接口
Java的线程,即一个Thread实例。
Java的线程执行过程有两种实现方式:
- 子类继承Thread类,并且重写void run()方法。
- 自定义类实现Runnable接口,并且实现void run()方法。并在Thead构造时,将Runnable实例放入Thead。
Thread类
创建一个新线程必须实例化一个Thread对象。
使用方法:
- 子类继承Thread类。重写Thread的run()方法。
- 实例化该子类。
- 执行Thread的start()方法启动一个线程。
例:Thread使用方法
/** * Thread类。 */ class MyThread extends Thread { public void run() { //... 线程执行 } } public class TestThread1 { public static void main(String args[]) { MyThread thread = new MyThread (); //新建线程。 thread .start(); //线程开始执行。 //主线程其他方法 } }
Runnable接口
实现Runnable接口需要实现run()方法.run()方法代表线程需要完成的任务.因此把run方法称为线程执行体.
使用方法:
- 类实现Runnable接口,并且实现run()方法。
- 实例化该类。Runnable runnable=new MyRun();
- 把该类注入到Thread对象中,即通过Thread的构造方法注入。Thread thread=new Thread(runnable);
- 调用Thread实例的start()方法。启动线程。thread.start();
例:Runnable使用方法
class MyRunner implements Runnable { //实现Runnable接口 public void run() { // ...... 线程执行 } } public class TestThread1 { public static void main(String args[]) { MyRunner runner= new Runner1(); Thread t = new Thread(runner); //新建线程。 t.start(); //线程开始执行。 // ...... 主线程继续执行 } }
两种方式所创建线程的对比
实现Runnable接口方式的多线程:
- 编程稍复杂。
- 如果需要访问当前线程,必须使用Thread.currentThread()方法。
- 线程只是实现了Runnable接口,还可以继承其他类。
- 在这种方式下,可以多个线程共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU,代码,数据分开,形成清晰的模型,较好地体现了面向对象的思想。
继承Thread类方式的多线程:
- 编程简单。
- 如果需要访问当前线程直接使用this。
- 已经继承了Thread类,无法再继承其他父类。
Thread的常用API
1 Thread类的构造方法
- Thread()
- Thread(Runnable target)
- Thread(Runnable target, String name)
- Thread(String name)
- Thread(ThreadGroup group, Runnable target)
- Thread(ThreadGroup group, Runnable target, String name)
- Thread(ThreadGroup group, Runnable target, String name, long stackSize)
- Thread(ThreadGroup group, String name)
2常用方法
(1) 启动线程
- void run() :如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。
- void start() :使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
(2) 线程状态控制
- boolean isAlive() 测试线程是否处于活动状态。
- int getPriority() 返回线程的优先级。
- void setPriority(int newPriority) 更改线程的优先级。
- static void sleep(long millis)
- static void sleep(long millis, int nanos) 当前线程暂停运行指定的毫秒数(加指定的纳秒数),但此线程不失去已获得的锁旗标.
- void join() 等待该线程终止。
- void join(long millis) 等待该线程终止的时间最长为 millis 毫秒。
- void join(long millis, int nanos) 等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。
- void interrupt() 中断线程。 调用该方法引发线程抛出InterruptedException异常。
- static boolean interrupted() 测试当前线程是否已经中断。
- boolean isInterrupted() 测试线程是否已经中断。
- static void yield() 暂停当前正在执行的线程对象,并执行其他线程。
- Thread.State getState() 返回该线程的状态。
(3) 当前线程
- static Thread currentThread() : 返回对当前正在执行的线程对象的引用。
- String getName() : 返回该线程的名称.
- void setName(String name) : 设置线程名称.
- void setDaemon(boolean on) 将该线程标记为守护线程或用户线程。
- boolean isDaemon() :测试该线程是否为守护线程。
- String toString() : 返回该线程的字符串表示形式,包括线程名称 优先级和线程组。
- long getId() :返回该线程的标识符。
- void checkAccess() :判定当前运行的线程是否有权修改该线程。
- ClassLoader getContextClassLoader() :返回该线程的上下文 ClassLoader。
- static Thread.UncaughtExceptionHandler getDefaultUncaughtExceptionHandler() :返回线程由于未捕获到异常而突然终止时调用的默认处理程序。
- Thread.UncaughtExceptionHandler getUncaughtExceptionHandler() :返回该线程由于未捕获到异常而突然终止时调用 的处理程序。
(4) 线程组
- static int enumerate(Thread[] tarray) :将当前线程的线程组及其子组中的每一个活动线程复制到指定的数组中。
- StackTraceElement[] getStackTrace() :返回一个表示该线程堆栈转储的堆栈跟踪元素数组。
- static int activeCount() :返回当前线程的线程组中活动线程的数目。
- ThreadGroup getThreadGroup() :返回该线程所属的线程组。
(5) 其他
- static Map<Thread,StackTraceElement[]> getAllStackTraces() 返回所有活动线程的堆栈跟踪的一个映射。
线程的生命周期
1 新建和就绪状态
当程序使用new关键字创建了一个线程后,该线程就处于新建状态。JVM为Thread对象分配内存。初始化其成员变量的值。线程对象调用start()方法之后,该线程处于就绪状态。 JVM会为其创建方法调用栈和程序计数器。
就绪状态的线程并没有开始运行,它只是表示该线程可以运行了。JVM的线程调度器调度该线程运行。
注意:
- 调用start()启动线程之后,run()方法才会被看作线程执行体。
- 直接调用run()方法,则run()方法就只是一个普通方法。
2 运行和阻塞状态
就绪状态的线程获得了CPU,开始执行run方法的线程执行体。则该线程处于运行状态。线程在执行过程中可能会被中断,以使其他线程获得执行的机会,线程调度取决于底层平台采用的策略。
现代桌面和服务器操作系统一般都采用抢占式策略。一些小型设备如手机则可能采用协作式调度。 抢占式策略的系统:系统给每个可执行的线程一个小时间段来处理任务;当该时间段用完,系统会剥夺该线程所占有的资源,让其他线程获得执行机会.在选择下一个线程时,系统会考虑线程的优先级.
3 线程进入阻塞状态
- 线程调用sleep方法主动放弃所占用的处理器资源。
- 线程调用了一个阻塞式的IO方法,该方法返回之前,该线程被阻塞。
- 线程试图获得一个同步监视器,但同步监视器正被其他线程所持有。
- 线程在等待某个通知(notify)。
- 线程调用了线程的suspend方法将线程挂起。不过这个方法容易导致死锁,所以程序应该尽量避免使用该方法。
4 阻塞线程重写进入就绪状态
- 调用sleep方法的线程经过了指定的时间。
- 线程调用的阻塞式IO方法已经返回。
- 线程成功地获得了试图取得同步监视器。
- 线程正在等待某个通知时,其他线程发出了一个通知。
- 处于关闭状态的线程被调用了resume恢复方法。
5 线程死亡
线程在以下情况下死亡:
- run()方法执行完成,线程正常结束.
- 线程抛出一个未捕获的Exception或Error.
- 直接调用该线程的stop()方法来结束该线程.该方法容易导致死锁,通常不推荐使用.
Thread对象的isAlive()方法,查看线程是否死亡。
注意:
- 死亡的线程不能再用start()方法重新启动。
- 一个线程的start()方法不能两次调用。
6 线程睡眠:sleep
sleep方法将线程转入阻塞状态。时间到即再转入就绪状态。
线程的操作与特性
1 线程让步: yeild
yeild() 方法是静态方法。该方法使当前线程让出CPU资源,继续参与线程竞争。
2 join线程
public final void join() throws InterruptedException public final synchronized void join(long millis) throws InterruptedException
join()方法表示当前线程等待指定线程的结束,等待的线程结束后继续执行当前线程。
例:join方法示例。
public class JoinMain { public volatile static int i=0; public static class AddThread extends Thread{ @Override public void run() { for(i=0;i<10000000;i++); } } public static void main(String[] args) throws InterruptedException { AddThread at=new AddThread(); at.start(); at.join(); // 当前线程(主线程)等待at线程运行结束。 System.out.println(i); } }
join的本质
查看join方法的源码,可以了解到,join即相当于以下代码:
while ( 指定的线程.isAlive()) { wait(0); }
当前线程会一直调用wait()方法,所以当前线程会一直处于等待状态。当被等待的线程结束(即指定的线程结束),JVM会自动调用notifyAll()方法来通知当前线程wait已经结束。注意,此处不要手动调用thread.notifyAll()方法。(关于join方法,JDK文档有更详细的说明)
3 线程优先级
每个线程都有优先级。优先级高的线程获得较多的执行机会。默认情况下,main线程具有普通优先级。每个线程默认优先级与创建它的父线程具有同样的优先级。
Java提供的优先级范围是1~10。默认优先级为5。
Thread提供静态常量:
- static int MAX_PRIORITY 线程可以具有的最高优先级。 值为10。
- static int MIN_PRIORITY 线程可以具有的最低优先级。值为1。
- static int NORM_PRIORITY 分配给线程的默认优先级。值为5。
注意:
- 不同操作系统的优先级不同.应尽量避免直接为线程指定优先级,而应使用以上三个静态常量类设置优先级.
4 后台线程
运行在后台,用于为用户线程提供服务。又称为“守护线程”。所有用户线程都结束后,后台线程也会结束,JVM退出。
- main方法的主线程是前台线程。
- 前台线程创建的线程默认是前台线程。后台线程创建的线程默认是后台线程。
比较:
- 普通线程:若主线程结束,普通线程不会结束,JVM不会退出。
- 守护线程:若主线程以及所有普通线程都结束,则后台线程会直接死亡,JVM退出。
常用API
- 调用Thread对象的setDeamon(true)方法可以将指定线程设置成后台线程。
- Thread对象的isDeamon()方法用于判断指定线程是否为后台线程。
5 停止线程
若需要停止线程,不推荐使用stop()方法。stop方法强制线程停止,切线程会释放所有monitor。由于stop方法过于粗暴,已经被废弃。
例:现在有两条记录。两条线程的其中一个线程要写对象,另一个线程要读对象。两个线程对对象加锁。
若使用stop方法,可能导致数据一致性的错误。
public class StopThreadTest { static Student stu = new Student(); public static void main(String[] args) throws InterruptedException { Thread writeThread = new Thread(){ @Override public void run() { // 假设writeThread比readThread先拿到stu的锁。 synchronized(stu){ stu.id = 2; // 可能该语句执行完后,主线程中 writeThread.stop(); 开始产生作用。 // 若主线程执行 writeThread.stop();则该线程在此处将强行结束。 java_label_stop: stu.name = "小王"; } } }; Thread readThread = new Thread(){ @Override public void run() { // 假设writeThread比readThread先拿到stu的锁。 synchronized(stu){ // 若在java_label_stop处writeThread被结束,则该语句将打印出 2 ; 小明 。从而导致数据一致性错误 System.out.println(stu.id + ";" + stu.name); } } }; // 主线程设置stu。 stu.id = 1; stu.name = "小明"; // 假设writeThread比readThread先拿到stu的锁。 writeThread.start(); readThread.start(); // 强制停止写线程。 writeThread.stop(); } } class Student{ public int id; public String name; }
6 线程中断
public void Thread.interrupt() // 中断线程。修改线程的中断状态,但线程本身不会有任何响应,依旧运行。 public boolean Thread.isInterrupted() // 判断是否被中断。 public static boolean Thread.interrupted() // 判断是否被中断,并清除当前中断状态
调用线程中断方法只是给线程“打个招呼”,线程不会本身不会做任何相应。
例:调用interrupt()方法,线程依旧运行。
public class InterruptThreadTest { public static void main(String[] args) { Thread t1 = new Thread(){ @Override public void run(){ while(true){ Thread.yield(); } } }; t1.start(); // 启动线程。 t1.interrupt(); // 终端线程,线程会依旧运行。 } }
线程中断方法可以用于结束线程,非常优雅方便。
例:线程外部使用中断方法,结束线程。
public class InterruptStopThreadTest { public static void main(String[] args) { Thread t1 = new Thread() { @Override public void run() { while (true) { // 检测线程被中断 if (Thread.currentThread().isInterrupted()) { // 若线程中断,则推出run方法。 System.out.println("Interruted!"); break; } Thread.yield(); } } }; t1.start(); // 启动线程。 t1.interrupt(); // 终端线程,但线程依旧运行。t1.isInterrupted()方法将返回true。 } }
InterruptedException异常
大部分线程的等待方法都会抛出InterruptedException异常,中断标志位将会被清空。Java方法中默认的等待线程一旦被interrupt(),则等待方法会立即抛出InterruptedException异常。
例:处于sleep的线程被interrupt,立即抛出InterruptedException。
public class InterruptedExceptionTest { public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread() { @Override public void run() { while (true) { if (Thread.currentThread().isInterrupted()) { System.out.println("Interruted!"); break; } try { Thread.sleep(2000); } catch (InterruptedException e) { System.out.println("Interruted When Sleep"); // 抛出异常后会清除中断标记位。所以重新设置中断状态。 Thread.currentThread().interrupt(); } Thread.yield(); } } }; t1.start(); // 让主线程在恰当的时间去中断t1。 Thread.sleep(1000); // 此时t1处于sleep状态。interrupt() 方法会使t1中的sleep方法抛出InterruptedException异常。 t1.interrupt(); } }
打印输出:
Interruted When Sleep
Interruted!
7 suspend与resume
suspend() 方法表示将线程挂起。resume() 方法表示继续执行挂起的线程。
但需要注意suspend()不会释放锁。如果加锁发生在resume()之前,则发生死锁。
这两个方法都已经被废弃,不要使用。
例:运行以下程序,程序将被锁死。
package sjq.thread.suspend_resume; package sjq.thread.suspend_resume; public class SuspendResumeThreadTest { public static Object u = new Object(); public static ChangeObjectThread t1 = new ChangeObjectThread("t1"); public static ChangeObjectThread t2 = new ChangeObjectThread("t2"); public static class ChangeObjectThread extends Thread { public ChangeObjectThread(String name){ super.setName(name); } @Override public void run() { synchronized(u){ System.out.println("in " + super.getName()); Thread.currentThread().suspend(); } } } public static void main(String[] args) throws InterruptedException { t1.start(); Thread.sleep(100); t2.start(); t1.resume(); // 主线程通知t1要释放resume,但t1线程可能还未执行suspend,当t1执行了suspend后,则t1将永远被挂起。且不会释放资源。 t2.resume(); // 主线程通知t2要释放resume,但t2线程可能还未执行suspend,当t2执行了suspend后,则t2将永远被挂起。且不会释放资源。 t1.join(); t2.join(); } }
控制台显示:
该程序通过jstack查看线程情况,发现t2线程处于RUNNABLE状态,被suspend0方法挂起。且t2拥有Object的锁,只要t2线程不结束,Object的锁就不会被释放。