14.1 线程相关概念
14.1.1 程序 program
是为了完成特定任务,用某种语言编写的一组指令的集合
简单地说就是我们写的代码
14.1.2 进程
- 进程是指运行中的程序,比如我们使用QQ,就启动了一个进程,操作系统就会为该进程分配内存空间。当我们使用迅雷,又启动了一个进程,操作系统将为迅雷分配新的内存空间
- 进程是程序的一次执行过程,或者正在运行的一个程序。是动态过程:具有它自身的产生、存在和消亡的过程
14.1.3 线程
- 线程是由进程创建的,是进程的一个实体
- 一个进程可以有多个线程
14.1.4 其他相关概念
- 单线程:同一个时刻,只允许执行一个线程
- 多线程:同一个时刻,可以执行多个进程。比如:一个QQ进程,可以同时打开多个聊天窗口,一个迅雷进程,可以同时下载多个文件
- 并发:同一个时刻,多个任务交替执行,造成一种“貌似同时”的错觉,简单来说,单核CPU实现的多任务就是并发
- 并行:同一个时刻,多个任务同时执行。多核CPU可以实现并行
- 并发和并行可以同时进行
14.2 线程的基本使用
14.2.1 创建线程的两种方式
- 继承 Thread 类,重写 run 方法
- 实现 Runnable 接口,重写 run 方法
- Java是单继承的,在某些情况下一个类可能已经继承了某个父类,这是再用继承 Thread 的方法来创建线程显然不可能了
- 因此Java设计者提供了另一种方式创建线程,就是通过实现 Runnable 接口来创建线程
14.2.2 继承Thread类实例
- 请编写程序,开启一个线程,该线程每隔1秒。在控制台输出“喵喵,我是小猫咪"
- 对上题改进:当输出80次喵喵,我是小猫咪,结束该线程
- 使用JConsole监控线程执行情况,并画出程序示意图!
public class ExtendsThreadDemo {
public static void main(String[] args) throws InterruptedException {
//创建 Cat 对象,可以当做线程使用
Cat cat = new Cat();
cat.start();
/*
(1)
public synchronized void start() {
start0();
}
(2)
//start0() 是本地方法,是 JVM 调用, 底层是 c/c++实现
//真正实现多线程的效果, 是 start0(), 而不是 run
private native void start0();
*/
//cat.run();//run 方法就是一个普通的方法, 没有真正的启动一个线程,就会把 run 方法执行完毕,才向下执行
//说明: 当 main 线程启动一个子线程 Thread-0, 主线程不会阻塞, 会继续执行
//这时 主线程和子线程是交替执行.
for(int i = 0; i < 60; i++) {
System.out.println("主线程 " + Thread.currentThread().getName() + " 继续执行 i=" + i);
//主线程 main 继续执行 i=0
//让主线程休眠
Thread.sleep(1000);
}
}
}
//1. 当一个类继承了 Thread 类, 该类就可以当做线程使用
//2. 我们会重写 run 方法,写上自己的业务代码
//3. Thread 类 实现了 Runnable 接口的 run 方法
/*
@Override
public void run() {
if (target != null) {
target.run();
}
}
*/
class Cat extends Thread {
int times = 0;
@Override
public void run() {//重写run方法,写上自己的业务逻辑
while (times < 80){
System.out.println("喵喵,我是小猫咪" + ++times + "线程名:" + Thread.currentThread().getName());
//喵喵,我是小猫咪1线程名:Thread-0
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
14.2.3 start()方法
真正创建线程的是start()方法,start()方法调用start0()方法后,该线程不一定会立马执行,只是将线程变成了可运行状态(Ready状态),具体什么时候执行(Running状态),取决于CPU,由CPU统一调度。
14.2.4 实现Runnable接口实例
请编写程序,该程序可以每隔1秒,在控制台输出“hi!",当输出10次后,自动退出。请使用实现Runnable接口的方式实现。【这里底层使用了设计模式[静态代理模式]】
public class RunnableThread {
public static void main(String[] args) {
Dog dog = new Dog();
// dog.start();//这里不能调用start()
Thread thread = new Thread(dog);
thread.start();
}
}
class Dog implements Runnable {//通过实现Runnable接口创建线程
int count = 0;
@Override
public void run() {
while (count < 10) {
System.out.println("小狗汪汪叫..hi " + ++count + " 线程名:" + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
代码模拟实现Runnable接口开发线程的机制
public class SimulateRunnable {
public static void main(String[] args) {
Tiger tiger = new Tiger();
ThreadProxy threadProxy = new ThreadProxy(tiger);
threadProxy.start();
}
}
class Animal {}
class Tiger extends Animal implements Runnable {
@Override
public void run() {
System.out.println("老虎嗷嗷叫");
}
}
//线程代理类,模拟了一个极简的Thread
class ThreadProxy implements Runnable { //可以把ThreadProxy类当作 Thread类
private Runnable target = null; //属性,类型是Runnable
public ThreadProxy(Runnable target) {
this.target = target;//动态绑定(运行类型Tiger)
}
@Override
public void run() {
if (target != null) {
target.run();
}
}
public void start() {
start0();//这个方法时真正实现多线程方法
}
public void start0() {
run();
}
}
14.2.5 多线程执行案例
请编写一个程序,创建两个线程,一个线程每隔1秒输出"hello,world",输出10 次,退出,一个线程每隔1秒输出"hi" ,输出5次退出
public class MultithreadDemo {
public static void main(String[] args) {
new Thread(new T1()).start();
new Thread(new T2()).start();
}
}
class T1 implements Runnable {
@Override
public void run() {
//每隔1秒输出"hello,world",输出10次
int count = 0;
for (int i = 0; i < 10; i++){
System.out.println("hello,world");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class T2 implements Runnable {
@Override
public void run() {
//每隔1秒输出"hello,world",输出10次
for (int i = 0; i < 5; i++){
System.out.println("hi");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
14.2.6 继承 Thread 和 实现 Runnable 的区别
- 从java的设计来看,通过继承Thread或者实现Runnable接口来创建线程本质上没有区别,从jdk帮助文档我们可以看到Thread类本身就实现了 Runnable 接口
- 实现Runnable接口方式更加适合多个线程共享一个资源的情况,并且避免了单继承的限制,建议使用Runnable
示例:[售票系统],编程模拟三个售票窗口售票100 ,分别使用继承Thread和实现Runnable方式,并分析有什么问题?
public class SellTicket {
public static void main(String[] args) {
//继承Thread方法
// new SellTicket01().start();
// new SellTicket01().start();
// new SellTicket01().start();
//会出现票数超卖现象
//实现Runnable接口方法
SellTicket02 sellTicket02 = new SellTicket02();
new Thread(sellTicket02).start();
new Thread(sellTicket02).start();
new Thread(sellTicket02).start();
//同样会出现票数超卖现象
}
}
class SellTicket01 extends Thread {
private static int ticketNum = 100;//多个线程共享,用static
@Override
public void run() {
super.run();
while (true) {
if (ticketNum <= 0) {
System.out.println("售票结束");
break;
}
//休眠50ms
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("窗口 " + Thread.currentThread().getName() +
" 售出了一张票,还剩 " + --ticketNum);
}
}
}
class SellTicket02 implements Runnable {
private int ticketNum = 100;//一个对象,不用static
@Override
public void run() {
while (true) {
if (ticketNum <= 0) {
System.out.println("售票结束");
break;
}
//休眠50ms
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("窗口 " + Thread.currentThread().getName() +
" 售出了一张票,还剩 " + --ticketNum);
}
}
}
14.3 线程终止
14.3.1 说明
- 当线程完成任务后,会自动退出
- 还可以通过 使用变量 来控制 run 方法退出的方式停止线程,即通知方式
14.3.2 示例
启动一个线程t,要求在 main 方法中去停止线程t
public class NotifyExit {
public static void main(String[] args) {
AThread aThread = new AThread();
new Thread(aThread).start();
for (int i = 0; i < 60; i++) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main线程运行中 " + i);
if (i == 20) {
aThread.setLoop(false);
System.out.println("停止了AThread");
}
}
}
}
class AThread implements Runnable {
private boolean loop = true;
@Override
public void run() {
while (loop) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("AThread运行中...");
}
}
public void setLoop(boolean loop) {
this.loop = loop;
}
}
14.4 线程常用方法
14.4.1 第一组方法
- setName:设置线程名称,使之与参数 name 相同
- getName:返回线程的名称
- start:使该线程开始执行,Java虚拟机底层调用该线程的 start0 方法
- run:调用该线程对象的 run 方法
- setPriority:更改线程的优先级
- getPriority:获取线程的优先级
- sleep:在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)
- interrupt:中断线程
14.4.2 使用细节
- start 底层会创建新的线程,调用run。run 就是一个简单的方法调用,不会启动新的线程
- 线程优先级范围:
- MAX_PRIORITY:10
- MIN_PRIORITY:1
- NORM_PRIORITY:5
- interrupt 中断线程,但并没有真正的结束线程。所以一般用于中断正在休眠的线程
- sleep:线程的静态方法,使当前线程休眠
14.4.3 示例
public class ThreadMethod {
public static void main(String[] args) throws InterruptedException {
T t = new T();
t.setName("tom");
t.setPriority(Thread.MIN_PRIORITY);
t.start();
//主线程输出 5 个 "hi" 然后就中断 子线程休眠
for (int i = 0; i < 5; i++) {
Thread.sleep(1000);
System.out.println("hi " + i);
}
System.out.println(t.getName() + " 线程的优先级:" + t.getPriority());
t.interrupt();//执行到这里就会中断t线程的休眠
}
}
class T extends Thread {
@Override
public void run() {
super.run();
while (true){
for (int i = 0; i < 100; i++) {
//Thread.currentThread().getName() 获取当前线程的名称
System.out.println(Thread.currentThread().getName() + " 吃包子~~~ " + i);
}
try {
System.out.println(Thread.currentThread().getName() + " 休眠中~~~");
Thread.sleep(20000);
} catch (InterruptedException e) {
//当线程执行到一个 interrupt 方法时,就会 catch 一个异常,可以加入自己的业务代码
//InterruptedException 是捕获到一个中断异常
System.out.println(Thread.currentThread().getName() + " 被 interrupt 了");
}
}
}
}
14.4.4 第二组方法
- yield:线程的礼让。让出 cpu ,让其他线程执行,但礼让的时间不确定,所以也不一定礼让成功
- join:线程的插队。插队的线程一旦插队成功,则肯定先执行完插队线程的所有任务
14.4.5 示例
- 主线程每隔1s,输出hi,一共10次
- 当输出到hi 5时,启动一个子线程(要求 实现Runnable),每隔1s输出hello,等 该线程输出10次hello后,退出
- 主线程继续输出hi,直到主线程退出.
public class ThreadCutInLine {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new HelloThread());
for (int i = 1; i < 11; i++) {
System.out.println("hi " + i);
Thread.sleep(1000);
if (i == 5) {
thread.start();
thread.join();
}
}
}
}
class HelloThread implements Runnable {
@Override
public void run() {
for (int i = 1; i < 11; i++) {
System.out.println("hello " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
14.4.6 用户线程和守护线程
- 用户线程:也叫工作线程,当线程的任务执行完或通知方式结束
- 守护线程:一般是为工作线程服务的,当所有的用户线程结束,守护线程自动结束
- 常见的守护线程:垃圾回收机制
- 通过调用
setDaemon(true);
方法将线程设为守护线程
14.4.7 示例
public class DaemonThread {
public static void main(String[] args) throws InterruptedException {
MyDaemonThread dt = new MyDaemonThread();
dt.setDaemon(true);
dt.start();
for (int i = 1; i < 21; i++) {
Thread.sleep(500);
System.out.println("主线程执行中...");
}
}
}
class MyDaemonThread extends Thread {
@Override
public void run() {
super.run();
for (; ; ) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("MyDaemonThread守护线程正在执行...");
}
}
}
14.5 线程的生命周期
14.5.1 JDK 中用 Thread.State 枚举表示了线程的几种状态
14.5.2 线程状态转换图
public class ThreadState {
public static void main(String[] args) throws InterruptedException {
TS ts = new TS();
System.out.println(ts.getName() + " 的状态:" + ts.getState());
ts.start();
while (Thread.State.TERMINATED != ts.getState()) {
System.out.println(ts.getName() + " 的状态:" + ts.getState());
Thread.sleep(500);
}
System.out.println(ts.getName() + " 的状态:" + ts.getState());
}
}
class TS extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("hi " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
14.6 线程的同步
14.6.1 线程同步机制
- 在多线程编程,一些敏感数据不允许多个线程同时访问,此时就使用同步访问计数,保证数据在任何同一时刻,最多有一个线程访问,以保证数据的完整性
- 也可以这样理解:线程同步,即当有一个线程在对内存进行操作时,其他线程都不能对这个内存地址进行操作,知道该线程完成操作,其他线程才能对该内存地址进行操作
14.6.2 同步具体方法 synchronized
-
同步代码块:
synchronized (对象) { //得到对象的锁,才能操作同步代码 //需要被同步的代码 }
-
同步方法:
public synchronized void 方法名(参数列表) { //同一时刻只能有一个线程操作该方法 //需要被同步的代码 }
14.6.3 互斥锁
- Java语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性
- 每个对象都对应于一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只 能有一个线程访问该对象
- 关键字synchronized来与对象的互斥锁联系。当某个对象用synchronized修饰时, 表明该对象在任一时刻只能由一个线程访问
- 同步的局限性:导致程序的执行效率要降低
- 同步方法(非静态的)的锁可以是this,也可以是其他对象(要求是同一个对象)
- 同步方法(静态的)的锁即为当前类本身
使用细节:
- 同步方法如果没有使用 static 修饰(对象锁):默认锁对象为 this
- 如果方法使用 static 修饰(类锁):默认锁对象为当前类.class
- 带你真的理解synchronize的对象锁和类锁的使用
- 关于synchronized,对象锁的理解
- 实现步骤:
- 先分析需要上锁的代码
- 选择同步代码块或同步方法
- 要求多个线程的锁对象为同一个即可
14.6.4 售票问题
public class SellTicket2 {
public static void main(String[] args) {
SellTicket03 sellTicket03 = new SellTicket03();
new Thread(sellTicket03).start();
new Thread(sellTicket03).start();
new Thread(sellTicket03).start();
}
}
//同步方法
class SellTicket03 implements Runnable {
private int ticketNum = 100;//一个对象,不用static
private boolean loop = true;
//1. public synchronized void sell() {} 就是一个同步方法
//2. 这时锁在 this 对象
//3. 也可以在代码块上写 synchronize =>同步代码块, 互斥锁还是在 this 对象
public synchronized void sell() {
if (ticketNum <= 0) {
System.out.println("售票结束");
loop = false;
return;
}
//休眠50ms
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("窗口 " + Thread.currentThread().getName() +
" 售出了一张票,还剩 " + --ticketNum);
}
@Override
public void run() {
while (loop) {
sell();
}
}
}
//同步代码块
class SellTicket04 implements Runnable {
private int ticketNum = 100;//一个对象,不用static
private boolean loop = true;
Object object = new Object();
public void sell() {
synchronized (/*this或*/object){
//synchronized (对象)表示对象锁,可以是当前对象this,也可以是其他任意对象
//如果线程进入,则得到当前对象锁,那么别的线程在该类所有对象上的任何操作都不能进行
if (ticketNum <= 0) {
System.out.println("售票结束");
loop = false;
return;
}
//休眠50ms
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("窗口 " + Thread.currentThread().getName() +
" 售出了一张票,还剩 " + --ticketNum);
}
}
@Override
public void run() {
while (loop) {
sell();
}
}
}
class SellTicket05 extends Thread {
public void m3() {
//当前为继承Thread类的方式创建线程
//创建的方式是:
//new SellTicket05().start();
//new SellTicket05().start();
//每次都用不同的对象创建线程、执行相应方法
//若此时任用同步代码块锁this对象,每个对象的this都为不同对象
//因此这时用当前对象锁多个线程争夺的不是同一把锁,不能起到作用
//可以用类锁
synchronized (this) {
System.out.println("m3");
}
}
}
//类锁
class SellTicket06 extends Thread {
//同步方法(静态的)的锁为当前类本身
//此时在同一时刻,只允许一个当前类(通过类名调用静态成员)或者当前类的对象进行操作
//m1() 锁是加在 SellTicket03.class
public synchronized static void m1() {
}
//如果在静态方法中,实现一个同步代码块
public static void m2() {
synchronized (SellTicket06.class) {
System.out.println("m2");
}
}
}
14.6.5 死锁
多个线程都占用了对方的锁资源,但不肯相让,导致了死锁。死锁非常危险,尽量避免
public class DeadLock {
public static void main(String[] args) {
DeadLockDemo A = new DeadLockDemo(true);
A.setName("A线程");
DeadLockDemo B = new DeadLockDemo(false);
B.setName("B线程");
A.start();
B.start();
}
}
class DeadLockDemo extends Thread {
static Object o1 = new Object();// 保证多线程,共享一个对象,这里使用 static
static Object o2 = new Object();
boolean flag;
public DeadLockDemo(boolean flag) {//构造器
this.flag = flag;
}
@Override
public void run() {
//下面业务逻辑的分析
//1. 如果 flag 为 T, 线程 A 就会先得到/持有 o1 对象锁, 然后尝试去获取 o2 对象锁
//2. 如果线程 A 得不到 o2 对象锁,就会 Blocked
//3. 如果 flag 为 F, 线程 B 就会先得到/持有 o2 对象锁, 然后尝试去获取 o1 对象锁
//4. 如果线程 B 得不到 o1 对象锁,就会 Blocked
if (flag) {
synchronized (o1) {//对象互斥锁, 下面就是同步代码
System.out.println(Thread.currentThread().getName() + " 进入 1");
synchronized (o2) { // 这里获得 li 对象的监视权
System.out.println(Thread.currentThread().getName() + " 进入 2");
}
}
} else synchronized (o2) {
System.out.println(Thread.currentThread().getName() + " 进入 3");
synchronized (o1) { // 这里获得 li 对象的监视权
System.out.println(Thread.currentThread().getName() + " 进入 4");
}
}
}
}
14.6.6 释放锁
以下操作会释放锁:
- 当前线程的同步方法、同步代码块执行结束
- 案例:上厕所,完事出来
- 当前线程在同步代码块、同步方法中遇到break, return。
- 案例:没有正常的完事,经理叫他修改bug,不得已出来
- 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束
- 案例:没有正常的完事,发现忘带纸,不得已出来
- 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。
- 案例:没有正常完事,觉得需要酝酿下,所以出来等会再进去
以下操作不会释放锁:
- 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方 法暂停当前线程的执行,不会释放锁
- 案例:上厕所,太困了,在坑位上眯了一会
- 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起, 该线程不会释放锁。 提示:应尽量避免使用suspend()和resume()来控制线程,方法不再推荐使用
14.7 练习
-
/** * 在main方法中启动两个线程 * 第一个线程循环随机打印100以内的整数 * 直到第二个线程从键盘读取到了“Q”命令 */ public class ThreadExer01 { public static void main(String[] args) { AThread aThread = new AThread(); BThread bThread = new BThread(aThread); new Thread(aThread).start(); new Thread(bThread).start(); } } class AThread implements Runnable { private boolean loop = true; public void setLoop(boolean loop) { this.loop = loop; } @Override public void run() { while (loop) { System.out.println((int)(Math.random()*100) + 1); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("A线程退出..."); } } class BThread implements Runnable { private AThread aThread; private Scanner sc = new Scanner(System.in); public BThread(AThread aThread) { this.aThread = aThread; } @Override public void run() { while (true){//接受用户输入 System.out.println("请输入你的指令(Q退出):"); char key = sc.next().toUpperCase().charAt(0); if (key == 'Q') { aThread.setLoop(false); System.out.println("B线程退出..."); break; } } } }
-
/** * 有两个用户分别从同一张卡上取钱(总额:10000) * 每次都取1000,当余额不足时,就不能取款了 * 不能出现超取现象=》线程同步问题 */ public class ThreadExer02 { public static void main(String[] args) { Count count = new Count(); Usr usr1 = new Usr(count); Usr usr2 = new Usr(count); new Thread(usr1).start(); new Thread(usr2).start(); } } class Count { private static int balance = 10000; public int getBalance() { return balance; } public void setBalance(int balance) { Count.balance = balance; } public void withdrawMoney(int amount) { Count.balance -= amount; } } class Usr implements Runnable { private final Count count; public Usr(Count count) { this.count = count; } @Override public void run() { while (true){ synchronized (count) { if (count.getBalance() <= 0){ System.out.println("余额为0," + Thread.currentThread().getName() + "停止取钱..."); break; } count.withdrawMoney(1000); System.out.println(Thread.currentThread().getName() + "取出了1000,余额还有:" + count.getBalance()); } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }