java基础--多线程

进程和线程

        进程:执行中的程序,一个进程至少包含一个线程。

         线程:进程中负责程序执行的执行单元, 线程本身依靠程序进行运行,线程是程序中的顺序控制流,只能使用分配给程序的资源和环境。

单线程和多线程

         单线程:程序中只存在一个线程,实际上主方法就是一个主线程。

         多线程:在一个程序中运行多个任务目的是更好地使用CPU资源。

线程的实现

继承Thread类

java.lang包中定义, 继承Thread类必须重写run()方法

1
2
3
4
5
6
7
8
9
10
11
12
class MyThread extends Thread{
     private static int num = 0 ;
 
     public MyThread(){
         num++;
     }
 
     @Override
     public void run() {
         System.out.println( "主动创建的第" +num+ "个线程" );
     }
}

创建好了自己的线程类之后,就可以创建线程对象了,然后通过start()方法去启动线程。注意,不是调用run()方法启动线程,run方法中只是定义需要执行的任务,如果调用run方法,即相当于在主线程中执行run方法,跟普通的方法调用没有任何区别,此时并不会创建一个新的线程来执行定义的任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test {
     public static void main(String[] args)  {
         MyThread thread = new MyThread();
         thread.start();
     }
}
class MyThread extends Thread{
     private static int num = 0 ;
     public MyThread(){
         num++;
     }
     @Override
     public void run() {
         System.out.println( "主动创建的第" +num+ "个线程" );
     }
}

在上面代码中,通过调用start()方法,就会创建一个新的线程了。为了分清start()方法调用和run()方法调用的区别,请看下面一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Test {
     public static void main(String[] args)  {
         System.out.println( "主线程ID:" +Thread.currentThread().getId());
         MyThread thread1 = new MyThread( "thread1" );
         thread1.start();
         MyThread thread2 = new MyThread( "thread2" );
         thread2.run();
     }
}
 
class MyThread extends Thread{
     private String name;
 
     public MyThread(String name){
         this .name = name;
     }
 
     @Override
     public void run() {
         System.out.println( "name:" +name+ " 子线程ID:" +Thread.currentThread().getId());
     }
}

运行结果:

从输出结果可以得出以下结论:

1)thread1和thread2的线程ID不同,thread2和主线程ID相同,说明通过run方法调用并不会创建新的线程,而是在主线程中直接运行run方法,跟普通的方法调用没有任何区别;

2)虽然thread1的start方法调用在thread2的run方法前面调用,但是先输出的是thread2的run方法调用的相关信息,说明新线程创建的过程不会阻塞主线程的后续执行。

实现Runnable接口

在Java中创建线程除了继承Thread类之外,还可以通过实现Runnable接口来实现类似的功能。实现Runnable接口必须重写其run方法。
下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Test {
     public static void main(String[] args)  {
         System.out.println( "主线程ID:" +Thread.currentThread().getId());
         MyRunnable runnable = new MyRunnable();
         Thread thread = new Thread(runnable);
         thread.start();
     }
}
class MyRunnable implements Runnable{
     public MyRunnable() {
     }
 
     @Override
     public void run() {
         System.out.println( "子线程ID:" +Thread.currentThread().getId());
     }
}

Runnable的中文意思是“任务”,顾名思义,通过实现Runnable接口,我们定义了一个子任务,然后将子任务交由Thread去执行。注意,这种方式必须将Runnable作为Thread类的参数,然后通过Thread的start方法来创建一个新线程来执行该子任务。如果调用Runnable的run方法的话,是不会创建新线程的,这根普通的方法调用没有任何区别。

事实上,查看Thread类的实现源代码会发现Thread类是实现了Runnable接口的。

在Java中,这2种方式都可以用来创建线程去执行子任务,具体选择哪一种方式要看自己的需求。直接继承Thread类的话,可能比实现Runnable接口看起来更加简洁,但是由于Java只允许单继承,所以如果自定义类需要继承其他类,则只能选择实现Runnable接口。

通过Callable和Future创建线程

