栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Java 基础——线程的创建与使用

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Java 基础——线程的创建与使用

一、线程的创建方式

构造函数

public Thread(ThreadGroup group, Runnable target, String name, long stackSize)

参数讲解:

1)target 为启动此线程时调用其 run 方法的对象,如果为 null ,则调用此线程的 run 方法; 2)name 为线程名称;
3)group 为线程所属的线程组 , 如果是 null 并且有一个安全管理器,则该组由 SecurityManager.getThreadGroup() 决定,如果没有安全管理员或SecurityManager.getThreadGroup() 返回 null ,则该组将设置为当前线程的线程组;
4)stackSize 为线程具有的堆栈大小 ,堆栈大小是虚拟机为该线程的堆栈分配的大致的地址空间字节数,stackSize参数的影响(如果有的话)与平台有关。

1、继承 Thread 类

① 定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
② 创建Thread子类的实例,即创建了线程对象。
③ 调用线程对象的start()方法来启动该线程。

线程特性:

● 线程能被标记为守护线程,也可以是用户线程

● 每个线程均分配一个name,默认为(Thread-自增数字)的组合

● 每个线程都有优先级(1-10),默认为5。高优先级线程优先于低优先级线程执行。

● main所在的线程组为main,构造线程的时候没有显示的指定线程组,线程组默认和父线程一样。

● 当线程中的run()方法代码里面又创建了一个新的线程对象时,新创建的线程优先级和父线程优先级一样.

● 当且仅当父线程为守护线程时,新创建的线程才会是守护线程。

● 当JVM启动时,通常会有唯一的一个非守护线程(这一线程用于调用指定类的main()方法)。

// 定义一个继承Thread类的子类,重写Thread的run()方法
class MyThread extends Thread{
 
	 @Override
	public void run() {
		 doSomething();
	}
 
	private void doSomething() {
		System.out.println("我是一个线程中的方法");
	}
}

public class NewThread {
	public static void main(String[] args) {
		// 创建Thread子类的实例,即创建了线程对象
		MyThread myThread=new MyThread();
		// 调用线程对象的start()方法来启动该线程
		myThread.start();
	}
}

注:在main()方法里创建一个MyThread对象,调用该对象的start()方法,start()方法会通过对系统底层的一系列操作,创建出一个相应的线程,与当前线程并发执行。如果直接调用run()方法,程序将执行完run()方法后才会执行main()方法中后面的代码,这样就是单线程执行而不是多线程并发执行了。

2、实现 Runnable 接口

① 定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
② 创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
③ 调用线程对象的start()方法来启动该线程。

// 定义runnable接口的实现类,并重写该接口的run()方法
class RunnableThread implements Runnable{
 
	@Override
	public void run() {
		doSomeThing();
	}
 
	private void doSomeThing() {
		System.out.println("我是一个线程方法");
	} 
}

public class NewThread {
	public static void main(String[] args) {
		// 创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象
		Runnable runnable=new RunnableThread();
		Thread thread=new Thread(runnable);
		// 调用线程对象的start()方法来启动该线程
		thread.start();
	}
}
3、使用 Callable 和 Future 创建线程

① 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
② 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
③ 使用FutureTask对象作为Thread对象的target创建并启动新线程。
④ 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。

// Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值
class CallableThread implements Callable{
 
	@Override
	public String call() throws Exception {
		doSomeThing();
		// call()方法必须要有返回值
		return "需要返回的值";
	}
 
	private void doSomeThing() {
		System.out.println("我是线程中的方法");
	} 
}

public class NewThread {
	public static void main(String[] args) {
		Callable callable=new CallableThread();
		// 使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值
		FutureTask futureTask=new FutureTask(callable);
		// 使用FutureTask对象作为Thread对象的target创建并启动新线程
		Thread thread=new Thread(futureTask);
		thread.start();
		try {
			// 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
			futureTask.get();
		} catch (InterruptedException | ExecutionException e) {
			e.printStackTrace();
		}
	}
}

以上三种方法的比较

① 采用实现Runnable、Callable接口的方式创见多线程时

● 优势
线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
● 劣势
编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。

② 用继承Thread类的方式创建多线程时

● 优势
编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
● 劣势
线程类已经继承了Thread类,所以不能再继承其他父类。

二、线程的常用方法

线程是一个动态执行的过程,它也有一个从产生到死亡的过程。

