目录
2.5 继承Thread类 和 实现Runnable接口的区别
1.线程相关概念
1.1 程序
是为了完成特定任务,用某种语言编写的一组指令的集合。
简单的说,就是我们写的代码
1.2 进程
- 进程是指运行中的程序,比如我们使用了QQ,就启动了一个进程,操作系统就会为该进程分配内存空间。当我们玩游戏时,又启动了一个进程,操作系统会为该游戏分配新的内存空间。
- 进程是程序的一次执行过程,或者是正在运行的一个程序,是动态过程:有他自身的产生,存在和消亡过程。
1.3 线程
-
线程是由进程创建的,是进程的一个实体
-
一个进程可以拥有多个线程,如下:
-
-
单线程:同一个时刻,只允许执行一个线程
-
多线程:同一个时刻,可以执行多个线程,比如:一个qq进程,可以同时打开多个聊天窗口,一个迅雷进程,可以下载多个文件
1.4 并发与并行
并发:同一个时刻,多个任务交替执行,造成一种“貌似同时”的错觉,简单地说,单核cpu实现的多任务就是并发。(比如当一个人在开车时,他不可以做其他事情,比如打电话)
并行:同一个时刻,多个任务同时执行,多核cpu可以实现并行,并发和并行
2.线程的基本使用
2.1 创建线程的两种方式
在Java中线程常用的有两种方法
- 继承Thread类,重写run方法
- 实现Runnable接口,重写run方法
2.2 线程应用案例1.继承Thread类
要求:编写一个线程,该线程每隔一秒,在控制台输出“喵喵,我是小猫咪”。当输出10次时,结束该线程。可以使用JConsole监控线程执行情况。
public class Thread01 {
public static void main(String[] args) throws InterruptedException {
//创建 Cat 对象,可以当做线程使用
Cat1 cat = new Cat1();
cat.start();
//启动线程-> 最终会执行 cat 的 run 方法
//run 方法就是一个普通的方法, 没有真正的启动一个线程,就会把 run 方法执行完毕,才向下执行
// 说明: 当 main 线程启动一个子线程 Thread-0, 主线程不会阻塞, 会继续执行
//这时 主线程和子线程是交替执行..
System.out.println("主线程继续执行" + Thread.currentThread().getName());
for (int i = 0; i < 10; i++) {
System.out.println("主线程 i=" + i);
//让主线程休眠
Thread.sleep(1000);
}
}
//1. 当一个类继承了 Thread 类, 该类就可以当做线程使用
//2. 我们会重写 run 方法,写上自己的业务代码
//3. Thread 类 实现了 Runnable 接口的 run 方法
}
class Cat1 extends Thread {
int times = 0;
@Override
public void run() {//重写 run 方法,写上自己的业务逻辑
while (true) {
//该线程每隔 1 秒。在控制台输出 “喵喵, 我是小猫咪”
System.out.println("喵喵, 我是小猫咪" + (++times) + " 线程名=" + Thread.currentThread().getName());
//让该线程休眠 1 秒
try {//此时有异常,需要处理或抛出
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (times == 10) {
break;//当 times 到 10, 退出 while, 这时线程也就退出..
}
}
}
}
运行结果:
主线程继续执行main
喵喵, 我是小猫咪1 线程名=Thread-0
主线程 i=0
主线程 i=1
喵喵, 我是小猫咪2 线程名=Thread-0
喵喵, 我是小猫咪3 线程名=Thread-0
主线程 i=2
主线程 i=3
喵喵, 我是小猫咪4 线程名=Thread-0
主线程 i=4
喵喵, 我是小猫咪5 线程名=Thread-0
主线程 i=5
喵喵, 我是小猫咪6 线程名=Thread-0
喵喵, 我是小猫咪7 线程名=Thread-0
主线程 i=6
喵喵, 我是小猫咪8 线程名=Thread-0
主线程 i=7
主线程 i=8
喵喵, 我是小猫咪9 线程名=Thread-0
主线程 i=9
喵喵, 我是小猫咪10 线程名=Thread-0
可以看出,主线程和cat线程是交替执行的。
->start()方法的底层调用了start0方法。
(暂时不懂没关系)
2.3 线程应用案例2-实现Runnable接口
说明:
- Java是单继承的,在某些情况下一个类可能已经继承了某个父类,这时在用继承Thread类方法来创建线程显然不可能了
- Java设计者提供了另一种方式来创建线程,就是实现Runnable接口来创建线程
应用案例:编写程序,该程序每隔一秒,在控制台输出“hi”,当输出10次后,自动退出。
public class Thread2 {
public static void main(String[] args) {
Hi hi = new Hi();
//hi.start; 不能这样做
Thread thread = new Thread(hi);
thread.start();
}
}
class Hi implements Runnable {
int count = 1;
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("hi" + count++);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行结果:(每隔一秒输出一次)
hi1
hi2
hi3
hi4
hi5
hi6
hi7
hi8
hi9
hi10
2.4 线程使用应用案例-多线程执行
要求:请编写一个程序,创建两个线程,一个线程每隔1秒输出“hello,world”,输出10次,退出,另一个线程每隔一秒输出“hi”,输出5次退出
public class Thread03 {
public static void main(String[] args) {
T1 t1 = new T1();
T2 t2 = new T2();
Thread thread1 = new Thread(t1);
Thread thread2 = new Thread(t2);
thread1.start();//启动第 1 个线程
thread2.start();//启动第 2 个线程
}
}
class T1 implements Runnable {
int count = 0;
@Override
public void run() {
while (true) {
//每隔 1 秒输出 “hello,world”,输出 10 次
System.out.println("hello,world " + (++count));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count == 10) {
break;
}
}
}
}
class T2 implements Runnable {
int count = 0;
@Override
public void run() {
//每隔 1 秒输出 “hi”,输出 5 次
while (true) {
System.out.println("hi " + (++count));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count == 5) {
break;
}
}
}
}
运行结果:
hello,world 1
hi 1
hello,world 2
hi 2
hi 3
hello,world 3
hello,world 4
hi 4
hi 5
hello,world 5
hello,world 6
hello,world 7
hello,world 8
hello,world 9
hello,world 10
-> 可以看出两个线程都存在时,是交替执行的
2.5 继承Thread类 和 实现Runnable接口的区别
- 从Java的设计来看,通过继承Thread或者实现Runnable接口来创建线程本质上没有区别,Thread类本身就实现了Runnable接口
- 实现Runnable接口方式更加适用多个线程共享一个资源的情况,并且避免了单继承的限制,建议使用Runnable
3. 线程常用方法
3.1 常用方法第一组
-
setName 设置线程名称
-
getName 返回该线程的名称
-
start 使线程开始执行
-
run 调用线程对象run方法
-
setPriority 更改线程的优先级
-
getPriority 获取线程的优先级
-
sleep 让当前正在执行的线程休眠指定毫秒数
-
interrupt 中断线程
3.2 常用方法第二组
- yield: 线程的礼让,让出cpu,但礼让的时间不确定,所以也不一定礼让成功
- join:线程的插队,插队的线程一但插队成功,则肯定先执行完插入的线程的所有的任务。
-> 测试join方法:
public class ThreadMethodExercise {
public static void main(String[] args) throws InterruptedException {
T2 t2 = new T2();
Thread thread = new Thread(t2);
for (int i = 1; i <= 5; i++) {
Thread.sleep(1000);
System.out.println("hi"+ i );
if(i==3){
thread.start();
thread.join();//线程插队
}
}
}
}
class T2 implements Runnable {
private int count = 0;
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("hello" + (++count));
if (count == 5) {
break;
}
}
}
}
运行结果:
hi1
hi2
hi3
hello1
hello2
hello3
hello4
hello5
hi4
hi5
4. 用户线程和守护线程
4.1 基本介绍:
- 用户线程:也叫工作线程,当线程的任务执行完以通知方式结束
- 守护线程:一般是为工作线程服务的,当所有的用户线程结束,守护线程自动结束
- 常见的守护线程:垃圾回收机制
4.2 如何将一个线程设置成守护线程(setDaemon)
/**
* 把一个线程设置为守护线程
* 守护线程:当所有的用户线程结束时,守护线程自动结束。
* 这里MyDaemonThread为守护线程
* main线程为用户线程44
*/
public class ThreadMethod02 {
public static void main(String[] args) throws InterruptedException {
MyDaemonThread myDaemonThread = new MyDaemonThread();
//如果我们希望当main线程结束后,子线程自动结束
//将子线程设置为守护线程即可
myDaemonThread.setDaemon(true); //daemon 守护线程
myDaemonThread.start();
for (int i = 1; i <= 3 ; i++) {
System.out.println("主线程......");
Thread.sleep(1000);
}
}
}
class MyDaemonThread extends Thread{//守护线程
@Override
public void run() {
while(true){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("守护线程....");
}
}
}
运行结果:
5. 线程的生命周期
5.1 线程的几种状态
5.2 编写程序查看线程的几种状态
public class ThreadState_ {
public static void main(String[] args) throws InterruptedException {
TT t = new TT();
System.out.println(t.getName() + " 状态 " + t.getState());
t.start();
while (Thread.State.TERMINATED != t.getState()) { //当t的状态不是TERMINATED时
System.out.println(t.getName() + " 状态 " + t.getState());
Thread.sleep(500);
}
//线程结束的状态
System.out.println(t.getName() + " 状态 " + t.getState());
}
}
class TT extends Thread {
@Override
public void run() {
while (true) {
for (int i = 0; i < 3; i++) {
System.out.println("hi " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
break;
}
}
}
运行结果:
Thread-0 状态 NEW
Thread-0 状态 RUNNABLE
hi 0
Thread-0 状态 TIMED_WAITING
hi 1
Thread-0 状态 TIMED_WAITING
Thread-0 状态 TIMED_WAITING
hi 2
Thread-0 状态 TIMED_WAITING
Thread-0 状态 TIMED_WAITING
Thread-0 状态 TERMINATED
6. 线程的同步
6.1 线程同步机制
-
在多线程编程,一些敏感数据不允许被多个线程同时访问,此时就使用同步线程访问技术,保证数据在任意同一时刻,最多有一个线程访问,以保证数据的完整性
-
也可以这样理解:当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才可对该内存地址进行操作
6.2 同步具体方法(Synchronized )
1.同步代码块
Synchronized(对象){ //得到对象的锁,才能操作同步代码)
//需要同步代码
}
2. 同步方法
public Synchronized void m(String name){
//需要被同步的代码
}
6.3 同步分析
->多个线程会去争夺一把锁,每次只能有一个线程进入
7. 互斥锁
7.1 互斥锁基本介绍
- Java语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性
- 每个对象都对应于一个可称为“互斥锁”的标记,这个标记用来保证在任意时刻,只能有一个线程访问该对象
- 关键字Synchronized来与对象的互斥锁联系,当某个对象用Synchronized修饰时,表明该对象在任意时刻只能有一个线程访问
- 同步的局限性:导致程序的执行效率要降低
- 同步方法(非静态的)的锁可以是this,也可以是其他对象(要求是一个对象)
- 同步方法(静态的)的锁为当前类本身
7.2 实现的步骤
- 先分析上锁的代码
- 选择同步代码块或同步方法
- 要求多个线程的锁对象为同一个即可
7.3 多线程模拟售票问题
/**
* 使用多线程,模拟三个接口同时售票
* 若不使用线程同步机制,会导致票数超卖,小伙伴们可以自己去尝试
*/
public class SellTicket {
public static void main(String[] args) {
//多个线程的锁对象必须为同一个
SellTicked01 sellTicked01 = new SellTicked01();
new Thread(sellTicked01).start();
new Thread(sellTicked01).start();
new Thread(sellTicked01).start();
// 错误的写法
// SellTicked01 sellTicked01 = new SellTicked01();
// SellTicked01 sellTicked02 = new SellTicked01();
// SellTicked01 sellTicked03 = new SellTicked01();
//
// sellTicked01.start();
// sellTicked02.start();
// sellTicked03.start();
// new了三个对象 所以锁不住
}
}
//使用synchronized实现线程同步 在方法上加锁
class SellTicked01 implements Runnable {
private /*static*/ int tickNum = 10; //让多个线程共享tickNum
private /*static*/ boolean loop = true;
// 同步方法,在同一时刻,只能有一个线程来执行sell 方法
// 这时锁在this对象
// 也可以在代码块上写synchronized -> 同步代码块
public /*static*/ synchronized void sell() { //在静态方法中只能访问静态属性或静态方法
if (tickNum <= 0) {
System.out.println("售票结束");
loop = false;
return;
}
//休眠50毫秒,模拟
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("窗口" + Thread.currentThread().getName() + "售出一张票 " + "剩余票数=" + (--tickNum));
}
@Override
public void run() {
while (loop) {
sell(); //sell方法是一个同步方法
}
}
}
//使用synchronized实现线程同步 同步代码块实现
class SellTicked04 implements Runnable {
private int tickNum = 10; //让多个线程共享tickNum
private boolean loop = true;
// 同步方法,在同一时刻,只能有一个线程来执行sell 方法
// 这时锁在this对象
// 也可以在代码块上写synchronized -> 同步代码块 互斥锁还是加在this对象
public /*synchronized */ void sell() {
synchronized (this) {
if (tickNum <= 0) {
System.out.println("售票结束");
loop = false;
return;
}
//休眠50毫秒,模拟
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("窗口" + Thread.currentThread().getName() + "售出一张票 " + "剩余票数=" + (--tickNum));
}
}
@Override
public void run() {
while (loop) {
sell();
}
}
}
运行结果:
窗口Thread-0售出一张票 剩余票数=9
窗口Thread-0售出一张票 剩余票数=8
窗口Thread-0售出一张票 剩余票数=7
窗口Thread-2售出一张票 剩余票数=6
窗口Thread-2售出一张票 剩余票数=5
窗口Thread-2售出一张票 剩余票数=4
窗口Thread-2售出一张票 剩余票数=3
窗口Thread-2售出一张票 剩余票数=2
窗口Thread-2售出一张票 剩余票数=1
窗口Thread-2售出一张票 剩余票数=0
售票结束
售票结束
售票结束
8. 线程的死锁
->基本介绍:死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程)。
->如下图:线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
模拟线程死锁:
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");
}
}
}
}
}
运行结果:(我们会发现程序会堵塞,进入了死锁状态,无法继续运行)
9. 释放锁
会释放锁的操作:
- 当前线程的同步方法,同步代码块执行结束
- 当前线程在同步代码块,同步方法中遇到break,return
- 当前线程在同步代码块,同步方法中出现了未处理的Error或Exception,导致异常结束运行
- 当前线程在同步代码块,同步方法中执行了线程对象的wait()方法,导致线程暂停,并释放锁
不会释放锁的操作:
- 当前线程在同步代码块,同步方法中调用了Thread.sleep(),Thread.yield()方法暂停当前线程的执行,不会释放锁
- 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