Java多线程编程

线程的创建和启动

在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还提供了一种可以获取线程执行结果的方式,即通过使用CallableFutureCallable接口类似于Runnable接口,不同之处在于它的call()方法可以返回执行结果,而Runnable接口的run()方法没有返回值。下面是一个通过使用CallableFuture创建线程并获取执行结果的示例代码:

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()方法启动线程后,我们可以通过FutureTaskget()方法获取线程的执行结果。

同步和互斥

在多线程编程中,同步和互斥是重要的概念。当多个线程同时访问共享资源时,可能会导致数据不一致或线程间的竞争条件。为了避免这种情况,我们需要使用同步机制来保证线程之间的互斥访问。下面将详细介绍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()用于创建定时任务的线程池。

参数属性说明

在创建线程池时,我们可以为其指定一些参数来控制线程池的行为。

  1. corePoolSize:核心线程池大小,指的是线程池中一直保持运行的核心线程数量。
  2. maximumPoolSize:最大线程池大小,指的是线程池中允许存在的最大线程数量。
  3. keepAliveTime:线程空闲时间,当线程池中线程数量超过核心线程池大小时,多余的空闲线程会等待指定的时间后会被回收。
  4. unit:时间单位,用于指定keepAliveTime的时间单位。
  5. workQueue:工作队列,用于存放待执行的任务。
  6. threadFactory:线程工厂,用于创建新的线程。
  7. 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并发包提供了许多线程安全的集合类,包括ConcurrentHashMapConcurrentLinkedQueueCopyOnWriteArrayList等等。

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并发包提供了多个原子类,包括AtomicIntegerAtomicBooleanAtomicLong等等。

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并发包提供了多个同步器,包括CountDownLatchCyclicBarrierSemaphoreReentrantLock等等。

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并发包提供了多种锁,包括ReentrantLockStampedLockReadWriteLock等等。

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并发包提供了多个条件实现类,包括ConditionReadWriteLock等等。

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()方法发出通知。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值