Java线程基础
相关概念
一个进程包含多个线程, 这些线程共享进程的资源(内存空间和文件句柄), 还有属于自己的内存空间, 这段空间是建立线程时由系统分配的, 用来保存线程内部所使用的数据, 如线程执行函数中所定义的变量;
Java中的多线程是抢占机制;
并发: 同一时刻, 多个任务交替执行, 造成一种同时执行的错觉. 比如单核CPU实现的多任务就是并发;
并行: 同一时刻, 多个任务同时进行. 多核CPU就可以实现并行;
创建线程
创建线程有两种方法, 一个是继承Thread类, 第二种是实现Runnable接口. 两种方法都需要重写run()
方法, 但要注意, 重写的run()
方法可以被JVM调用, 但重载的不行;
run()
方法中抛出的异常只能由方法内部捕获处理, 或使用记录异常等方式通知调用方, 不能直接抛出异常;
继承Thread类
由于Java的单继承机制, 不推荐使用这种方法;
public class Thread01 {
public static void main(String[] args) throws InterruptedException {
Cat cat = new Cat();
new Thread(cat).start();
}
}
class Cat extends Thread {
@Override
public void run() {
System.out.println("Cat Thread");
}
}
使用Runnable接口
实现Runnable接口的类不能通过对象名.start()
来启动线程, 而要创建一个Thread对象并传入实现了Runnable接口的对象作为参数, 然后使用thread.start()
来启动线程;
public class Thread02 {
public static void main(String[] args) {
Dog dog = new Dog();
new Thread(dog).start();
}
}
class Dog implements Runnable {
@Override
public void run() {
System.out.println("Dog Thread");
}
}
执行过程
run()
Thread中的run()
方法实际是实现了Runnable的run()
, 源码为:
@Override
public void run() {
if (target != null) {
target.run();
}
}
当程序启动后, 会创建一个主线程(通常称为"main"线程), 主线程会调用其他线程(例如"cat"线程)来执行其中的任务, 但这个调用不会阻塞主线程的继续执行;
主线程结束了不代表程序结束;
start()
在创建Cat对象后, 调用start()
方法, 最终会调用Cat中的run()
方法开启线程, 那么为什么不直接调用run()
方法呢? 如果直接调用, 那么就相当于直接由主线程调用Cat中的run()
方法, 此时输出的线程名称不是Thread-01是main, 也就是说并没有真正的开启线程. 程序会先执行完run()方法的内容再执行main的后续内容.
start()
方法执行后调用start0()
这个方法, start0()
方法是由JVM调用, 由它来调用run()方法实现多线程.
在start0()
方法被调用后, 该线程不会马上执行, 而是将线程变为可运行状态, 具体什么时候执行, 取决于CPU, 由CPU统一调度.
常用方法
方法名 | 作用 |
---|---|
setName() |
设置线程名称, 使之与参数name相同 |
getName() |
返回该线程的名称 |
start() |
使线程开始执行 |
run() |
调用线程对象的run() 方法 |
setPriority() |
更改线程优先级 |
getPriority() |
获取线程优先级 |
sleep() |
在指定的毫秒数内让当前正在运行的线程休眠 |
interrupt() |
中断线程 |
start()
底层会创建新的线程, 调用run()
,run
就是一个简单的方法调用, 不会启动新的线程- 线程优先级常用的有三种: MAX_PRIORITY(10), MIN_PRIORITY(1), NORM_PRIORITY(5); 优先级高的线程只是执行几率比优先级低的高
sleep()
指的是最小不执行时间, 因为休眠时间结束无法保证会被JVM立即调度; 线程休眠时不会失去拥有的对象锁, 而是让出CPU, 给其他线程执行的机会interrupt()
中断线程而不是结束线程, 一般用于中断正在休眠的线程
// main线程启动t线程, 在休眠2秒后中断t线程
public class ThreadMethod01 {
public static void main(String[] args) throws InterruptedException {
T t = new T();
t.start();
Thread.sleep(2000);
t.interrupt();
}
}
// 每输出20次, 休眠5秒
class T extends Thread {
@Override
public void run() {
while (true) {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + " 次数: " + i);
}
try {
System.out.println(Thread.currentThread().getName() + "休眠中");
Thread.sleep(5000);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "被Interrupt了");
}
}
}
}
方法 | 作用 |
---|---|
yield() |
让出CPU让其他线程执行, 但让出时间不确定 |
join() |
线程插队, 若插队成功则肯定先执行完插入的线程的所有任务 |
yield()
方法用于让出CPU使用权, 给同等优先权的线程运行, 若没有同等优先权的进程,yield()
不会起作用
// main线程在循环五次后让tm2线程先执行
public class ThreadMethod02 {
public static void main(String[] args) throws InterruptedException {
Tm2 tm2 = new Tm2();
tm2.start();
for (int i = 0; i < 20; i++) {
System.out.println("hi");
Thread.sleep(1000);
if (i == 5) {
System.out.println("主线程让子线程先执行");
tm2.join();
}
}
}
}
// 输出20次hello
class Tm2 extends Thread {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
线程销毁
线程销毁有两种方式:
- 当线程完成任务后, 会自动退出.
- 通过使用变量控制
run()
方法退出, 这种方式也称为通知方式.
比如启动一个线程t, main线程休眠五秒后去停止线程t:
public class ThreadExit {
public static void main(String[] args) throws InterruptedException {
T t = new T();
t.start();
Thread.sleep(5000);
t.setLoop(false);
}
}
class T extends Thread {
private boolean loop = true;
@Override
public void run() {
while (loop) {
System.out.println("线程还在运行");
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void setLoop(boolean loop) {
this.loop = loop;
}
}
守护线程和用户线程
用户线程: 也叫工作线程, 线程的结束靠的是任务执行完或通知方式结束.
守护线程: 一般为工作线程服务, 当所有用户线程结束, 守护线程自动结束. 常见的比如垃圾回收机制.
当JVM中不存在任何一个正在运行的非守护线程时, JVM进程就会退出.
设置守护线程
通过setDaemon(true)
将该线程设为守护线程.
public class DaemonThread {
public static void main(String[] args) throws InterruptedException {
MyDaemonThread myDaemonThread = new MyDaemonThread();
myDaemonThread.setDaemon(true);
myDaemonThread.start();
for (int i = 0; i < 10; i++) {
System.out.println("主线程工作中...");
Thread.sleep(100);
}
}
}
class MyDaemonThread extends Thread {
@Override
public void run() {
while (true) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程还没退出, 守护线程还在运行中...");
}
}
}
线程的生命周期
JDK中使用Thread.State枚举了线程的几种状态:
状态 | 行为 |
---|---|
NEW | 尚未启动的线程处于此状态 |
RUNNABLE | 在Java虚拟机中执行的线程处于此状态 |
BLOCKED | 被阻塞等待监视器锁定的线程处于此状态 |
WAITING | 正在等待另一个线程执行特定动作的线程处于此状态 |
TIMED_WAITING | 正在等待另一个线程执行动作达到指定等待时间的线程处于此状态 |
TERMINATED | 已退出的线程处于此状态 |
- 当一个线程实例被创建就属于NEW状态, 当前状态的线程有自己的内存空间
- 调用线程实例的
start()
方法使线程进入RUNNABLE状态, 此时线程若在等待系统分配CPU, 则是READY状态, 一旦获取CPU, 则进入RUNNING状态, 线程的run()
方法开始执行 - 只有线程处于BLOCKED状态时才会在锁释放时竞争锁
线程同步状态
线程同步机制: 在多线程中一些敏感数据不允许被多个线程同时访问, 此时就要使用同步技术, 保证数据在任何时刻最多只有一个线程访问, 以保证数据的完整性; 通俗来讲就是当一个线程A对内存进行操作时, 其他线程都不能对这个内存地址操作, 直到A线程完成操作, 其他线程才能对该内存地址进行操作;
Synchronized
同步代码块:
Synchronized (对象) { //得到对象的锁, 才能操作同步代码
// 需要被同步的代码
}
同步方法:
public synchronized void method(String name) {
// 需要被同步的代码
}
互斥锁
Java中引入了互斥锁的概念保证共享数据操作的完整性;
每个对象都对应一个可称为互斥锁的标记, 这个标记用来保证在任意时刻, 只能有一个线程访问该对象;
关键字synchronized来与对象的互斥锁联系, 当某个对象用synchronized修饰时, 表明该对象在任意时刻只能由一个线程访问;
同步的局限性: 导致程序的执行效率降低
同步方法(非静态的)的锁可以是this, 也可以是其他对象(要求是同一个对象)
如下就不是同一个对象
synchronized(new Object()) {
//other code...
}
如下就是同一个对象:
Object obj = new Object();
synchronized(obj) {
//other code...
}
下面代码中前两个锁在它们的代码块/方法上, 最后一个代码块的锁加载Thread01类的obj对象上
public void method02() {
synchronized (Object.class) {
//other code...
}
}
public synchronized void method02() {
//other code...
}
public class SellTicket {
public static void main(String[] args) {
Thread01 thread = new Thread01();
new Thread(thread).start();
new Thread(thread).start();
new Thread(thread).start();
}
}
class Thread01 {
Object obj = new Object();
public void method02() {
synchronized (obj) {
// other code...
}
}
}
同步方法(静态的)的锁为当前类本身: 下面两个锁都是在它们的类上, 而非代码块/方法上
public static void method02() {
synchronized (Thread01.class) {
//other code...
}
}
public synchronized static void method02() {
//other code...
}
死锁
多个线程占用了对方的资源且都不释放, 就形成了死锁.
释放锁
下面的操作会释放锁:
-
当前线程的同步方法,同步代码块执行结束
-
当前线程在同步方法,同步代码块中遇到break, return
-
当前线程在同步方法,同步代码块中出现了未处理的Error或Exception, 导致异常结束
-
当前线程在同步方法,同步代码块中执行了线程对象wait()方法, 当前线程暂停, 并释放锁
下面的操作不会释放锁:
- 线程执行同步方法,同步代码块时, 程序调用Thread.sleep(), Thread.yield()方法暂停当前线程的执行
- 线程执行同步方法,同步代码块时, 其他线程调用了该线程的suspend()方法将该线程挂起(尽量避免使用suspend()和resume()来控制线程)