Java多线程基础
在开始之前,先来搞清楚两个概念:进程和线程。
进程:进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,是系统进行资源分配和调度的一个独立单位。
线程:线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位,线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),一个线程可以创建和撤销另一个线程;
进程和线程的关系:
(1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
(2)资源分配给进程,同一进程的所有线程共享该进程的所有资源。
(3)线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。
(4)处理机分给线程,即真正在处理机上运行的是线程。
(5)线程是指进程内的一个执行单元,也是进程内的可调度实体。
线程与进程的区别:
(1)调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位。
(2)并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可以并发执行。
(3)拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。
(4)系统开销:在创建或撤销进程的时候,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤销线程时的开销。但进程有独立的地址空间,进程崩溃后,在保护模式下不会对其他的进程产生影响,而线程只是一个进程中的不同的执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但是在进程切换时,耗费的资源较大,效率要差些。
创建一个线程(Java中提供了三种创建线程的方法):
通过实现Runnable接口;
通过继承Thread类本身;
通过Callable和Future创建线程。
一、通过实现Runnable接口来创建线程(推荐使用)
实现Java.lang.Runnable接口,并重写run()方法。
优势:可继承其它类,多线程可共享同一个Thread对象;
劣势:编程方式稍微复杂,如需访问当前线程,需调用Thread.currentThread()方法。
1 package com.bjwyj.thread; 2 /** 3 * 创建线程一: 4 * 1、实现Runnable接口+重写run 5 * 2、启动:创建子类对象+Thread对象+start 6 * 7 * 推荐,避免单继承的局限性,优先使用接口 8 * 方便共享资源 9 * @author 吴永吉 10 * 11 */ 12 public class ThreadDemo01 implements Runnable{ 13 14 /** 15 * 线程入口点 16 */ 17 @Override 18 public void run() { 19 20 } 21 22 public static void main(String[] args) { 23 //以下使用Lambda表达式 24 new Thread(()->{ 25 for(int i=0;i<5;i++) { 26 System.out.println(Thread.currentThread().getName()+"线程"); 27 } 28 }).start(); 29 30 for(int i=0;i<5;i++) { 31 System.out.println(Thread.currentThread().getName()+"线程"); 32 } 33 } 34 }
结果:
main线程 main线程 Thread-0线程 main线程 Thread-0线程 main线程 Thread-0线程 main线程 Thread-0线程 Thread-0线程
二、通过继承Thread类本身来创建线程(不推荐使用==>Java中单继承的局限性)
继承Java.lang.Thread类,并覆盖run() 方法。
优势:编写简单;
劣势:无法继承其它父类
1 package com.bjwyj.thread; 2 /** 3 * 创建线程二: 4 * 1、继承Thread+重写run 5 * 2、启动:创建子类对象+start 6 * @author 吴永吉 7 * 8 */ 9 public class ThreadDemo02 extends Thread{ 10 11 /** 12 * 线程入口点 13 */ 14 @Override 15 public void run() { 16 for(int i=0;i<5;i++) { 17 System.out.println(Thread.currentThread().getName()+"线程"); 18 } 19 } 20 21 public static void main(String[] args) { 22 //创建子类对象 23 ThreadDemo02 td = new ThreadDemo02(); 24 //启动 25 td.start(); //不保证立即运行,CPU调用 26 27 for(int i=0;i<5;i++) { 28 System.out.println(Thread.currentThread().getName()+"线程"); 29 } 30 } 31 }
结果:
Thread-0线程 main线程 main线程 Thread-0线程 main线程 Thread-0线程 Thread-0线程 Thread-0线程 main线程 main线程
三、通过Callable和Future创建线程(属于JUC高级编程,初学者了解即可)
实现Callable接口,重写call方法。
优势:性能更优。
劣势:编写复杂。
1 package com.bjwyj.thread; 2 3 import java.util.concurrent.Callable; 4 import java.util.concurrent.ExecutionException; 5 import java.util.concurrent.ExecutorService; 6 import java.util.concurrent.Executors; 7 import java.util.concurrent.Future; 8 9 /** 10 * 了解创建线程的方式三 11 * @author 吴永吉 12 * 13 */ 14 public class ThreadDemo03 implements Callable<String>{ 15 String name; //名字 16 17 public ThreadDemo03(String name) { 18 this.name = name; 19 } 20 21 @Override 22 public String call() throws Exception { 23 return this.name; 24 } 25 26 public static void main(String[] args) throws InterruptedException, ExecutionException { 27 ThreadDemo03 td1 = new ThreadDemo03("线程1"); 28 ThreadDemo03 td2 = new ThreadDemo03("线程2"); 29 ThreadDemo03 td3 = new ThreadDemo03("线程3"); 30 31 //创建执行服务 32 ExecutorService ser = Executors.newFixedThreadPool(3); 33 //提交任务 34 Future<String> result1 = ser.submit(td1); 35 Future<String> result2 = ser.submit(td2); 36 Future<String> result3 = ser.submit(td3); 37 //获取结果 38 String r1 = result1.get(); 39 String r2 = result2.get(); 40 String r3 = result3.get(); 41 System.out.println(r1); 42 System.out.println(r2); 43 System.out.println(r3); 44 //关闭服务 45 ser.shutdownNow(); 46 } 47 }
结果:
线程1
线程2
线程3
从以上方式一和方式二可以看出:各个线程之间几乎是穿插着运行并输出的,那么,多线程情况下,难道真的是同时在运行吗?答案不是绝对的。首先,一个处理器(CPU)在某一个时间点上永远都只能是一个线程!双核CPU可以理解为两块CPU,4核,8核等依次类推,就单个CPU而言,某个时间点只能是一个线程在运行,所谓的多线程是通过调度获取CPU的时间片实现的。其实就相当于CPU是一个人,多线程是几件事,CPU一下子干这件事,干一会时间片到了就干另一件。由于CPU计算速度很快很快,所以看起来就像是几件事情在同时做着。不过现在CPU都是双核四核八核的,这些是真的一起干的,因为这是几个人干几件事,所以,多CPU当然是真多线程。
线程是一个动态执行的过程,它也有一个从产生到死亡的过程,线程生命周期有五种状态:
新建(new Thread)
当创建Thread类的一个实例(对象)时,此线程进入新建状态(未被启动)。
例如:Thread t1=new Thread();
就绪(runnable)
线程已经被启动,正在等待被分配给CPU时间片,也就是说此时线程正在就绪队列中排队等候得到CPU资源。
例如:t1.start();
运行(running)
线程获得CPU资源正在执行任务(run()方法),此时除非此线程自动放弃CPU资源或者有优先级更高的线程进入,线程将一直运行到结束。
死亡(dead)
当线程执行完毕或被其它线程杀死,线程就进入死亡状态,这时线程不可能再进入就绪状态等待执行。
自然终止:正常运行run()方法后终止。
异常终止:调用stop()(已弃用)方法让一个线程终止运行。
堵塞(blocked)
由于某种原因导致正在运行的线程让出CPU并暂停自己的执行,即进入堵塞状态。
正在睡眠:用sleep(long t) 方法可使线程进入睡眠方式。一个睡眠着的线程在指定的时间过去可进入就绪状态。
正在等待:调用wait()方法。(调用notify()方法回到就绪状态)
被另一个线程所阻塞:调用suspend()(已弃用)方法。(调用resume()(已弃用)方法恢复)