线程的创建
1. 通过继承Thread类实现多线程
通过继承Thread
类来自定义一个线程类, 并重写run()
方法. run()
方法是一个线程的线程体, 也就是开启这个线程后它要执行的部分. 之后, 直接在main
方法中通过new()
的方式创建两个线程实例, 再通过start()
方法, 让这两个线程运行起来. 运行起来之后就开始执行run()
方法中的代码了.
为了方便我们看到结果, 使用getName()
方法来获取当前线程的name
. 这个方法是父类Thread
中的方法, 直接调用即可.
public class ThreadTest extends Thread {public ThreadTest() {System.out.println(this.getName());}/*** 线程的线程体*/@Overridepublic void run() {System.out.println(this.getName() + "线程开始");for (int i = 0; i < 20; i++) {System.out.println(this.getName() + " " + i);}System.out.println(this.getName() + "线程结束");}public static void main(String[] args) {System.out.println("主线程开始");// 创建线程ThreadTest t1 = new ThreadTest();ThreadTest t2 = new ThreadTest();// 启动线程t1.start();t2.start();System.out.println("主线程结束");}
}
2. 通过实现Runnable接口实现多线程
通过继承Thread
类的方式确实可以帮助我们创建自己的线程, 但是由于java单继承的特点, 我们自定义的线程类无法再继承其他的类, 这样会给我们带来一些限制. 此时我们可以采取实现Runnable
接口的方式创建我们自己的线程. 查看源码可知, Thread
类本身也是继承了Runnable
接口的.
// Thread类源码
public
class Thread implements Runnable {...
}
Runnable
接口中只有一个方法, 就是run()
方法, 可以看出, 这就是我们多线程的核心方法.
// Runnable接口源码
public interface Runnable {public abstract void run();
}
在我们的测试类中, 依然是对run()
方法的重写, 不同的是由于我们没有继承Thread
类, 于是就没有了getName()
方法. 不过也可以使用Thread.currentThread()
方法得到当前的线程的对象实体, 再调用getName()
方法就可以了.
Thread.currentThread()
方法是Thread
类中的一个静态方法, 返回的就是当前线程的对象实体.
// Thread类源码
/*** Returns a reference to the currently executing thread object.** @return the currently executing thread.*/
public static native Thread currentThread();
由于我们的测试类只是实现了Runnable
接口, 所以它并没有start()
方法, 那么怎么让它启动呢? 这时我们就需要把测试方法包装成Thread
对象. 方式就是Thread t1 = new Thread(new RunnableTest());
.
这里还有一点需要注意的是虽然我们的测试类的构造方法Runnable()
是我们自定义线程的构造方法, 但是在我们new RunnableTest()
时, 这个构造方法是在主线程(即main
)中执行的, 也就是说line3这段代码的输出结果是main
. 其原因就是这段代码是在main
线程中执行的, 此时我们的自定义线程还没开启呢.
我们的自定义线程通过start()
方法启动之后, 就开始执行run()
方法, 在run()
方法中(即线程体中)执行的Thread.currentThread().getName()
得到的才是我们自定义线程的名字.
public class RunnableTest implements Runnable {public RunnableTest() {System.out.println(Thread.currentThread().getName());}/*** 当前线程的线程体*/@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "线程开始");for (int i = 0; i < 20; i++) {System.out.println(Thread.currentThread().getName() + " " + i);}System.out.println(Thread.currentThread().getName() + "线程结束");}public static void main(String[] args) {System.out.println("主线程开始");Thread t1 = new Thread(new RunnableTest());Thread t2 = new Thread(new RunnableTest());t1.start();t2.start();System.out.println("主线程结束");}
}
另外, 如果我们自定义的线程类只需要使用一次, 那么可以使用匿名内部类的方式书写如下:
Thread t1 = new Thread(new Runnable(){@Overridepublic void run() {...}
});
Runnable
接口中只有一个方法, 这种接口我们称之为函数式接口, 那么就可以使用lambda表达式来书写. 如果方法体只有一行, {}
都可以省略.
Thread t3 = new Thread(() -> {...
});
线程的声明周期
新生状态(New)
用 new 关键字建立一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间,通过调用 start 方法进入就绪状态。
就绪状态(Runnable)
处于就绪状态的线程已经具备了运行条件,但是还没有被分配到 CPU,处于“线程就绪队列”,等待系统为其分配 CPU。就绪状态并不是执行状态,当系统选定一个等待执行的 Thread 对象后,它就会进入执行状态。一旦获得 CPU,线程就进入运行状态并自动调用自己的 run 方法。有 4 中原因会导致线程进入就绪状态:
-
新建线程:调用
start()
方法,进入就绪状态; -
阻塞线程:阻塞解除,进入就绪状态;
-
运行线程:调用
yield()
方法,直接进入就绪状态; -
运行线程:JVM 将 CPU 资源从本线程切换到其他线程。
运行状态(Running)
在运行状态的线程执行自己 run 方法中的代码,直到调用其他方法而终止或等待某资源而阻塞或完成任务而死亡。如果在给定的时间片内没有执行结束,就会被系统给换下来回到就绪状态。也可能由于某些“导致阻塞的事件”而进入阻塞状态。
阻塞状态(Blocked)
阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪)。有 4 种原因会导致阻塞:
-
执行
sleep(int millsecond)
方法,使当前线程休眠,进入阻塞状态。当指定的时间到了后,线程进入就绪状态。 -
执行
wait()
方法,使当前线程进入阻塞状态。当使用nofity()
方法唤醒这个线程后,它进入就绪状态。 -
线程运行时,某个操作进入阻塞状态,比如执行 IO 流操作
read()
/write()
方法本身就是阻塞的方法)。只有当引起该操作阻塞的原因消失后,线程进入就绪状态。 -
join()
线程联合: 当某个线程等待另一个线程执行结束后,才能继续执行时,使用join()
方法。
死亡状态(Terminated)
死亡状态是线程生命周期中的最后一个阶段。线程死亡的原因有两个。一个是正常运行的线程完成了它 run()方法内的全部工作; 另一个是线程被强制终止,如通过执行 stop()
或 destroy()
方法来终止一个线程(注:stop()
/destroy()
方法已经被 JDK 废弃,不推荐使用)。
当一个线程进入死亡状态以后,就不能再回到其它状态了。
线程的使用
1. 终止线程
如果我们想在一个线程中终止另一个线程我们一般不使用 JDK 提供的 stop()
/destroy()
方法(它们本身也被 JDK 废弃了)这种方式过于粗暴, 线程会直接挂掉, 就算有后续的收尾工作也不会执行。
通常的做法是提供一个 boolean
型的终止变量,当这个变量值为 false
时,则终止线程的运行。
public class StopThreadTest implements Runnable{private boolean flag = true;@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "线程开始");int i = 0;while(flag) {System.out.println(Thread.currentThread().getName() + " " + i++);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}//模拟线程的收尾工作System.out.println(Thread.currentThread().getName() + "线程结束");}public void stop() {this.flag = false;}public static void main(String[] args) throws IOException {System.out.println("主线程开始");StopThreadTest st = new StopThreadTest();Thread t1 = new Thread(st);t1.start();System.in.read();st.stop();System.out.println("主线程结束");}
}
2. 暂停线程
sleep()方法
**作用: **让正在运行的线程休眠一段时间并进入阻塞状态, 等待休眠结束后再进入就绪队列.
public class SleepThreadTest implements Runnable{@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "线程开始");for (int i = 0; i < 20; i++) {System.out.println(Thread.currentThread().getName() + " " + i);try {// 让线程暂停, 并设置时间为1000, 单位是毫秒(millis)Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread.currentThread().getName() + "线程结束");}public static void main(String[] args) {System.out.println("主线程开始");Thread t0 = new Thread(new SleepThreadTest());t0.start();System.out.println("主线程结束");}
}
yield()方法
**作用: **让正在运行的线程停止运行, 让出cpu, 进入就绪队列排队.
注意: yield()
方法是Thread
类的一个静态方法. 使用yield()
方法使线程重新排队后, 该线程有一定几率会被调度程序再次选中, 所以该方法让出cpu是有可能失败的.
public class YieldThreadTest implements Runnable{@Overridepublic void run() {for (int i = 0; i < 20; i++) {if (i == 0 && "Thread-1".equals(Thread.currentThread().getName())) {// 暂停线程让出cpu, 但是可能会失败Thread.yield();}System.out.println(Thread.currentThread().getName() + " " + i);}}public static void main(String[] args) {Thread t0 = new Thread(new YieldThreadTest());Thread t1 = new Thread(new YieldThreadTest());t0.start();t1.start();}
}
3. 联合线程
线程A邀请线程B优先执行, 在线程B执行完之后线程A才能继续执行, 类似于方法的嵌套调用. "邀请"的方式是调用线程B的join()
方法.
join() 方法的使用
join()
方法就是指调用该方法的线程在执行完 run()
方法后,再执行 join ()
方法后面的代码, 即将两个线程合并,用于实现同步控制.
在下面的实例代码中, 主线程(main
)和在执行的过程中执行了t0.join()
, 联合了Thread-0
线程, 于是main
线程停止执行, 并等待Thread-0
执行完再接着执行. 于此同时Thread-1
线程不受任何影响. 依然是与其他线程并发执行.
public class JoinTreadTest {public static void main(String[] args) throws InterruptedException{Thread t0 = new Thread(new ThreadA());Thread t1 = new Thread(new ThreadB());t0.start();t1.start();for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName() + " "+ i);if (i == 2) {t0.join();}Thread.sleep(1000);}}
}class ThreadA implements Runnable {@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName() + " " + i);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}
class ThreadB implements Runnable {@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName() + " " + i);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}
联合线程实例
使用线程联合的方式实现同步.
public class JoinDemo {public static void main(String[] args) {System.out.println("爸爸和儿子买烟的故事");Thread t = new Thread(new FatherTread());t.start();}
}class FatherTread implements Runnable {@Overridepublic void run() {System.out.println("爸爸想抽烟, 发现烟抽完了");System.out.println("爸爸让儿子去买一包红塔山");Thread t = new Thread(new SonThread());System.out.println("等待儿子买烟回来");t.start();try {t.join();} catch (InterruptedException e) {e.printStackTrace();System.out.println("儿子丢了, 爸爸出门找儿子");System.exit(1);}System.out.println("爸爸抽上了烟, 并把零钱给了儿子");}
}
class SonThread implements Runnable {@Overridepublic void run() {System.out.println("儿子出门买烟");System.out.println("儿子买烟需要十分钟");for (int i = 0; i < 10; i++) {System.out.println("第" + i + "分钟");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}
4. Thread类其他常用方法
4.1 获取线程名称
this.getName()
获取当前线程名称, 适用于继承Thread
类实现的多线程方式.Thread.currentThread.getName()
先获取当前线程的实例, 再获取当前线程名称.
4.2 设置线程名称
-
通过构造方法设置线程名称
Thread
类有一个public Thread(String name)
的构造方法, 子类继承Thread
类之后, 可以在子类的构造方法的第一行调用父类的这个构造方法super(name)
-
通过
Thread
类的setName()
方法设置线程名称无论是通过继承
Thread
类, 还是通过实现Runnable
接口实现的多线程, 都可以在线程对象创建完成后, 直接调用Thread
类的普通方法setName()
来为线程设置名称.
4.3 判断当前线程是否存活
thread.isAlive()
方法: 判断当前线程是否处于活动状态.
线程创建并启动之后(执行thread.start()
方法), 到执行完自己的run()
(线程体)之前, 都属于活动状态.
线程的优先级
1. 什么是线程优先级
每一个线程都是有优先级的,我们可以为每个线程定义线程的优先级,但是这并不能保 证高优先级的线程会在低优先级的线程前执行。线程的优先级用数字表示,范围从 1 到 10, 一个线程的缺省优先级是 5。 Java的线程优先级调度会委托给操作系统去处理,所以与具体的操作系统优先级有关, 如非特别需要,一般无需设置线程优先级。
注意:线程的优先级,不是说哪个线程优先执行,如果设置某个线程的优先级高。那就 是有可能被执行的概率高。并不是优先执行。
2. 线程优先级的使用
thread.getPriority()
方法可以获取线程的优先级thread.setPriority()
方法可以设置线程的优先级
**注意: **线程优先级必须在线程启动之前(start()
)设置, 启动之后设置是无效的.
守护线程
1. 什么是守护线程
在 Java 中有两类线程:
-
User Thread(用户线程):就是应用程序里的自定义线程。
-
Daemon Thread(守护线程):服务于用户线程的线程, 比如垃圾回收线程,就是最典型的守护线程。
守护线程特点: 守护线程会随着用户线程死亡而死亡
2. 守护线程与用户线程的区别
用户线程,不随着主线程的死亡而死亡。
用户线程只有两种情况会死掉,
- 在
run()
方法执行过程中异常终止。 - 正常把
run()
方法执行完毕,线程死亡。
守护线程,随着用户线程的死亡而死亡,当用户线程死亡守护线程也会随之死亡.
3. 守护线程的使用
使用setDaemon()
方法设置守护线程
public class DaemonThreadTest {public static void main(String[] args) {Thread t = new Thread(new UserThread());t.start();}
}/*** 守护线程*/
class DaemonThread implements Runnable {@Overridepublic void run() {for (int i = 0; i < 20; i++) {System.out.println(Thread.currentThread().getName() + " " + i);try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}}}
}/*** 用户线程*/
class UserThread implements Runnable {@Overridepublic void run() {Thread daemonThread = new Thread(new DaemonThread());// 将该线程设为守护线程daemonThread.setDaemon(true);daemonThread.start();for (int i = 0; i < 5; i++) {System.out.println(Thread.currentThread().getName() + " " + i);try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}}}
}
线程同步
1. 什么是线程同步
同步是指程序中用于控制不同线程间操作发生相对顺序的机制。
1.1 线程冲突
当多个线程同时操作某个资源时很容易就会出现冲突. 如果多个线程同时执行写操作就容易出现覆盖的情况.
1.2 线程同步的概念
处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。 这时候,我们就需要用到“线程同步”。 线程同步其实就是一种等待机制,多个需要同时 访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用.
2. 实现线程同步
2.1 synchronized关键字
由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突 的问题。Java 语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线 程同时访问造成的这种问题。这套机制就是 synchronized 关键字。
// synchronized 语法结构:
public synchronized void accessVal(int newVal); synchronized(锁对象){ 同步代码
}
synchronized 关键字使用时需要考虑的问题:
- 需要对那部分的代码在执行时具有线程互斥的能力(线程互斥:并行变串行)。
- 需要对哪些线程中的代码具有互斥能力(通过 synchronized 锁对象来决定)。 它包括两种用法:synchronized 方法和 synchronized 块。
2.2 synchronized关键字的使用
-
synchronized 方法
通过在方法声明中加入 synchronized 关键字来声明,语法如下:
public synchronized void method() {}
synchronized 在方法声明时使用:放在访问权限修饰符(public)之后,返回类型声明(void) 之前, 也可以放在最前面, 但是不太规范。这时同一个对象下synchronized 方法在多线程中执行时,该方法是同步的,即一次 只能有一个线程进入该方法,其他线程要想在此时调用该方法,只能排队等候,当前线程(就是在
synchronized
方法内部的线程)执行完该方法后,别的线程才能进入。 -
synchronized 块
synchronized 方法的缺陷:如果直接将一整个方法声明为 synchronized 将会大大影响效率。
Java 为我们提供了更好的解决办法,那就是
synchronized
块, 可以让我们精确地控制到具体的“成员变量”,缩小同步的范围,提高效率.synchronized(锁对象){ ... }
synchronized关键字不能被继承
按对象锁的类型分类
使用this作为锁
在不同线程中, 调用相同对象的synchronized(){}
会发生互斥.
本质是对同步代码所在的对象加锁.
- 语法结构
synchronized(this) {// 同步代码
}
// 等同于对普通方法加synchronized
public synchronized void method() {}
使用字符串作为锁
所有使用统一字符串为对象锁的synchronized(){}
代码块都会同步.
本质是对字符串常量对象加锁
- 语法结构
synchronized("String") {// 同步代码
}
使用Class作为锁
在不同的线程中, 使用统一Class对象的synchronized(){} 代码块会同步执行.
本质是对类的class对象加锁,
- 语法结构
synchronized(XX.class) {// 同步代码
}
// 等同于对类的静态方法加synchronized修饰
public synchronized static void staticMethod() {// 同步代码
}
总结
无论是使用何种类型的对象锁, 本质都是对对象加锁, 字符串和类对象的本质也是对象, 在对象的内存结构中, 对象头的位置, 存储量该对象的锁信息, 加锁的实质就是修改这个锁信息, 当某个线程需要获取锁时, 会先判断对象是否已经加锁, 如果没有就自己加锁, 然后执行同步代码, 如果已被加锁, 就需要等待锁被释放, 再去尝试加锁.
使用自定义对象作为锁
按synchronized
关键字的使用方式分类
- 修饰普通方法: 锁当前对象
- 修饰静态方法: 锁当前对象所属类的类对象
- 包裹代码块: 小括号里写什么就对什么加锁
3. wait 和 notify
方法名 | 所用 |
---|---|
final void wait() | 表示线程一直等待, 直到得到其他线程通知 |
void wait(long timeout) | 线程等待指定的毫秒数 |
final void wait(long timeout, int nanos) | 线程等待指定的毫秒, 微秒时间 |
final void notify() | 唤醒一个处于等待状态的线程 |
final void notifyAll() | 唤醒同一个对象上, 所有调用wait() 方法的线程, 优先级高的先运行 |
以上方法斗是
java.lang.Object
类的方法;都只能在同步方法或者同步代码块中使用, 否则会抛出异常.
死锁
1. 死锁的概念
1.1 死锁的定义
多个线程由于竞争资源而造成的一种互相等待的状态, 若无外力作用, 这些线程将无法推进.
1.2 死锁产生的原因
- 系统资源的竞争
- 线程推进顺序不合理
1.3 死锁产生的必要条件
- 互斥资源–在一段时间内只能被一个线程使用的资源, 具有排他性
- 不可剥夺–线程获得的资源在使用完之前不能被其他线程强行夺走, 只能主动释放
- 请求保持–线程已经保持了至少一个资源, 同时又在请求新的资源
- 循环等待–这些陷入死锁的线程中存在一个循环等待链
| final void wait(long timeout, int nanos) | 线程等待指定的毫秒, 微秒时间 |
| final void notify() | 唤醒一个处于等待状态的线程 |
| final void notifyAll() | 唤醒同一个对象上, 所有调用wait()
方法的线程, 优先级高的先运行 |
以上方法斗是
java.lang.Object
类的方法;都只能在同步方法或者同步代码块中使用, 否则会抛出异常.
死锁
1. 死锁的概念
1.1 死锁的定义
多个线程由于竞争资源而造成的一种互相等待的状态, 若无外力作用, 这些线程将无法推进.
1.2 死锁产生的原因
- 系统资源的竞争
- 线程推进顺序不合理
1.3 死锁产生的必要条件
- 互斥资源–在一段时间内只能被一个线程使用的资源, 具有排他性
- 不可剥夺–线程获得的资源在使用完之前不能被其他线程强行夺走, 只能主动释放
- 请求保持–线程已经保持了至少一个资源, 同时又在请求新的资源
- 循环等待–这些陷入死锁的线程中存在一个循环等待链