Java之多线程(一):线程之基础

      多线程很久没用一下就忘了,先写篇博客来记录下。

      第一个,先解决一个问题,什么是线程,什么是进程?

      1.线程是资源调度的最小单位,而进程是资源分配的最小单位。2.进程之间资源不能互相访问,同一个线程中的资源是可以互相访问的。  3.线程是属于进程的,一个进程可以包含多个线程,而一个线程只属于一个进程。4.进程有自己独立的地址空间,而线程没有,线程必须依赖于进程而存在。   5.进程之间的切换开销大,而线程之间的切换开销则较小。

      我们先来看看在java中创建线程的几种方式:

      1.继承Thread类创建线程类。 开启线程是通过start方法开启,并不是run方法。

         

public class MyThread extends Thread {  
  public void run() {  
   System.out.println("MyThread.run()");  
  }  
}  
 
此处是入口
MyThread myThread1 = new MyThread();  
MyThread myThread2 = new MyThread();  
myThread1.start();  
myThread2.start(); 
输出:
    "MyThread.run()"
    "MyThread.run()"

2.实现Runnable接口创建线程类。(该方式需要创建该类的实例,并且作为参数传入到Thread实例中)

public class MyThread extends OtherClass implements Runnable {  
  public void run() {  
   System.out.println("MyThread.run()");  
  }  
}
入口:
MyThread myThread = new MyThread();  
Thread thread = new Thread(myThread);  
thread.start(); 

输出:
    "MyThread.run()"

3.通过实现Callable接口,注意,此方式重写的是call方法,并不是run方法,且该方法是有返回值的;需要把Callable的实例作为参数,传入到FutureTask包装器中,再由此包装器作为参数传入到Thread的实例中,该包装器实现了Future和Runnable接口。

接口
public interface Callable<V>   { 
  V call() throws Exception;   
} 

public class SomeCallable<V> extends OtherClass implements Callable<V> {

    @Override
    public V call() throws Exception {
        return null;
    }

}

入口
Callable<V> oneCallable = new SomeCallable<V>();   
//由Callable<Integer>创建一个FutureTask<Integer>对象:   
FutureTask<V> oneTask = new FutureTask<V>(oneCallable);   
//注释:FutureTask<Integer>是一个包装器,它通过接受Callable<Integer>来创建,它同时实现了Future和Runnable接口。 
  //由FutureTask<Integer>创建一个Thread对象:   
Thread oneThread = new Thread(oneTask);   
oneThread.start();   
//至此,一个线程就创建完成了。

4.通过线程池创建线程,写一个线程池类,实现了Runnable接口,然后从ExecutorService创建线程,再为线程池分配任务。

该线程池实现了Runnable接口
class ThreadPool implements Runnable {
 
	@Override
	public void run() {
		for(int i = 0 ;i<10;i++){
			System.out.println(Thread.currentThread().getName() + ":" + i);
		}
	}


入口
	创建线程池,10个
	ExecutorService executorService = Executors.newFixedThreadPool(10);
	ThreadPool threadPool = new ThreadPool();
	for(int i =0;i<5;i++){
		为线程池分配任务
		executorService.submit(threadPool);
	}
	关闭线程池
	executorService.shutdown();

          下面再来看看线程的生命周期:

          1.新建状态:

           用new关键字和Thread类或其子类建立一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态(runnable)。

          注意:不能对已经启动的线程再次调用start()方法,否则会出现Java.lang.IllegalThreadStateException异常。

          2.就绪状态

          处于就绪状态的线程已经具备了运行条件,但还没有分配到CPU,处于线程就绪队列(尽管是采用队列形式,事实上,把它称为可运行池而不是可运行队列。因为cpu的调度不一定是按照先进先出的顺序来调度的),等待系统为其分配CPU。等待状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会从等待执行状态进入执行状态,系统挑选的动作称之为“cpu调度”。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。

          提示:如果希望子线程调用start()方法后立即执行,可以使用Thread.sleep()方式使主线程睡眠一伙儿,转去执行子线程。

          3.运行状态

         处于就绪状态的线程,如果获得了cpu的调度,就会从就绪状态变为运行状态,执行run()方法中的任务。如果该线程失去了cpu资源,就会又从运行状态变为就绪状态。重新等待系统分配资源。也可以对在运行状态的线程调用yield()方法,它就会让出cpu资源,再次变为就绪状态。

          注: 当发生如下情况是,线程会从运行状态变为阻塞状态:

          ①、线程调用sleep方法主动放弃所占用的系统资源

          ②、线程调用一个阻塞式IO方法,在该方法返回之前,该线程被阻塞

          ③、线程试图获得一个同步监视器,但更改同步监视器正被其他线程所持有

          ④、线程在等待某个通知(notify)

          ⑤、程序调用了线程的suspend方法将线程挂起。不过该方法容易导致死锁,所以程序应该尽量避免使用该方法。

          当线程的run()方法执行完,或者被强制性地终止,例如出现异常,或者调用了stop()、desyory()方法等等,就会从运行状态转变为死亡状态。

          4.阻塞状态