1、非静态方法

(1)start() 与 run()

1)start() 的作用是启动一个新线程,新线程会执行相应的run()方法,start()不能被重复调用。
2)run() 就和普通的成员方法一样,可以被重复调用。单独调用run()的话,会在当前线程中执行run(),而并不会启动新线程!

class MyThread extends Thread{  
      public MyThread(String name) {
          super(name);
      }
  
      public void run(){
          System.out.println(Thread.currentThread().getName()+" is running");
      } 
 }; 
 
 
public class Demo {  
     public static void main(String[] args) {  
     
         Thread mythread=new MyThread("mythread");
 
         System.out.println(Thread.currentThread().getName()+" call mythread.run()");
         mythread.run();
 
         System.out.println(Thread.currentThread().getName()+" call mythread.start()");
         mythread.start();
     }  
 }

运行结果:

main call mythread.run()
main is running
main call mythread.start()
mythread is running

可以发现:mythread.run() 是在主线程 main 中调用的,该 run() 方法直接运行在主线程 main 上;mythread.start() 会启动线程 mythread,线程 mythread 启动之后,会调用 run() 方法;此时的 run() 方法是运行在线程 mythread 上。

(2)join()

join() 的作用是等待该线程终止,这里需要理解的就是该线程是指的主线程等待子线程的终止才能执行。
在很多情况下,主线程生成并起动了子线程。如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束。但是如果主线程处理完其他的事务后需要用到子线程的处理结果,那么主线程就需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。

1)不加 join() 案例:

class Thread1 extends Thread{
	private String name;
    public Thread1(String name) {
    	super(name);
       this.name=name;
    }
    
	public void run() {
		System.out.println(Thread.currentThread().getName() + " 线程运行开始!");
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程"+name + "运行 : " + i);
            try {
                sleep((int) Math.random() * 10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + " 线程运行结束!");
	}
}
 
public class Main {
 
	public static void main(String[] args) {
		System.out.println(Thread.currentThread().getName()+"主线程运行开始!");
		Thread1 mTh1=new Thread1("A");
		Thread1 mTh2=new Thread1("B");
		mTh1.start();
		mTh2.start();
		System.out.println(Thread.currentThread().getName()+ "主线程运行结束!");
	}
 
}

运行结果:

可以发现在子线程结束之前,主线程已经结束了。

2)加 join() 案例:

public class Main {
 
