线程的创建和启动
在Java中,多线程编程是实现并发性的重要手段之一。通过利用多线程,我们可以同时执行多个任务,提高程序的性能和响应能力。下面将详细介绍Java中线程的创建和启动的方法,并通过示例代码来说明。
通过继承Thread类创建线程
Java中,我们可以通过继承Thread
类来创建线程。通过创建一个继承自Thread
类的自定义类,我们可以重写run()
方法来定义线程的执行逻辑。下面是一个通过继承Thread
类创建线程的示例代码:
public class MyThread extends Thread {
@Override
public void run() {
// 线程的执行逻辑
}
}
public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
在上面的示例中,我们创建了一个名为MyThread
的类,它继承自Thread
类。在MyThread
类中,我们重写了run()
方法来定义线程的执行逻辑。在Main
类中,我们创建了一个MyThread
的实例,并通过调用start()
方法来启动线程。
通过实现Runnable接口创建线程
除了继承Thread
类,Java还提供了通过实现Runnable
接口来创建线程的方式。通过实现Runnable
接口,我们可以将线程的执行逻辑与线程本身分离,提高代码的灵活性。下面是一个通过实现Runnable
接口创建线程的示例代码:
public class MyRunnable implements Runnable {
@Override
public void run() {
// 线程的执行逻辑
}
}
public class Main {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
}
}
在上面的示例中,我们创建了一个名为MyRunnable
的类,它实现了Runnable
接口。在MyRunnable
类中,我们实现了run()
方法来定义线程的执行逻辑。在Main
类中,我们创建了一个MyRunnable
的实例,并将其作为参数传递给Thread
类的构造方法来创建线程对象,在调用start()
方法来启动线程。
通过Callable和Future创建线程并获取执行结果
除了通过继承Thread
类和实现Runnable
接口来创建线程,Java还提供了一种可以获取线程执行结果的方式,即通过使用Callable
和Future
。Callable
接口类似于Runnable
接口,不同之处在于它的call()
方法可以返回执行结果,而Runnable
接口的run()
方法没有返回值。下面是一个通过使用Callable
和Future
创建线程并获取执行结果的示例代码:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() {
// 线程的执行逻辑
return 100;
}
}
public class Main {
public static void main(String[] args) {
MyCallable callable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start();
try {
int result = futureTask.get();
System.out.println("线程执行结果:" + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
在上面的示例中,我们创建了一个名为MyCallable
的类,它实现了Callable
接口,并将call()
方法的返回值设定为Integer
类型。在MyCallable
类中,我们实现了call()
方法来定义线程的执行逻辑,同时返回了一个整数结果。在Main
类中,我们创建了一个MyCallable
的实例,并将其作为参数传递给FutureTask
类的构造方法创建一个FutureTask
对象。然后,我们创建了一个线程对象,并将FutureTask
对象作为参数传递给Thread
类的构造方法来创建线程。在调用start()
方法启动线程后,我们可以通过FutureTask
的get()
方法获取线程的执行结果。
同步和互斥
在多线程编程中,同步和互斥是重要的概念。当多个线程同时访问共享资源时,可能会导致数据不一致或线程间的竞争条件。为了避免这种情况,我们需要使用同步机制来保证线程之间的互斥访问。下面将详细介绍Java中的同步和互斥机制,以及常用的同步方式和方法。
使用synchronized实现同步
Java中提供了Synchronized
关键字来实现同步和互斥。通过使用synchronized
关键字,我们可以指定某个代码块或方法在同一时间只能被一个线程执行,从而保证线程安全。
同步代码块
下面是一个使用synchronized
关键字实现同步代码块的示例代码:
public class Counter {
private int count;
public void increment() {
synchronized (this) {
count++;
}
}
}
在上面的示例中,我们使用synchronized
关键字来修饰一个代码块。代码块的锁对象通常是该类的实例对象,也可以是其他共享对象。在使用synchronized
关键字修饰的代码块中,只有一个线程可以同时执行该代码块,其他线程需要等待当前线程执行完毕后才能进入。
同步方法
除了同步代码块,Java中也支持使用synchronized
关键字修饰方法来实现同步。下面是一个使用synchronized
关键字实现同步方法的示例代码:
public class Counter {
private int count;
public synchronized void increment() {
count++;
}
}
在上面的示例中,我们使用synchronized
关键字修饰了increment()
方法。当某个线程调用increment()
方法时,其他线程不能同时调用该方法,只能等待当前线程执行完毕。
使用Lock接口实现同步
除了synchronized
关键字,Java中还提供了Lock
接口来实现同步和互斥。相比于synchronized
关键字,Lock
接口提供了更灵活和细粒度的控制。
下面是一个使用Lock
接口实现同步的示例代码:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private int count;
private Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}
在上面的示例中,我们使用ReentrantLock
类实现了Lock
接口,并在increment()
方法中使用了lock()
方法获取锁,使用unlock()
方法释放锁。使用Lock
接口可以灵活地控制锁的获取和释放,同时支持更细粒度的锁控制。
使用volatile关键字保证可见性
在多线程编程中,除了保证线程之间的互斥访问,还需要保证可见性。Java中提供了volatile
关键字来实现可见性。当一个共享变量被volatile
修饰时,当一个线程修改了该变量的值,其他线程能够立即看到该变量的修改。
下面是一个使用volatile
关键字保证可见性的示例代码:
public class VolatileExample {
private volatile boolean flag = false;
public void setFlag() {
flag = true;
}
public void printFlag() {
System.out.println("Flag: " + flag);
}
}
在上面的示例中,flag
变量被volatile
关键字修饰,确保了可见性。当一个线程调用setFlag()
方法将flag
设置为true
时,其他线程在调用printFlag()
方法时能够立即看到flag
的修改。
线程池和Executor框架
在多线程编程中,频繁地创建和销毁线程会消耗大量的系统资源,并且会影响程序的性能。为了避免这种情况,Java提供了线程池和Executor框架,能够有效地管理和复用线程。线程池是一种管理线程的机制,它维护着一个线程池,其中包含了多个可用的线程。下面将详细介绍Java中的线程池和Executor框架,以及如何使用它们来提高程序的性能。
线程池概述
通过使用线程池,我们可以重复利用线程,避免频繁地创建和销毁线程。
Java中的线程池由ThreadPoolExecutor
类实现,它是ExecutorService
接口的一个实现。ThreadPoolExecutor
类提供了一些方法用于创建和管理线程池,以及提交和执行任务。
创建线程池
在Java中,我们可以通过Executors
类来创建线程池。Executors
类提供了一些静态方法,用于创建不同类型的线程池。
例如,我们可以使用newFixedThreadPool()
方法创建一个固定大小的线程池,该线程池中的线程数量始终保持不变。
ExecutorService executor = Executors.newFixedThreadPool(5);
还可以使用newCachedThreadPool()
方法创建一个可缓存的线程池,该线程池中的线程数量可以根据需要的任务数量自动调整。
ExecutorService executor = Executors.newCachedThreadPool();
除此之外,还有其他一些方法可以创建不同类型的线程池,例如newSingleThreadExecutor()
用于创建只有一个线程的线程池,newScheduledThreadPool()
用于创建定时任务的线程池。
参数属性说明
在创建线程池时,我们可以为其指定一些参数来控制线程池的行为。
corePoolSize
:核心线程池大小,指的是线程池中一直保持运行的核心线程数量。maximumPoolSize
:最大线程池大小,指的是线程池中允许存在的最大线程数量。keepAliveTime
:线程空闲时间,当线程池中线程数量超过核心线程池大小时,多余的空闲线程会等待指定的时间后会被回收。unit
:时间单位,用于指定keepAliveTime
的时间单位。workQueue
:工作队列,用于存放待执行的任务。threadFactory
:线程工厂,用于创建新的线程。handler
:拒绝策略,用于当线程池和工作队列已满时如何拒绝新的任务。
参数值选择
在选择参数值时,需要根据实际情况和需求来确定。一般来说,可以遵循以下原则:
corePoolSize
的大小可以根据系统的负载情况和服务器的处理能力来确定。如果系统的并发请求数较高,可以适当增加corePoolSize
的值,以提高并发处理能力。maximumPoolSize
的大小可以根据系统的负载情况和服务器的处理能力来确定,一般可以设置为corePoolSize
的两倍或三倍。keepAliveTime
可以根据实际需求来调整,如果系统需要快速响应请求,可以将其设置为较小的值;如果系统的任务处理时间较长,可以适当增大keepAliveTime
。workQueue
的选择可以根据实际的任务类型来确定。如果任务数量比较多,可以选择一个容量较大的阻塞队列,例如LinkedBlockingQueue
;如果任务数量比较少,可以选择一个容量较小的阻塞队列,例如ArrayBlockingQueue
。threadFactory
可以根据实际需求来定制,例如可以设置线程的名称、优先级等。handler
可以根据实际需求来选择,常见的拒绝策略有:抛出异常、直接丢弃、丢弃最旧的任务、使用调用者所在的线程执行任务。
示例说明
下面是一个示例,演示了如何创建并使用一个固定大小的线程池:
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建一个固定大小的线程池,大小为5
ExecutorService executor = Executors.newFixedThreadPool(5);
// 提交10个任务给线程池执行
for (int i = 0; i < 10; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("Task " + taskId + " is running on thread " + Thread.currentThread().getName());
});
}
// 关闭线程池
executor.shutdown();
}
}
在上述示例中,我们创建了一个固定大小为5的线程池,然后向线程池提交了10个任务。每个任务打印了它自己的任务ID和所在线程的名称。最后,我们调用shutdown()
方法关闭线程池。
提交和执行任务
通过ExecutorService
接口提供的方法,我们可以向线程池提交和执行任务。下面是一些常用的方法:
submit(Runnable task)
:向线程池提交一个Runnable
任务,并返回一个表示该任务的结果的Future
对象。submit(Callable task)
:向线程池提交一个Callable
任务,并返回一个表示该任务的结果的Future
对象。execute(Runnable task)
:向线程池提交一个Runnable
任务,无返回值。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Main {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(5);
// 提交Runnable任务
executor.submit(new MyTask());
// 提交Callable任务
Future<Integer> future = executor.submit(new MyTask());
try {
int result = future.get();
System.out.println("任务执行结果:" + result);
} catch (Exception e) {
e.printStackTrace();
}
executor.shutdown();
}
}
在上面的示例中,我们通过submit()
方法向线程池提交了一个Runnable
任务和一个Callable
任务。对于Callable
任务,我们通过Future
对象的get()
方法获取任务的执行结果。
线程池的关闭
当我们使用完线程池后,应该及时关闭线程池以释放资源。通过调用ExecutorService
接口的shutdown()
方法可以平缓地关闭线程池。该方法会启动线程池的关闭序列,不再接受新的任务提交,但会等待之前提交的任务执行完毕。
executor.shutdown();
除了shutdown()
方法外,还可以使用shutdownNow()
方法立即关闭线程池,并尝试终止正在执行的任务。
executor.shutdownNow();
Java并发包的使用
随着计算机硬件性能的提高,多线程编程已经成为当今软件开发中的常见需求。Java并发包提供了一系列的类和接口,能够很好地支持多线程编程和并发控制。下面介绍Java并发包的使用,包括线程安全集合、原子操作、同步器、锁、条件等内容。
线程安全集合
在多线程编程中,线程安全的集合类对于保证程序的正确性和性能是非常重要的。Java并发包提供了许多线程安全的集合类,包括ConcurrentHashMap
、ConcurrentLinkedQueue
、CopyOnWriteArrayList
等等。
ConcurrentHashMap
ConcurrentHashMap
是线程安全的哈希表,支持高效地并发访问。下面是一个ConcurrentHashMap
的使用示例代码:
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class Main {
public static void main(String[] args) {
Map<Integer, String> map = new ConcurrentHashMap<>();
map.put(1, "hello");
map.put(2, "world");
System.out.println(map.get(1));
}
}
在上面的示例中,我们创建了一个ConcurrentHashMap
对象,并向其中添加了两个元素。在多线程并发访问时,ConcurrentHashMap
能够保证高效的并发访问和修改,避免了线程安全问题。
ConcurrentLinkedQueue
ConcurrentLinkedQueue
是线程安全的队列,支持高效地并发访问。下面是一个ConcurrentLinkedQueue
的使用示例代码:
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
public class Main {
public static void main(String[] args) {
Queue<String> queue = new ConcurrentLinkedQueue<>();
queue.add("hello");
queue.add("world");
System.out.println(queue.poll());
}
}
在上面的示例中,我们创建了一个ConcurrentLinkedQueue
对象,并向其中添加了两个元素。在多线程并发访问时,ConcurrentLinkedQueue
能够保证高效的并发访问和修改,避免了线程安全问题。
CopyOnWriteArrayList
CopyOnWriteArrayList
是线程安全的动态数组,支持高效的并发读操作。下面是一个CopyOnWriteArrayList
的使用示例代码:
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class Main {
public static void main(String[] args) {
List<String> list = new CopyOnWriteArrayList<>();
list.add("hello");
list.add("world");
System.out.println(list.get(0));
}
}
在上面的示例中,我们创建了一个CopyOnWriteArrayList
对象,并向其中添加了两个元素。在多线程并发访问时,CopyOnWriteArrayList
能够保证高效的并发读操作,避免了线程安全问题。
原子操作
在多线程编程中,对于一组数据的多线程写入时,常常需要保证原子性,在一个线程修改数据时,其它线程不可同时修改该数据。Java并发包提供了多个原子类,包括AtomicInteger
、AtomicBoolean
、AtomicLong
等等。
AtomicInteger
AtomicInteger
是一个原子类,用于原子性地操作整型变量。下面是一个AtomicInteger
的使用示例代码:
import java.util.concurrent.atomic.AtomicInteger;
public class Main {
public static void main(String[] args) {
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();
counter.addAndGet(2);
System.out.println(counter.get());
}
}
在上面的示例中,我们创建了一个AtomicInteger
对象,并进行原子性地自增和加2操作。其中的incrementAndGet()
和addAndGet()
方法都是线程安全的原子方法,能够确保线程安全地进行操作。
同步器
在多线程编程中,同步器常常用于协调多个线程的执行顺序和状态。Java并发包提供了多个同步器,包括CountDownLatch
、CyclicBarrier
、Semaphore
、ReentrantLock
等等。
CountDownLatch
CountDownLatch
是一种同步器,用于协调多个线程的执行顺序。下面是一个CountDownLatch
的使用示例代码:
import java.util.concurrent.CountDownLatch;
public class Main {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(2);
Thread t1 = new MyThread(latch);
Thread t2 = new MyThread(latch);
t1.start();
t2.start();
latch.await();
System.out.println("All tasks finished.");
}
}
class MyThread extends Thread {
private CountDownLatch latch;
public MyThread(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void run() {
// do something
latch.countDown();
}
}
在上面的示例中,我们创建了一个CountDownLatch
对象,并将计数器设定为2。然后,我们创建了两个线程,并将CountDownLatch
对象传递给线程。在线程的run()
方法中,我们执行了一些操作,并在完成后通过countDown()
方法将计数器减1。最后,在主线程中我们通过await()
方法阻塞,并等待所有的线程执行完成。
CyclicBarrier
CyclicBarrier
是一种同步器,用于协调多个线程的执行状态。下面是一个CyclicBarrier
的使用示例代码:
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class Main {
public static void main(String[] args) throws InterruptedException, BrokenBarrierException {
CyclicBarrier barrier = new CyclicBarrier(2, new Runnable() {
@Override
public void run() {
System.out.println("All threads have reached the barrier.");
}
});
Thread t1 = new MyThread(barrier);
Thread t2 = new MyThread(barrier);
t1.start();
t2.start();
t1.join();
t2.join();
}
}
class MyThread extends Thread {
private CyclicBarrier barrier;
public MyThread(CyclicBarrier barrier) {
this.barrier = barrier;
}
@Override
public void run() {
// do something
try {
barrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}
}
在上面的示例中,我们创建了一个CyclicBarrier
对象,并将计数器设定为2。然后,我们创建了两个线程,并将CyclicBarrier
对象传递给线程。在线程的run()
方法中,我们执行了一些操作,并通过await()
方法等待其他线程到达屏障。在所有线程都到达屏障后,我们通过Runnable
对象中的run()
方法执行一些操作。
锁
在多线程编程中,锁常常用于保证临界区的原子性,避免数据竞争和线程安全问题。Java并发包提供了多种锁,包括ReentrantLock
、StampedLock
、ReadWriteLock
等等。
ReentrantLock
ReentrantLock
是可重入锁,支持在同一线程中嵌套地获取和释放锁。下面是一个ReentrantLock
的使用示例代码:
import java.util.concurrent.locks.ReentrantLock;
public class Main {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// do something
} finally {
lock.unlock();
}
}
}
在上面的示例中,我们创建了一个ReentrantLock
对象,并使用lock()
方法获取锁,在finally
块中使用unlock()
方法释放锁。
StampedLock
StampedLock
是乐观锁,支持读锁和写锁。下面是一个StampedLock
的使用示例代码:
import java.util.concurrent.locks.StampedLock;
public class Main {
private double x, y;
private final StampedLock lock = new StampedLock();
public void move(double deltaX, double deltaY) {
long stamp = lock.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
lock.unlockWrite(stamp);
}
}
public double distanceFromOrigin() {
long stamp = lock.tryOptimisticRead();
double currentX = x, currentY = y;
if (!lock.validate(stamp)) {
stamp = lock.readLock();
try {
currentX = x;
currentY = y;
} finally {
lock.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}
在上面的示例中,我们通过StampeLock
实现了动态增加(move()
方法)和计算距离(distanceFromOrigin()
方法)的操作,其中move()
方法使用写锁,distanceFromOrigin()
方法使用乐观读锁,如果乐观读锁操作失败则使用读锁。
条件
在多线程编程中,条件(Condition)常常用于控制多个线程之间的执行顺序和状态。Java并发包提供了多个条件实现类,包括Condition
、ReadWriteLock
等等。
Condition
Condition
是锁的条件,通过await()
和signal()
方法实现等待和通知的控制。下面是一个Condition
的使用示例代码:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class Main {
private static ReentrantLock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new MyThread();
Thread t2 = new MyThread();
t1.start();
t2.start();
Thread.sleep(1000);
lock.lock();
try {
condition.signal();
} finally {
lock.unlock();
}
}
static class MyThread extends Thread {
@Override
public void run() {
lock.lock();
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
// do something
}
}
}
在上面的示例中,我们创建了一个Condition
对象,并通过await()
方法等待调用signal()
方法的通知。在MyThread
线程中,我们使用await()
方法等待条件的满足。在主线程中,我们使用signal()
方法发出通知。