文章目录
一、什么是线程
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
进程与线程的区别
1.进程是系统进行资源分配和调度的一个独立单位,线程是程序执行的最小单位。
2. 进程有自己的内存地址空间,线程只独享指令流执行的必要资源,如寄存器和栈。
3. 由于同一进程的各线程间共享内存和文件资源,可以不通过内核进行直接通信。
4. 线程的创建、切换及终止效率更高。
二、为什么需要线程
1.创建一个新线程的代价要比创建一个新进程小得多
2. 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
3. 线程占用的资源要比进程少很多
4. 能充分利用多处理器的可并行数量
5. 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
6. 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
7. I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
三、线程的基本知识
1.创建一个线程
方法一:继承Thread类
可以通过继承 Thread 来创建一个线程类
class MyThread extends Thread {
@Override
public void run() {
System.out.println("这里是线程运行的代码");
}
}
MyThread t = new MyThread();
t.start(); // 线程开始运行
方法二:实现Runnable接口
通过实现 Runnable 接口,并且调用 Thread 的构造方法时将 Runnable 对象作为 target 参数传入来创建线程对象。
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("这里是线程运行的代码");
}
}
Thread t = new Thread(new MyRunnable());
t.start(); // 线程开始运行
2.Thread类及常见方法
Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关联。
线程中常见的构造方法
方法 | 说明 |
---|---|
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用Runnable对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnable target,String name) | 使用Runnable对象创建线程对象,并命名 |
Thread的几个常见属性
属性 | 获取方法 |
---|---|
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
• ID 是线程的唯一标识,不同线程不会重复
• 名称是各种调试工具用到
• 状态表示线程当前所处的一个情况
• 优先级高的线程理论上来说更容易被调度到
• 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
• 是否存活,即简单的理解,为 run 方法是否运行结束了
• 线程的中断问题
启动一个线程
线程对象被创建出来并不意味着线程就开始运行
启动线程必须要调用 start 方法。
中断一个线程
目前常见的有以下两种方式:
- 通过共享的标记来进行沟通
public class ThreadDemo {
private static class MyRunnable implements Runnable {
public volatile boolean isQuit = false;
@Override
public void run() {
while (!isQuit) {
System.out.println("子线程运行中");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("子线程该停止了");
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target);
System.out.println("子线程开始运行");
thread.start();
Thread.sleep(10 * 1000);
System.out.println("让子线程停止");
target.isQuit = true;
}
}
- 调用 interrupt() 方法来通知
public class ThreadDemo {
private static class MyRunnable implements Runnable {
@Override
public void run() {
while (!Thread.interrupted()) {
System.out.println("子线程正在运行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("子线程该停止了");
break;
}
}
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target);
System.out.println("子线程开始运行");
thread.start();
Thread.sleep(10 * 1000);
System.out.println("让子线程停止");
thread.interrupt();
}
}
第二种方法中:
- 通过 thread 对象调用 interrupt() 方法通知该线程停止运行
- thread 收到通知的方式有两种:
1.如果线程调用了 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException 异常的形式通知,清除中断标志
2.否则,只是内部的一个中断标志被设置,thread 可以通过
(1)Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志
(2)Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志
第二种方式通知收到的更及时,即使线程正在 sleep 也可以马上收到。
等待一个线程
通过调用join()方法来等待当前线程结束再执行后续操作
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
Runnable target = () -> {
for (int i = 0; i < 10; i++) {
try {
System.out.println("子线程还在工作!");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("子线程结束了!");
};
Thread thread = new Thread(target);
System.out.println("子线程开始工作");
thread.start();
thread.join();
System.out.println("子线程工作结束了");
}
}
2.状态的转移
观察线程的所有状态
public class ThreadState {
public static void main(String[] args) {
for (Thread.State state : Thread.State.values()) {
System.out.println(state);
}
}
}
NEW
RUNNABLE
BLOCKED
WAITING
TIMED_WAITING
TERMINATED
线程状态和状态转移
NEW : 初始化
RUNNABLE :RUNNING 正在运行;READY 等待系统调度
BLOCKED、WAITING、TIMED_WAITING :等待直到一些事情发生
TERMINATED:执行完成
wait()方法
其实wait()方法就是使线程停止运行。
- 方法wait()的作用是使当前执行代码的线程进行等待,wait()方法是Object类的方法,该方法是用来将当前线程置入“预执行队列”中,并且在wait()所在的代码处停止执行,直到接到通知或被中断为止。
- wait()方法只能在同步方法中或同步块中调用。如果调用wait()时,没有持有适当的锁,会抛出异常。
- wait()方法执行后,当前线程释放锁,线程与其它线程竞争重新获取锁。
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
synchronized (object) {
System.out.println("等待中...");
object.wait();
System.out.println("等待已过...");
}
System.out.println("main方法结束...");
}
notify()方法
notify方法就是使停止的线程继续运行。
- 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。如果有多个线程等待,则有线程规划器随机挑选出一个呈wait状态的线程。
- 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
class MyThread implements Runnable {
private boolean flag;
private Object obj;
public MyThread(boolean flag, Object obj) {
super();
this.flag = flag;
this.obj = obj;
}
public void waitMethod() {
synchronized (obj) {
try {
while (true) {
System.out.println("wait()方法开始.. " + Thread.currentThread().getName());
obj.wait();
System.out.println("wait()方法结束.. " + Thread.currentThread().getName());
return;
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public void notifyMethod() {
synchronized (obj) {
try {
System.out.println("notify()方法开始.. " + Thread.currentThread().getName());
obj.notify();
System.out.println("notify()方法结束.. " + Thread.currentThread().getName());
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
public void run() {
if (flag) {
this.waitMethod();
} else {
this.notifyMethod();
}
}
}
public class TestThread {
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
MyThread waitThread = new MyThread(true, object);
MyThread notifyThread = new MyThread(false, object);
Thread thread1 = new Thread(waitThread, "wait线程");
Thread thread2 = new Thread(notifyThread, "notify线程");
thread1.start();
Thread.sleep(1000);
thread2.start();
System.out.println("main方法结束!!");
}
}
从结果上来看第一个线程执行的是一个waitMethod方法,该方法里面有个死循环并且使用了wait方法进入等待状态将释放锁,如果这个线程不被唤醒的话将会一直等待下去,这个时候第二个线程执行的是notifyMethod方法,该方法里面执行了一个唤醒线程的操作,并且一直将notify的同步代码块执行完毕之后才会释放锁然后继续执行wait结束打印语句。
notifyAll()方法
notifyAll方法可以一次唤醒所有的等待线程
class MyThread implements Runnable {
private boolean flag;
private Object obj;
public MyThread(boolean flag, Object obj) {
super();
this.flag = flag;
this.obj = obj;
}
public void waitMethod() {
synchronized (obj) {
try {
while (true) {
System.out.println("wait()方法开始.. " + Thread.currentThread().getName());
obj.wait();
System.out.println("wait()方法结束.. " + Thread.currentThread().getName());
return;
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public void notifyMethod() {
synchronized (obj) {
try {
System.out.println("notifyAll()方法开始.. " + Thread.currentThread().getName());
obj.notifyAll();
System.out.println("notifyAll()方法结束.. " + Thread.currentThread().getName());
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
public void run() {
if (flag) {
this.waitMethod();
} else {
this.notifyMethod();
}
}
}
public class TestThread {
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
MyThread waitThread1 = new MyThread(true, object);
MyThread waitThread2 = new MyThread(true, object);
MyThread waitThread3 = new MyThread(true, object);
MyThread notifyThread = new MyThread(false, object);
Thread thread1 = new Thread(waitThread1, "wait线程A");
Thread thread2 = new Thread(waitThread2, "wait线程B");
Thread thread3 = new Thread(waitThread3, "wait线程C");
Thread thread4 = new Thread(notifyThread, "notify线程");
thread1.start();
thread2.start();
thread3.start();
Thread.sleep(1000);
thread4.start();
System.out.println("main方法结束!!");
}
}
wait 和 sleep 的对比
- wait 之前需要请求锁,而wait执行时会先释放锁,等被唤醒时再重新请求锁。
- sleep 是无视锁的存在的,即之前请求的锁不会释放,没有锁也不会请求。
- wait 是 Object 的方法
- sleep 是 Thread 的静态方法
*yield()只是让出CPU,并不会改变自己的状态
四、线程共享资源
线程共享的环境包括:进程代码段、进程的公有数据(利用这些共享的数据,线程很容易的实现相互之间的通讯)、进程打开的文件描述符、信号的处理器、进程的当前目录和进程用户ID与进程组ID。
【个性】进程拥有这许多共性的同时,还拥有自己的个性,才能实现并发性
- 线程ID:每个线程都有自己的线程ID,这个ID在本进程中是唯一的。进程用此来标识线程。
- 寄存器组的值:由于线程间是并发运行的,每个线程有自己不同的运行线索,当从一个线程切换到另一个线程上 时,必须将原有的线程的寄存器集合的状态保存,以便将来该线程在被重新切换到时能得以恢复。
- 线程的堆栈:堆栈是保证线程独立运行所必须的。线程函数可以调用函数,而被调用函数中又是可以层层嵌套的,所以线程必须拥有自己的函数堆栈, 使得函数调用可以正常执行,不受其他线程的影响。
- 错误返回码:由于同一个进程中有很多个线程在同时运行,可能某个线程进行系统调用后设置了errno值,而在该 线程还没有处理这个错误,另外一个线程就在此时被调度器投入运行,这样错误值就有可能被修改。所以,不同的线程应该拥有自己的错误返回码变量。
- 线程的信号屏蔽码:由于每个线程所感兴趣的信号不同,所以线程的信号屏蔽码应该由线程自己管理。但所有的线程都 共享同样的信号处理器。
- 线程的优先级:由于线程需要像进程那样能够被调度,那么就必须要有可供调度使用的参数,这个参数就是线程的优先级。
五、线程安全问题
1.什么是线程安全
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。
2.线程不安全的原因
原子性
指事务的不可分割性,一个事务的所有操作要么不间断地全部被执行,要么一个也没有执行。
一条 java 语句不一定是原子的,也不一定只是一条指令,比如i++,其实是由三步操作组成的:
- 从内存把数据读到 CPU
- 进行数据更新
- 把数据写回到 CPU
不保证原子性会给多线程带来什么问题?
如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的。
内存可见性
为了提高效率,JVM在执行过程中,会尽可能的将数据在工作内存中执行,但这样会造成一个问题,共享变量在多线程之间不能及时看到改变,这个就是可见性问题。
代码重排序
一段代码是这样的:
1.去前台取下 U 盘
2. 去教室写 10 分钟作业
3. 去前台取下快递
如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题,可以少跑一次前台。这种叫做指令重排序
代码重排序会给多线程带来什么问题?
刚才那个例子中,单线程情况是没问题的,优化是正确的,但在多线程场景下就有问题了。可能快递是在你写作业的10分钟内被另一个线程放过来的,或者被人变过了,如果指令重排序了,代码就会是错误的。
3.怎么判断代码是否有线程安全风险
1.是否是多线程环境?
单线程不会涉及线程安全问题。
2.是否有共享数据?
没有共享数据的多线程不会涉及线程安全问题
3.是否有多条语句操作共享数据?
没有多条语句操作共享数据不会涉及线程安全问题
4.通过什么机制保护线程安全
synchronized关键字
class MyThread implements Runnable {
private int ticket = 1000 ; // 一共十张票
@Override public void run() {
for (int i = 0; i < 1000; i++) {
// 在同一时刻,只允许一个线程进入代码块处理
synchronized(this) { // 表示为程序逻辑上锁
if(this.ticket>0) { // 还有票
try {
Thread.sleep(20);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName()+",还有" +this.ticket -- +" 张票");
}
}
}
}
}
public class TestDemo {
public static void main(String[] args) {
MyThread mt = new MyThread();
Thread t1 = new Thread(mt,"黄牛A");
Thread t2 = new Thread(mt,"黄牛B");
Thread t3 = new Thread(mt,"黄牛C");
t1.start();
t2.start();
t3.start();
}
}
volatile 关键字
修饰的共享变量,可以保证可见性,部分保证顺序性
class ThraedDemo {
private volatile int n;
}