一.认识线程(Thread)
1. 1) 线程是什么
线程(Thread)是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。线程是独立调度的基本单位,在进程中有多个线程同时执行时,可以显著提高程序的运行效率。
一个进程可以拥有多个线程,这些线程共享进程的资源(如内存、文件句柄等),但每个线程都有自己独立的执行路径和堆栈。这意味着,虽然它们共享进程的内存空间,但每个线程可以独立地执行不同的代码路径,处理不同的数据。
1. 2) 为什么会有线程
线程的存在是为了解决在现代计算机系统中如何更有效地利用系统资源、提高程序执行效率和响应速度的问题。线程通过并行处理、资源共享、适应现代计算机体系结构以及简化编程模型等方式,为现代软件的发展提供了强有力的支持。
a、提高资源利用率和程序效率
- 并行处理:线程允许程序中的多个任务并行执行,而不是顺序执行。这意味着当某个线程因为等待I/O操作(如磁盘读写)或网络响应而被阻塞时,其他线程可以继续执行,从而提高了CPU的利用率和程序的执行效率。
- 资源共享:线程共享其所属进程的内存空间和系统资源,这使得线程间的数据共享和通信变得更加高效和方便。相比之下,进程之间的通信通常需要通过操作系统提供的机制(如管道、消息队列等),这些机制通常比线程间的通信更加复杂和开销更大。
b、适应现代计算机体系结构
- 多核处理器:现代计算机普遍采用多核处理器,每个核心都可以独立执行指令。线程的存在使得程序能够充分利用多核处理器的计算能力,通过并行执行多个线程来加速程序的执行。
- 对称多处理(SMP)环境:在SMP环境中,多个处理器共享相同的内存和总线结构。线程使得程序能够在这个环境中更好地运行,因为它们可以跨多个处理器核心并行执行,从而进一步提高程序的执行效率。
c、简化编程模型
- 模块化设计:线程允许程序员将复杂的程序分解为多个相对简单的模块(即线程),每个模块负责完成特定的任务。这种模块化设计使得程序更加易于理解和维护。
- 并发编程:线程是实现并发编程的基本单位。通过并发编程,程序员可以编写出能够同时处理多个任务的程序,从而提高程序的响应速度和用户体验。
d、适应现代软件需求
- 实时性要求:许多现代软件(如实时控制系统、在线游戏等)对实时性有很高的要求。线程允许这些软件通过并行处理多个任务来满足实时性要求。
- 交互式应用:在交互式应用中(如GUI程序、Web服务器等),线程可以提高程序的响应速度,使得用户能够更快地获得反馈和结果。
1.3) 进程和线程的区别
- 进程是包含线程的. 每个进程至少有⼀个线程存在,即主线程。
- 进程和进程之间不共享内存空间. 同⼀个进程的线程之间共享同⼀个内存空间.
- 进程是系统分配资源的最小单位,线程是系统调度的最小单位。
- ⼀个进程挂了⼀般不会影响到其他进程. 但是⼀个线程挂了, 可能把同进程内的其他线程⼀起带走(整 个进程崩溃)
二.创建线程(主要方式)
方法1:继承Thread类
public class ThreadDemo {
public static void main(String[] args) {
/*
* 多线程的第一种启动方式
* 1.自己定义一个继承Thread
* 2.重写run方法
* 3.创建子类的对象,并启动线程
* */
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.setName("黄");
t2.setName("红");
t1.start();
t2.start();
}
}
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName() + ": Hello World");
}
}
}
Java是单继承, 继承Thread类之后无法再继承其他类,需要为每个需要线程执行的方法单独创建一个class文件,开发效率较低,一般不推荐使用
方法2:实现Runnable接口
public class ThreadDemo {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start();
}
}
public class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("Hello World");
}
}
匿名内部类的实现方式
public class ThreadDemo {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("金黄金黄");
}
});
t1.start();
}
}
只需要实现Runnable接口, 不影响正常代码结构,可以通过匿名内部类的方式实现, 开发简单
方法3:实现Callable接口
public class ThreadDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建MyCallable的对象(表示多线程要执行的结果)
MyCallable mc = new MyCallable();
//创建FutureTask的对象 (作用管理多线程运行的结果)
FutureTask<Integer> ft = new FutureTask<>(mc);
//创建Thread类的对象,并启动(表示线程)
Thread t = new Thread(ft);
//开启线程
t.start();
Integer result = ft.get();
System.out.println(result);
}
}
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i <= 100; i++) {
sum = sum + i;
}
return sum;
}
}
可以获取到线程处理完成结果的返回值, 在特定情况下使用,代码复杂, 开发成本较
三、线程的状态
首先我们要区分线程的状态到底是五种还是六种?
在传统(操作系统)的线程模型中线程被分为五种状态
而在java线程中,线程被分为六种状态
1、传统线程模型(操作系统)中线程状态(五种)
线程的五种状态:
1.新建(new)
创建了一个新的线程对象
2.就绪(runnable)
调用线程的start()方法,处于就绪状态
3.运行(running)
获得了CPU时间片,执行程序代码
就绪状态是进入到运行状态的唯一入口
4.阻塞(block)
因为某种原因,线程放弃对CPU的使用权,停止执行,直到进入就绪状态在有可能再次被CPU调度
阻塞又分为三种:
1)等待阻塞:运行状态的线程执行wait方法,JVM会把线程放在等待队列中,使本线程进入阻塞状态。
2)同步阻塞:线程在获得synchronized同步锁失败,JVM会把线程放入锁池中,线程进入同步阻塞。对于锁池和等待池,可以看这篇文章
3)其他阻塞:调用线程的sleep()或者join()后,线程会进入道阻塞状态,当sleep超时或者join终止或超时,线程重新转入就绪状态
5.死亡(dead)
线程run()、main()方法执行结束,或者因为异常退出了run()方法,则该线程结束生命周期
死亡的线程不可再次复生
2、Java线程中的状态(六种)
通过查看Thread类的State方法,我们可以看到Java线程其实是六种状态
public enum State {
/**
* Thread state for a thread which has not yet started.
*/
NEW,
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
RUNNABLE,
/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
*/
BLOCKED,
/**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
* <ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </ul>
*
* <p>A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called <tt>Object.wait()</tt>
* on an object is waiting for another thread to call
* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
* that object. A thread that has called <tt>Thread.join()</tt>
* is waiting for a specified thread to terminate.
*/
WAITING,
/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
TIMED_WAITING,
/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
TERMINATED;
}
我们可以看到线程实际上是分为六种状态的,既
1.初始状态(NEW)
线程被构建,但是还没有调用start方法
2.运行状态(RUNNABLE)
Java线程把操作系统中就绪和运行两种状态统一称为“运行中”
3.阻塞状态(BLOCKED)
表示线程进入等待状态,也就是线程因为某种原因放弃了CPU的使用权,阻塞也分为几种情况(当一个线程试图获取一个内部的对象锁(非java.util.concurrent库中的锁),而该锁被其他线程持有,则该线程进入阻塞状态。)
等待阻塞:运行的线程执行了Thread.sleep、wait、join等方法,JVM会把当前线程设置为等待状态,当sleep结束,join线程终止或者线程被唤醒后,该线程从等待状态进入阻塞状态,重新占用锁后进行线程恢复
同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被其他线程锁占用了,那么JVM会把当前项城放入到锁池中
其他阻塞:发出I/O请求,JVM会把当前线程设置为阻塞状态,当I/O处理完毕则线程恢复
4.等待(WAITING)
等待状态,没有超时时间(无限等待),要被其他线程或者有其他的中断操作
执行wait、join、LockSupport.park()
5.超时等待(TIME_WAITING)
与等待不同的是,不是无限等待,超时后自动返回
执行sleep,带参数的wait等可以实现
6.终止(TERMINATED)
代表线程执行完毕
线程状态间的转换
具体的转换场景,图中描述的比较清楚,此处不再详细赘述。
注意:
1)sleep、join、yield时并不释放对象锁资源,在wait操作时会释放对象资源,wait在被notify/notifyAll唤醒时,重新去抢夺获取对象锁资源。
2)sleep可以在任何地方使用,而wait,notify,notifyAll只能在同步控制方法或者同步控制块中使用。
3)调用obj.wait()会立即释放锁,以便其他线程可以执行notify(),但是notify()不会立刻立刻释放sycronized(obj)中的对象锁,必须要等notify()所在线程执行完sycronized(obj)同步块中的所有代码才会释放这把锁。然后供等待的线程来抢夺对象锁。
四、什么是线程安全,怎么确保线程安全
简单来说,线程安全是多个线程访问同一段代码,不会造成不确定的结果。
线程安全就是多线程访问时,采用了加锁机制,同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再对共享数据进行操作,确保不会出现数据不一致或者数据污染。
线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据
那怎么保证线程安全问题呢
我们先了解一下并发编程的三大特性:
1.原子性(Atomicity):原子性是指一个操作是不可中断的,要么全部执行完成,要么完全不执行。在并发编程中,原子性是保证多线程操作共享变量的线程安全性的基础。
2.可见性(Visibility):可见性是指当一个线程修改了共享变量的值后,其他线程能够立即看到这个修改。在多线程环境下,由于线程之间的指令重排、缓存不一致等原因,共享变量的修改可能对其他线程不可见,导致数据不一致的问题。
3.有序性(Ordering):有序性是指程序执行的结果按照一定的顺序来保证,即使在多线程环境下也能得到正确的结果。在多线程环境下,由于指令重排的存在,程序的执行顺序可能与代码的编写顺序不一致,导致结果出现错误。
说一下造成线程不安全的三个原因,主要是:
原子性:一个或多个线程操作 CPU 执行的过程中被中断,互斥性称为操作的原子性。
可见性:一个线程对共享变量的修改,其他线程不能立刻看到。
有序性:程序执行的顺序没有按照代码的先后顺序执行。
针对上述三个造成线程不安全的问题,java 程序如何保证线程安全呢?
1. 针对原子性:
JDK 里面提供了很多 atomic 类,比如 AtomicInteger、AtomicLong、AtomicBoolean 等等,这些类本身可以通过 CAS 来保证操作的原子性。另外 Java 也提供了各种锁机制,来保证锁内的代码块在同一时刻只能有一个线程执行,比如使用 synchronized 加锁,保证一个线程在对资源进行读、写时,其他线程不可对此资源进行操作,从而保证了线程的安全性。
2. 针对可见性:
同样可以通过 synchronized 关键字加锁来解决,与此同时,java 还提供了 volatile 关键字,要优于 synchronized 的性能,同样可以保证修改对其他线程的可见性。volatile 一般用于对变量的写操作不依赖于当前值的场景中,比如状态标记量等。
3. 针对重排序问题:
可以通过 synchronized 关键字定义同步代码块或者同步方法保障有序性,另外也可以通过 Lock 接口来保障有序性。
五、其他相关问题
5.1 Wait(1000)和Sleep(1000)的区别
sleep和wait的区别:
1、sleep是Thread的静态方法,wait是Object的方法,任何对象实例都能调用。
2、sleep不会释放锁,它也不需要占用锁。wait会释放锁,但调用它的前提是当前线程占有锁(即代码要在synchronized中)。
3、它们都可以被interrupted方法中断。
具体来说:
Thread.Sleep(1000) 意思是在未来的1000毫秒内本线程不参与CPU竞争,1000毫秒过去之后,这时候也许另外一个线程正在使用CPU,那么这时候操作系统是不会重新分配CPU的,直到那个线程挂起或结束,即使这个时候恰巧轮到操作系统进行CPU 分配,那么当前线程也不一定就是总优先级最高的那个,CPU还是可能被其他线程抢占去。另外值得一提的是Thread.Sleep(0)的作用,就是触发操作系统立刻重新进行一次CPU竞争,竞争的结果也许是当前线程仍然获得CPU控制权,也许会换成别的线程获得CPU控制权。
wait(1000)表示将锁释放1000毫秒,到时间后如果锁没有被其他线程占用,则再次得到锁,然后wait方法结束,执行后面的代码,如果锁被其他线程占用,则等待其他线程释放锁。注意,设置了超时时间的wait方法一旦过了超时时间,并不需要其他线程执行notify也能自动解除阻塞,但是如果没设置超时时间的wait方法必须等待其他线程执行notify。
5.2 如何停止一个正在运行的线程
1. 使用标志位
最常见且推荐的方式是使用一个共享的标志位来控制线程的执行。这个标志位可以是volatile
类型的,以确保线程间的可见性。
public class MyThread extends Thread {
private volatile boolean running = true;
public void run() {
while (running) {
// 执行任务
}
}
public void stopThread() {
running = false;
}
}
在这个例子中,running
标志位用于控制while
循环的继续执行。通过调用stopThread()
方法,可以将running
设置为false
,从而中断循环,使线程停止执行。
2. 使用interrupt()
方法
Java提供了interrupt()
方法来中断线程。但是,这并不会立即停止线程的执行,而是会设置线程的中断状态。线程需要定期检查这个中断状态,并据此决定是否需要停止执行。
public class MyThread extends Thread {
public void run() {
try {
while (!Thread.currentThread().isInterrupted()) {
// 执行任务
}
} catch (InterruptedException e) {
// 处理中断异常
Thread.currentThread().interrupt(); // 保持中断状态
}
}
}
// 在其他地方
MyThread thread = new MyThread();
thread.start();
// ...
thread.interrupt(); // 请求中断线程
注意,某些阻塞操作(如Thread.sleep()
、Object.wait()
等)在接收到中断请求时会抛出InterruptedException
。在这些情况下,你需要在catch
块中调用Thread.currentThread().interrupt()
来重新设置中断状态,以便上层调用者能够感知到中断。
3. 使用stop()
方法(不推荐)
Java的Thread
类提供了stop()
和suspend()
方法,但这些方法都是过时的,并且不建议使用。stop()
方法会立即停止线程,但这可能会导致数据不一致或其他问题,因为它会立即终止线程的执行,而不会等待线程中的同步块或IO操作完成。
结论
推荐使用标志位或interrupt()
方法来停止线程,因为它们提供了更可控、更安全的线程停止机制。这些机制允许线程在停止前完成必要的清理工作,并避免了数据不一致的风险。