线程的创建
1. 通过继承Thread类实现多线程
通过继承Thread
类来自定义一个线程类, 并重写run()
方法. run()
方法是一个线程的线程体, 也就是开启这个线程后它要执行的部分. 之后, 直接在main
方法中通过new()
的方式创建两个线程实例, 再通过start()
方法, 让这两个线程运行起来. 运行起来之后就开始执行run()
方法中的代码了.
为了方便我们看到结果, 使用getName()
方法来获取当前线程的name
. 这个方法是父类Thread
中的方法, 直接调用即可.
public class ThreadTest extends Thread {
public ThreadTest() {
System.out.println(this.getName());
}
/**
* 线程的线程体
*/
@Override
public 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());
}
/**
* 当前线程的线程体
*/
@Override
public 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(){
@Override
public 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;
@Override
public 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{
@Override
public 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{
@Override
public 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 {
@Override
public 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 {
@Override
public 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 {
@Override
public 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 {
@Override
public 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 {
@Override
public 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 {
@Override
public 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 死锁产生的必要条件
- 互斥资源–在一段时间内只能被一个线程使用的资源, 具有排他性
- 不可剥夺–线程获得的资源在使用完之前不能被其他线程强行夺走, 只能主动释放
- 请求保持–线程已经保持了至少一个资源, 同时又在请求新的资源
- 循环等待–这些陷入死锁的线程中存在一个循环等待链