Java 多线程编程
一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
线程和进程关系:一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。
有效利用多线程的关键是理解程序是并发执行而不是串行执行的。例如:程序中有两个子系统需要并发执行,这时候就需要利用多线程编程。
上下文的切换开销也很重要,如果你创建了太多的线程,CPU 花费在上下文的切换的时间将多于执行程序的时间!
状态
- 新建状态:
使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
- 就绪状态:
当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
- 运行状态:
如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
- 阻塞状态:
如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
-
等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
-
同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
-
其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
-
- 死亡状态:
一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
线程的优先级
每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。
Java 线程的优先级是一个整数,其取值范围是 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。
默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。
具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。
创建一个线程
Java 提供了三种创建线程的方法:
- 通过实现 Runnable 接口;
- 通过继承 Thread 类本身;
- 通过 Callable 和 Future 创建线程。
1 class RunnableDemo implements Runnable { 2 private Thread t; 3 private String threadName; 4 5 RunnableDemo( String name) { 6 threadName = name; 7 System.out.println("Creating " + threadName ); 8 } 9 10 public void run() { 11 System.out.println("Running " + threadName ); 12 try { 13 for(int i = 4; i > 0; i--) { 14 System.out.println("Thread: " + threadName + ", " + i); 15 // 让线程睡眠一会 16 Thread.sleep(50); 17 } 18 }catch (InterruptedException e) { 19 System.out.println("Thread " + threadName + " interrupted."); 20 } 21 System.out.println("Thread " + threadName + " exiting."); 22 } 23 24 public void start () { 25 System.out.println("Starting " + threadName ); 26 if (t == null) { 27 t = new Thread (this, threadName); 28 t.start (); 29 } 30 } 31 } 32 33 public class TestThread { 34 35 public static void main(String args[]) { 36 RunnableDemo R1 = new RunnableDemo( "Thread-1"); 37 R1.start(); 38 39 RunnableDemo R2 = new RunnableDemo( "Thread-2"); 40 R2.start(); 41 } 42 }
通过继承Thread来创建线程
创建一个线程的第二种方法是创建一个新的类,该类继承 Thread 类,然后创建一个该类的实例。
继承类必须重写 run() 方法,该方法是新线程的入口点。它也必须调用 start() 方法才能执行。
class ThreadDemo extends Thread { private Thread t; private String threadName; ThreadDemo( String name) { threadName = name; System.out.println("Creating " + threadName ); } public void run() { System.out.println("Running " + threadName ); try { for(int i = 4; i > 0; i--) { System.out.println("Thread: " + threadName + ", " + i); // 让线程睡眠一会 Thread.sleep(50); } }catch (InterruptedException e) { System.out.println("Thread " + threadName + " interrupted."); } System.out.println("Thread " + threadName + " exiting."); } public void start () { System.out.println("Starting " + threadName ); if (t == null) { t = new Thread (this, threadName); t.start (); } } } public class TestThread { public static void main(String args[]) { ThreadDemo T1 = new ThreadDemo( "Thread-1"); T1.start(); ThreadDemo T2 = new ThreadDemo( "Thread-2"); T2.start(); } }
Thread 方法
序号 | 方法描述 |
---|---|
* | Thread 对象调用 |
1 | public void start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。 |
2 |
public void run() 如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。 |
3 |
public final void setName(String name) 改变线程名称,使之与参数 name 相同。 |
4 |
public final void setPriority(int priority) 更改线程的优先级。 |
5 |
public final void setDaemon(boolean on) 将该线程标记为守护线程或用户线程。 |
6 |
public final void join(long millisec) 等待该线程终止的时间最长为 millis 毫秒。 |
7 |
public void interrupt() 中断线程。 |
8 |
public final boolean isAlive() 测试线程是否处于活动状态。 |
* | Thread 类的静态方法 |
9 | public static void sleep(long millisec) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。 |
10 | public static void yield() 暂停当前正在执行的线程对象,并执行其他线程。 |
11 | public static boolean holdsLock(Object x) 当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。 |
12 | public static Thread currentThread() 返回对当前正在执行的线程对象的引用。 |
13 | public static void dumpStack() 将当前线程的堆栈跟踪打印至标准错误流。 |
示例
文件DisplayMessage.java 代码
// 文件名 : DisplayMessage.java // 通过实现 Runnable 接口创建线程 public class DisplayMessage implements Runnable { private String message; public DisplayMessage(String message) { this.message = message; } public void run() { while(true) { System.out.println(message); } }
文件名 : GuessANumber.java
// 文件名 : GuessANumber.java // 通过继承 Thread 类创建线程 public class GuessANumber extends Thread { private int number; public GuessANumber(int number) { this.number = number; } public void run() { int counter = 0; int guess = 0; do { guess = (int) (Math.random() * 100 + 1); System.out.println(this.getName() + " guesses " + guess); counter++; } while(guess != number); System.out.println("** Correct!" + this.getName() + "in" + counter + "guesses.**"); } }
// 文件名 : ThreadClassDemo.java
// 文件名 : ThreadClassDemo.java public class ThreadClassDemo { public static void main(String [] args) { Runnable hello = new DisplayMessage("Hello"); Thread thread1 = new Thread(hello); thread1.setDaemon(true); thread1.setName("hello"); System.out.println("Starting hello thread..."); thread1.start(); Runnable bye = new DisplayMessage("Goodbye"); Thread thread2 = new Thread(bye); thread2.setPriority(Thread.MIN_PRIORITY); thread2.setDaemon(true); System.out.println("Starting goodbye thread..."); thread2.start(); System.out.println("Starting thread3..."); Thread thread3 = new GuessANumber(27); thread3.start(); try { thread3.join(); }catch(InterruptedException e) { System.out.println("Thread interrupted."); } System.out.println("Starting thread4..."); Thread thread4 = new GuessANumber(75); thread4.start(); System.out.println("main() is ending..."); }
运行结果如下,每一次运行的结果都不一样。
Starting hello thread... Starting goodbye thread... Hello Hello Hello Hello Hello Hello Goodbye Goodbye Goodbye Goodbye Goodbye .......
通过 Callable 和 Future 创建线程
-
1. 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。
-
2. 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。
-
3. 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
-
4. 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。
1 public class CallableThreadTest implements Callable<Integer> { 2 public static void main(String[] args) 3 { 4 CallableThreadTest ctt = new CallableThreadTest(); 5 FutureTask<Integer> ft = new FutureTask<>(ctt); 6 for(int i = 0;i < 100;i++) 7 { 8 System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i); 9 if(i==20) 10 { 11 new Thread(ft,"有返回值的线程").start(); 12 } 13 } 14 try 15 { 16 System.out.println("子线程的返回值:"+ft.get()); 17 } catch (InterruptedException e) 18 { 19 e.printStackTrace(); 20 } catch (ExecutionException e) 21 { 22 e.printStackTrace(); 23 } 24 25 } 26 @Override 27 public Integer call() throws Exception 28 { 29 int i = 0; 30 for(;i<100;i++) 31 { 32 System.out.println(Thread.currentThread().getName()+" "+i); 33 } 34 return i; 35 } 36 }
创建线程的三种方式的对比
-
1. 采用实现 Runnable、Callable 接口的方式创建多线程时,线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类。
-
2. 使用继承 Thread 类的方式创建多线程时,编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用 this 即可获得当前线程。
线程的几个主要概念
在多线程编程时,你需要了解以下几个概念:
- 线程同步
- 线程间通信
- 线程死锁
- 线程控制:挂起、停止和恢复
线程同步
先看一段代码
1 //不安全的买票 2 //线程不安全,有负数或者多人买到同一张票 3 public class UnsafeBuyTicket { 4 public static void main(String[] args) { 5 BuyTicket buyTicket = new BuyTicket(); 6 7 new Thread(buyTicket,"苦逼的我").start(); 8 new Thread(buyTicket,"牛逼的你们").start(); 9 new Thread(buyTicket,"可恶的黄牛党").start(); 10 } 11 } 12 13 class BuyTicket implements Runnable{ 14 //票数 15 private int num = 10; 16 private boolean flag = true;//判断是否停止线程 Thread的stop已经废弃不建议使用 17 18 @Override 19 public void run() { 20 //买票 21 while (flag){ 22 buy(); 23 } 24 } 25 26 public void buy(){ 27 //判断是否有票 28 if(num<=0){ 29 flag = false; 30 return ; 31 } 32 33 try { 34 //模拟延时(放大问题的发生性) 35 Thread.sleep(100); 36 } catch (InterruptedException e) { 37 e.printStackTrace(); 38 } 39 //买票 40 System.out.println(Thread.currentThread().getName()+"买到了第"+num--+"票"); 41 42 } 43
解决方法:通过关键字synchronized来设置同步锁 在buy()方法前面添加synchronized关键字来锁定这个方法
public synchronized void buy(){ //判断是否有票 if(num<=0){ flag = false; return ; } try { //模拟延时(放大问题的发生性) Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } //买票 System.out.println(Thread.currentThread().getName()+"买到了第"+num--+"票"); }
或者
1 public void buy(){ 2 synchronized (account){ 3 //判断是否有票 4 if(num<=0){ 5 flag = false; 6 return ; 7 } 8 9 try { 10 //模拟延时(放大问题的发生性) 11 Thread.sleep(100); 12 } catch (InterruptedException e) { 13 e.printStackTrace(); 14 } 15 //买票 16 System.out.println(Thread.currentThread().getName()+"买到了第"+num--+"票"); 17 } 18 19 }
线程死锁
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去,如下图所示
对任何线程来说,如果它们想恢复目标线程,同时又试图使用任何一个锁定的资源,就 会造成死锁。
建议看一下JVM介绍
死锁产生的原因
主要是由以下四个条件造成的:
- 互斥条件:系统要求对所分配的资源进行排他性控制,即在一段时间内某个资源仅为一个进程所占有(比如:打印机,同一时间只能一个人打印)。此时若有其他进程请求该资源,则请求只能等待,直到有资源释放了位置;
- 请求和保持条件:进程已经持有了一个资源,但是又要访问一个新的被其他进程占用的资源那么就会阻塞,并且对自己占用的一个资源保持不放;
- 不剥夺条件:进程对已经获取的资源未使用完之前不能被剥夺,只能使用完之后自己释放。
- 环路等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。
看代码
1 public class ThreadDeadLock { 2 public static void main(String[] args) { 3 Object lockA = new Object(); 4 Object lockB = new Object(); 5 Thread t1 = new Thread(new Runnable() { 6 @Override 7 public void run() { 8 synchronized (lockA) { 9 System.out.println("线程1 获得锁A"); 10 try { 11 Thread.sleep(1000); 12 } catch (InterruptedException e) { 13 e.printStackTrace(); 14 } 15 System.out.println("线程1 等待锁B"); 16 synchronized (lockB) { 17 System.out.println("线程1 获得锁B"); 18 } 19 } 20 } 21 }); 22 t1.start(); 23 Thread t2 = new Thread(new Runnable() { 24 @Override 25 public void run() { 26 synchronized (lockB) { 27 System.out.println("线程2 获得锁B"); 28 try { 29 Thread.sleep(1000); 30 } catch (InterruptedException e) { 31 e.printStackTrace(); 32 } 33 System.out.println("线程2 等待锁A"); 34 synchronized (lockA) { 35 System.out.println("线程2 获得锁A"); 36 } 37 } 38 } 39 }); 40 t2.start(); 41 } 42 } 43
小编嘴笨写的有点多,但绝对保姆教学
代码首先创建了两个资源,并创建了两个线程。从结果输出可以看出,线程调度器先调度了线程 1,也就是把 CPU 资源分配给了线程 A,线程 A 使用 synchronized(lockA) 方法获得到了 lockA 的监视器锁,然后调用 sleep 函数休眠 1s,休眠 1s 是为了保证线程 1 在获取 lockB 对应的锁前让 线程 2 抢占到 CPU,获取到资源 lockB 对象的监视器锁资源,然后调用 sleep 函数休眠 1s。 好了,到了这里线程 1 获取到了 lockA 资源,线程 2 获取到了 lockB 资源。线程 1 休眠结束后会企图获取 lockB 资源,而 lockB 资源被线程 2 所持有,所以线程 1 会阻塞而等待。而同时线程 2 休眠结束后会企图获取 lockA 资源,而 lockA 资源已经被线程 1 所持有,**所以线程 1 和线程 2 就陷入了相互等待的状态,也就产生了死锁. 首先,lockA 和 lockB 都是互斥资源,当线程 1 调用了 synchronized(lockA) 方法获得到 lockA 上的监视器并释放前,线程 2 再调用 synchronized(lockA) 方法尝试获取该资源会被阻塞,只有线程 1 主动释放该锁,线程 2 才能获得,这满足了资源互斥条件。 线程 1 首先通过 synchronized(lockA) 方法获取到 lockA 上的监视器锁资源,然后通过 synchronized(lockB) 方法等待获取 lockB 上的监视器锁资源,这就构成了请求并持有条件。 线程 1 在获取 lockA 上的监视器锁资源后,该线程不会被线程 2 掠夺走,只有线程 1 自己主动释放 lockA 资源时,他才会放弃对该资源的持有权,这构成了资源不可剥夺条件。 线程 1 持有 lockA 资源并等待获取 lockB 资源,而线程 2 只有 lockB 资源并等待获取 lockA 资源,这构成了环路等待条件。所以线程 1 和线程 2 就进入了死锁状态。代码解释
如何避免死锁
要想避免死锁,只需要破坏掉至少一个构造死锁的必要条件即可,而在操作系统中,互斥条件和不可剥夺条件是系统规定的,这也没办法人为更改,而且这两个条件很明显是一个标准的程序应该所具备的特性。所以目前只有请求并持有和环路等待条件是可以被破坏的。
(1)保持加锁顺序:当多个线程都需要加相同的几个锁的时候(例如上述情况一的死锁),按照不同的顺序加锁那么就可能导致死锁产生,所以我们如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。
(2)获取锁添加时限:上述死锁代码情况二就是因为出现了获取锁失败无限等待的情况,如果我们在获取锁的时候进行限时等待,例如wait(1000)或者使用ReentrantLock的tryLock(1,TimeUntil.SECONDS)这样在指定时间内获取锁失败就不等待;
(3)进行死锁检测:我们可以通过一些手段检查代码并预防其出现死锁。
造成死锁的原因其实和申请资源的顺序有很大关系 使用资源申请的有序性原则就可以避免死锁,那么什么是资源申请的有序性呢?我们对上面线程 2 的代码进行如下修改。
看代码
// 创建线程 2 Thread t2 = new Thread(new Runnable() { @Override public void run() { synchronized (lockA) { System.out.println("线程2 获得锁A"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程2 等待锁B"); synchronized (lockB) { System.out.println("线程2 获得锁B"); } } } }); t2.start(); }
如上代码让在线程 2 中获取资源的顺序和在线程 1 中获取资源的顺序保持一致,其实资源分配有序性就是指,假如线程 1 和线程 2 都需要资源 1, 2, 3, … . , ,对资源进行排序,线程 1 和线程 2 有在获取了资源 n-1 时才能去获取资源 n。 我们可以简单分析一下为何资源的有序分配会避免死锁,比如上面的代码,假如线程1 和线程 2 同时执行到了 synchronized(lockA),只有1个线程可以获取到 lockA 上的监视器锁,假如线程 1 获取到了,那么线程 2 就会被阻塞而不会再去获取资源 B,线程 1 获取 lockA 的监视器锁后会去申请 lockB 的监视器锁资源,这时候线程 1 是可以获取到的,线程 1 获取到 lockB 资源并使用后会放弃对资源 lockB 的持有,然后再释放对 lockA 的持有,释放 lockA 后线程 2 才会被从阻塞状态变为激活状态。所以资源的有序性破坏了资源的请求并持有条件和环路等待条件,因此避免了死锁。代码解释
死锁的检测
Java中死锁检测手段最多的就是使用JDK带有的jstack和JConsole工具了。下面我们以jstack为例来进行死锁的检测;
使用jps查看到的进程ID对其进行jstack 进程分析 D:\aa>jstack 4184
Java线程间的通信
1、线程间通信的定义
线程的通信可以被定义为:当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以避免无效的资源争夺。
2、“等待-通知”通信方式的原理
使用“等待-通知”通信方式进行生产者与消费者之间的线程通信是一个非常有效的办法。
在数据区满时,可以让生产者等待,等到下次数据区中可以加入数据时,给生产者发通知,把生产者唤醒。可以在消费者取出一个数据后,由消费者去唤醒等待的生产者。
在数据区空时,可以让消费者等待,等到下次数据区中可以取出数据时,给消费者发通知,把消费者唤醒。可以在生产者加入一个数据后,由生产者去唤醒等待的消费者。
Java语言中“等待-通知”方式的线程间通信使用对象的wait()、notify()两类方法来实现。
等待-通知”通信方式是Java中使用普遍的线程间通信方式,其经典的案例是“生产者-消费者”模式。
3、wait方法和notify方法的原理
使用“等待-通知”通信方式进行生产者与消费者之间的线程通信是一个非常有效的办法。
在数据区满时,可以让生产者等待,等到下次数据区中可以加入数据时,给生产者发通知,把生产者唤醒。可以在消费者取出一个数据后,由消费者去唤醒等待的生产者。
在数据区空时,可以让消费者等待,等到下次数据区中可以取出数据时,给消费者发通知,把消费者唤醒。可以在生产者加入一个数据后,由生产者去唤醒等待的消费者。
Java语言中“等待-通知”方式的线程间通信使用对象的wait()、notify()两类方法来实现。
3、wait方法和notify方法的原理
3.1、对象的wait()方法
对象的wait()方法的主要作用是让当前线程阻塞并等待被唤醒。wait()方法对对象监视器紧密相关,使用wait()方法时一定要放在同步块中。wait()方法的调用方法如下:
Object类中的wait()方法有三个版本:
(1)void wait()
这是一个基础版本,当前线程调用了同步对象locko的wait()实例方法后,将导致当前的线程等待,当前线程进入locko的监视器WaitSet,等待被其他线程唤醒。
(2)void wait(long timeout)
这是一个限时等待版本,导致当前的线程等待,等待被其他线程唤醒,或者指定的时候timeout用完,线程不再等待。
(3)void wait(long timeout, int nanos)
这是一个高精度限时等待版本,其主要作用是更精确地控制等待时间。参数nanos是一个附加的纳秒级别的等待时间,从而实现更加高精度的等待时间控制。
3.2、wait()方法的核心原理
1)当线程调用了locko(某个同步锁对象)的wait()方法后,JVM会将当前线程加入locko监视器的WaitSet(等待集),等待被其他线程唤醒。
2)当前线程会释放locko对象监视器的Owner权限,让其他线程可以抢夺locko对象的监视器。
3)让当前线程等待,其状态编程WAITING。
在线程调用了同步对象locko的wait()方法之后,同步对象locko的监视器内部状态大致如下图所示:
3.3、对象的notify()方法
对象的notify()方法的主要作用是唤醒在等待的线程。notify()方法与对象监视器紧密相关,调用notify()方法时也需要放在同步块中。notify()方法的调用方法如下:
notify()方法有两个版本:
(1)void notify()
notify()方法的主要作用为:locko.notify()调用后,唤醒locko监视器等待集中的第一条等待线程;被唤醒的线程进入EntryList,其状态从WAITING变成BLOCKED。
(2)void notifyAll()
locko.notifyAll()被调用后,唤醒locko监视器等待集中的全部等待线程,所有被唤醒的线程进入EntryList,线程状态从WAITING变成BLOCKED。
3.4、notify()方法的核心原理
1)当线程调用了locko(某个同步锁对象)的notify()方法后,JVM会唤醒locko监视器WaitSet中的第一条等待线程。
2)当线程调用了locko的notifyAll()方法后,JVM会唤醒locko监视器WaitSet中的所有等待线程。
3)等待线程被唤醒后,会从监视器的WaitSet移动到EntryList,线程具备了排队抢夺监视器Owner权利的资格,其状态从WAITING变成BLOCKED。
4)等待线程被唤醒后,会从监视器的WaitSet移动到EntryList,线程具备了排队抢夺监视器Owner权利的资格,其状态从WAITING变成BLOCKED。
在线程调用了同步对象locko的wait()或者notifyAll()方法之后,同步对象locko的监视器内部状态大致如下图所示:
4、wait方法和notify方法的注意事项
1)调用某个同步对象locko的wait()和notify()类型方法前,必须要取得这个锁对象的监视锁,所以wait()和notify()类型方法必须放在synchronized(locko)同步块中,如果没有获得监视锁,JVM就会报IllegalMonitorStateException异常。
2)调用wait()方法时使用while进行条件判断,如果是在某种条件下进行等待,对条件的判断就不能使用if语句做一次性判断,而是使用while循环进行反复判断。只有这样才能在线程被唤醒后继续检查wait的条件,并在条件没有满足的情况下继续等待。
线程挂起、恢复与终止
挂起和恢复线程
suspend()方法容易发生死锁。调用suspend()的时候,目标线程会停下来,但却仍然持有在这之前获得的锁定。此 时,其他任何线程都不能访问锁定的资源,除非被”挂起”的线程恢复运行。对任何线程来说,如果它们想恢复目标线程,同时又试图使用任何一个锁定的资源,就 会造成死锁。
所以不应该使用suspend(),而应在自己的Thread类中置入一个标志,指出线程应该活动还是挂起。若标志指出线程应该挂起,便用wait()命其进入等待状态。若标志指出线程应当恢复,则用一个notify()重新启动线程。
所以不应该使用suspend(),而应在自己的Thread类中置入一个标志,指出线程应该活动还是挂起。若标志指出线程应该挂起,便用wait()命其进入等待状态。若标志指出线程应当恢复,则用一个notify()重新启动线程。
如下
public class AlternateSuspendResume extends Object implements Runnable { private volatile int firstVal; private volatile int secondVal; //增加标志位,用来实现线程的挂起和恢复 private volatile boolean suspended; public boolean areValuesEqual() { return ( firstVal == secondVal ); } public void run() { try { suspended = false; firstVal = 0; secondVal = 0; workMethod(); } catch ( InterruptedException x ) { System.out.println("interrupted while in workMethod()"); } } private void workMethod() throws InterruptedException { int val = 1; while ( true ) { //仅当贤臣挂起时,才运行这行代码 waitWhileSuspended(); stepOne(val); stepTwo(val); val++; //仅当线程挂起时,才运行这行代码 waitWhileSuspended(); Thread.sleep(200); } } private void stepOne(int newVal) throws InterruptedException { firstVal = newVal; Thread.sleep(300); } private void stepTwo(int newVal) { secondVal = newVal; } public void suspendRequest() { suspended = true; } public void resumeRequest() { suspended = false; } private void waitWhileSuspended() throws InterruptedException { //这是一个“繁忙等待”技术的示例。 //它是非等待条件改变的最佳途径,因为它会不断请求处理器周期地执行检查, //更佳的技术是:使用Java的内置“通知-等待”机制 while ( suspended ) { Thread.sleep(200); } } public static void main(String[] args) { AlternateSuspendResume asr = new AlternateSuspendResume(); Thread t = new Thread(asr); t.start(); //休眠1秒,让其他线程有机会获得执行 try { Thread.sleep(1000); } catch ( InterruptedException x ) { } for ( int i = 0; i < 10; i++ ) { asr.suspendRequest(); //让线程有机会注意到挂起请求 //注意:这里休眠时间一定要大于 //stepOne操作对firstVal赋值后的休眠时间,即300ms, //目的是为了防止在执行asr.areValuesEqual()进行比较时, //恰逢stepOne操作执行完,而stepTwo操作还没执行 try { Thread.sleep(350); } catch ( InterruptedException x ) { } System.out.println("dsr.areValuesEqual()=" + asr.areValuesEqual()); asr.resumeRequest(); try { //线程随机休眠0~2秒 Thread.sleep( ( long ) (Math.random() * 2000.0) ); } catch ( InterruptedException x ) { //略 } } System.exit(0); //退出应用程序 } }
终止线程
反对使用stop(),是因为它不安全。(使用 stop() 释放锁将会给数据造成不一致性的结果。如果出现这样的情况,程序处理的数据就有可能遭到破坏,最终导致程序执行的流程错误,一定要特别注意。)它会解除由线程获取的所有锁定,而且如果对象处于一种不连贯状态,那么其他线程能在那种状态下检查和修改它们。interrupt()、sleep()
终止线程的替代方法:同样是使用标志位,通过控制标志位来终止线程。
通常我们很多人认为只要调用interrupt方法就会结束线程,这实际上理解有误,一定要想捕获InterruptedException异常再通过break跳出循环,才能正常结束run方法。
import org.joda.time.DateTime; import java.util.concurrent.TimeUnit; public class ThreadSafe extends Thread { @Override public void run() { //在非阻塞过程中判断中断标志来退出 while (!isInterrupted()) { try { //在阻塞过程中捕获中断异常来退出 TimeUnit.SECONDS.sleep(1); System.out.println(new DateTime().toString("yyyy-MM-dd HH:mm:ss")); } catch (InterruptedException e) { e.printStackTrace(); //捕获到异常后执行break跳出循环 break; } } } public static void main(String[] args) throws InterruptedException { final ThreadSafe safe = new ThreadSafe(); safe.start(); TimeUnit.MILLISECONDS.sleep(500); safe.interrupt(); } }
在调用线程的interrupt方法时,会抛出InterruptedException异常。我们通过在代码中捕获异常,然后通过break跳出状态检测循环,结束这个线程的执行。
- 线程处于阻塞状态。此时,使用isInterrupted方法判断线程的中断标识来退出循环,在调用interrupt方法时,中断标识会被设置为true。此时并不能立刻退出线程,而是需要执行终止前的资源释放操作,等待资源释放完毕后方可安全退出线程。
标签:JAVA,复习,Thread,void,System,线程,println,public From: https://www.cnblogs.com/laogao2333/p/17273109.html