(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。

(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。

(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。

(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

实例代码:

[java]  view plain  copy
  1. package com.thread;  
  2.   
  3. import java.util.concurrent.Callable;  
  4. import java.util.concurrent.ExecutionException;  
  5. import java.util.concurrent.FutureTask;  
  6.   
  7. public class CallableThreadTest implements Callable<Integer>  
  8. {  
  9.   
  10.     public static void main(String[] args)  
  11.     {  
  12.         CallableThreadTest ctt = new CallableThreadTest();  
  13.         FutureTask<Integer> ft = new FutureTask<>(ctt);  
  14.         for(int i = 0;i < 100;i++)  
  15.         {  
  16.             System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i);  
  17.             if(i==20)  
  18.             {  
  19.                 new Thread(ft,"有返回值的线程").start();  
  20.             }  
  21.         }  
  22.         try  
  23.         {  
  24.             System.out.println("子线程的返回值:"+ft.get());  
  25.         } catch (InterruptedException e)  
  26.         {  
  27.             e.printStackTrace();  
  28.         } catch (ExecutionException e)  
  29.         {  
  30.             e.printStackTrace();  
  31.         }  
  32.   
  33.     }  
  34.   
  35.     @Override  
  36.     public Integer call() throws Exception  
  37.     {  
  38.         int i = 0;  
  39.         for(;i<100;i++)  
  40.         {  
  41.             System.out.println(Thread.currentThread().getName()+" "+i);  
  42.         }  
  43.         return i;  
  44.     }  
  45.   
  46. }  


Runnable和Callable的区别是,
(1)Callable规定的方法是call(),Runnable规定的方法是run().
(2)Callable的任务执行后可返回值,而Runnable的任务是不能返回值得
(3)call方法可以抛出异常,run方法不可以

(4)运行Callable任务可以拿到一个Future对象,Future 表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果。计算完成后只能使用 get 方法来获取结果,如果线程没有执行完,Future.get()方法可能会阻塞当前线程的执行;如果线程出现异常,Future.get()会throws InterruptedException或者ExecutionException;如果线程已经取消,会跑出CancellationException。取消由cancel 方法来执行。isDone确定任务是正常完成还是被取消了。一旦计算完成,就不能再取消计算。如果为了可取消性而使用 Future 但又不提供可用的结果,则可以声明Future<?> 形式类型、并返回 null 作为底层任务的结果。

线程的状态

  • 创建(new)状态: 准备好了一个多线程的对象
  • 就绪(runnable)状态: 调用了start()方法, 等待CPU进行调度
  • 运行(running)状态: 执行run()方法
  • 阻塞(blocked)状态: 暂时停止执行, 可能将资源交给其它线程使用
  • 终止(dead)状态: 线程销毁

线程从创建到消亡之间的状态:


注:sleep和wait的区别:

  • sleepThread类的方法,waitObject类中定义的方法.
  • Thread.sleep不会导致锁行为的改变, 如果当前线程是拥有锁的, 那么Thread.sleep不会让线程释放锁.
  • Thread.sleepObject.wait都会暂停当前的线程. OS会将执行时间分配给其它线程. 区别是, 调用wait后, 需要别的线程执行notify/notifyAll才能够重新获得CPU执行时间.


上下文切换

对于单核CPU来说(对于多核CPU,此处就理解为一个核),CPU在一个时刻只能运行一个线程,当在运行一个线程的过程中转去运行另外一个线程,这个叫做线程上下文切换(对于进程也是类似)。

由于可能当前线程的任务并没有执行完毕,所以在切换时需要保存线程的运行状态,以便下次重新切换回来时能够继续切换之前的状态运行。举个简单的例子:比如一个线程A正在读取一个文件的内容,正读到文件的一半,此时需要暂停线程A,转去执行线程B,当再次切换回来执行线程A的时候,我们不希望线程A又从文件的开头来读取。

因此需要记录线程A的运行状态,那么会记录哪些数据呢?因为下次恢复时需要知道在这之前当前线程已经执行到哪条指令了,所以需要记录程序计数器的值,另外比如说线程正在进行某个计算的时候被挂起了,那么下次继续执行的时候需要知道之前挂起时变量的值时多少,因此需要记录CPU寄存器的状态。所以一般来说,线程上下文切换过程中会记录程序计数器、CPU寄存器状态等数据。

说简单点的:对于线程的上下文切换实际上就是 存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行

虽然多线程可以使得任务执行的效率得到提升,但是由于在线程切换时同样会带来一定的开销代价,并且多个线程会导致系统资源占用的增加,所以在进行多线程编程时要注意这些因素。

线程的常用方法

编号方法说明
1public void start()使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
2public void run()如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。
3public final void setName(String name)改变线程名称,使之与参数 name 相同。
4public final void setPriority(int priority)更改线程的优先级。
5public final void setDaemon(boolean on)将该线程标记为守护线程或用户线程。
6public final void join(long millisec)等待该线程终止的时间最长为 millis 毫秒。
7public void interrupt()中断线程。
8public final boolean isAlive()测试线程是否处于活动状态。
9public static void yield()暂停当前正在执行的线程对象,并执行其他线程。
10public static void sleep(long millisec)在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
11public static Thread currentThread()返回对当前正在执行的线程对象的引用。

静态方法

currentThread()方法

currentThread()方法可以返回代码段正在被哪个线程调用的信息。

1
2
3
4
5
public class Run1{
     public static void main(String[] args){                
     System.out.println(Thread.currentThread().getName());
     }
}

sleep()方法

方法sleep()的作用是在指定的毫秒数内让当前“正在执行的线程”休眠(暂停执行)。这个“正在执行的线程”是指this.currentThread()返回的线程。

sleep方法有两个重载版本:

1
2
sleep( long millis)     //参数为毫秒
sleep( long millis, int nanoseconds)    //第一参数为毫秒,第二个参数为纳秒

sleep相当于让线程睡眠,交出CPU,让CPU去执行其他的任务。
但是有一点要非常注意,sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。看下面这个例子就清楚了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Test {
 
     private int i = 10 ;
     private Object object = new Object();
 
     public static void main(String[] args) throws IOException  {
         Test test = new Test();
         MyThread thread1 = test. new MyThread();
         MyThread thread2 = test. new MyThread();
         thread1.start();
         thread2.start();
     }
 
     class MyThread extends Thread{
         @Override
         public void run() {
             synchronized (object) {
                 i++;
                 System.out.println( "i:" +i);
                 try {
                     System.out.println( "线程" +Thread.currentThread().getName()+ "进入睡眠状态" );
                     Thread.currentThread().sleep( 10000 );
                 } catch (InterruptedException e) {
                     // TODO: handle exception
                 }
                 System.out.println( "线程" +Thread.currentThread().getName()+ "睡眠结束" );
                 i++;
                 System.out.println( "i:" +i);
             }
         }
     }
}

输出结果:

从上面输出结果可以看出,当Thread-0进入睡眠状态之后,Thread-1并没有去执行具体的任务。只有当Thread-0执行完之后,此时Thread-0释放了对象锁,Thread-1才开始执行。

注意,如果调用了sleep方法,必须捕获InterruptedException异常或者将该异常向上层抛出。当线程睡眠时间满后,不一定会立即得到执行,因为此时可能CPU正在执行其他的任务。所以说调用sleep方法相当于让线程进入阻塞状态。

yield()方法

调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。

注意,调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。
代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyThread  extends Thread{
     @Override
     public void run() {
         long beginTime=System.currentTimeMillis();
         int count= 0 ;
         for ( int i= 0 ;i< 50000000 ;i++){
             count=count+(i+ 1 );
             //Thread.yield();
         }
         long endTime=System.currentTimeMillis();
         System.out.println( "用时:" +(endTime-beginTime)+ " 毫秒!" );
     }
}
 
public class Run {
     public static void main(String[] args) {
         MyThread t= new MyThread();
         t.start();
     }
}

执行结果:

1
用时: 3 毫秒!

如果将 //Thread.yield();的注释去掉,执行结果如下:

1
用时: 16080 毫秒!

   sleep和yield的区别

     sleep是让当前线程处于阻塞状态。

     yidld是让当前线程处于就绪状态。



对象方法

start()方法

start()用来启动一个线程,当调用start方法后,系统才会开启一个新的线程来执行用户定义的子任务,在这个过程中,会为相应的线程分配需要的资源。

run()方法

run()方法是不需要用户来调用的,当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入run方法体去执行具体的任务。注意,继承Thread类必须重写run方法,在run方法中定义具体要执行的任务。

getId()

getId()的作用是取得线程的唯一标识
代码:

1
2
3
4
5
6
public class Test {
     public static void main(String[] args) {
         Thread t= Thread.currentThread();
         System.out.println(t.getName()+ " " +t.getId());
     }
}

输出:

1
main 1
isAlive()方法

方法isAlive()的功能是判断当前线程是否处于活动状态
代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyThread  extends Thread{
     @Override
     public void run() {
         System.out.println( "run=" + this .isAlive());
     }
}
public class RunTest {
     public static void main(String[] args) throws InterruptedException {
         MyThread myThread= new MyThread();
         System.out.println( "begin ==" +myThread.isAlive());
         myThread.start();
         System.out.println( "end ==" +myThread.isAlive());
     }
}

程序运行结果:

1
2
3
begin == false
run= true
end == false

方法isAlive()的作用是测试线程是否偶处于活动状态。什么是活动状态呢?活动状态就是线程已经启动且尚未终止。线程处于正在运行或准备开始运行的状态,就认为线程是“存活”的。
有个需要注意的地方

1
System.out.println( "end ==" +myThread.isAlive());

虽然上面的实例中打印的值是true,但此值是不确定的。打印true值是因为myThread线程还未执行完毕,所以输出true。如果代码改成下面这样,加了个sleep休眠:

1
2
3
4
5
6
7
public static void main(String[] args) throws InterruptedException {
         MyThread myThread= new MyThread();
         System.out.println( "begin ==" +myThread.isAlive());
         myThread.start();
         Thread.sleep( 1000 );
         System.out.println( "end ==" +myThread.isAlive());
     }

则上述代码运行的结果输出为false,因为mythread对象已经在1秒之内执行完毕。

join()方法
 

Thread的非静态方法join()让一个线程B“加入”到另外一个线程A的尾部。在A执行完毕之前,B不能工作。例如:
        Thread t = new MyThread();
        t.start();
        t.join();
另外,join()方法还有带超时限制的重载版本。 例如t.join(5000);则让线程等待5000毫秒,如果超过这个时间,则停止等待,变为可运行状态。
 
线程的加入join()对线程栈导致的结果是线程栈发生了变化,当然这些变化都是瞬时的。
复制代码
 1 public class TestJoin {
 2  
 3   public static void main(String[] args) {
 4    
 5     MyThread2 t1 = new MyThread2("TestJoin");
 6     t1.start();
 7     try {
 8       t1.join();  //join()合并线程,子线程运行完之后,主线程才开始执行
 9      }catch (InterruptedException e) {  }
10       
11      for(int i=0 ; i <10; i++)
12               System.out.println("I am Main Thread");
13    }
14  }
15  
16  class MyThread2 extends Thread {
17   
18     MyThread2(String s) {
19      super(s);
20      }
21      
22   public void run() {
23     for(int i = 1; i <= 10; i++) {
24      System.out.println("I am "+getName());
25      try {
26       sleep(1000); //暂停,每一秒输出一次
27       }catch (InterruptedException e) {
28       return;
29      }
30      }
31    }
32   }
复制代码
程序运行结果:
复制代码
I am TestJoin
I am TestJoin
I am TestJoin
I am TestJoin
I am TestJoin
I am TestJoin
I am TestJoin
I am TestJoin
I am TestJoin
I am TestJoin
I am Main Thread
I am Main Thread
I am Main Thread
I am Main Thread
I am Main Thread
I am Main Thread
I am Main Thread
I am Main Thread
I am Main Thread
I am Main Thread
复制代码

总结:也就是说那个线程调用join()方法 那么 调用join()方法的线程会先于其他线程执行,直到该线程(调用join方法的线程)执行完毕后,其他线程才会执行。


getName和setName

用来得到或者设置线程名称。

getPriority和setPriority

用来获取和设置线程优先级。

setDaemon和isDaemon

用来设置线程是否成为守护线程和判断线程是否是守护线程。

守护线程和用户线程的区别在于:守护线程依赖于创建它的线程,而用户线程则不依赖。举个简单的例子:如果在main线程中创建了一个守护线程,当main方法运行完毕之后,守护线程也会随着消亡。而用户线程则不会,用户线程会一直运行直到其运行完毕。在JVM中,像垃圾收集器线程就是守护线程。

Thread类中的方法调用到底会引起线程状态发生怎样的变化呢?下面一幅图就是在上面的图上进行改进而来的:

停止线程

  • 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止
  • 使用stop方法强行终止线程,但是不推荐使用这个方法,因为stop和suspend及resume一样,都是作废过期的方法,使用他们可能产生不可预料的结果。
  • 使用interrupt方法中断线程,但这个不会终止一个正在运行的线程,还需要加入一个判断才可以完成线程的停止。

暂停线程

interrupt()方法

线程的优先级

在操作系统中,线程可以划分优先级,优先级较高的线程得到的CPU资源较多,也就是CPU优先执行优先级较高的线程对象中的任务。
设置线程优先级有助于帮“线程规划器”确定在下一次选择哪一个线程来优先执行。
设置线程的优先级使用setPriority()方法,此方法在JDK的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public final void setPriority( int newPriority) {
         ThreadGroup g;
         checkAccess();
         if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
             throw new IllegalArgumentException();
         }
         if ((g = getThreadGroup()) != null ) {
             if (newPriority > g.getMaxPriority()) {
                 newPriority = g.getMaxPriority();
             }
             setPriority0(priority = newPriority);
         }
     }

在Java中,线程的优先级分为1~10这10个等级,如果小于1或大于10,则JDK抛出异常throw new IllegalArgumentException()。
JDK中使用3个常量来预置定义优先级的值,代码如下:

1
2
3
public final static int MIN_PRIORITY = 1 ;
public final static int NORM_PRIORITY = 5 ;
public final static int MAX_PRIORITY = 10 ;

线程优先级特性:

  • 继承性
    比如A线程启动B线程,则B线程的优先级与A是一样的。
  • 规则性
    高优先级的线程总是大部分先执行完,但不代表高优先级线程全部先执行完。
  • 随机性
    优先级较高的线程不一定每一次都先执行完。


守护线程

在Java线程中有两种线程,一种是User Thread(用户线程),另一种是Daemon Thread(守护线程)。
Daemon的作用是为其他线程的运行提供服务,比如说GC线程。其实User Thread线程和Daemon Thread守护线程本质上来说去没啥区别的,唯一的区别之处就在虚拟机的离开:如果User Thread全部撤离,那么Daemon Thread也就没啥线程好服务的了,所以虚拟机也就退出了。

守护线程并非虚拟机内部可以提供,用户也可以自行的设定守护线程,方法:public final void setDaemon(boolean on) ;但是有几点需要注意:

  • thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。 (备注:这点与守护进程有着明显的区别,守护进程是创建后,让进程摆脱原会话的控制+让进程摆脱原进程组的控制+让进程摆脱原控制终端的控制;所以说寄托于虚拟机的语言机制跟系统级语言有着本质上面的区别)
  • 在Daemon线程中产生的新线程也是Daemon的。 (这一点又是有着本质的区别了:守护进程fork()出来的子进程不再是守护进程,尽管它把父进程的进程相关信息复制过去了,但是子进程的进程的父进程不是init进程,所谓的守护进程本质上说就是“父进程挂掉,init收养,然后文件0,1,2都是/dev/null,当前目录到/”)
  • 不是所有的应用都可以分配给Daemon线程来进行服务,比如读写操作或者计算逻辑。因为在Daemon Thread还没来的及进行操作时,虚拟机可能已经退出了。

本文参考自:http://www.importnew.com/21136.html,

                    https://www.cnblogs.com/jpwz/p/6248000.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值