          处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己的运行,进入阻塞状态。 

          在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续运行。

           5.死亡状态

          当线程的run()方法执行完,或者被强制性地终止,就认为它死去。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

           线程管理:java提供的一些方法用于线程状态的控制,java的线程调度是不能做到精准调度的,也就是说有不确定性。

           1.线程睡眠——sleep方法

           如果我们需要让当前正在执行的线程暂停一段时间,并进入到阻塞状态,可以使用该方法。sleep方法是一个Thread类的静态方法,调用该方法始终是让当前正在运行的线程睡眠,而不是调用它的线程对象,它只对正在运行状态的线程对象有效。也就是说你在哪个线程的run方法中调用,就是睡眠该线程。

public class Test1 {  
    public static void main(String[] args) throws InterruptedException {  
        System.out.println(Thread.currentThread().getName());  
        MyThread myThread=new MyThread();  
        myThread.start();  
        myThread.sleep(1000);//这里sleep的就是main线程,而非myThread线程  
        Thread.sleep(10);  
        for(int i=0;i<100;i++){  
            System.out.println("main"+i);  
        }  
    }  
} 

           2.线程让步——yield方法

           yield方法也是一个Thread类的静态方法,它可以让当前正在执行的线程暂停,让出cpu给其它线程,与sleep不同的是,该方法并不会进入到阻塞状态,而是进入到了就绪状态。yield()方法只是让当前线程暂停一下,重新进入就绪的线程池中,让系统的线程调度器重新调度器重新调度一次,完全可能出现这样的情况:当某个线程调用yield()方法之后,线程调度器又将其调度出来重新进入到运行状态执行。

           实际上,当某个线程调用了yield()方法暂停之后,优先级与当前线程相同,或者优先级比当前线程更高的就绪状态的线程更有可能获得执行的机会,当然,只是有可能,因为我们不可能精确的干涉cpu调度线程。

public class Test1 {  
    public static void main(String[] args) throws InterruptedException {  
        new MyThread("低级", 1).start();  
        new MyThread("中级", 5).start();  
        new MyThread("高级", 10).start();  
    }  
}  
  
class MyThread extends Thread {  
    public MyThread(String name, int pro) {  
        super(name);// 设置线程的名称  
        this.setPriority(pro);// 设置优先级  
    }  
  
    @Override  
    public void run() {  
        for (int i = 0; i < 30; i++) {  
            System.out.println(this.getName() + "线程第" + i + "次执行!");  
            if (i % 5 == 0)  
                Thread.yield();  
        }  
    }  
} 

           注:关于sleep()方法和yield()方的区别如下:

           ①、sleep方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态。而yield方法调用后 ,是直接进入就绪状态,所以有可能刚进入就绪状态,又被调度到运行状态。

           ②、sleep方法声明抛出了InterruptedException,所以调用sleep方法的时候要捕获该异常,或者显示声明抛出该异常。而yield方法则没有声明抛出任务异常。

           ③、sleep方法比yield方法有更好的可移植性,通常不要依靠yield方法来控制并发线程的执行。

           3.线程合并——join

           让一个线程等待另一个线程完成才继续执行。如在A线程线程执行体中调用B线程的join()方法,则A线程被阻塞,直到B线程执行完为止,A才能得以继续执行,如在主线程中调用了某个线程的join方法,则该主线程会进入阻塞,等待该线程执行完毕后才继续执行。

            join有三种重载的方法:

void join()      
     当前线程等该加入该线程后面,等待该线程终止。    
void join(long millis)  
     当前线程等待该线程终止的时间最长为 millis 毫秒。 如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度  
void join(long millis,int nanos)   
     等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度

          4.设置线程的优先级

          每个线程执行时都有一个优先级的属性,优先级高的线程可以获得较多的执行机会,而优先级低的线程则获得较少的执行机会。与线程休眠类似,线程的优先级仍然无法保障线程的执行次序。只不过,优先级高的线程获取CPU资源的概率较大,优先级低的也并非没机会执行。

          设置线程优先级:setPriority(int priorityLevel)。参数priorityLevel范围在1-10之间,获取线程优先级:getPriority()。

           5.后台线程(守护线程)

           守护线程使用的情况较少,但并非无用,举例来说,JVM的垃圾回收、内存管理等线程都是守护线程。还有就是在做数据库应用时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监控连接个数、超时时间、状态等等。调用线程对象的方法setDaemon(true),则可以将其设置为守护线程。守护线程的用途为:

           守护线程通常用于执行一些后台作业,例如在你的应用程序运行时播放背景音乐,在文字编辑器里做自动语法检查、自动保存等功能。

           Java的垃圾回收也是一个守护线程。守护线的好处就是你不需要关心它的结束问题。例如你在你的应用程序运行的时候希望播放背景音乐,如果将这个播放背景音乐的线程设定为非守护线程,那么在用户请求退出的时候,不仅要退出主线程,还要通知播放背景音乐的线程退出;如果设定为守护线程则不需要了。

猜你喜欢

转载自blog.csdn.net/m0_37914588/article/details/81517194