多线程
Java提供了丰富的多线程支持,使开发者能够轻松创建和管理多线程应用程序。以下是Java多线程的一些重要概念和相关内容:
线程(Thread):
线程是程序执行的最小单位,它代表了一个独立的执行路径。在Java中,线程是由Thread类表示的,可以通过创建Thread类的实例来创建和管理线程。创建线程:
有两种主要的方式来创建线程:- 继承Thread类:可以创建一个新的类,继承Thread类并覆盖其run()方法,在run()方法中定义线程的执行逻辑。
- 实现Runnable接口:可以创建一个实现了Runnable接口的类,并实现其run()方法,然后将该实现类的实例传递给Thread类的构造函数创建线程。
启动线程:
创建线程后,需要调用线程的start()方法来启动线程。start()方法会在新的线程中调用run()方法,并使线程进入可运行(Runnable)状态。线程生命周期:
线程生命周期包括以下几个状态:- 新建(New):线程被创建但尚未启动。
- 可运行(Runnable):线程已经启动并可以运行。
- 阻塞(Blocked):线程被阻塞,等待某些条件的发生。
- 等待(Waiting):线程等待其他线程的特定操作。
- 计时等待(Timed Waiting):线程等待一段指定的时间。
- 终止(Terminated):线程执行完毕或被中断。
线程同步:
多个线程访问共享资源时可能引发线程安全问题,如竞态条件和数据不一致。Java提供了多种机制来实现线程同步,包括:- synchronized关键字:通过在方法或代码块上加锁,确保同一时间只有一个线程可以进入被锁定的代码段。
- ReentrantLock类:提供了显示的锁定和解锁机制,比synchronized关键字更灵活。
- volatile关键字:用于保证变量的可见性,禁止指令重排序。
- 同步容器类:Java提供了线程安全的容器类,如ConcurrentHashMap和ConcurrentLinkedQueue等。
线程间通信:
多个线程之间可以通过以下机制进行通信:- wait()、notify()和notifyAll()方法:在synchronized代码块中使用,线程通过wait()方法等待通知,其他线程通过notify()或notifyAll()方法发送通知。
- Lock和Condition接口:使用ReentrantLock类的条件变量来实现线程间的等待和通知机制。
- 线程间共享变量:多个线程通过共享变量来进行通信,需要使用同步机制确保线程安全。
线程池(ThreadPool):
线程池是一组预先创建的线程,用于执行任务并管理线程的生命周期。Java提供了ThreadPoolExecutor类来创建线程池,可以有效地管理线程的创建和销毁,提高应用程序的性能和资源利用率。并发工具类:
Java还提供了一些并发工具类来简化多线程编程,如CountDownLatch、CyclicBarrier、Semaphore和BlockingQueue等,用于控制线程的执行顺序、同步和通信。
通过合理地使用多线程,可以充分发挥多核处理器的优势,提高程序的并发性和性能。同时,也需要注意线程安全和避免常见的多线程问题,如死锁、活锁和资源竞争等。
以下是Java中Thread类常用的方法:
构造方法:
Thread(): 创建一个新的线程对象。Thread(Runnable target): 创建一个带有指定Runnable任务的线程对象。Thread(String name): 创建一个新的线程对象,指定线程名称。Thread(Runnable target, String name): 创建一个带有指定Runnable任务和名称的线程对象。Thread(ThreadGroup group, Runnable target): 创建一个带有指定线程组和Runnable任务的线程对象。Thread(ThreadGroup group, Runnable target, String name): 创建一个带有指定线程组、Runnable任务和名称的线程对象。
线程状态控制方法:
void start(): 启动线程,使其进入可运行状态。线程启动后会调用run()方法。void run(): 线程的主体方法,需要自行实现。当线程处于可运行状态时,run()方法会被调用。void sleep(long millis) throws InterruptedException: 让线程休眠指定的毫秒数。在休眠期间,线程处于阻塞状态。static void sleep(long millis, int nanos) throws InterruptedException: 让线程休眠指定的毫秒数和纳秒数。void join() throws InterruptedException: 等待调用线程结束。在调用线程执行完毕前,当前线程会一直阻塞。void join(long millis) throws InterruptedException: 最多等待指定毫秒数,然后继续执行。void join(long millis, int nanos) throws InterruptedException: 最多等待指定毫秒数和纳秒数。void interrupt(): 中断线程。可以通过检查线程的中断状态(使用isInterrupted()方法)来决定是否停止线程的执行。
线程信息查询方法:
String getName(): 获取线程名称。long getId(): 获取线程的唯一标识符。Thread.State getState(): 获取线程的当前状态。boolean isAlive(): 判断线程是否处于活动状态。boolean isInterrupted(): 判断线程是否被中断,但不清除中断状态。static Thread currentThread(): 获取当前正在执行的线程对象。
线程优先级控制方法:
void setPriority(int priority): 设置线程的优先级,范围从Thread.MIN_PRIORITY到Thread.MAX_PRIORITY。int getPriority(): 获取线程的优先级。
线程同步方法:
static void yield(): 提示线程调度器当前线程愿意放弃当前对处理器的使用,以便其他线程有机会执行。
线程组相关方法:
ThreadGroup getThreadGroup(): 获取线程所属的线程组。static int activeCount(): 获取当前线程组及其子组中活动线程的总数。
其他方法:
static void sleep(long millis) throws InterruptedException: 让当前线程休眠指定毫秒数。
这些方法是Thread类的常用方法,可以用于创建和管理线程,控制线程的执行顺序和状态。使用这些方法可以实现多线程编程中的线程控制、同步和通信等功能。
终端线程
1.t.interrupt()
try {
Thread.sleep(100);
hello.join();
//当一个线程调用了一个阻塞方法(如 Thread.sleep()、Object.wait()、Thread.join() 等)或者等待某个锁时,它可能会进入等待或阻塞状态。如果此时其他线程调用了该线程的 interrupt() 方法,就会导致被阻塞的线程抛出 InterruptedException 异常。
} catch (InterruptedException e) {
System.out.println("interrupted!");
break;
}
2.共享变量
线程间共享的变量public volatile boolean running = true;
volatile关键字的目的是告诉虚拟机:
- 每次访问变量时,总是获取主内存的最新值;
- 每次修改变量后,立刻回写到主内存。
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
Main Memory
│ │
┌───────┐┌───────┐┌───────┐
│ │ var A ││ var B ││ var C │ │
└───────┘└───────┘└───────┘
│ │ ▲ │ ▲ │
─ ─ ─│─│─ ─ ─ ─ ─ ─ ─ ─│─│─ ─ ─
│ │ │ │
┌ ─ ─ ┼ ┼ ─ ─ ┐ ┌ ─ ─ ┼ ┼ ─ ─ ┐
▼ │ ▼ │
│ ┌───────┐ │ │ ┌───────┐ │
│ var A │ │ var C │
│ └───────┘ │ │ └───────┘ │
Thread 1 Thread 2
└ ─ ─ ─ ─ ─ ─ ┘ └ ─ ─ ─ ─ ─ ─ ┘
如果我们去掉volatile关键字,运行上述程序,发现效果和带volatile差不多,这是因为在x86的架构下,JVM回写主内存的速度非常快,但是,换成ARM的架构,就会有显著的延迟。
守护线程
有一种线程的目的就是无限循环,例如,一个定时触发任务的线程
JVM退出时,不必关心守护线程是否已结束。
Thread t = new MyThread();
t.setDaemon(true);
t.start();
线程同步
多个线程同时读写共享变量->原子操作
┌───────┐ ┌───────┐
│Thread1│ │Thread2│
└───┬───┘ └───┬───┘
│ │
│ILOAD (100) │
│ │ILOAD (100)
│ │IADD
│ │ISTORE (101)
│IADD │
│ISTORE (101)│
▼ ▼
class Counter {
public static final Object lock = new Object();
public static int studentCount = 0;
public static int teacherCount = 0;
}
synchronized(Counter.lock) { // 获取锁
...
} // 释放锁
- 操作需要进行同步,以避免多个线程同时修改相同的数据库记录或文件内容,引发冲突和数据错误。
- 线程间的通信和协作:当线程之间需要进行协作、通信或等待特定条件时,需要使用同步锁来确保线程的正确等待和唤醒,以避免竞态条件和不正确的执行顺序。
- 缓冲区或队列的操作:在多线程环境下使用缓冲区或队列时,需要使用同步锁来保护共享数据结构,以确保线程安全的插入、删除或读取操作。
- 单例模式:在多线程环境下使用单例模式时,需要对单例对象的创建和访问进行同步锁的保护,以避免多个线程同时创建多个实例或访问未完全初始化的实例。
- 跨对象的操作:当多个线程同时操作多个相关的对象时,需要使用同步锁来保护整个操作过程,以避免不一致的状态或并发修改引起的问题。
下面是一些常见的单条原子操作的语句,它们不需要额外的同步保护:
- 基本数据类型的赋值操作,如
int、long、boolean、char等。
int x = 5; // 原子操作,不需要同步
- 引用类型的赋值操作,当引用变量的赋值操作是原子性的时候。
MyObject obj = new MyObject(); // 原子操作,不需要同步
volatile关键字修饰的变量的读取和写入操作。
volatile boolean flag = true; // 原子操作,不需要同步
- 对象的引用传递。
public void doSomething(MyObject obj) {
// ...
}
在上述情况下,由于操作是原子的,不会出现线程安全问题,因此不需要额外的同步措施。
然而,需要注意的是,当多个原子操作组合成一个复合操作时,就需要考虑同步问题了。
同步方法
线程自己选择锁对象?不利于封装.把synchronized逻辑封装起来!
当一个线程进入同步方法时,它将锁定该方法所属对象,其他线程将被阻塞直到该线程执行完毕释放锁。
public synchronized void synchronizedMethod() {
// 这里是需要同步的代码
}
//或者说
public class Counter {
private int count = 0;
public void add(int n) {
synchronized(this) {
count += n;
}
}
public void dec(int n) {
synchronized(this) {
count -= n;
}
}
public int get() {
return count;
}
}
var c1 = Counter();
var c2 = Counter();
// 对c1进行操作的线程:
new Thread(() -> {
c1.add();
}).start();
new Thread(() -> {
c1.dec();
}).start();
// 对c2进行操作的线程:
new Thread(() -> {
c2.add();
}).start();
new Thread(() -> {
c2.dec();
}).start();
public synchronized void add(int n) { // 锁住this
count += n;
} // 解锁
in android(java)
关于Android的多线程,其实现方式有以下几种:
- Handler - 可以配合Message、Runnable使用,实现线程间通信和任务调度,比较适合用于UI线程与后台线程之间的通讯。
- Thread - 通过继承Thread类,实现run()方法,直接创建线程对象来实现多线程。
- AsyncTask - 异步任务,实现了多线程功能,同时方便与UI线程交互。
- Executor / ThreadPoolExecutor - 线程池,可以重用线程,避免频繁创建销毁。
所以Android多线程主要是通过Handler、Thread、AsyncTask、Executor这些方式来实现的。
Handler用来配合线程间消息传递和任务调度。
Thread通过直接继承Thread来创建后台线程。
AsyncTask方便做后台线程和UI线程的交互。
Executor/ThreadPoolExecutor用来管理线程池,重用线程。
这些方式各有优劣,可以根据实际需求选择合适的多线程实现方式。常见的是配合Handler + Thread/AsyncTask或者Executor + Runnable来实现多线程。
