多线程基础
什么是多线程
在Java中,多线程(Multithreading)是并发编程的一种形式,它允许在一个程序中同时运行多个线程。每个线程都是程序的一个独立执行流,拥有自己的堆栈和程序计数器,但共享程序的其他部分(如内存和静态变量)。
以下是多线程的几个关键概念:
- 线程: 线程是程序执行流的最小单元。Java虚拟机(JVM)允许一个进程并发地执行多个线程。
- 并发: 并发意味着多个线程在同一时间段内执行。然而,由于CPU资源有限,实际上同一时刻只有一个线程在执行,但线程的切换速度非常快,使得多个线程看起来像是在同时执行。
- 并行: 并行是并发的特例,当多个线程在同一时刻(在不同的CPU或处理器核心上)同时执行时,称为并行。
- 线程状态: Java中的线程具有多种状态,包括新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)。
- 线程同步: 由于多个线程共享程序的资源,因此需要对这些资源进行同步以防止数据不一致或冲突。Java提供了多种同步机制,如synchronized关键字、Lock接口和相关的类(如ReentrantLock)、volatile关键字和原子类(如AtomicInteger)等。
- 线程安全: 线程安全意味着多个线程在并发执行时能够正确地访问共享资源,而不会导致数据不一致或其他问题。编写线程安全的代码是并发编程中的一项重要挑战。
- 线程池: 线程池是一种用于管理和复用线程的机制。通过线程池,可以减少线程的创建和销毁开销,提高程序的性能。Java提供了几种线程池实现,如FixedThreadPool、CachedThreadPool和ScheduledThreadPool等。
在Java中,可以通过实现Runnable接口或继承Thread类来创建线程。然后,可以使用Thread类的start()方法启动线程。此外,Java还提供了丰富的并发工具类库,如java.util.concurrent包中的类,以支持更复杂的并发编程需求。
第一种实现方式:继承Thread类的方式进行实现
package com.Java基础.多线程.Thread001;
public class Thread01 {
public static void main(String[] args) {
/*
* 多线程第一种启动方式:
* 1.自己定义一个Thread
* 2.重写run方法
* 3.创建子类对象,并启动线程
*/
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
myThread1.setName("第一个");
myThread2.setName("第二个");
myThread1.start();
myThread2.start();
}
}
第二种实现方式:实现Runnable接口的方式进行实现
package com.Java基础.多线程.Thread002;
public class Thread1 {
public static void main(String[] args) {
/*
* 第二种实现方式
* 1.自己定义一个类实现Runnable接口
2.重写里面的run方法
3.创建自己的类的对象
4.创建一个Thread类的对象,并开启线程
*/
//创建MyRun的对象
//表示多线程要执行的任务
MyRun myRun = new MyRun();
//创建线程对象
Thread t1 = new Thread(myRun);
Thread t2 = new Thread(myRun);
t1.setName("线程1");
t2.setName("线程2");
//开启线程
t1.start();
t2.start();
}
}
第三种实现方式:利用Callable接口和Future接口方式实现
package com.Java基础.多线程.Thread003;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Thread03 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
/*
* 多线程实现第三个方式: 可以获得多线程运行的结果
* 1.创建一个类MyCallable实现callable接口
* 2.重写ca11 (是有返回值的,表示多线程运行的结果)
* 3.创建MyCallable的对象(表示多线程要执行的任务)
* 4.创建Futurerask的对象(作用管理多线程运行的结果)
* 5.创建Thread类的对象,并启动(表示线程)
*/
//创建MyCallable的对象(表示多线程要执行的任务)
MyCallable mc = new MyCallable();
//创建Futurerask的对象(作用管理多线程运行的结果)
FutureTask<Integer> ft = new FutureTask<>(mc);
//创建Thread类的对象,并启动(表示线程)
Thread t1 = new Thread(ft);
t1.start();
//获取结果
Integer result = ft.get();
System.out.println(result);
}
}
多线程三种启动方式对比
在Java多线程里面常见的成员方法
在Java多线程编程中,Thread
类和其他相关类(如 Runnable
、ExecutorService
、Future
等)提供了许多用于管理和控制线程的成员方法。以下是一些在Java多线程编程中常见的成员方法:
-
Thread类方法
start(): 启动一个新线程并执行此线程中的 run() 方法。
run(): 如果此线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。
currentThread(): 返回对当前正在执行的线程对象的引用。
interrupt(): 中断此线程。除非此线程当前正在等待、睡眠或以其他方式被阻塞,否则该线程将不会受到中断的影响。
isInterrupted(): 测试此线程是否已被中断。线程的中断状态由 interrupt() 方法设置。
sleep(long millis): 使当前正在执行的线程在指定的毫秒数内处于休眠(暂停执行)状态。
join(): 等待该线程终止。(插队线程/插入线程)
setPriority(int newPriority): 更改此线程的优先级。
getPriority(): 返回此线程的优先级。
setDaemon(boolean on): 将此线程标记为守护线程或用户线程。
isDaemon(): 测试此线程是否为守护线程。
getState(): 返回此线程的状态。
yield(): 暂停当前正在执行的线程对象,并执行其他线程。(出让线程/礼让线程)
stop(): 已废弃。停止此线程的执行。此方法会引发 ThreadDeath 异常。
resume(): 已废弃。恢复此线程的执行。
suspend(): 已废弃。暂停此线程的执行。 -
Runnable接口的方法:
run(): 由线程执行的方法。实现此接口的类必须定义此未指定的方法。
-
ExecutorService接口的方法(常用于线程池):
submit(Runnable task): 提交一个 Runnable 任务用于执行,并返回一个表示该任务的未来结果的 Future。
submit(Callable task): 提交一个 Callable 任务用于执行,并返回一个表示该任务的未来结果的 Future。
shutdown(): 启动此执行程序的有序关闭序列,在此序列中,先前提交的任务将被执行,但不会接受新任务。
shutdownNow(): 试图停止所有正在执行的活动任务,暂停处理正在等待的任务,并返回等待执行的任务列表。
execute(Runnable command): 在未来某个时间点为给定命令执行一个任务。
awaitTermination(long timeout, TimeUnit unit): 如果此执行程序在此给定的超时时间之前终止,则返回 true(以及如果已终止则为 true)。
isTerminated(): 如果所有任务都已完成执行,则返回 true。
isShutdown(): 如果此执行程序已关闭,则返回 true。 -
Future接口的方法(常与 ExecutorService 一起使用)
get(): 如有必要,等待计算完成,然后检索其结果。
get(long timeout, TimeUnit unit): 如有必要,最多等待给定的时间以获取结果(如果可用)。
cancel(boolean mayInterruptIfRunning): 尝试取消此任务的执行。
isDone(): 如果任务已完成,则返回 true。
isCancelled(): 如果任务在完成前被取消,则返回 true。
这些只是Java多线程编程中常见的一部分方法。根据具体的需求和场景,可能会使用到更多的方法和类。
线程优先级: 线程先完成的概率
守护线程: 当其他非守护线程执行完毕之后,守护线程也会陆续结束
线程的生命周期
线程的安全问题
同步代码块
把操作共享数据的代码锁起来
synchronized (object) {//锁对象必须是唯一的
// 需要同步的代码
}
- 特点1:锁默认打开,有一个线程进去了,锁自动关闭
- 特点2:里面的代码全部执行完毕,线程出来,锁自动打开
同步方法
就是把synchronized关键字加到方法上
修饰符 synchronized 返回值 方法名(方法参数){.....}
- 特点1:同步方法是锁住方法里面的所有代码
- 特点2:锁对象不能自己指定,非静态:this 静态:当前类的字节码文件对象
案例1:
public class MyThread extends Thread{
static int ticket=0;//0~99
//锁对象必须唯一
static Object lock=new Object();
@Override
public void run() {
while(true)
{
synchronized (lock) {
if(ticket<100)
{
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(getName()+"票正在卖"+ticket);
}else
{
break;
}
}
}
}
}
案例2:
public class My implements Runnable {
int ticket=0;
@Override
public void run() {
//1.循环
// 2.同步代码块(同步方法)
//3.判断共享数据是否到了末尾,如果到了末尾
// 4.判断共享数据是否到了末尾,如果没有到末尾
while(true){
synchronized (My.class) {
if(ticket==100){
break;
}
else{
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ticket++;
System.out.println(Thread.currentThread().getName()+"正在卖票"+ticket);
}
}
}
}
}
Lock锁
虽然我们可以理解同步代码块和同步方法的锁对象问题但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock
Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作Lock中提供了获得锁和释放锁的方法
void lock()
:获得锁
void unlock()
:释放锁
手动上锁、手动释放锁
Lock是接口不能直接实例化这里采用它的实现类ReentrantLock来实例化
ReentrantLock的构造方法
创建一个ReentrantLock的实例ReentrantLock():
案例:
public class My implements Runnable {
int ticket=0;
Lock lock = new ReentrantLock();
@Override
public void run() {
//1.循环
// 2.同步代码块(同步方法)
//3.判断共享数据是否到了末尾,如果到了末尾
// 4.判断共享数据是否到了末尾,如果没有到末尾
while(true){
//synchronized (My.class) {
lock.lock();
try {
if(ticket==100){
break;
}
else{
Thread.sleep(10);
ticket++;
System.out.println(Thread.currentThread().getName()+"正在卖票"+ticket);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
//}
}
}
}
死锁
在Java中,死锁是指两个或多个线程在互相请求对方占用的资源时处于等待状态,导致程序无法继续执行的现象。具体来说,每个线程都持有一个资源并且同时等待另一个资源,形成一种僵局,没有任何一个线程能够释放其持有的资源,也无法获得它所需的资源。
以下是关于Java死锁的一些注意事项:
- 死锁的必要条件:死锁的发生通常需要满足以下四个条件,也被称为死锁的必要条件:
- 互斥条件:至少有一个资源在任意时刻只能被一个线程占用。
- 保持和等待条件:一个线程至少持有一个资源,并等待获取一个当前被其他线程持有的资源。
- 不可剥夺条件:资源只能由持有它的线程释放,资源不可以被剥夺。
- 循环等待条件:存在一个等待资源的循环,即每个线程都在等待下一个线程释放资源。
为了避免Java中的死锁,可以采取以下策略:
- 避免嵌套锁:尽量避免一个线程在持有一个锁的同时去请求另一个锁。如果确实需要获取多个锁,应该确保所有线程以相同的顺序请求和释放锁。
- 使用定时锁:使用tryLock()方法来尝试获取锁,并设置一个超时时间。如果超时时间内没有获取到锁,那么就放弃获取锁,并做一些其他的处理,比如重试或者返回错误。
- 锁分割:将大的锁分割成几个小的锁,使得不同的线程可以同时访问不同的资源。
- 检测死锁:使用工具或JVM内置功能(如JConsole)来监控和检测系统中的死锁,然后进行相应的处理。
- 资源分配策略:设计合理的资源分配策略,确保线程在请求资源时不会形成循环等待的情况。
总之,死锁是Java多线程编程中需要特别注意的问题。为了避免死锁的发生,需要深入理解死锁的原理和必要条件,并采取相应的策略来预防和处理死锁。
等待唤醒机制(生产者和消费者)
在Java中,等待唤醒机制是线程间通信的一种重要手段,它允许一个线程等待某个条件成立,而另一个线程可以通知等待的线程该条件已经成立。等待唤醒机制主要依赖于wait()和notify()/notifyAll()这几个方法。
使用方法:
wait() 方法:
当线程调用某个对象的wait()方法时,它会释放该对象的锁,并进入等待状态,直到其他线程调用该对象的notify()或notifyAll()方法。
wait()方法只能在同步方法或同步块中调用,否则会抛出IllegalMonitorStateException异常。
notify() 方法:
当线程调用某个对象的notify()方法时,它会唤醒在该对象上等待的单个线程(如果有的话)。
notify()方法也必须在同步方法或同步块中调用,否则会抛出IllegalMonitorStateException异常。
notifyAll() 方法:
当线程调用某个对象的notifyAll()方法时,它会唤醒在该对象上等待的所有线程。
同样,notifyAll()方法也必须在同步方法或同步块中调用。
注意事项:
必须在同步块或同步方法中调用:wait()、notify()和notifyAll()方法必须在同步块或同步方法中调用,因为它们是用于操作对象锁的。
释放锁:当线程调用wait()方法时,它会释放当前持有的锁,并进入等待状态。这使得其他线程有机会获取该锁并执行。
唤醒机制:notify()方法只会唤醒等待队列中的一个线程(如果有的话),而notifyAll()方法会唤醒等待队列中的所有线程。但是,这并不意味着被唤醒的线程会立即执行,它们需要等待重新获取锁。
虚假唤醒:即使没有线程调用notify()或notifyAll()方法,等待的线程也可能被唤醒(这被称为虚假唤醒)。因此,在wait()方法后,应该使用一个循环来检查条件是否真正满足。
异常处理:wait()方法可能会抛出InterruptedException异常,因此在使用时需要处理这个异常。通常的做法是捕获该异常并重新等待,或者根据具体情况进行其他处理。
避免死锁:虽然等待唤醒机制本身不会导致死锁,但在多线程编程中仍然需要注意避免死锁的发生。例如,确保线程在获取多个锁时以相同的顺序进行,或者使用其他避免死锁的策略。
Java多线程的六种状态
在Java多线程中,线程共有六种状态,它们分别是:
- NEW(新建):这是线程的初始状态。线程对象已经被创建,但是还没有调用start()方法。
- RUNNABLE(可运行):这个状态实际上包括了就绪(ready)和运行中(running)两种子状态。当线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法,该线程就进入可运行线程池中,等待被线程调度选中,获取CPU的使用权。此时,线程处于就绪状态。当线程获得CPU时间片后,它就变为运行中状态。
- BLOCKED(阻塞):当线程尝试获取某个对象的内置锁,而该锁已经被其他线程持有时,该线程就会进入阻塞状态。此时,线程会暂停执行,直到它获得锁或者等待条件变为真。
- WAITING(等待):当线程执行了wait()、join()或park()等方法时,线程就会进入等待状态。进入该状态的线程需要等待其他线程做出一些特定动作(如通知或中断)才能继续执行。
- TIMED_WAITING(定时等待):这个状态与WAITING状态类似,但是线程可以在指定的时间后自行返回,而不需要等待其他线程的动作。这通常是因为线程调用了带有时间参数的sleep()、wait()或join()等方法。
- TERMINATED(终止):当线程执行完毕,或者因为异常而退出时,线程就进入终止状态。这个状态是线程生命周期的最后一个状态。
以上就是Java多线程中的六种状态。
线程池
容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程的操作,无需反复创建线程而消耗过多的资源。
为何引入线程池
如果并发的线程数量过多,并且每个线程都是执行一个时间很短的任务就结束,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要消耗时间,线程也属于宝贵的系统资源,因此,线程池就是为了能使线程可以复用而创建的。
线程池好处
- 降低资源的消耗,减少创建和销毁线程的次数,每个工作线程都可以被重复使用,可执行多个任务
- 提高响应速度,不需要频繁地创建线程,如果有线程可以直接使用,避免了系统僵死
- 提高线程的可管理性
核心思想:线程复用
案例
package com.Java基础.多线程.Thread007;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Thread01 {
public static void main(String[] args) {
// 1.创建一个线程池,指定线程的数量为4
ExecutorService pools = Executors.newFixedThreadPool(4);
// 2.添加线程任务
Runnable target = new MyRunnable();
pools.submit(target); // 第一次提交任务,此时创建新线程
pools.submit(target); // 第二次提交任务,此时创建新线程
pools.submit(target); // 第三次提交任务,此时创建新线程
pools.submit(target); // 第四次提交任务,此时创建新线程
pools.submit(target); // 第五次提交任务,复用之前的线程
pools.shutdown(); // 当所有任务全部完成后才关闭线程池
// pools.shutdownNow(); // 立即关闭线程池
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
for(int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"正在执行任务"+i);
}
}
}