Java高并发学习(一)
初始线程:线程的基本操作
进行java并发设计的第一步,就是必须了解Java虚拟机提供的线程操作的API。比如如何创建并启动线程,如何终止线程,中断线程等。
1.定义线程:
(1).继承Thread方法,形式如下
public static class T1 extends Thread{
@Override
public void run(){
System.out.println("fist.T1.run()");
}
}
(2) .实现Runnable接口创建线程方式,形式如下
class MyThread implements Runnable{
@Override
public void run(){
System.out.println("fist.T2.run()");
}
}
public class fist {
public static void main(String args[]){
MyThread mythread = new MyThread();
Thread t1 = new Thread(mythread);
t1.start();
}
}
说明:Thread类有一个非常重要的构造方法public Thread(Runnable target),他传入一个Runnable实例,在start()方法调用时,新的线程会执行Runnable.run()方法。
实际上,默认的Thread.run()就是这么做的:
Public void run(){
if(target != null){
Target.run();
}
}
2.启动线程:
启动线程很简单。只需要使用new关键字创建一个线程对象,并且将它start()起来即可。
Thread t1 = new Thread();
t1.start();
注意一下:
下面的代码能通过编译,也能正常执行。但是,却不能创建一个新的线程,而是在当前线程中调用run()方法,只是作为一个普通方法去调用。
Thread t1 = new Thread();
t1.run();
因此希望大家特别注意,调用start()方法和run()方法的区别。
3.终止线程:
如何正常的关闭一个线程呢?查阅JDK,不难发现Thread提供了一个stop方法。如果使用stop()方法,就可以立即将一个线程终止,非常方便。但是eclipse会提示你stop()方法为废弃方法。也就是说,在将来JDK会删除该方法。
为什么stop()方法会被废弃呢?原因是stop()方法太过暴力,强行把在执行的线程停止,可能会导致数据不一致问题。
那如果需要停止一个线程时,应该怎么做呢?其实方法很简单,只是需要由我们自行决定线程何时退出就可以了。
例如:
class MyThread extends Thread{
volatile boolean stopme = false;
public void stopeMe(){
stopme = true;
}
@Override
public void run(){
while(true){
if(stopme){
System.out.println("exit by stop me");
break;
}
}
}
}
代码中定义了一个标记变量stopme,用于指示线程是否需要退出。当stopMe()方法被调用,stopme被设置为true,此时,线程会检测到这个改动,线程就自然退出了。
4.线程中断:
从表面上理解,中断就是让线程暂停执行的意思,实际上并非如此。在上一节中我们已经讨论了stop()方法停止线程的害处,并且提供了一套完善线程退出功能。那在JDK中是否提供更强大的支持呢?答案是肯定的,那就是线程中断。
与线程中断有关的有三个方法,这三个方法看起来很像,所以可能会引起混淆和误用,请大家注意。
public void Thread.interrupt() //中断线程
Public void boolean Thread.isInterrupted() //判断线程是否被中断
Public static boolean Thread.interrupted() //判断是否被中断,并请除当前中断状态
Thread.interrupt()设置中断标志位,Thread.isInterrupted()检查中断标志位,Thread.interrupted()清除中断标志位。
下面这段代码对t1进行了中断,那么t1中断后,t1会停止执行吗?
class MyThread extends Thread{
@Override
public void run(){
while(true){
System.out.println("Thread running");
}
}
}
public class fist{
public static void main(String args[]){
Thread t1 = new MyThread();
t1.start();
t1.interrupt();
}
}
在这里虽然会t1进行了中断,但是t1并没处理中断的逻辑,因此,即使t1线程被设置了中断,但是这个中断不会发生任何作用。
如果希望t1在中断后退出,就必须为他增加相应的中断处理代码:
class MyThread extends Thread{
@Override
public void run(){
while(true){
System.out.println("Thread running");
if(Thread.currentThread().isInterrupted()){
System.out.println("interrput");
break;
}
}
}
}
public class fist{
public static void main(String args[]) throws InterruptedException{
Thread t1 = new MyThread();
t1.start();
Thread.sleep(2000);
t1.interrupt();
}
}
特别注意,如果在循环体中出现了wait()或者sleep()这样操作的时候,中断可能会被忽略。
Thread.sleep()方法会让当前线程休眠若干时间,他会抛出一个interruptException中断异常。interruptException不是运行时异常,也就是程序必须捕获并处理他。当线程在休眠时,如果被中断,这个异常就会产生。
class MyThread extends Thread{
@Override
public void run(){
while(true){
System.out.println("Thread running");
if(Thread.currentThread().isInterrupted()){
System.out.println("interrput");
break;
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println("when sleep interrupt");
Thread.currentThread().interrupt();
}
System.out.println("Thread end");
}
}
}
public class fist{
public static void main(String args[]) throws InterruptedException{
Thread t1 = new MyThread();
t1.start();
Thread.sleep(1000);
t1.interrupt();
}
}
如果线程运行到了sleep()代码段,主程序中断线程,线程这这时候抛出异常,进入catch的异常处理。在catch代码段中,由于捕获到了中断,我们可以立即退出线程。在这里我们并没有这么做,因为也许在这段代码中,我们还必须进行后续处理,保证数据的一致性和完整性,因此,执行了Thread.interrupt()方法在次中断自己,设置中断标志位。只有这么做才能当线程休眠时响应中断。
注意:Thread.sleep()方法由于中断而抛出异常,此时,它会清除中断标志位,如果不加处理,那么在下一次循环开始前,就无法捕获这个中断,故在异常处理中,在次设置中断标记位。
5.等待(wait)和通知(notify)
为了支持多线程之间的协作,JDK提供了两个非常重要的接口,线程等待wait()方法和线程通知方法notify()。这两个方法不是在Thread类中的,而是Object类的。这也意味着任何对象都能调用者这两种方法。
public final void wait() throws InterruptException
public final native void notify()
那wait()和notify()是怎么工作的呢?如果一个线程调用了object.wait()方法,那么他就会进入object对象的等待队列。这个队列中可能会有多个等待线程,因为系统运行同时等待某个对象。当object.notify()被调用时,他就会从这个等待队列中随机选择一个线程唤醒。这里希望大家注意,这个唤醒过程是不公平的,并不是先等待的线程会优先选择。
除了object.notify()之外还有object.notifyAll()方法,他会唤醒等待队列中的所有线程。
这里还需要注意一点,object.wait()方法和object.notify()方法并不是可以随便调用的。他必须包含在对应的synchronized语句中,无论是wait还是notify都需要首先获得目标对象的一个监视器。
这里给出简单实用wait和notify的案例:
import java.util.Objects;
public class fist{
final static Object object = new Object();
public static class MyThread_1 extends Thread{
@Override
public void run(){
synchronized (object) {
System.out.println(System.currentTimeMillis()+"T1 start");
try {
System.out.println(System.currentTimeMillis()+"T1 wait");
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(System.currentTimeMillis()+"T1 end");
}
}
}
public static class MyThread_2 extends Thread{
@Override
public void run(){
synchronized (object) {
System.out.println(System.currentTimeMillis()+"T2 start and notify");
object.notify();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String args[]){
Thread t1 = new MyThread_1();
Thread t2 = new MyThread_2();
t1.start();
t2.start();
}
}
上述代码中,开启了两个线程t1和t2。t1执行了object.wait()方法。注意,在wait方法执行前,t1申请了object对象锁。因此在执行object.wait()时,他说持有锁的。Wait()方法执行后,t1会进行等待,并且释放object对象的锁。t2在执行notify之前也会获得object对象锁,在notify执行后释放object对象的锁。
输出结果如下:
6.挂起(suspend)和继续执行(resume):
这两个操作是一对相反的操作,被挂起的线程必须等到resume()操作后才能继续执行。但如果仔细阅读文档后会发现,他们早已被标注为废弃方法,不推荐使用。
不推荐使用suspend()去挂起线程的原因,是因为suspend()在导致线程暂停的同时,并不会去释放任何资源锁。此时,任何线程想访问它占用的锁时,都会被牵连,无法正常运行。
如果,resume操作意外的再suspend前就执行了,那么被挂起的线程就很难有机会继续执行了。并且更严重的是他的锁并不会被释放,因此可能会导致整个系统无法正常工作。而且,对于被挂起的线程,从线程状态上看,居然还是runnable,这也会严重的影响到我们对系统当前状态的判断。
import java.util.Objects;
public class fist{
final static Object object = new Object();
public static class MyThread extends Thread{
public MyThread(String name){
super.setName(name);
}
@Override
public void run(){
synchronized (object) {
System.out.println("in "+getName());
Thread.currentThread().suspend();
}
System.out.println("end "+getName());
}
}
public static void main(String args[]) throws InterruptedException{
Thread t1 = new MyThread("t1");
Thread t2 = new MyThread("t2");
t1.start();
Thread.sleep(2000);
t2.start();
t1.resume();
t2.resume();
}
}
输出:
这段代码的程序不会退出。而是会挂起。使用jstack命令打印系统线程信息可以看到。
这时我们需要注意,当前系统中,t2其实是被挂起的。但是他的线程状态是runnable,这很有可能对导致我们误判当前系统的状态。同时,虽然主程序已经调用了resume(),但是由于时间先后顺序的缘故,resume并没有生效!这就导致了线程t2被永久挂起,并且永久占用了对象object的锁。这对于系统来说极有可能是致命的。
没有输出”end t2”的原因是t2.resume()在t2.suspend()前就运行了,导致t2永远被挂起,如果把代码改写成如下,才会输出”end t2”。
import java.util.Objects;
public class fist{
final static Object object = new Object();
public static class MyThread extends Thread{
public MyThread(String name){
super.setName(name);
}
@Override
public void run(){
synchronized (object) {
System.out.println("in "+getName());
Thread.currentThread().suspend();
}
System.out.println("end "+getName());
}
}
public static void main(String args[]) throws InterruptedException{
Thread t1 = new MyThread("t1");
Thread t2 = new MyThread("t2");
t1.start();
Thread.sleep(2000); //设置延迟
t2.start();
t1.resume();
t2.resume();
}
}
输出:
7.等待线程结束(join)和谦让(yield):
很多时候,一个线程的输入可能会非常依赖另一个或多个线程的输出,此时,这个线程就需要等待依赖线程的执行完毕,才能继续执行。JDK提供了join()操作来实现这个功能,如下所示,显示了两个join()方法:
public final void join() throws InterruptedException
public final synchronized void join() throws InterruptedException
第一个join方法表示无限等待,他会一直阻塞线程,直到目标线程执行完毕。第二个给出了一个最大等待时间,如果超过给定时间目标线程还在执行,当前线程也会“等不及了”,而继续往下执行。
这里提供一个简单的join实例,以供参考:
import java.util.Objects;
public class fist{
public volatile static int i = 0;
public static class MyThread extends Thread{
@Override
public void run(){
for(i=0;i<10000000;i++);
}
}
public static void main(String args[]) throws InterruptedException{
Thread t = new MyThread();
t.start();
t.join();
System.out.println(i);
}
}
主函数中,如果不使用join等待MyThread线程,那么得到的i可能是0或者是一个很小的数值。因为MyThread还没开始执行,i的值就被打印出来了。但在使用join方法后,表示主线程愿意等待MytThread线程执行完毕,跟着MyThread一起往前走,故在join返回时,MyThread已经执行完毕,故i总是10000000。
另外一个比较有趣的方法,Thread.yield()。
这是一个静态方法,一旦执行,它会使当前线程让出cpu。但要注意让出cpu不代表不在执行了。当前线程在让出cpu后,还会进行cpu的资源争夺,但是能否被在次分配到,就不一定了。
如果你觉得一个线程不那重要,或者优先级非常低,而且又害怕它会占用太多的cpu资源,那么可以在适当的时候调用yield,给予其他重要线程更多工作机会。