	public static void main(String[] args) {
		System.out.println(Thread.currentThread().getName()+"主线程运行开始!");
		Thread1 mTh1=new Thread1("A");
		Thread1 mTh2=new Thread1("B");
		mTh1.start();
		mTh2.start();
		try {
			mTh1.join();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		try {
			mTh2.join();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(Thread.currentThread().getName()+ "主线程运行结束!");
 
	} 
}

运行结果:

可以发现:主线程一定会等子线程都结束了才结束。

(3)setPriority()

更改线程的优先级。
MIN_PRIORITY = 1
NORM_PRIORITY = 5
MAX_PRIORITY = 10

(4)interrupt()

Thread.interrupt() 方法的作用是中断线程,将会设置该线程的中断状态位为 true,中断的结果线程是死亡、还是等待新的任务或是继续运行至下一步,就取决于这个程序本身。线程会不时地检测这个中断标示位,以判断线程是否应该被中断(中断标示值是否为true),它并不像stop方法那样会中断一个正在运行的线程。
interrupt() 方法只是改变中断状态,不会中断一个正在运行的线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出interruptedException的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。这一方法实际完成的是,给受阻塞的线程发出一个中断信号,这样受阻线程检查到中断标识,就得以退出阻塞的状态。

更确切的说,如果线程被 Object.wait, Thread.join 和 Thread.sleep 三种方法之一阻塞,此时调用该线程的 interrupt() 方法,那么该线程将抛出一个 InterruptedException中断异常(该线程必须事先预备好处理此异常),从而提早地终结被阻塞状态。如果线程没有被阻塞,这时调用 interrupt() 将不起作用,直到执行到 wait()、sleep()、join() 时,才马上会抛出 InterruptedException。

2、静态方法



(1)yield()

yield() 用来暂停当前正在执行的线程对象,并执行其他线程。yield() 应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程可以获得运行机会。因此,使用 yield() 的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证 yield() 达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
注意:yield() 不是线程转到等待/睡眠/阻塞状态,而是转到可运行状态。

class ThreadYield extends Thread{
    public ThreadYield(String name) {
        super(name);
    }
 
    @Override
    public void run() {
        for (int i = 1; i <= 50; i++) {
            System.out.println("" + this.getName() + "-----" + i);
            // 当i为30时,该线程就会把CPU时间让掉,让其他或者自己的线程执行(也就是谁先抢到谁执行)
            if (i ==30) {
                Thread.yield();
            }
        }
	
	}
 
	public static void main(String[] args) {
		
		ThreadYield yt1 = new ThreadYield("张三");
    	ThreadYield yt2 = new ThreadYield("李四");
        yt1.start();
        yt2.start();
	}
 
}

运行结果有两种,第一种情况:李四(线程)当执行到30时会CPU时间让掉,这时张三(线程)抢到CPU时间并执行;第二种情况:李四(线程)当执行到30时会CPU时间让掉,这时李四(线程)抢到CPU时间并执行。

(2)sleep() 方法

sleep(long millis):在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。当线程睡眠时,它睡在某个地方,在苏醒之前是不会返回到可运行状态。当睡眠时间到期,则返回到可运行的状态。所以,sleep()方法指定的时间为线程不会运行的最短时间。当线程休眠时间结束后,会返回到可运行状态,注意不是运行状态,如果要到运行状态还需要等待CPU调度执行。
线程的 sleep() 方法应该写在线程的 run() 方法里,就能让对应的线程睡眠。

class Runner1 implements Runnable{
	@Override
	public void run() {		
        try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
        for (int i = 0; i < 3; i++) {
        	System.out.println("Runner1 : " + i);
        }
	}	
}

public class Thread1 {
	public static void main(String[] args) {
		Runner1 r1 = new Runner1();		
		Thread t = new Thread(r1);		
		t.start();
		for (int i = 0; i < 3; i++) {
			System.out.println("main thread :"+i);
		}		
	}
}

结果:

到底是让哪个线程睡眠

sleep() 方法只能让当前线程睡眠。调用某一个线程类的对象 t.sleep(),睡眠的不是 t,而是 t 所在的当前线程。

class Thread1 extends Thread{ 	public void run() {		
            for (int i = 0; i < 3; i++) {
        	System.out.println("Runner1 : " + i);
            } 	
         }	
   }

public class Thread2 { 
	public static void main(String[] args) {

	 	Thread1 t = new Thread1();		 		
		t.start(); 		
		try { 			
			Thread.sleep(5000);    // 此处是类名.sleep()
			System.out.println("当前运行的线程名称: "+ Thread1.currentThread().getName());       		
		} catch(InterruptedException e) { 			
			e.printStackTrace(); 		
			} 		
		for (int i = 0; i < 3; i++) { 			
			System.out.println("main thread :"+i); 		
			}		 	
		}
 }

运行结果如下:

Runner1 : 0 
Runner1 : 1 
Runner1 : 2
---------------------------------  此处睡眠5秒,5秒后出现以下: 
当前运行的线程名称: main 
main thread :0 
main thread :1 
main thread :2 

代码验证:在 Thread1 的 run() 中不写 sleep(),在主线程中写 Thread1.sleep(5000),结果不是
Thread1 睡眠,还是主线程睡眠。

sleep() 和 yield() 的区别:

sleep() 使当前线程进入停滞状态,所以执行 sleep() 的线程在指定的时间内肯定不会被执行;

yield() 只是使当前线程重新回到可执行状态,所以执行 yield() 的线程有可能在进入到可执行状态后马上又被执行。

实际上,yield() 方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把CPU的占有权交给此线程,否则,继续运行原来的线程。 所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程。

另外,sleep 方法允许较低优先级的线程获得运行机会,但 yield() 方法执行时,当前线程仍处在可运行状态,所以,不可能让出较低优先级的线程些时获得 CPU 占有权。
在一个运行系统中,如果较高优先级的线程没有调用 sleep 方法,又没有受到 IO 阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。

(3)wait()

wait() 方法是 Object 对象的方法。线程与锁是分不开的,线程的同步、等待、唤醒都与对象锁是密不可分的。wait() 方法会将当前获得对象锁的线程放入 wait set,主动释放对象所,并放弃锁对象上的所有同步声明,当前线程会因为线程调度的原因处于休眠状态而不可用,等待被唤醒。自动释放锁后,JVM会在wait()对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执行。只有通过以下四个方法可以主动唤醒:notify()、notifyAll()、Thread.interrupt() 和等待时间过完。
当线程被唤醒后,线程就从wait set 中移除并且重新获得线程调度能力,同时像其它线程一样持有锁对象。需要注意的是 notify() 唤醒调用后,并不是马上就释放对象锁的,而是在相应的 synchronized(){} 语句块执行结束后。
从语法角度来说就是Obj.wait()、Obj.notify必须在synchronized(Obj){…}语句块内。

一段 synchronized 的代码被一个线程执行之前,他要先拿到执行这段代码的权限, 在Java里边就是拿到某个同步对象的锁(一个对象只有一把锁); 如果这个时候同步对象的锁被其他线程拿走了,他(这个线程)就只能等了(线程阻塞在锁池等待队列中)。
取到锁后,他就开始执行同步代码(被 synchronized 修饰的代码); 线程执行完同步代码后马上就把锁还给同步对象,其他在锁池中等待的某个线程就可以拿到锁执行同步代码了。 这样就保证了同步代码在统一时刻只有一个线程在执行。

Thread.sleep() 与 Object.wait() 的区别

Thread.sleep() 与 Object.wait() 二者都可以暂停当前线程,释放 CPU 控制权。
主要的区别在于Object.wait() 在释放 CPU 同时,释放了对象锁的控制。

三、线程的停止

停止线程是在多线程开发时很重要的技术点,掌握此技术可以对线程的停止进行有效的处理。停止线程在 Java 语言中并不像 break 语句那样干脆,需要一些技巧性的处理。
在java中有三种方法可以停止线程:
1)使用退出标志,让线程正常退出,也就是当run方法执行完之后终止;
2)使用stop方法强制终止线程,但是不推荐使用,因为stop和suspend及resume一样,是java废弃的方法;
3)使用interrupt方法中断线程(推荐使用)

