一、多线程介绍:
①进程与线程:
-
什么是进程?
- 进程是操作系统中运行的一个任务(一个应用程序运行在一个进程中)。
- 进程(process)是一块包含了某些资源的内存区域。操作系统利用进程把它的工作划分为一些功能单元。
- 进程所包含的一个或多个执行单元称之为线程(thread)。进程还拥有一个私有的虚拟地址空间,该空间仅能被它所包含的线程访问。
- 线程只能归属于一个进程并且它只能访问该进程所拥有的资源。当操作系统创建一个进程之后,给进程会自动申请一个名为主线程或者首要线程的线程。
-
什么是线程?
- 一个线程是进程的一个顺序执行流。
- 同类的多个线程共享一块内存地址和一组系统资源,线程本身有一个供程序执行时的堆栈。线程在切换时负荷小,因此,线程也被称为轻负荷进程,一个进程中可以包含多个线程。
-
进程与线程之间的区别:
- 一个进程至少包含一个线程。
- 线程的划分尺度小于进程,使得多线程程序的并发性高。另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
- 线程在执行过程中与进程的区别在于每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程是不能独立执行的,必须依存在应用程序中,由应用程序提供多个线程执行控制。
- 从逻辑角度看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但是操作系统并没有将多个线程看做多个独立的应用来实现进程的调度和管理以及资源分配。
-
线程使用的场合:
- 线程通常用于在一个程序中需要同时完成多个任务的情况。我们可以将每一个任务定义为一个线程,使得他们可以一同工作。
- 也可以用于单一线程中可以完成,但是使用多线程可以更快的情况;比如下载文件。
②并发原理:
- 多个线程“同时”运行知识我们感官上的一种表现。事实上线程是并发运行的,OS将时间划分为很多时间片段(时间片),尽可能均匀分配给每一个线程,获取时间片段的线程被CPU运行,而其他线程全部等待。所以微观上是走走停停的,宏观上都在运行。这种现象叫并发,但是不是绝对意义上的“同时发生”。
③线程状态:
④线程的创建方法:
-
创建方式一:
- 继承 Thread 并重写 run 方法,run 方法中就是希望线程执行的逻辑。
public class Test { public static void main(String[] args) { Thread1 thread1 = new Thread1(); Thread2 thread2 = new Thread2(); /* 启动线程要调用 start 方法,而不是直接调用 run 方法;当 start 方法调用完毕后,run 方法很快会被线程自行调用。 */ thread1.start(); thread2.start(); } } class Thread1 extends Thread{ @Override public void run() { for (int i=0;i<100;i++){ System.out.println("你是谁啊?"); } } } class Thread2 extends Thread{ @Override public void run() { for (int i=0;i<100;i++){ System.out.println("我是你dad!"); } } } //output: //你是谁啊? //你是谁啊? //我是你dad! //我是你dad! //我是你dad! //你是谁啊? //你是谁啊? // ……
- 第一种创建线程的方式比较简单直接,但是缺点主要有两个:
- 由于需要继承线程,这导致不能再继承其他类,实际开发中经常需要复用某个超类的功能,那么在继承线程之后不能再继承其他类会有很多不便。
- 定义线程类的同时重写了 run 方法,这会导致线程与线程要执行的任务有一个必然的耦合关系,不利于线程的重用。
-
创建方式二:
- 实现 Runnable 接口,单独定义线程任务。
public class Test { public static void main(String[] args) { //实例化两个任务 Runnable r1 = new Runnable1(); Runnable r2 = new Runnable2(); //创建两个线程并指派任务 Thread t1 = new Thread(r1); Thread t2 = new Thread(r2); t1.start(); t2.start(); } } class Runnable1 implements Runnable{ @Override public void run() { for (int i=0;i<100;i++){ System.out.println("你是谁啊?"); } } } class Runnable2 implements Runnable{ @Override public void run() { for (int i=0;i<100;i++){ System.out.println("我是你dad!"); } } }
⑤线程相关方法及性质的介绍:
-
currentThread:
-
线程提供了一个静态方法:
static Thread currentThread()
- 该方法用来获取运行这个方法的线程,main 方法也是靠一个线程运行的,当 JVM 启动后会自动创建一个线程来执行 main 方法。而这个线程的名字叫做“main”,我们称其为主线程。
-
样例一:
public class Test { public static void main(String[] args) { Thread thread = Thread.currentThread(); System.out.println("运行main方法的线程为:"+thread); } } //output: //运行main方法的线程为:Thread[main,5,main] //第一个main指线程名,5指的是线程的优先级,第二个main指的是线程所在的组
-
样例二:
public class Test { public static void main(String[] args) { Thread thread = Thread.currentThread(); System.out.println("运行main方法的线程为:"+thread); Something(); } public static void Something(){ Thread thread = Thread.currentThread(); System.out.println("运行Something方法的线程为:"+thread); } } //output: //运行main方法的线程为:Thread[main,5,main] //运行Something方法的线程为:Thread[main,5,main] //第一个main指线程名,5指的是线程的优先级,第二个main指的是线程所在的组
-
样例三:
public class Test { public static void main(String[] args) { Thread thread = Thread.currentThread(); System.out.println("运行main方法的线程为:"+thread); Something(); Thread t = new Thread(){ @Override public void run() { Thread t = Thread.currentThread(); System.out.println("自定义线程"+t); Something(); } }; t.start(); } public static void Something(){ Thread thread = Thread.currentThread(); System.out.println("运行Something方法的线程为:"+thread); } } //output: //运行main方法的线程为:Thread[main,5,main] //运行Something方法的线程为:Thread[main,5,main] //自定义线程Thread[Thread-0,5,main] //运行Something方法的线程为:Thread[Thread-0,5,main]
-
-
线程自身信息的获取:
public class Test { public static void main(String[] args) { Thread thread = Thread.currentThread(); //获取线程的名字 String name = thread.getName(); System.out.println("name:"+name); //获取线程的唯一标识(id) long id = thread.getId(); System.out.println("ID:"+id); //获取线程的优先级(1~10),默认值是5 int priority = thread.getPriority(); System.out.println("优先级:"+priority); //线程是否还处于活动状态 boolean isAlive = thread.isAlive(); System.out.println("isAlive?"+isAlive); //线程是否被中断 boolean isInterrupted = thread.isInterrupted(); System.out.println("是否被中断?"+isInterrupted); //线程是否为守护线程 boolean isDaemon = thread.isDaemon(); System.out.println("是否为守护线程?"+isDaemon); } } //output: //name:main //ID:1 //优先级:5 //isAlive?true //是否被中断?false //是否为守护线程?false
-
线程优先级:
public class Test { public static void main(String[] args) { Thread max = new Thread(){ @Override public void run() { for (int i=0;i<100;i++) System.out.println("max"); } }; Thread min = new Thread(){ @Override public void run() { for (int i=0;i<100;i++) System.out.println("min"); } }; Thread normal = new Thread(){ @Override public void run() { for (int i=0;i<100;i++){ System.out.println("normal"); } } }; max.setPriority(Thread.MAX_PRIORITY); min.setPriority(Thread.MIN_PRIORITY); max.start(); min.start(); normal.start(); } }
-
sleep阻塞:
-
线程提供了一个静态方法:
static void sleep(long ms)
- 使得运行这个方法的线程阻塞指定毫秒;超时后该线程会自动回到 Runnable 状态,等待再次并发运行。
public class Test { public static void main(String[] args) { System.out.println("程序开始了!"); try { Thread.sleep(5000);//卡5秒 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("程序结束了!"); } } //output: //程序开始了! //(期间等了5秒) //程序结束了!
-
sleep 方法要求必须处理中断异常,原因在于当一个线程调用了 sleep 方法处于阻塞状态的过程中若被调用了它的 interrupt 方法中断时,它就会在 sleep 方法中抛出中断异常。此时并非是将这个线程直接中断,而是中断了它的阻塞状态。
public class Test { public static void main(String[] args) { Thread thread_rest = new Thread(){ @Override public void run() { System.out.println("开始休息!"); try { Thread.sleep(100000); } catch (InterruptedException e) { System.out.println("休息打断!"); } System.out.println("休息结束!"); } }; Thread thread_interrupt = new Thread(){ @Override public void run() { System.out.println("闹钟开启!"); for (int i=0;i<3;i++){ System.out.println("闹钟响了!"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("闹钟结束!"); //中断thread_rest线程 thread_rest.interrupt(); } }; thread_rest.start(); thread_interrupt.start(); } } //output: //开始休息! //闹钟开启! //闹钟响了! //闹钟响了! //闹钟响了! //闹钟结束! //休息打断! //休息结束!
- JDK8 之前,由于 JVM 内存分配问题,当一个方法的局部变量想被这个方法的其他内部类所使用的的时候这个变量必须是final的。此时上文中的 thread_rest 对象的声明需要改为:
final Thread thread_rest = new Thread(){ ...}
-
-
守护线程:
-
又称为后台线程,默认创建的线程都是普通线程或称之为前台线程,线程提供了一个方法:
void setDaemon(boolean on)
- 只有调用该方法并传入参数 true 时,该线程才会设置为守护线程。
-
守护线程在使用上与普通线程没有差别,但是在结束时机上有一个区别:进程结束时,所有正在运行的守护线程都会被强制停止。(进程结束:当一个进程中所有的普通线程都结束时进程既结束。)
public class Test { public static void main(String[] args) { Thread thread_sos = new Thread(){ @Override public void run() { for (int i=0;i<3;i++){ System.out.println("Help!"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("Death!"); } }; Thread thread_help = new Thread(){ @Override public void run() { while (true){ System.out.println("I'm coming!"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }; thread_sos.start(); //将thread_help设置为守护线程,必须在启动前进行设置。 thread_help.setDaemon(true); thread_help.start(); System.out.println("main线程结束了!"); } } //output: //main线程结束了! //I'm coming! //Help! //I'm coming! //Help! //I'm coming! //Help! //I'm coming! //Death!
- 过程分析:上述程序中,实际有三个线程执行,main线程是第一个结束运行的,在 thread_sos 和 thread_help 线程执行完 start 方法之后就结束了 main 线程的任务,然后就是 thread_sos 线程和 thread_help 线程的执行,直到 thread_sos 结束运行;即使 thread_help 中有一个无限循环,但是它是一个守护线程,所以 thread_sos 一结束运行, thread_help 也就结束了运行;此时整个进程就此结束。
-
-
join阻塞:
-
线程提供了一个方法:
void join()
- 该方法可以协调线程之间的同步运行。
-
同步与异步:
- 同步运行——运行有顺序。
- 异步运行——运行代码无顺序,多线程并发运行就是异步运行。
public class Test { //标识图片是否下载完毕 private static boolean isFinish = false; public static void main(String[] args) { Thread download = new Thread(){ @Override public void run() { System.out.println("开始下载图片……"); for (int i = 1;i<=100;i++){ System.out.println("down:"+i+"%"); try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("down:下载图片完毕!"); isFinish = true; } }; Thread show = new Thread(){ @Override public void run() { System.out.println("show:开始显示图片!"); //加载图片之前应当先等待下载线程将图片下载完毕! try { /* show 线程在调用 download.join() 方法之后就进入了阻塞状态,直到 download 线程的 run 方法执行完毕才会解除阻塞。 */ download.join(); } catch (InterruptedException e) { e.printStackTrace(); } if (!isFinish){ throw new RuntimeException("加载图片失败!"); } System.out.println("显示图片完毕!"); } }; download.start(); show.start(); } } //output: //开始下载图片…… //show:开始显示图片! //down:1% //down:2% //…… //down:99% //down:100% //down:下载图片完毕! //显示图片完毕!
-
-
yield方法:
-
Thread 的静态方法 yield:
static void yield()
- 该方法用于使当前线程主动让出当次 CPU 时间片回到 Runnable 状态,等待分配时间片。
-
⑥并发安全问题:
-
问题的产生:
- 当多个线程并发操作同一资源时,由于线程切换实际的不确定性,会导致执行操作资源的代码顺序未按照设计顺序执行,出现操作混乱的情况。严重时可能会导致系统瘫痪。
-
问题的解决:
- 将并发操作同一资源改为同步操作,即:有先后顺序的操作。
-
问题代码举例:
public class Test { public static void main(String[] args) { Table table = new