1. 多线程
1.1. 本章目标
1. 程序,进程,线程概念
2. java实现多线程(2种)
3. 静态代理设计模式
4. 线程的状态和方法
5. 线程的基本信息和优先级
1.2. 什么是程序?
程序:Program ,是一个静态的概念
只要是计算机中安装的,在控制面板中能够看到的,都是程序
1.3. 什么是进程?
当你双击的Microsoft Word的图标,你就开始运行的Word的进程。在Windows系统中,一个运行的exe就是一个进程。
计算机的核心是CPU,它承担了所有的计算任务。它就像一座工厂,时刻在运行。
假定工厂的电力有限,一次只能供给一个车间使用。也就是说,一个车间开工的时候,其他车间都必须停工。背后的含义就是,单个CPU一次只能运行一个任务。
进程就好比工厂的车间,它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。
进程的概括:Process,是一个动态的概念。
u 进程是程序的一次动态执行过程,占用特定的地址空间。
u 每个进程都是独立的,由3部分组成cpu,data,code
u 缺点:内存的浪费,cpu的负担(程序开太多,会卡)
1.4. 什么是线程?
一个车间里,可以有很多工人。他们协同完成一个任务。线程就好比车间里的工人。一个进程可以包括多个线程。
由于一个进程可以由多个线程,线程可以被认为是“轻量级”的进程。因此,一个线程和一个进程之间的本质区别在于,每一个用来完成的工作。线程用于小任务,而进程用于更多的“重量级”的任务。
线程:Thread,是进程中一个“单一的连续控制流程”(a single sequential flow of control)/执行路径
线程又被称为轻量级进程(lightweight process)
Threads run at the same time,independently of one another
一个进程可拥有多个并行的(concurrent)线程
一个进程中的线程共享相同的内存单元/内存地址空间---->可以访问相同的变量和对象,而且它们从同一堆中分配对象---->通信、数据交换、同步操作
由于线程间的通信是在同一块地址空间上进行的,所以不需要额外的通信进制,这就使得通信更简便而且信息传递的速度更快。
1.5. 什么是多线程?
1。单进程单线程:一个人在一个桌子上吃菜。
2。单进程多线程:多个人在同一个桌子上一起吃菜。
3。多进程单线程:多个人每个人在自己的桌子上吃菜。
多线程的问题是多个人同时吃一道菜的时候容易发生争抢,例如两个人同时夹一个菜,一个人刚伸出筷子,结果伸到的时候已经被夹走菜了。此时就必须等一个人夹一口之后,在还给另外一个人夹菜,也就是说资源共享就会发生冲突争抢。对于 Windows 系统来说,“开桌子”的开销很大,因此 Windows 鼓励大家在一个桌子上吃菜。因此 Windows 多线程学习重点是要大量面对资源争抢与同步方面的问题。
一个进程中的所有线程可以同时执行,也可以按照要求顺序执行、部分执行、互相等待或完全停止。
1.6. 进程和线程的区别?
l 根本区别。进程是资源分配单位,线程是调度和执行单位。进程能做的所有事情,都是进程中的线程在做,进程是对这些线程总和的称呼。如果一个进程中一个线程都没有,表示这个进程什么都做不了。
l 内存空间。每个进程拥有自己的地址空间,进程中的所有线程共享相同的内存单元,可以访问相同的变量和对象。
l 通信。由于线程间的通信是在同一地址空间上进行,所以不需要额外的通信机制,这就使得通信更简单而且消息传递的速度也更快。进程间由于地址空间不共享,通信效率更低。
l 运行调度。进程中的一个线程崩溃会导致整个进程崩溃,但一个进程崩溃不会导致所有进程崩溃。
l 软件开发管理。进程可以交给不同的团队开发, 但是多线程必须是同一个团队开发,因为调试时要启动进程,所有线程都会被启动,调试要同步。
l
面试题:什么是进程?什么是线程? 进程和线程有什么区别?
关键点: 每个进程独立的内存空间,互不影响 ,线程:线程是进程的一部分,是进程中的多条执行路径, 区别:表中红色字体
总结:进程是系统运行程序的基本单位,每一个进程都有自己独立的一块内存空间,一组系统资源。每一个进程的内部数据和状态都是完全独立的。
线程:是进程中执行运算的最小单位,是进程内部的一个执行单元。可看成轻量级进程,同一类线程共享代码和数据空间。
区别:一个进程中至少要有一个线程。资源分配给进程,同一进程的所有线程共享该进程的所有资源。处理机分配给线程,即真正在处理机上运行的是线程。
1.7. 多线程实现的两种方法
java中实现多线程(一)
l 在java中负责线程的这个功能的是Java.lang.Thread这个类
l 可以通过创建Thread的实例来创建新的线程
l 每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,方法run()称为线程体。
l 通过调用Thread类的start()方法来启动一个线程。
举例:龟兔赛跑
package com.njwb.threadmethod;
/**
* 1.类继承java.lang.Thread +重写run方法
* 2.创建该类的实例 +start()方法开启多线程
* 程序中 main 主线程 后台gc 回收堆中不用的变量的内存 自动调用 ,异常 打印堆栈
* @author Administrator
*
*/
public class Rabbit extends Thread {
@Override
public void run() {
for(int i=0;i<1000;i++){
System.out.println("兔子跑了"+i+"步");
/*try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
}
}
}
class Tortise extends Thread{
@Override
public void run() {
for(int i=0;i<1000;i++){
System.out.println("乌龟跑了"+i+"步");
/*try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
}
}
}
package com.threadmethod;
public class RabbitApp {
public static void main(String[] args) throws InterruptedException {
Rabbit ra = new Rabbit();
//开启多线程不能调用run() ,如果直接写ra.run()表示调用普通方法run
//ra.run();
//start() 线程处于就绪状态,该线程会被加入线程组中,等待cpu的调度,如果cpu调度到,就可以执行,会自动调用run方法
ra.start();
Tortise tor = new Tortise();
//tor.run();
tor.start();
for(int i=0;i<1000;i++){
System.out.println("main......"+i);
//休眠 暂停线程
//Thread.sleep(200);
}
}
}
练习:在线程中输出1-100的整数。
package com.threadmethod;
public class NumDemo extends Thread{
@Override
public void run() {
for(int i=1;i<=100;i++){
System.out.println(i);
}
}
}
package com.threadmethod;
public class NumDemoApp {
public static void main(String[] args) {
//实例化线程类对象
NumDemo nd = new NumDemo();
nd.start();
}
}
JAVA中实现多线程(二)
l 继承Thread类方式的缺点:那就是如果我们的类已经从一个类继承(如小程序必须继承自Applet类),则无法再继承Thread类
l 通过Runnable接口实现多线程
l 优点:可以同时实现继承(实现接口的方式,该线程类以后可以去继承其他的类,将父类预留出来)。实现Runnable接口方式要通用一些。
1) 避免单继承
2) 方便共享资源,同一份资源,多个代理访问
举例:使用Runnable接口方式
package com.njwb.staticproxy;
/**
* 1.类 实现Runnable接口 +重写run方法
* 2.调用的时候,实例化该类,将该类的对象作为参数传入代理Thread的构造方法中,通过代理start()开启多线程
* @author Administrator
*
*/
public class ThreadDemo implements Runnable{
@Override
public void run() {
for(int i=0;i<1000;i++){
System.out.println("ThreadDemo....."+i);
}
}
}
package com.staticproxy;
public class ThreadDemoApp {
public static void main(String[] args) {
//创建真实角色类
ThreadDemo td = new ThreadDemo();
//不能td.start(),因为ThreadDemo类没有start()方法,
//td.start();
//创建代理角色,代理持有对真实角色的引用
Thread th = new Thread(td);
//通过代理启动
th.start();
}
}
举例:向线程类传递数据,带参构造
package com.njwb.threadrunnable;
public class SomethingDemo implements Runnable {
private String doSomething;
public SomethingDemo(String doSomething) {
super();
this.doSomething = doSomething;
}
@Override
public void run() {
while(true){
System.out.println("一边"+doSomething);
}
}
}
package com.threadrunnable;
public class TestSomethingDemo {
public static void main(String[] args) {
//创建真实角色
SomethingDemo sd = new SomethingDemo("聊QQ");
//创建代理角色,代理持有对真实角色的引用
Thread th = new Thread(sd);
//通过代理启动
th.start();
//创建真实角色
SomethingDemo sd2 = new SomethingDemo("打游戏");
//创建代理角色,代理持有对真实角色的引用
Thread th2 = new Thread(sd2);
//通过代理启动
th2.start();
while(true){
System.out.println("一边唱歌...");
}
}
}
两种方法的不同
l 使用直接继承Thread,虽然简单,但是由于java是单继承,有局限。
l 使用Runnable接口,虽然程序复杂了,但是接口能多继承(推荐使用)
总结
当线程目标run()方法结束时该线程完成。
线程的调度是JVM的一部分,实际上一次只能运行一个线程,众多可运行线程中的某一个会被选中做为当前线程,谁先谁后顺序没有保障。
1.8. 静态代理
package com.njwb.staticproxy;
/**
* 1.真实角色类,代理角色类要实现相同的接口
* 2.代理角色持有对真实角色的引用 (真实角色在代理类,作为属性存在,后期调用的时候通过带参构造传入)
* 3.调用方法时,通过代理调用
* @author Administrator
*
*/
//公有的接口
interface Marry{
void toMarry();
}
//真实角色类
class You implements Marry{
@Override
public void toMarry() {
System.out.println("你和小红结婚了");
}
}
//代理角色类
class WeddingCompany implements Marry{
private Marry you;
public WeddingCompany(Marry you) {
super();
this.you = you;
}
public void before(){
System.out.println("布置场地....");
}
@Override
public void toMarry() {
before();
this.you.toMarry();
after();
}
public void after(){
System.out.println("收拾残局....");
}
}
public class StaticProxyDemo {
public static void main(String[] args) {
//创建真实角色
Marry you = new You();
//创建代理角色+持有对真实角色的引用
WeddingCompany com = new WeddingCompany(you);
//通过代理调用方法
com.toMarry();
}
}
1.9. 线程状态
l 新生状态:使用new关键字和Thread类或其子类建立一个线程对象后,该线程处于新生状态,有自己的内存空间,当调用start方法,进入就绪状态
l 就绪状态:处于就绪状态的线程已经具备了运行条件,但还没有分配到CPU,处于线程就绪队列,等待系统为其分配CPU.等待状态并不是执行 状态,当系统选定一个等待执行的Thread对象后,它就会从等待执行状态进入执行状态,系统挑选的动作称之为“CPU调度”。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。
l 运行状态: 在运行状态中线程将执行自己run方法,直到调用其他方法而终止、或者等待某些资源而阻塞、或完成任务而死亡,如果在给定的时间片内未执行完,就会被系统换下来进入就绪状态,等待CPU重新分配后方可进入运行状态
l 等待/阻塞/睡眠状态:处在运行状态的线程在某种情况下,如执行sleep()(睡眠方法),或等待I/O设备资源(input.next()),将让出CPU并暂停停止自己的运行,进入阻塞状态,在阻塞状态的线程不能立即进入就绪队列,只有当引起阻塞的原因消除时,如睡眠时间到了或者I/O设备空闲了,线程就转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续运行(当获取到CPU时又可以进入运行状态)
l 死亡状态:死亡状态是线程生命周期中的最后一个阶段。死亡状态有两个,一是线程正常执行结束,二是线程被强制终止,如通过执行stop或destroy方法来终止一个线程(不推荐使用这2个方法,前者会产生异常,后者是强制终止,不会释放锁。)使用flag控制
Rabbit ra = new Rabbit(); //新生状态
ra.start(); //就绪状态
比方说龟兔赛跑,各跑1000步,1000步跑完,线程正常的终止(执行结束) --à进入死亡状态
比方说:3秒后 ,人为终止线程 ,线程也被终止掉,--à进入死亡状态
Main方法中,如果i等于50的时候,main线程会进入阻塞状态,等待输入,不输入的话,始终处在阻塞状态,如果遇到sleep(),在休眠的时间内,也是出于阻塞状态。
for(int i=0;i<1000;i++){
System.out.println("main......"+i);
//阻塞事件 等待输入
/*if(i==50){
int num = new Scanner(System.in).nextInt();
}*/
//阻塞事件,线程的休眠
//Thread.sleep(200);
}
1.10. 线程名称
运行中的线程总是有名字的,名字有两个来源,一个是虚拟机自己给的名字,一个是你定的名字。在没有指定线程名字的情况下,虚拟机总会为线程指定名字,并且主线程的名字总是main,非主线程的名字不确定。
举例:设置线程名称,获取名称 ,判断线程的状态(是否alive)
Thread.currentThread 获取的当前的线程对象
getName() 获取的线程名称
setName() 设置线程名称
isAlive() 判断线程的状态 如果线程死亡的话 isAlive() false 如果就绪状态或者运行状态或者阻塞状态的话 ,isAlive() true
boolean | isAlive() |
举例:线程类见1.14终止线程
测试类1
package com.njwb.threadrunnable;
/**
* 主线程:main
* @author Administrator
*
*/
public class TestThreadDemo {
public static void main(String[] args) throws InterruptedException {
ThreadDemo t = new ThreadDemo();
Thread th = new Thread(t);
th.setName("线程A");
System.out.println("当前线程的名字主线程:"+Thread.currentThread().getName());
System.out.println("已开启的线程A的名字:"+th.getName());
th.start();
System.out.println("启动后线程A的状态:"+th.isAlive());
Thread.sleep(10);
//10ms后终止线程
t.stop();
//延时
Thread.sleep(3);
System.out.println("终止后线程A的状态:"+th.isAlive());
}
}
测试类2
package com.njwb.threadrunnable;
/**
* 主线程:main
* 如果线程没有指定名称,将有默认的名称,自动编号 Thread-0,Thread-1,Thread-2....
* @author Administrator
*
*/
public class TestThreadDemo02 {
public static void main(String[] args) throws InterruptedException {
ThreadDemo t = new ThreadDemo();
Thread th = new Thread(t);
ThreadDemo t2 = new ThreadDemo();
Thread th2 = new Thread(t2);
th.start();
th2.start();
Thread.sleep(2000);
//10ms后终止线程
t.stop();
t2.stop();
}
}
测试类3
package com.njwb.threadrunnable;
/**
* Thread(Runnable target, String name)
* 第1个参数是线程类(实现了Runnable接口的实现类),第2个参数可以指定线程的名字
分配新的 Thread 对象。
* @author Administrator
*
*/
public class TestThreadDemo03 {
public static void main(String[] args) throws InterruptedException {
ThreadDemo t = new ThreadDemo();
Thread th = new Thread(t,"线程A");
ThreadDemo t2 = new ThreadDemo();
Thread th2 = new Thread(t2,"线程B");
th.start();
th2.start();
Thread.sleep(2000);
//10ms后终止线程
t.stop();
t2.stop();
//1个代理,对应1个线程对象 ,不要1个代理对应多个线程对象
/*ThreadDemo t = new ThreadDemo();
Thread th = new Thread(t,"线程A");
ThreadDemo t2 = new ThreadDemo();
th = new Thread(t2,"线程B");
th.start();
th.start();
Thread.sleep(2000);
//10ms后终止线程
t.stop();
t2.stop();
t2.stop();*/
}
}
1.11. 资源争抢
web12306订票,
package com.njwb.threadrunnable;
public class Web12306 implements Runnable {
private int ticket = 50;
@Override
public void run() {
while(true){
if(ticket<=0){
break;
}
System.out.println(Thread.currentThread().getName()+"抢到了"+ticket--);
}
}
}
package com.njwb.threadrunnable;
public class Web12306App {
public static void main(String[] args) {
//同一份资源 50张票
Web12306 we = new Web12306();
//创建代理角色
Thread th1 = new Thread(we,"黄牛甲");
Thread th2 = new Thread(we,"黄牛已");
Thread th3 = new Thread(we,"黄牛丙");
//人为的控制
th1.setPriority(10);
th2.setPriority(1);
th3.setPriority(1);
//同时启动
th1.start();
th2.start();
th3.start();
}
}
1.12. 线程睡眠
什么是线程睡眠?
Thread.sleep(long millis)静态方法强制当前正在执行的线程休眠(暂停执行),以“减慢线程”。当线程睡眠时,它入睡在某个地方,在苏醒之前不会返回到可运行状态。当睡眠时间到期,则返回到可运行状态(就绪状态)。
为什么要睡眠?
线程执行太快。线程睡眠是帮助其他线程获得运行机会的最好方法。一个线程睡眠了,其他线程就有机会了。线程睡眠到期自动苏醒,并返回到可运行状态,不是运行状态。
注意:
线程睡眠到期自动苏醒,并返回到可运行状态,不是运行状态。sleep()中指定的时间是线程不会运行的最短时间。因此,sleep()方法不能保证该线程睡眠到期后就开始执行。
sleep()是静态方法,只能控制当前正在运行的线程。
举例:(1)1秒1个,倒数10个数
10
9
8…..
(2)一个计数器,计数到100,在每个数字之间暂停1秒,每隔10个数字输出一个字符串
1
2
3
…
10
*********************
11
……
20
*********
(3)倒计时
时:分:秒倒计时 ,假设当前时间是 15:38:27 ,10秒后 15:38:37 ,从10秒后往当前时间去显示
15:38:37
15:38:36
15:38:35
…..
15:38:27
分析: DateFormat df = new SimpleDateFormat(“HH:mm:ss”) String str = df.format(endTime ); ,
10秒后时间对象 Date endTime = new Date(System.currenteMillin()+10*1000);
/**
* 方式1: 次数
*/
public static void calTime(){
//获取10秒后的时间
Date endTime = new Date(System.currentTimeMillis()+10000);
//显示的格式 HH:mm:ss
DateFormat df = new SimpleDateFormat("HH:mm:ss");
for(int i=1;i<=11;i++){
System.out.println(df.format(endTime));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//构建下一秒的时间
endTime = new Date(endTime.getTime()-1000);
}
}
/**
* 方式2:break控制循环
*/
public static void calTime2(){
//获取10秒后的时间
Date endTime = new Date(System.currentTimeMillis()+10000);
//获取10秒的时间,对应的毫秒数
long end = endTime.getTime();
//显示的格式 HH:mm:ss
DateFormat df = new SimpleDateFormat("HH:mm:ss");
while(true){
System.out.println(df.format(endTime));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//构建下一秒的时间
endTime = new Date(endTime.getTime()-1000);
//end-10000表示的当前时间 ,endTime不断的构建下一秒的时间,如果不控制,就会一直往下减,
if(end-10000>endTime.getTime()){
break;
}
}
}
1.13. 线程优先级
优先级的定义有部分在不同系统间有差别。1-10,10表示优先级最高,默认值是5
举例: 该例题的线程类见 终止线程中的例题
package com.njwb.threadrunnable;
/**优先级:1-10
* MAX_PRIORITY 10
* MIN_PRIORITY 1
* 如果没有设置优先级,线程的默认级别是5 ,NORM_PRIORITY
* @author Administrator
*
*/
public class TestThreadDemo04 {
public static void main(String[] args) throws InterruptedException {
ThreadDemo t = new ThreadDemo();
Thread th = new Thread(t,"线程A");
ThreadDemo t2 = new ThreadDemo();
Thread th2 = new Thread(t2,"线程B");
th.setPriority(Thread.MAX_PRIORITY);
//th2.setPriority(7);
//等价于 th2.setPrority(1);
//th2.setPriority(Thread.MIN_PRIORITY);
//该句相当于设置的是5 ,和不设置是一样的,因为默认值就是5
th2.setPriority(Thread.NORM_PRIORITY);
th.start();
th2.start();
Thread.sleep(200);
//10ms后终止线程
t.stop();
t2.stop();
}
}
注意:不要假定高优先级的线程一定先于低优先级的线程执行,不要有逻辑依赖于线程优先级,否则可能产生意外结果
1.14. 终止线程
l 线程正常执行完毕
l 外部干涉
(1) 线程类中定义一个标志位
(2) 线程体中使用该标志位
(3) 提供一个改变标志位的方法
(4) 外部根据条件调用改变标志的方法
举例:
public class ThreadDemo implements Runnable {
int num = 0;
private boolean flag = true;
@Override
public void run() {
while(flag){
System.out.println(Thread.currentThread().getName()+"...--->跑了"+(num++)+"步");
}
}
public void stop(){
flag=!flag;
}
}
1.15. Yield(暂停线程)
package com.njwb.sleep;
public class YieldDemo implements Runnable{
@Override
public void run() {
for(int i=0;i<1000;i++){
System.out.println("YieldDemo...."+i);
/*if(i%20==0){
Thread.yield(); //暂停本线程th
}*/
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
package com.njwb.sleep;
/**
* static void yield() 静态方法 ,类名
暂停当前正在执行的线程对象,并执行其他线程。
yield()方法出现在哪个线程体内,就是暂停的谁
* @author Administrator
*
*/
public class YieldDemoApp {
public static void main(String[] args) throws InterruptedException {
//创建真实角色
YieldDemo yd = new YieldDemo();
//创建代理角色,代理持有对真实角色的引用
Thread th = new Thread(yd);
//代理启动
th.start();
for(int i=0;i<1000;i++){
if(i%20==0){
Thread.yield(); //暂停main
}
System.out.println("main....."+i);
Thread.sleep(100);
}
}
}
1.16. Join(合并线程)
package com.njwb.sleep;
public class JoinDemo implements Runnable{
public void run() {
for(int i=0;i<1000;i++){
System.out.println("JoinDemo...."+i);
}
}
}
package com.njwb.sleep;
/**
* void join() 写在哪个线程体内,就阻塞的哪个线程
等待该线程终止。
* @author Administrator
*
*/
public class JoinDemoApp {
public static void main(String[] args) {
JoinDemo jd = new JoinDemo();
Thread th = new Thread(jd,"线程Join");
th.start();
for(int i=0;i<1000;i++){
if(i==70){
try {
th.join(); //合并的main线程,阻塞的main
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("main....."+i);
}
}
}
1.17. 作业
(1)创建一个集合aList
写一个线程,每隔1秒向集合中添加一个随机数字
写一个线程,每隔1秒检查一下,当数字超过10个时,遍历输出集合中所有元素并清空集合。
思路: 订票 ,2个线程共享同一个ArrayList对象