1、 interrupt 方法中断线程

(1)中断线程

线程的 interrupt() 方法是中断线程,将会设置该线程的中断状态位,即设置为 true,中断的结果线程是死亡、还是等待新的任务或是继续运行至下一步,就取决于这个程序本身。线程会不时地检测这个中断标示位,以判断线程是否应该被中断(中断标示值是否为true)。它并不像 stop 方法那样会中断一个正在运行的线程。

(2)判断线程是否被中断

判断某个线程是否已被发送过中断请求,则使用 Thread.currentThread().isInterrupted() 方法(因为它将线程中断标示位设置为 true 后,不会立刻清除中断标示位,即不会将中断标设置为 false),而不要使用 thread.interrupted()(该方法调用后会将中断标示位清除,即重新设置为 false)方法来判断。

private static void test2() {
 Thread thread = new Thread(() -> {
  while (true) {
   Thread.yield();
  
   // 响应中断
   if (Thread.currentThread().isInterrupted()) {
    System.out.println("Java技术栈线程被中断,程序退出。");
    return;
   }
  }
 });
 thread.start();
 thread.interrupt();
}

(3)中断线程抛出异常

如果一个线程处于了阻塞状态(如线程调用了 thread.sleep、thread.join、thread.wait 以及可中断的通道上的 I/O 操作方法后可进入阻塞状态),则线程在检查中断标示时如果发现中断标示为true,则会在这些阻塞方法(sleep、join、wait 及可中断的通道上的 I/O 操作方法)调用处抛出InterruptedException异常,并且在抛出异常后立即将线程的中断标示位清除,即重新设置为 false。
抛出异常是为了线程从阻塞状态醒过来,并在结束线程前让程序员有足够的时间来处理中断请求,根据自己的需求确认是否可以直接忽略该中断,还是应该马上退出。这样,我们就可以捕捉到中断异常,并根据实际情况对该线程从阻塞方法中异常退出而进行一些处理。

	public void run() {  
            try {  
                while (true){  
                    Thread.sleep(1000l);//阻塞状态,线程被调用了interrupte()方法,清除中断标志,抛出InterruptedException  
                    //dosomething  
                    boolean isIn = this.isInterrupted();  
                    //运行状态,线程被调用了interrupte()方法,中断标志被设置为true  
                    //非阻塞状态中进行中断线程操作  
                    if(isIn) break;//退出循环,中断进程  
                }  
            }catch (InterruptedException e){//阻塞状态中进行中断线程操作  
                boolean isIn = this.isInterrupted();//退出阻塞状态,且中断标志被清除,重新设置为false,所以此处的isIn为false  
                return;//退出run方法,中断进程  
            }  
        }  

