19、多线程基础
1 什么是线程?
1.1 几个概念
- 程序:由程序员编写的代码
- 进程:指运行中的程序,进程是程序的执行过程,或者是正在运行的程序。是动态过程:产生、存在和消亡的过程
- 线程:线程由进程创建,是一个实体。一个进程可以由多个线程
- 打开迅雷——一个进程产生了
- 迅雷下载多个任务——一个进程产生了多个线程
1.2 线程
(1)单线程:同一时刻,只允许执行一个线程
(2)多线程:同一时刻,可以执行多个进程
(3)并发:同一时刻,多个任务交替执行,造成一种【貌似同时】的错觉,简单地说,单核CPU实现多任务就是并发
(4)并行:同一时刻,多个任务同时执行,多核CPU可以实现并行
2 线程的实现
2.1 继承Thread类,重写run方法
(1)入门代码
public class Thread01 {
public static void main(String[] args) throws InterruptedException {
//创建线程
Cat cat = new Cat();
//启动子线程:子线程会和main主线程并行
cat.start();
//如果使用cat.run()方法,则没有调用cat线程,仍为main线程内调用的一个方法,将会在执行完run方法后在执行下面的语句
//cat.run();
//当main线程启动子线程时,主线程不会阻塞,会继续执行
//主线程运行结束,子线程会继续执行完毕
for (int i = 0; i < 10; i++) {
System.out.println("主线程继续执行" + Thread.currentThread().getName() + "\ti=" + i);
Thread.sleep(1000);
}
}
}
/**
* 方法一:继承Thread类,该类就可以当作线程使用
* 重写父类Thread类的run方法,而Thread类实现了Runnable接口的抽象方法
*/
class Cat extends Thread {
int times = 0;
@Override
public void run() {//按业务逻辑重写run方法
while (true) {
//每隔一秒输出一次
System.out.println("我是一只小喵喵" + ++times + "线程名" + Thread.currentThread().getName());
//让线程休眠1秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (times == 8) {
break;
}
}
}
}
(2)运行流程
(3)start方法的底层源码分析
start()
方法调用start0()
方法start0()
方法是本地方法,由JVM调用,底层由c/c++实现start0()
是真正实现多线程的方法,而不是run方法start()
方法调用start0()
方法后,该线程并不会立刻执行,只是将线程编程可执行的状态,具体执行取决于CPU,由CPU同意调度
2.2 实现Runnable接口,重写run方法
(1)为什么需要另外一种方法,实现多线程
Java是单继承的,在某些情况下,一个类可能已经继承了某个父类,此时通过继承Thread类来创建线程显然是不可能的
(2)入门代码——【使用了静态代理模式】
public class Thread02 {
public static void main(String[] args) {
Dog dog = new Dog();
//此时无法调用start()方法,直接调用run()方法没有创建线程,所以创建Thread对象,将dog对象传入Thread
//【如何实现,见(3)】
Thread thread = new Thread(dog);
thread.start();
}
}
class Dog implements Runnable {
int count = 0;
@Override
public void run() {
while (true) {
System.out.println("小狗哇哇叫" + ++count + "\t线程" + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count == 10) {
break;
}
}
}
}
(3)代码模拟实现Runnable接口开发线程的机制
public class ProxyMX {
public static void main(String[] args) throws InterruptedException {
//声明线程
Tiger tiger = new Tiger();
//声明线程代理类,利用构造器将实现了Runnable接口的对象传入线程代理类
Proxy proxy = new Proxy(tiger);
//调用代理类中的方法,最终在多线程的情况下启动子线程【注意:这里仅演示线程代理类的流程,并没有实现多线程】
proxy.start();
for (int i = 0; i < 10; i++) {
System.out.println("主线程在执行" + Thread.currentThread().getName() + ++i);
Thread.sleep(1000);
}
}
}
class Animal {
}
class Tiger extends Animal implements Runnable {
int count = 0;
@Override
public void run() {
while (true) {
System.out.println("老虎嗷嗷叫" + ++count);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count == 10) {
break;
}
}
}
}
//线程代理类,模拟了一个极简的thread类
class Proxy implements Runnable {//proxy代理
private Runnable target = null;
public Proxy(Runnable target) {
this.target = target;
}
@Override
public void run() {
if (target != null) {
target.run();
}
}
public void start() {
start0();
}
public void start0() {
run();
}
}
3 线程的使用
public class Thread03 {
public static void main(String[] args) {
T1 t1 = new T1();
T2 t2 = new T2();
Thread thread01 = new Thread(t1);
Thread thread02 = new Thread(t2);
//通过匿名内部类可以清楚地看到是实现了Runnable接口的实例化对象
Thread thread03 = new Thread(new Runnable() {
int count = 0;
@Override
public void run() {
while (true) {
System.out.println("我试试匿名内部类");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count == 20) {
break;
}
}
}
});
thread01.start();
thread02.start();
thread03.start();
}
}
class T1 implements Runnable {
int count = 0;
@Override
public void run() {
while (true) {
System.out.println("hello, world!\t" + ++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() {
while (true) {
System.out.println("hi\t" + ++count);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count == 5) {
break;
}
}
}
}
4 继承Thread和实现Runnable的区别
(1)从Java设计来看,通过继承Thread类和实现Runnable接口来创建线程,本质上没有任何区别
(2)实现Runnable接口的方式更加适合多个线程共享一个资源的情况,并且避免了单继承的限制
- 【多个线程共享一个资源】:即多个线程代理类启动同一个实现了Runnable的类的实例化对象
5 线程常用方法
方法名 | 作用 |
---|---|
setName | 设置线程名称,使之与参数name相同 |
getName | 返回该线程的名称 |
start | 执行线程 |
run | 调用线程对象run方法 |
setPriority | 更改线程的优先级 |
getPriority | 获取线程优先级 |
sleep | 指定线程休眠时间 |
interrupt | 中断线程,一般用于中断休眠 |
【注意事项和细节】
- start底层会创建一个新的线程,调用run不会启动新的线程
- 线程优先级的范围
- interrupt中断线程,但并没有真正的结束线程,一般用于中断正在休眠的线程
- sleep:线程的静态方法,使当前线程休眠
方法名 | 作用 |
---|---|
yield | 【线程的礼让】让出cpu,让其他线程执行,礼让事件不确定,因此也不一定礼让成功 |
join | 【线程的插队】插队的线程一定插入成功,即优先执行完插入的线程所有任务 |
6 用户线程和守护线程
(1)几个概念
- 用户线程:也叫工作线程,当线程的任务执行完或通知后结束
- 守护线程:一般是为工作线程服务的,当所有的用户线程结束,守护线程自动结束
- 常见的守护线程:垃圾回收机制
(2)守护线程的使用
public class ThreadMethod03 {
public static void main(String[] args) throws InterruptedException {
MyDaemonThread myDaemonThread = new MyDaemonThread();
//将子线程设置为守护线程,当main线程结束后,子线程自动结束
myDaemonThread.setDaemon(true);
myDaemonThread.start();
for (int i = 0; i <= 10; i++) {
System.out.println("宝强在辛苦地工作。。。");
Thread.sleep(1000);
}
}
}
class MyDaemonThread extends Thread {
@Override
public void run() {
for (;;) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("马蓉和宋泽在快乐聊天。。。");
}
}
}
7 线程的七大状态
状态 | |
---|---|
NEW | 尚未启动的线程 |
Runnable | 在JVM中执行的线程 |
【Ready】 | |
【Running】 | |
TimeWaiting | 正在等待另一个线程执行动作达到指定等待时间的线程 |
Waiting | 正在等待另一个线程执行特定动作的线程 |
Blocked | 被阻塞等待监视器锁定的线程 |
Teminated | 已退出的线程 |
8 线程的同步
8.1 线程的同步机制
在多线程编程,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何同意时刻,最多有一个线程访问,一保证数据的完整性
所谓的【线程同步】,即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,知道该线程完成操作,其他线程才能对该内存地址进行操作。
8.2 同步的实现——关键字Synchronized
通常将共享资源的操作放置在synchronized定义的区域内,这样当其他线程要获取该锁时,必须等待锁被释放才能进入该区域
(1)修饰代码块——同步代码块
- 作用范围:大括号内的代码
- 作用对象:调用该代码块的对象
synchronized (Object) {
//需要被同步的代码
}
- Object为任意一个对象
- 每个对象都存在一个标志位,并具有两个值,反别为0,1
- 一个线程运行到同步块时首先检查该对象的标志位
- 若为0,表明此同步块中存在其他线程运行,此时该线程处于就绪状态,直到同步块中代码执行完为止,此时该对象的标志位被设置为1
- 若为1,该线程可以执行同步代码块中的代码,并将Object对象的标志位设置为0,防止其他线程执行代码块
(2)修饰方法——同步方法
将每个能访问公共资源的方法修饰为synchronized
- 作用范围:整个方法
- 作用对象:调用该方法的对象
public synchronized void m() {
//需要被同步的代码
}
(3)同步的使用
public class SellTicket {
public static void main(String[] args) {
SellTicket sellTicket = new SellTicket();
Thread thread1 = new Thread(sellTicket);
Thread thread2 = new Thread(sellTicket);
Thread thread3 = new Thread(sellTicket);
thread1.start();
thread2.start();
thread3.start();
}
}
//使用同步方法——synchronized实现同步
class SellTicket implements Runnable {
private static int ticketNum = 100;
private boolean loop = true;
public synchronized void sell() {//同一时刻只能有一个线程执行sell方法
//判断是否有余票
if (ticketNum <= 0) {
System.out.println("售票结束...");
loop = false;
return;
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("窗口:" + Thread.currentThread().getName() + "售票成功!\t"
+ "剩余:" + (--ticketNum) + "张");
}
@Override
public void run() {
while (loop) {
sell();
}
}
}
8.3 互斥锁
(1)基本介绍
- Java中引用对象互斥锁的概念,来保证共享数据操作的完整性
- 每个对象都对应一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只有一个线程可以访问该对象
- 关键字synchronized来与对象的互斥锁联系,当某个对象用synchronized修饰时,表明该对象在任一时刻只能由一个线程访问
- 同步的局限性:导致程序的执行效率降低
- 同步方法(非静态的)的锁可以是this,也可以是其他对象(要求是同一个对象)
- 同步方法(静态的)的锁为当前类本身
9 线程的死锁
(1)什么是死锁?
多个线程都占用了对方的锁资源,但互不相让导致死锁,需要避免
(2)模拟死锁
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();
/**
* (1)A.start()调用run方法,flag为true
* 持有o1锁,并试图获取o2锁
* (2)B.start()调用run方法,flag为false
* 持有o2锁,并试图获取o1锁
* (3)二线程并发运行,所以对(1)来说o2未释放,对(2)来说o1未释放
*/
B.start();
}
}
class DeadLockDemo extends Thread {
static Object o1 = new Object();
static Object o2 = new Object();
boolean flag;
public DeadLockDemo(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag) {
synchronized (o1) {
System.out.println(Thread.currentThread().getName() + "进入1");
synchronized (o2) {
System.out.println(Thread.currentThread().getName() + "进入2");
}
}
} else {
synchronized (o2) {
System.out.println(Thread.currentThread().getName() + "进入3");
synchronized (o1) {
System.out.println(Thread.currentThread().getName() + "进入4");
}
}
}
}
}
10 释放锁
(1)哪些操作会释放锁
- 当前线程的同步方法、同步代码块执行结束
- 当前线程在同步代码块,同步方法中遇到break、return
- 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束
- 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停了,并释放锁
(2)哪些操作不会释放锁
- 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行,不会释放锁
- 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