15. 多线程
15.1 进程
进程是系统进行资源分配和调用的独立单元,每一个进程都有它的独立内存空间和系统资源。
15.1.1单进程操作系统和多进程操作系统
单进程操作系统:dos(一瞬间只能执行一个任务)
多进程单用户操作系统:Windows(一瞬间只能执行多个任务)
多进程多用户操作系统:Linux(一瞬间只能执行多个任务)
注:在理论上,现在的多核CPU可以让系统在同一个时刻行多个任务
15.2 线程
15.2.1 什么是线程?
线程是进程里面的一条执行路径,每个线程同享进程里面的内存空间和系统资源
一个进程 可以有 多个线程:各个线程都有不同的分工
15.2.2 线程和进程
进程和进程之间的关系
进程之间的内存空间和系统资源是独立的。
同一个进程里的多条线程
线程之间的内存空间和系统资源是共享的。
线程是在进程里的,他们是包含关系
注:
一个进程里可以有一条或一条以上的线程
进程里只有一条线程的情况下,这条线程就叫做主线程
进程里有多条线程的情况下,只有一条线程叫做主线程
15.3 创建多线程
1.线程类
创建MyThread类,继承Thread,重写run方法
public class Test01 {
public static void main(String[] args) {
//创建线程的对象
MyThread t = new MyThread();
//启动线程
t.start();
}
}
//线程类
class MyThread extends Thread{
//当前线程抢到cpu资源后,就会执行run方法
@Override
public void run() {
System.out.println("当前线程抢到资源了");
}
}
2.任务类
创建Task类,实现Runnable接口中的run方法
public class Test01 {
public static void main(String[] args) {
Thread t = new Thread(new Task());
t.start();
}
}
//任务类
class Task implements Runnable{
//当前线程抢到cpu资源后,就会执行run方法
@Override
public void run() {
System.out.println("抢到资源了");
}
}
15.4 感受多线程
需求:编写一个多线程的应用程序,观察其输出的结果,体会多线程互相争抢资源的场景
创建子线程类
public class A extends Thread{ @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println("A:"+i); } } }
public class B extends Thread{ @Override public void run() { for (int i = 0; i < 100; i++) { Thread.yield(); System.out.println("B:"+i); } } }
public class C extends Thread{ @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println("C:"+i); } } }
测试方法
//创建子线程对象 A a = new A(); B b = new B(); C c = new C(); //开启线程 a.start(); b.start(); c.start(); //主线程打印 for(int i=0;i<100;i++){ System.out.println("主线程:"+i); }
观察输出结果,会发现每次运行的结果都不相同,主线程也并非每次都是最先打印,这说明,主线程和子线程,子线程和子线程,都在同时争夺cup资源,而这种争夺是相对随机的,所以,多线程是有不确定性的存在。
注:
进程 与 进程 的关系:独享内存空间和系统资源
线程 与 进程 的关系:有一个进程中至少包含一个线程
线程 与 线程 的关系:在同一个进程里,多个线程共享内存空间和系统资源
一个进程中可以包含多个线程,但只能有一个主线程
经典面试题:请问当我们编写一个单纯的main方法时,此时该程序是否为单线程的?为什么?
垃圾回收器是一个后台线程
如果子线程之间的任务类似,有多个重复,可以只定义一个线程类,创建多个对象,每一个对象就代表着一条子线程。
示例:
public class MyThread extends Thread { public MyThread(String name) { super(name); //调用父类的构造方法,进行线程命名 } @Override public void run() { for (int i = 0; i < 100; i++) { Thread t= Thread.currentThread(); //获取当前线程的对象 String name = t.getName(); //获取当前对象的名字 System.out.println(name+":"+i); } } }
测试方法
MyThread A = new MyThread("A"); MyThread B = new MyThread("B"); MyThread C = new MyThread("C"); A.start(); B.start(); C.start(); /* B:0 B:1 ...... B:12 C:0 C:1 A:0 A:1 ...... */ //输出顺序相对随机
15.5 Thead中的常用方法
15.5.1 setPriority–设置优先级
需求:在主线程中创3个子线程,并且设置不同优先级,观察其优先级对线程执行结果的”影响”。
示例:
//创建子线程对象 MyThread a = new MyThread("A"); MyThread b = new MyThread("B"); MyThread c = new MyThread("C"); //设置线程对象优先级 a.setPriority(Thread.MAX_PRIORITY); b.setPriority(Thread.NORM_PRIORITY); c.setPriority(Thread.MIN_PRIORITY); //开启线程 a.start(); b.start(); c.start();
通过观察结果发现,这种影响也是不稳定的,但a子线程设置的优先级最高,多次的运行结果表明,a线程先抢到资源的概率稍大,c线程先抢到资源的概率稍小,但也只是略有影响。
15.5.2 set/getName–设置/获取线程名
在15.4–感受多线程中,学会使用利用子线程类的构造函数,调用super父类构造方法,对线程名进行设置。
除此之外,还可以使用setName();方法进行手动设置。
示例:
//创建子线程对象。 MyThread A = new MyThread("A"); System.out.println(A.getName()); //A A.setName("AAAA"); System.out.println(A.getName()); //AAAA
15.5.3 sleep–线程休眠
需求:编写一个随机点名的程序,要求倒数三秒后输出被抽中的姓名
//创建随机数对象 Random random = new Random(); //创建点名数组 String[] names={"小红","小郑","小南","小贝","小东","小西"}; //生成随机数 int nextInt = random.nextInt(names.length); for(int i=3;i>0;i--){ System.out.println(i); Thread.sleep(1000); //主线程休眠一秒 } System.out.println(names[nextInt]); /* 3 2 1 小东 */
注:sleep方法一般很少使用。(浪费时间资源和空间资源)
15.5.4 yield–线程礼让
需求:创建两个线程A,B,分别各打印1-100的数字,其中B一个线程,每打印一次,就礼让一次,观察实验结果
子线程类
public MyThread(String name) { super(name); } @Override public void run() { for (int i = 0; i < 100; i++) { Thread t= Thread.currentThread(); String name = t.getName(); if(name.equals("B")) t.yield(); System.out.println(name+":"+i); } } }
测试方法
MyThread A = new MyThread("A"); MyThread B = new MyThread("B"); A.start(); B.start(); /* ...... A:10 A:11 B:0 B:1 B:2 B:3 B:4 B:5 B:6 A:12 B:7 A:13 B:8 B:9 ...... */
注:
并不是B线程只执行一次就轮到A线程执行,而是让前线程退出CPU资源,并转到就绪状态,接着再抢,会出现连续抢到的概率。
yield方法为静态方法,此方法写在哪个线程中,哪个线程就礼让
15.5.5 join–线程合并
需求:主线程和子线程各打印100次,从1开始每次增加1,当主线程打印到10之后,让子线程先打印完再打印主线程
//创建子线程对象 A a = new A();//见15.4 A类 //开启线程 a.start(); for(int i=0;i<100;i++){ System.out.println("主线程:"+i); if(i==10){ try { a.join(); //此方法需要加入受检异常,这里采用try/catch语句。 } catch (InterruptedException e) { e.printStackTrace(); } } } /* ..... 主线程:6 A:0 ..... A:5 A:6 主线程:7 A:7 主线程:8 主线程:9 主线程:10 A:8 A:9 A:10 ...... */ //注:运行结果充满随机
通过结果能够看出在主线程运行到10时,主线程暂停争抢CPU资源,让A子线程先打印结束在打印主线程内容。
15.5.5 线程的中断
方法介绍:
- isInterrupted():获取当前线程的状态(true-中断 false-存活)
- interrupt(): 改变线程状态
示例:
子线程类
public class MyThread extends Thread {
@Override
public void run() {
//isInterrupted() - 获取当前线程状态(true-中断 false-存活)
while(!Thread.currentThread().isInterrupted()){
System.out.println("111");
System.out.println("222");
System.out.println("333");
System.out.println("444");
}
}
}
测试方法
MyThread t = new MyThread();
t.start();
Thread.sleep(3000);
t.interrupt();//改变线程状态
//子线程开启,打印结果,在主线三秒休眠后,调用interrupt方法,改变子线程状态,子线程中断打印结束。
15.5.6 守护线程
守护线程 默默守护着前台线程,当所有的前台线程都消亡后,守护线程会自动消亡
注意:垃圾回收器就是守护线程
守护线程类
public class GuardThread extends Thread{ @Override public void run() { while(true){ System.out.println("守护线程~~~~"); try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
测试方法
public static void main(String[] args) { GuardThread thread = new GuardThread(); thread.setDaemon(true); //将线程置为守护线程 thread.start(); //开启线程 for(int i=0;i<=5;i++){ System.out.println("主线程~~~"+i); try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } /* 主线程~~~0 守护线程~~~~ 主线程~~~1 守护线程~~~~ 守护线程~~~~ 主线程~~~2 守护线程~~~~ 主线程~~~3 守护线程~~~~ 主线程~~~4 主线程~~~5 守护线程~~~~ 守护线程~~~~ */
由结果可以观察到,守护线程在前台线程执行完成之后,就自动销毁,并不需要手动关闭。
15.6 参数传递
15.6.1 传递单个参数
使用ConcurrentHashMap<Thread,参数>集合(ConcurrentHashMap为线程安全的集合)
使用示例:
public class A {
public void println(){
Integer num = Test_2.map.get(Thread.currentThread());
System.out.println(Thread.currentThread().getName()+" A类的println方法:"+num);
}
}
public class B {
public void println(){
Integer num = Test_2.map.get(Thread.currentThread());
System.out.println(Thread.currentThread().getName()+" B类的println方法:"+num);
}
}
public class Test_2 {
//ConcurrentHashMap集合传递单个参数。
public static ConcurrentHashMap<Thread,Integer> map =new ConcurrentHashMap<Thread, Integer>();
public static void main(String[] args) {
new Thread(new Runnable() {
public int num=10;
@Override
public void run() {
map.put(Thread.currentThread(), num);
A a = new A();
B b = new B();
a.println();
b.println();
}
}, "线程1").start();
new Thread(new Runnable() {
public int num=20;
@Override
public void run() {
map.put(Thread.currentThread(), num);
A a = new A();
B b = new B();
a.println();
b.println();
}
}, "线程2").start();
}
}
/*
线程1 A类的println方法:10
线程1 B类的println方法:10
线程2 A类的println方法:20
线程2 B类的println方法:20
*/
15.6.2 传递多个参数
-
依然能够使用使用ConcurrentHashMap集合,value值可以采用数组,数据类,集合。
-
但,线程中提供了一种专门的集合ThreadLocal,底层通过map实现,在线程中使用起来比ConcurrentHashMap更加方便。
示例:
public class A { public void println(){ Data data= Test_1.local.get(); System.out.println(Thread.currentThread().getName()+"A类的println方法:"+data); } }
public class B { public void println(){ Data data= Test_1.local.get(); System.out.println(Thread.currentThread().getName()+"B类的println方法:"+data); } }
package com.dream.test01; public class Data { private int num; private String name; //构造方法修饰为private,不允许外部创建对象 private Data(int num,String name){ this.name=name; this.num=num; } public int getNum() { return num; } public void setNum(int num) { this.num = num; } public String getName() { return name; } public void setName(String name) { this.name = name; } //通过方法setDate创建对象,保证每个线程只能创建一个data对象。 public static Data setData(int num,String name){ Data data = Test_1.local.get(); if(data==null){ return new Data(num,name); } else{ data.setName(name); data.setNum(num); } return data; } @Override public String toString() { return "Data [num=" + num + ", name=" + name + "]"; } }
public class Test_1 { //ConcurrentHashMap集合传递单个参数。 public static ConcurrentHashMap<Thread,Integer> map =new ConcurrentHashMap<Thread, Integer>(); //使用ThreadLocal传递多个参数(参数放在数据类中) public static ThreadLocal<Data> local= new ThreadLocal<>(); public static void main(String[] args) { new Thread(new Runnable() { public int num=10; public String name="线程1"; @Override public void run() { local.set(Data.setData(num, name)); A a = new A(); B b = new B(); a.println(); b.println(); } }, "线程1").start(); new Thread(new Runnable() { public int num=20; public String name="线程2"; @Override public void run() { local.set(Data.setData(num, name)); A a = new A(); B b = new B(); a.println(); b.println(); } }, "线程2").start();; } } /* 线程2A类的println方法:Data [num=20, name=线程2] 线程1A类的println方法:Data [num=10, name=线程1] 线程1B类的println方法:Data [num=10, name=线程1] 线程2B类的println方法:Data [num=20, name=线程2] */
15.7 线程的生命周期
1、新建状态
i. 在程序中用构造方法创建了一个线程对象后,新的线程对象便处于新建状态,此时,它已经有了相应的内存空间和其它资源,但还处于不可运行状态。新建一个线程对象可采用线程构造方法来实现。
ii. 例如:Thread thread=new Thread();
2、 就绪状态
i. 新建线程对象后,调用该线程的start()方法就可以启动线程。当线程启动时,线程进入就绪状态。此时,线程将进入线程队列排队,等待CPU调用,这表明它已经具备了运行条件。
3、运行状态
i. 当就绪状态的线程被调用并获得处理器资源时,线程就进入了运行状态。此时,自动调用该线程对象的run()方法。run()方法定义了该线程的操作和功能。
4、 阻塞状态
i. 一个正在执行的线程在某些特殊情况下,如被人为挂起,将让出CPU并暂时中止自己的执行,进入阻塞状态。在可执行状态下,如果调用sleep(2000)、wait()等方法,线程都将进入阻塞状态。阻塞时,线程不能进入排队队列,只有当引起阻塞的原因被消除后,线程才可以转入就绪状态。
5、死亡状态
i. 线程调用stop()方法时或run()方法执行结束后,线程即处于死亡状态。处于死亡状态的线程不具有继续运行的能力。
15.8 线程安全
线程安全的三种方式:
1.局部加锁
局部加锁采用synchronized关键字
结构:
synchronized(Object){
…需要加锁的代码块…
}
含义:进入代码块自动加锁,出代码块自动开锁。
注:在线程类中Object应该使用所有类对象共有的对象,如:static修饰的静态类变量(private static Object obj = new Object();),字符串常量,类的.class文件对象。
在任务类中应该使用各自对象拥有的类,如:成员变量,this。
2.方法加锁
采用synchronized修饰方法。
结构:
public(访问修饰符) synchronized void(返回类型) method(方法名){
…方法代码块…
}
进入方法自动加锁,退出方法,自动开锁。
注:该方法有static修饰时,锁对象为类的字节码文件对象(线程类使用),无static修饰,锁对象只有调用的本对象(任务类使用)。
3.手动加锁lock()/unlock()
结构:
Lock lock =new ReentrantLock(); public void run() { lock.lock(); //手动上锁 //...需要加锁的代码块... lock.unlock(); //手动关锁 }
注:在线程类中应该使用static修饰lock为静态变量,而在任务类中lock则不应该被修饰为静态变量。
原因:
子线程类中每创建一个线程就需要新new一个线程对象,需要lock锁能够锁住该线程类创建的所有线程对象,需要使用static静态修饰,如果不使用静态修饰,该锁将会无任何意义。
而在任务类中,因为任务类对象在创建对象之后,只需要加入新new的线程类,所以在上锁时,是以对象为单位,如果使用static修饰,该任务类的所有对象都将要共享这一个锁,会导致该任务类的其他任务对象也被锁。
详细示例可以见 拓展:多线程模拟售票。
拓展示例:
1.计算任务,一个包含了2万个整数的数组,分拆了多个线程来进行并行计算,最后汇总出计算的结果。
子线程类
public class MyThread extends Thread{ private int startindex; private int endindex; private int [] array; private int sum; public MyThread(String name,int startindex, int endindex, int[] array) { super(name); this.startindex = startindex; this.endindex = endindex; this.array = array; } public int add(){ return sum; } @Override public void run() { for (int i = startindex; i <endindex; i++) { sum+=array[i]; System.out.println(Thread.currentThread().getName()+"正在计算....."); } } }
测试方法:
public static void main(String[] args) throws InterruptedException { int [] array=new int[20000]; for(int i=0;i<20000;i++){ array[i]=i+1; } MyThread A = new MyThread("A",0,5000,array); MyThread B = new MyThread("B",5000,10000,array); MyThread C = new MyThread("C",10000,15000,array); MyThread D = new MyThread("D",15000,20000,array); A.start(); B.start(); C.start(); D.start(); A.join(); B.join(); C.join(); D.join(); System.out.println(A.add()+B.add()+C.add()+D.add()); } /* ...... 200010000 */
2.铁道部发布了一个售票任务,要求销售1000张票,要求有3个窗口来进行销售,请编写多线程程序来模拟这个效果。
i. 窗口001正在销售第1000张票
ii. 窗口001正在销售第999张票
iii. 窗口002正在销售第998张票
iv. 。。。
v. 窗口002正在销售第1张票
涉及到线程安全,要加锁。
线程类:
public class SellTicket extends Thread{ //设置总票数 private static int tickets=1000; private static String str = new String ("start"); public SellTicket(String name){ super(name); } @Override public void run() { while(tickets > 0) { synchronized(str)//同步代码块 { if(tickets > 0) { System.out.printf ("%s窗口正在售出第%d张票\n",Thread.currentThread().getName(),tickets); tickets--; } if(tickets<=0){ System.out.printf("%s窗口售罄\n",Thread.currentThread().getName()); } } } } }
测试方法:
public static void main(String[] args) { SellTicket A = new SellTicket("001"); SellTicket B = new SellTicket("002"); SellTicket C = new SellTicket("003"); A.start(); B.start(); C.start(); } /* ...... 002窗口正在售出第2张票 002窗口正在售出第1张票 002窗口售罄 003窗口售罄 001窗口售罄 */
详细示例可以见 拓展:多线程模拟售票。