(4)抛出异常后的处理

没有任何语言方面的需求一个被中断的线程应该终止。中断一个线程只是为了引起该线程的注意,被中断线程可以决定如何应对中断。某些线程非常重要,以至于它们应该不理会中断,而是在处理完抛出的异常之后继续执行,但是更普遍的情况是,一个线程将把中断看作一个终止请求,这种线程的run方法遵循如下形式:

public void run() {
    try {
        ...
        
        while (!Thread.currentThread().isInterrupted()&& more work to do) {
            do more work 
        }
    } catch (InterruptedException e) {
        //  线程在wait或sleep期间被中断了
    } finally {
        //  线程结束前做一些清理工作
    } 
}

上面是while循环在try块里,如果try在while循环里时,因该在catch块里重新设置一下中断标示,因为抛出InterruptedException异常后,中断标示位会自动清除,此时应该这样:

public void run() {
    while (!Thread.currentThread().isInterrupted()&& more work to do) {
        try {
            ...
            sleep(delay);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();//重新设置中断标示
        }
    } 
} 

1)try - catch 捕捉(不推荐)

sleep 方法抛出 InterruptedException 后,中断标识也被清空置为 false,如果我们在 catch 没有通过调用 interrupt() 方法再次将中断标识置为 true,这就导致程序吞掉了 InterruptedException,导致上层调用栈获取不到异常信息。

void mySubTask(){
    ...
    try{
        sleep(delay);
    }catch(InterruptedException e){}//不要这样做
    ...
}

2)重新中断(推荐)

有时候抛出 InterruptedException 并不合适,当一个阻塞方法检测到中断并抛出 InterruptedException 时,它会清除中断状态(中断标志位由true变为false)。如果捕捉到 InterruptedException 但是不能重新抛出它,那么应该保留中断发生的证据,以便调用栈中更高层的代码能知道中断,并对中断作出响应,这可以通过调用 interrupt() 以 “重新中断” 当前线程来完成。

	class Thread1 extends Thread{
        public Thread1(String name) {
            super(name);
        }

        @Override
        public void run() {
            while (true) {
                // 响应中断
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("Java技术栈线程被中断,程序退出。");
                    return;
                }

                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    System.out.println("Java技术栈线程休眠被中断,程序退出。");
                    Thread.currentThread().interrupt();
                }
            }
        }


        public static void main(String[] args) throws InterruptedException {

            Thread1 thread = new Thread1("lhj");
            thread.start();
            thread.sleep(2000);
            thread.interrupt();
        }
    }

运行结果:

sleep 方法抛出 InterruptedException 后,中断标识也被清空置为 false,如果我们在 catch 中没有通过调用 interrupt() 方法再次将中断标识置为 true,程序会一直循环下去,这就导致程序吞掉了 InterruptedException。总的来说,我们应该留意 InterruptedException,当我们捕获到该异常时,绝不可以默默的吞掉它,什么也不做,因为这会导致上层调用栈什么异常信息也获取不到。其实在编写程序时,捕获的任何受检异常我们都不应该吞掉。
如果不重新中断:

	class Thread1 extends Thread{
       public Thread1(String name) {
           super(name);
       }

       @Override
       public void run() {
           while (true) {
               // 响应中断
               if (Thread.currentThread().isInterrupted()) {
                   System.out.println("Java技术栈线程被中断,程序退出。");
                   return;
               }
               try {
                   Thread.sleep(3000);
               } catch (InterruptedException e) {
                   System.out.println("Java技术栈线程休眠被中断,程序退出。");
               }
           }
       }

       public static void main(String[] args) throws InterruptedException {
           Thread1 thread = new Thread1("lhj");
           thread.start();
           thread.sleep(2000);
           thread.interrupt();
       }
   }

运行结果:

sleep 方法抛出 InterruptedException 后,中断标识也被清空置为 false,但是由于在catch中没有重新中断,导致程序继续运行,不会停止。

2、死锁状态无法被被中断
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/350955.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号