Java 基础巩固-线程
一. 线程
1.1 线程相关概念
- 单线程:同一个时刻,只允许执行一个线程
- 多线程,同一时刻,可以执行多个线程。例如:一个qq进程,可以开多个窗口
- 并发(来回切换):同一时刻,多个任务交替执行,造成一种"貌似同时"的错觉,简单的说,单核cpu实现的多任务就是并发
- 并行(同时执行):同一时刻,多个任务同时执行。多核cpu可以实现并行
1.2 创建线程的几种方式
- 使用Thread类
- 实现
Runnable接口 - 使用
Callable和Future来创建线程 - 使用线程池
- 使用定时器
- 使用java8新特性
stream实现并发 - 使用
匿名内部类
1.3 为什么用start方法而不是直接用run方法
- 使用
run:相当于直接用主线程调用run方法,只有当run执行完毕以后,才会执行后面的代码 - 使用
start:相当于在主线程后面开了子线程,真正实现了多线程 - 源码分析
1. 首先会调用 start 方法 public synchronized void start() 2. 接着调用 native 修饰的 start0 本地方法,该方法由 jvm 调用 start0()
1.4 案例:使用 Runnable 接口方式实现多线程
public class Thread01 {
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("老虎叫");
}
}
class ThreadProxy implements Runnable {
Runnable target = null;
public ThreadProxy(Runnable target) {
this.target = target;
}
@Override
public void run() {
if (target != null) {
target.run();
}
}
public void start(){
start0();
}
public void start0() {
run();
}
}
1.5 继承 Thread vs 实现 Runnable 接口的区别
- 从 java 的设计来看,通过继承 Thread 或者实现 Runnable 接口来创建线程没有本质区别,因为 Thread 类本身就实现了 Runnable 接口
- 实现 Runnable 接口的方式更加适合
多个线程共享一个资源的情况,避免了单继承的限制T1 t1 = new T1(); Thread thread = new Thread(t1); Thread thread1 = new Thread(t1); thread.start(); thread1.start();
1.6 线程常用方法
1.6.1 第一组常用方法
setName() 设置线程名称
getName() 返回该线程名称
start() 使线程开始执行
run() 调用线程对象的 run 方法
setPriority() 更改线程优先级
getPriority() 获取线程优先级
sleep() 在指定的毫秒数内让当前正在执行的线程休眠
interrupt() 中断线程
1.6.2 第二组常用方法
yield() :让出当前进程的执行,不一定礼让成功
join() :线程插队
1.7 用户线程和守护线程
1.7.1 简介
- 用户线程:也叫工作线程,当线程的任务执行完毕或用通知方式结束
- 守护线程:一般为工作线程服务,当所有的用户线程结束,守护线程自动结束
- 常见守护线程:垃圾回收机制
1.7.2 守护线程案例
public class Method01 {
public static void main(String[] args) throws InterruptedException {
MyDaemonThread myDaemonThread = new MyDaemonThread();
// 将该线程设置为守护进程,会随着用户进程结束而结束
myDaemonThread.setDaemon(true);
myDaemonThread.start();
for (int i = 0; i < 5; i++) {
System.out.println("宝强辛苦的工作");
Thread.sleep(1000);
}
System.out.println("宝强回家啦");
}
}
class MyDaemonThread extends Thread {
@Override
public void run() {
while(true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("马蓉和宋喆快乐聊天");
}
}
}
1.8 线程生命周期
1.8.1 生命周期介绍
- NEW :尚未启动的线程处于该状态
- RUNNABLE :在 Java 虚拟机中执行的线程处于该状态
- BLOCKED :被阻塞等待监视器锁定的线程处于此状态
- WAITING :正在等待另一个线程执行特定动作的线程处于该状态
- TIMED_WAITING :正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
- TERMINATED :已退出线程处于此状态
1.8.2 生命周期图
1.9 Synchronized
1.9.1 线程同步机制
- 在多线程编程中,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何同一时刻,最多有一个线程访问,保证数据的完整性。
- 也可以理解为,当有一个线程对内存进行操作时,其他线程不允许对内存进行操作。直到该线程操作完,其他线程才能对该内存进行操作
1.9.2 同步具体方法 - Synchronized
1. 同步代码块
synchronized(对象) { // 得到对象的锁,才能操作同步代码
// 需要被同步的代码
}
2. synchronized 还可以放到方法声明中,表示该方法为同步方法
public synchronized void m(String name){
// 需要被同步的代码
}
1.10 互斥锁
1.10.1 互斥锁介绍
- Java 语言中,引入了对象互斥锁的概念,来保证共享数据的完整性
- 每个对象都对应一个可称为
“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象 - 同步的局限性:导致程序执行效率降低
- 同步方法(非静态)的锁可以是this,也可以是其他对象(要求是同一对象)
- 同步方法(静态)的锁为当前类本身
1.10.2 注意事项和细节
- 同步方法如果没有使用 static 修饰:默认锁对象 this
- 如果方法使用 static 修饰,默认锁对象为:当前类.class
- 实现的落地步骤
- 需要先分析上锁的代码
- 选择
同步代码快或者同步方法 - 要求多个线程的锁对象为同一个即可
1.11 死锁
多个线程都占用了对象的锁资源,都不肯让,导致了死锁
1.12 释放锁
1.12.1 以下操作会释放锁
- 当前线程的同步方法、同步代码快执行结束
- 当前线程在同步代码快、同步方法中遇到 break 、return
- 当前线程在同步代码快、同步方法中出现了未处理的
error或Exception,导致异常结束 - 当前线程在同步代码快、同步方法中执行了
wait方法,当前线程暂停
1.12.2 以下操作不会释放锁
- 线程执行同步代码块或同步方法时,执行了
Thread.sleep()方法 或者yield()方法暂停当前线程执行 - 线程执行同步代码块时,其他线程调用了该线程的
suspend()方法将该线程挂起,不会释放锁