课程内容
1、多线程相关的三组概念
2、多线程的实现方式
3、Thread类中的常用方法
4、多线程中的线程安全问题
5、线程状态
6、线程池
多线程相关的三组概念
程序和进程
1、程序(program):一个固定的运行逻辑和数据的集合,是一个静态的状态,一般存储在硬盘中
2、进程(process):一个正在运行的程序,是一个程序的一次运行,是一个动态的概念,一般存
储在内存中。
例如:ctrl + shift + esc,打开任务管理器可以看到所有进程
进程和线程
1、进程(process):一个正在执行的程序,有自己独立的资源分配,是一个独立的资源分配单位
Cpu、内存
2、线程(thread):一条独立的执行路径。多线程,在执行某个程序的时候,该程序可以有多个
子任务,每个线程都可以独立的完成其中一个任务。在各个子任务之间,没有什么依赖关系,
可以单独执行。
3、进程和线程的关系:
进程是用于分配资源的单位
一个进程中,可以有多条线程;但是一个进程中,至少有一条线程
线程不会独立的分配资源,一个进程中的所有线程,共享同一个进程中的资源
并行和并发
1、并行(parallel):多个任务(进程、线程)同时运行。在某个确定的时刻,有多个任务在执
行条件:有多个cpu,多核编程
2、并发(concurrent):多个任务(进程、线程)同时发起。不能同时执行的(只有一个cpu),
只能是同时要求执行。就只能在某个时间片内,将多个任务都有过执行。一个cpu在不同的任务
之间,来回切换,只不过每个任务耗费的时间比较短,cpu的切换速度比较快,所以可以让用户
感觉就像多个任务在同时执行。
Q&A:既然只有一个cpu,还需要在不同的任务之间来回切换,那么效率到底是提升了还是降低了?
1、提升了:整个系统的效率提升了(cpu的使用率提升了),对于某个具体的任务而言,效率是降
低了
2、并发技术,解决的是不同的设备之间速率不同的问题
Cpu:10^-9秒
内存:10^-6秒
磁盘:10^-3秒
人:10^0秒
cpu 的效率非常高,给磁盘一个指令,让磁盘寻找某个字节,cpu就赶紧切换到其他的任务,执行
其他的计算、存储的操作。这样子就可以让计算机系统中的所有设备都运转起来,提升了cpu的
使用率。绝不能出现cpu这种昂贵的设备等待磁盘这种廉价设备的情况。
多线程的实现方式
多线程实现的第一种方式:继承方式
步骤:
1、定义一个类,继承Thread类
2、重写自定义类中的run方法,用于定义新线程要运行的内容
3、创建自定义类型的对象
4、调用线程启动的方法:run方法
package com.ujiuye.demos;
public class Demo01_多线程的继承方式 {
public static void main(String[] args) {
//创建自定义类型的对象
MyThread mt = new MyThread();
//调用对象的start方法,启动线程
mt.start();
//让主线程(主方法本来就所在的线程)继续运行
for(int i = 1; i <= 100; i++) {
System.out.println(i + "...主线程");
}
}
}
//定义一个类,继承Thread类
class MyThread extends Thread {
//重写run方法,定义线程的运行内容
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println(i + "...MyThread");
}
}
}
多线程实现的第二种方式:实现方式
步骤:
1、定义一个任务类,实现Runnable接口
2、重写任务类中的run方法,用于定义任务的内容
3、创建任务类对象,表示任务
4、创建一个Thread类型的对象,用于执行任务类对象
5、调用线程对象的start方法,开启新线程
package com.ujiuye.demos;
public class Demo02_多线程的实现方式 {
public static void main(String[] args) {
MyTask mt = new MyTask();//创建任务类对象
Thread t = new Thread(mt);//创建线程对象,绑定要执行的任务
t.start();//启动新线程
for(int i = 1; i <= 100; i++) {
System.out.println(i + "...主线程");
}
}
}
//定义一个任务类
class MyTask implements Runnable {
//重写run方法,定义任务的内容
@Override
public void run() {
for(int i = 1; i <= 100; i++) {
System.out.println(i + "...MyTask");
}
}
}
两种方式的比较
1、代码复杂程度:
继承Thread方式简单
实现Runnable接口的方式比较复杂
2、实现原理:
继承方式:调用start方法,调用start0方法,start0是本地方法(native),由虚拟机实
现,是C语言实现的方法,所以在java中看不到代码。本地方法start0返回来调用java中的
run方法,run方法已经在子类中重写过了,所以最终运行的是子类重写了的run方法
实现方式:构造方法中,将Runnable的实现类对象传入构造方法中,经过一路init方法的传
递,最终,用于给Thread类型中的某个成员变量(target)赋值;调用对象的start方法,
最终也是返回来调用Thread类中的run方法,判断当前的成员变量target是否为null,如果
不为null,就调用target的run方法,而这个run方法我们已经重写过了,最终运行的是我们
重写过的run方法。
3、设计:java中只支持单继承、不支持多继承
继承方式:某个类继承了Thread类,那么就无法继承其他业务中需要的类型,就限制了我们
的设计。所以扩展性较差。
实现方式:某个类通过实现Runnable的方式完成了多线程的设计,仍然可以继承当前业务中
的其他类型,扩展性较强。
4、灵活性:
继承方式:将线程对象和任务内容绑定在了一起,耦合性较强、灵活性较差
实现方式:将线程对象和任务对象分离,耦合性就降低,灵活性增强:同一个任务可以被多个
线程对象执行,某个线程对象也可以执行其他的任务对象。并且将来还可以将任务类对象,提
交到线程池中运行;任务类对象可以被不同线程运行,方便进行线程之间的数据交互。
使用匿名内部类创建线程对象
package com.ujiuye.demos;
public class Demo03_匿名内部类创建线程对象 {
public static void main(String[] args) {
//第一条线程打印1-100,使用继承的方式
new Thread() {
@Override
public void run() {
for(int i = 1; i <= 100; i++) {
System.out.println(i + "...extends");
}
}
}.start();
//第二条线程打印1-100,使用实现的方式
new Thread(new Runnable() {
@Override
public void run() {
for(int i = 1; i <= 100; i++) {
System.out.println(i + "...implements" );
}
}
}).start();
}
}
Thread类中的常用方法
获取线程名称
1、getName():获取线程的名称
2、注意事项:
1、如果没有给线程命名,那么线程的默认名字就是Thread-x,x是序号,从0开始
2、可以使用对象的引用调用getName方法,也可以在线程类中,调用getName
3、Runnable的实现类中,没有getName方法
package com.ujiuye.demos;
public class Demo04_线程名称获取 {
public static void main(String[] args) {
Thread t = new Thread() {
public void run() {
System.out.println(666);
}
};
t.start();
//在主线程中,获取其他线程的名称
System.out.println(t.getName());
new Thread() {
public void run() {
for (int i = 1; i <= 5; i++) {
//在自己的线程中,获取自己的线程名称
System.out.println(this.getName());
}
}
}.start();
}
}
class MyRun implements Runnable {
@Override
public void run() {
//不是Thread类型,所以无法获取线程名称
// System.out.println(this.getName());
}
}
设置线程名称
1、setName(String name)
使用线程对象的引用,调用该方法给线程起名字
2、构造方法:
Thread(String name):给线程通过构造方法起名字
Thread(Runnable target, String name):在给线程target对象赋值的同时,也给线程
起名
3、注意事项:
线程设置名字,既可以在启动之前设置,也可以在启动之后设置
获取当前线程对象
1、作用:
某段代码只要是在执行,就一定是在某个方法中执行的,只要在某个方法中,代码就一定是
被某个线程执行的。所以任意一句代码,都有执行这句代码的线程对象。
可以在任意位置,获取当前正在执行这段代码的线程对象
2、方法:
Thread Thread.currentThread();
返回当前正在执行这段代码的线程对象的引用
哪条线程在执行这段代码,返回的就是哪条线程
package com.ujiuye.demos;
public class Demo06_获取当前线程对象 {
public static void main(String[] args) {
MyR mr = new MyR();
Thread t = new Thread(mr);
t.setName("线程1");
t.start();
Thread t2 = new Thread(mr);
t2.setName("线程2");
t2.start();
}
}
class MyR implements Runnable {
@Override
public void run() {
Thread c = Thread.currentThread();
System.out.println(c.getName());
}
}
练习
1、获取主方法所在的线程的名称
2、获取垃圾回收线程的线程名称
package com.ujiuye.demos;
public class Demo07_获取当前线程练习 {
/**
* 1、获取主方法所在的线程的名称
2、获取垃圾回收线程的线程名称
*/
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName());
//创建垃圾对象,让垃圾回收线程自动运行起来
for (int i = 1; i <= 999999; i++) {
//匿名对象就是垃圾对象
new Demo();
}
}
}
class Demo {
@Override
protected void finalize() throws Throwable {
//由于垃圾回收方法,被垃圾回收线程调用,所以里面的代码都是垃圾回收线程运行的
System.out.println(Thread.currentThread().getName());
}
}
线程休眠
1、Thread.sleep(long time)
让当前线程休眠
time:休眠的时间,单位是毫秒
2、作用:
当某段代码在某个位置需要休息的时候,就使用线程的休眠
无论哪个线程在运行这段代码,碰到sleep方法都需要停下休息
3、注意事项:
有一个异常,中断异常:InterruptedException
在普通方法中,可以声明
在run方法中,必须只能处理,不能声明
守护线程
1、setDaemon(boolean flag):每条线程默认不是守护线程,只有设定了flag为true之后,该
线程才变成守护线程
2、isDaemon():判断某条线程是否是守护线程
3、守护线程的特点:
守护线程就是用于守护其他线程可以正常运行的线程,在为其他的核心线程准备良好的运行
环境。如果非守护线程全部死亡,守护线程就没有存在的意义了,一段时间之后,虚拟机也
一同结束。
4、别名:
后台线程
package com.ujiuye.demos;
public class Demo09_守护线程 {
public static void main(String[] args) {
Thread t1 = new Thread() {
public void run() {
while(true) {//定义死循环作为线程内容
System.out.println(666 + "....newTh");
}
}
};
//设置为守护线程
t1.setDaemon(true);
t1.start();//启动之后,系统中就有两条线程:主线程和守护线程
for (int i = 1; i <= 999; i++) {
System.out.println(i + "...main");
}
//主线程运行完成之后,系统中就只剩下了守护线程
}
}
练习
分别从【作用上】和【代码】上:判断垃圾回收线程是否是守护线程
package com.ujiuye.demos;
public class Demo10_守护线程练习 {
//分别从【作用上】和【代码】上:判断垃圾回收线程是否是守护线程
public static void main(String[] args) {
new Gab();
System.gc();
}
}
class Gab {
@Override
public void finalize() {
System.out.println(Thread.currentThread().isDaemon());
}
}
线程优先级
1、通过某些方法,设定当前线程的优先级,优先级高的线程先运行(在前面的时间段内,高优先
级的线程会运行的多一些),优先级低的线程后运行(在后面的时间段内低优先级的线程运行
多一些)
2、setPriority(int p):通过给定优先级数字设定优先级,数字越大,优先级越高
数字范围:最小1,自大的是10,默认状态就是5
3、有三个优先级常量:
MAX_PRIORITY 值为10
NORM_PRIORITY 值是5
MIN_PRIORITY 值为1
多线程中的线程安全问题
问题描述
某段代码在没有执行完成的时候,cpu就可能被其他线程抢走,结果导致当前代码中的一些数据发
生错误
原因:没有保证某段代码的执行的完整性、原子性
希望:这段代码要么全都执行,要么全都没有执行
同步代码块
1、同步代码块:
使用一种格式,达到让某段代码执行的时候,cpu不要切换到影响当前代码的代码上去
这种格式,可以确保cpu在执行A线程的时候,不会切换到影响A线程执行的其他线程上去
2、使用格式
synchronized (锁对象) {
需要保证完整性、原子性的一段代码(需要同步的代码)
}
3、使用同步代码块之后的效果:
当cpu想去执行同步代码块的时候,需要先获取到锁对象,获取之后就可以运行代码块中的内
容;当cpu正在执行当前代码块的内容时,cpu可以切换到其他代码,但是不能切换到具有相
同锁对象的代码上。
当cpu执行完当前代码块中的代码之后,就会释放锁对象,cpu就可以运行其他具有当前锁对
象的同步代码块了
package com.ujiuye.demos;
public class Demo13_同步代码块 {
public static void main(String[] args) {
Printer p = new Printer();
Thread t1 = new Thread() {
public void run() {
while(true) {
p.print1();
}
}
};
Thread t2 = new Thread() {
public void run() {
while(true) {
p.print2();
}
}
};
t1.start();
t2.start();
}
}
class Printer {
Object obj = new Object();
public void print1() {
//线程1在获取了obj锁之后,线程2就无法获取obj锁,就只能等待线程1运行完这个方
法,两条线程才能再次共同争夺锁对象
synchronized (obj) {
System.out.print("中");
System.out.print("公");
System.out.print("教");
System.out.println("育");
}
}
public void print2() {
synchronized (obj) {
System.out.print("优");
System.out.print("就");
System.out.println("业");
}
}
}
同步方法
1、同步代码块:在某段代码执行的时候,不希望cpu切换到其他影响当前线程的线程上去,就在
这段代码上加上同步代码块
2、如果某个方法中,所有的代码都需要加上同步代码块,使用同步方法这种【简写格式】来替代
同步代码块
3、同步方法的格式:
权限修饰符 [静态修饰符] synchronized 返回值类型 方法名称(参数列表) {
需要同步的方法体
}
4、同步方法的锁对象
如果是非静态的方法,同步方法的锁对象就是this,当前对象,哪个对象调用这个同步方法,
这个同步方法使用的锁就是哪个对象
如果是静态的方法,同步方法的锁对象就是当前类的字节码对象,类名.class(在方法区的
一个对象),哪个类在调用这个同步方法,这个同步方法使用的锁就是哪个类的字节码对象
锁对象的说明
1、线程加上同步是为了让两条线程相互不要影响,如果相互影响,影响的是数据的正确性
2、使用锁对象,其实主要是为了锁数据。当某条线程在操作这个数据时,其他线程不能操作当前
的数据。
3、如果两条线程要操作相同的数据,那么这两条线程就需要在操作数据的部分都加上同步代码块
,并且使用相同的锁对象。
4、到底使用什么锁:要保护什么数据,就使用这个数据作为锁对象
死锁
1、A线程需要甲资源,同时拥有乙资源;B线程需要乙资源,同时拥有甲资源,两条线程都不肯释
放自己拥有的资源,同时也需要其他的资源时,就都无法进行运行。形成“死锁”现象。
2、代码表现:
有了同步代码块的嵌套,就可能发生死锁。某条线程获取了外层的锁对象A,需要内层的锁
对象B,等待;另外一条线程获取了外层的锁对象B,需要内层的锁对象A,等待。两条线程
就会形成死锁。
package com.ujiuye.demos;
public class Demo15_死锁 {
public static void main(String[] args) {
Thread t1 = new Thread("苏格拉底") {
public void run() {
while (true) {
synchronized ("筷子A") {
System.out.println("苏格拉底争取到了筷子A,等待筷子B");
synchronized ("筷子B") {
System.out.println("苏格拉底获取了所有的筷子,狂吃");
}
}
}
}
};
Thread t2 = new Thread("柏拉图") {
public void run() {
while (true) {
synchronized ("筷子B") {
System.out.println("柏拉图争取到了筷子B,等待筷子A");
synchronized ("筷子A") {
System.out.println("柏拉图获取了所有的筷子,狂吃");
}
}
}
}
};
t1.start();
t2.start();
}
}
线程安全火车票案例
除了使用同步代码块,还可以用lock接口
package 线程安全性问题;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Vector;
import java.util.Vector;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo08_多线程售票Lock {
public static void main(String[] args) {
//创建任务类对象
Seller s = new Seller();
//将任务类对象作为参数传入构造方法中
Thread t1 = new Thread(s, "窗口A");
Thread t2 = new Thread(s, "窗口B");
Thread t3 = new Thread(s, "窗口C");
t1.start();
t2.start();
t3.start();
}
}
class Seller implements Runnable{
Lock lk = new ReentrantLock();
int ticket = 100;
@Override
public void run() {
while(true){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
lk.lock();//给当前线程对象加锁
if(ticket <= 0){
break;
}
ticket--;
System.out.println(Thread.currentThread().getName()+"卖出一张票,
剩余"+ticket+"张票");
lk.unlock();//给当前线程对象释放锁
}
}
}
作业
1、有一辆班车除司机外只能承载80个人,假设前中后三个车门都能上车,如果坐满则不能再上。
请用线程模拟上车过程并且在控制台打印出是从哪个车门上车以及剩下的座位数。
比如:
(前门上车---还剩N个座...)
2、同时开启3个线程,共同输出100~200之间的所有数字,并且在输出奇数的时候将线程名称打
印出来
package 练习16_20;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/*1、有一辆班车除司机外只能承载80个人,假设前中后三个车门都能上车,如果坐满则不能再上
请用线程模拟上车过程并且在控制台打印出是从哪个车门上车以及剩下的座位数。
比如:
(前门上车---还剩N个座...)
*/
public class Demo01 {
public static void main(String[] args) {
Bus b = new Bus();//创建任务类对象
Thread t1 = new Thread(b,"前门");
Thread t2 = new Thread(b,"中门");
Thread t3 = new Thread(b,"后门");
t1.start();
t2.start();
t3.start();
}
}
class Bus implements Runnable{
int num =80;
Lock l = new ReentrantLock();
@Override
public void run() {
while(true) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
l.lock();
if(num<=0) {
System.out.println("座位已满,无法再上乘客");
break;
}
else{
num--;
System.out.println(Thread.currentThread().getName()+"上来了一名
乘客,还剩"+num+"个座位");
}
l.unlock();
}
}
}
package 练习16_20;
//2、同时开启3个线程,共同输出100~200之间的所有数字,并且在输出奇数的时候将线程名称
打印出来
public class Demo02 {
public static void main(String[] args) {
Printf P = new Printf();//要抢的任务
Thread t1 = new Thread(P,"线程1");
Thread t2 = new Thread(P,"线程2");
Thread t3 = new Thread(P,"线程3");
t1.start();
t2.start();
t3.start();
}
}
class Printf implements Runnable{
//int num = 100;
Object obc = new Object();
@Override
public void run() {
synchronized (obc) {
for (int i = 100; i <= 200; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(i);
if (!((i % 2) == 0)) {
System.out.println(Thread.currentThread().getName());
}
}
}
}
}
线程状态(一图解决)
等待唤醒案例(包子铺卖包子)
生成包子类
public class BaoZiPu extends Thread{
private List<String> list ;
public BaoZiPu(String name,ArrayList<String> list){
super(name);
this.list = list;
}
@Override
public void run() {
int i = 0;
while(true){
//list作为锁对象
synchronized (list){
if(list.size()>0){
//存元素的线程进入到等待状态
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果线程没进入到等待状态 说明集合中没有元素
//向集合中添加元素
list.add("包子"+i++);
System.out.println(list);
//集合中已经有元素了 唤醒获取元素的线程
list.notify();
}
}
}
}
}
消费包子类
public class ChiHuo extends Thread {
private List<String> list ;
public ChiHuo(String name,ArrayList<String> list){
super(name);
this.list = list;
}
@Override
public void run() {
//为了能看到效果 写个死循环
while(true){
//由于使用的同一个集合 list作为锁对象
synchronized (list){
//如果集合中没有元素 获取元素的线程进入到等待状态
if(list.size()==0){
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果集合中有元素 则获取元素的线程获取元素(删除)
list.remove(0);
//打印集合 集合中没有元素了
System.out.println(list);
//集合中已经没有元素 则唤醒添加元素的线程 向集合中添加元素
list.notify();
}
}
}
}
}
测试类
public class Demo {
public static void main(String[] args) {
//等待唤醒案例
List<String> list = new ArrayList<>();
// 创建线程对象
BaoZiPu bzp = new BaoZiPu("包子铺",list);
ChiHuo ch = new ChiHuo("吃货",list);
// 开启线程
bzp.start();
ch.start();
}
}
线程池
合理利用线程池能够带来三个好处:
- 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个
任务。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目
- 防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,
消耗的内存也就越大,最后死机)。
Runnable实现类代码:
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("我要一个教练");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("教练来了: " + Thread.currentThread().getName());
System.out.println("教我游泳,交完后,教练回到了游泳池");
}
}
线程池测试类:
public class ThreadPoolDemo {
public static void main(String[] args) {
// 创建线程池对象
ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
// 创建Runnable实例对象
MyRunnable r = new MyRunnable();
//自己创建线程对象的方式
// Thread t = new Thread(r);
// t.start(); ---> 调用MyRunnable中的run()
// 从线程池中获取线程对象,然后调用MyRunnable中的run()
service.submit(r);
// 再获取个线程对象,调用MyRunnable中的run()
service.submit(r);
service.submit(r);
// 注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。
// 将使用完的线程又归还到了线程池中
// 关闭线程池
//service.shutdown();
}
}
-
Future submit(Callable task) : 获取线程池中的某一个线程对象,并执行.
Future : 表示计算的结果.
-
V get() : 获取计算完成的结果。
Callable测试代码:
public class ThreadPoolDemo2 {
public static void main(String[] args) throws Exception {
// 创建线程池对象
ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
// 创建Runnable实例对象
Callable<Double> c = new Callable<Double>() {
@Override
public Double call() throws Exception {
return Math.random();
}
};
// 从线程池中获取线程对象,然后调用Callable中的call()
Future<Double> f1 = service.submit(c);
// Futur 调用get() 获取运算结果
System.out.println(f1.get());
Future<Double> f2 = service.submit(c);
System.out.println(f2.get());
Future<Double> f3 = service.submit(c);
System.out.println(f3.get());
}
}