Java并发编程实战:掌握多线程编程的交错优化和高效实践

在这里插入图片描述

Java并发编程是指在多个线程同时执行的情况下,协调和管理这些线程的过程。在现代计算机系统中,使用多线程并发编程可以显著提高应用程序的性能和响应速度。Java作为一门流行的编程语言,具有强大的并发编程能力。

本文将介绍Java并发编程的基本原理和实践技巧,帮助读者更好地掌握这一领域的知识。
🏅 欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!

一、Java并发编程的基本原理

1.1 线程和进程

  1. 线程是一种轻量级的执行单元,它可以在进程中创建和撤销,且可以与同一进程中的其他线程共享数据和资源。Java程序的所有线程都是运行在进程中。每一个Java线程都有其自己的执行堆栈和指令指针。当一个线程调用一个方法时,该方法的参数值、局部变量和返回值都保存在执行堆栈中。线程之间可以通过共享内存来通信。
  2. 进程是程序在执行时占用资源和运行的基本单位,一个进程可以包含多个线程。每个进程都有一个程序计数器(PC)、内存空间、文件资源和系统信号。Java程序的每个线程都在一个进程中执行。进程是操作系统中的一个概念,它可以分配CPU时间、内存空间、文件资源和系统信号。在操作系统中,每个进程都有唯一的进程标识符(PID),用来标识该进程。Java程序通过Java虚拟机(JVM)运行,每个Java程序都有一个独立的JVM实例作为进程。在JVM中运行的Java线程共享该JVM的内存区域,从而实现了线程之间共享数据的目的。在Java中,可以使用ProcessBuilder类创建子进程,并通过标准输入/输出流进行通信。子进程可以通过exitValue()方法获取其执行状态,以判断是否执行成功。

1.2 同步和互斥

1. 同步
同步是指多个线程在访问共享资源时,按照一定的顺序和时间互相通知,以保证数据同步和正确性。在Java中,同步可以通过以下机制来实现:

  • synchronized关键字:用于控制对共享资源的访问,从而避免多个线程同时访问同一个共享资源。
  • wait()/notify()方法:用于线程间的通信,当一个线程等待某个条件时,可以调用wait()方法阻塞自己,当另一个线程满足条件时,可以调用notify()方法唤醒等待线程继续执行。
  • Lock/Condition机制:与synchronized关键字类似,用于控制对共享资源的访问,但更加灵活和高效。
    同步机制能够保证多个线程访问共享资源时的正确性、安全性和一致性,但会影响并发性能,降低程序的效率。

2. 互斥
互斥是指多个线程在访问共享资源时,使用某种机制来防止共享资源被多个线程同时访问和修改。在Java中,互斥可以通过以下机制来实现:

  • synchronized关键字:同步机制中的synchronized关键字也能够实现互斥,当一个线程进入synchronized块时,会对共享资源加锁,防止其他线程同时访问。
  • Lock接口:Lock接口提供了与synchronized关键字类似的锁定机制,但更加灵活,支持公平锁和非公平锁、可重入锁、带条件的锁等。
    互斥机制能够保证多个线程访问共享资源时的排它性,避免竞争和冲突,提高程序的可靠性和安全性。但如果使用不当,互斥机制也可能导致死锁和饥饿等问题。

总之,同步和互斥是Java并发编程的基本原理之一,能够有效地保证程序的正确性、安全性和一致性。Java提供了多种机制来实现同步和互斥,可以根据具体场景选择最适合的机制来实现。

1.3 线程间的通信

Java提供了多种机制来实现线程间的通信,包括wait/notify机制、join方法、volatile关键字等。wait/notify机制可以让一个线程等待另一个线程的通知,join方法可以让一个线程等待另一个线程的结束,volatile关键字可以保证多个线程之间共享变量的可见性。

1.3.1 wait()/notify()/notifyAll()方法

这三个方法既可以用于同步机制中,也可以用于线程间通信。wait方法使当前线程进入阻塞状态,并释放同步锁,直到其他线程调用notify或notifyAll方法来唤醒它。notify方法随机唤醒一个等待该对象锁的线程,而notifyAll方法会唤醒所有等待该对象锁的线程。使用wait()/notify()/notifyAll()时,需要注意以下几点:

  • 必须在同步代码块或同步方法中调用wait()/notify()/notifyAll()方法;
  • 必须在获取对象锁后调用wait()方法;
  • 必须在notify()/notifyAll()方法调用前释放对象锁。

1.3.2 Condition接口

Condition接口是JDK 5.0引入的新特性,提供了类似wait()/notify()的机制,但更加高级和灵活。在Condition中,线程可以等待指定的条件变量并等待通知,以实现线程间的通信。Condition中包含了await()、signal()、signalAll()方法。

1.3.3 CountDownLatch类

CountDownLatch计数器是Java并发包中提供的一个同步工具,它允许在一个或多个线程中等待一组事件发生。CountDownLatch类包含一组await()和countDown()方法。await()方法会导致当前线程等待,直到计数器的值为0,而countDown()方法会减少计数器的值。

1.3.4 CyclicBarrier类

CyclicBarrier类提供了一种同步机制,允许一组线程等待彼此到达某个公共障碍点。它包含await()和reset()方法,await()方法会使当前线程等待,直到其他线程的到达,而reset()方法会重置障碍的计数器。

1.3.5 Semaphore类

Semaphore类是一种基于计数的信号量,用于控制多个线程对共享资源的访问。它包含acquire()和release()方法,acquire()方法会尝试获取信号量的许可证,如果许可证不可用则阻塞线程,而release()方法会释放信号量的许可证。

1.4 线程池

线程池是Java并发编程中一种重要的技术,它可以提高线程的使用效率、减少创建和销毁线程的开销,同时也能控制并发线程数量,从而避免线程资源的浪费和系统的负担。Java线程池的实现基于两种机制:ThreadPoolExecutor类和Executors工具类。

  • ThreadPoolExecutor类是Java中提供的原始的线程池实现方式,它包含了线程池的核心参数和方法,可以支持自定义线程池的各种配置。ThreadPoolExecutor的构造方法参数包括线程池的核心线程数、最大线程数、线程空闲时间、等待队列大小,还可以自定义拒绝策略等。它的主要方法包括execute()、shutdown()、shutdownNow()、awaitTermination()等。

  • Executors工具类是Java中提供的封装线程池的类集合,它提供了多种预定义的线程池,如newFixedThreadPool()、newCachedThreadPool()、newSingleThreadExecutor()等,可以根据不同的需求快速创建线程池,而无需关心核心参数的配置。这些方法的实现都是基于ThreadPoolExecutor的,同时提供了一些默认的配置参数和线程池特性。

Java线程池的优点:

  • 提高程序性能:线程池可以重复利用已创建的线程,从而减少创建线程和销毁线程的开销。
  • 提高程序可靠性:线程池可以控制并发线程的数量,避免资源被浪费和服务器崩溃的风险。
  • 提高程序可读性:使用Java线程池可以方便地管理线程,降低程序员的耦合度。

Java线程池的缺点:

  • 需要适当的配置:需要根据实际情况配置线程池的核心参数,如果配置不当,可能会影响整个系统的并发性能。
  • 可能会增加程序复杂度:线程池中可能存在线程安全问题,比如死锁和竞争条件等,需要程序员善于处理。

二、Java并发编程的实践技巧

2.1 使用volatile关键字

在多线程环境下,使用volatile关键字可以保证共享变量的可见性。volatile关键字可以防止指令重排,保证写操作的原子性,从而避免多线程操作时的数据冲突问题。

2.1.1 使用 volatile 关键字来保证多线程间的可见性示例代码

public class SharedObject {
    private volatile int count;

    public SharedObject(int initialCount) {
        this.count = initialCount;
    }

    public int getCount() {
        return count;
    }

    public void incrementCount() {
        count++;
    }
}

public class Main {
    public static void main(String[] args) {
        SharedObject sharedObject = new SharedObject(0);

        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                sharedObject.incrementCount();
            }
        });

        Thread thread2 = new Thread(() -> {
            while (sharedObject.getCount() < 10000) {
                // busy-waiting
            }
            System.out.println("Count has reached 10000");
        });

        thread1.start();
        thread2.start();
    }
}

代码说明

这段代码展示了一个使用Java多线程的例子,其中包含一个线程安全的共享对象SharedObject和两个线程thread1和thread2。

SharedObject类有一个私有的volatile整型成员变量count,和三个公共方法。构造函数初始化count的值,getCount()方法返回当前count的值,incrementCount()方法使count的值加1。由于多个线程都可以访问count,volatile关键字确保count的可见性。

在Main类的main()方法中,创建了一个共享对象sharedObject,然后创建两个线程thread1和thread2。thread1执行incrementCount()方法将count的值递增10000次,而thread2执行busy-waiting等待count的值达到10000,一旦达到则打印出一条消息。

在多线程环境中,由于线程之间的执行顺序和时间不确定,可能会导致一些并发问题,例如竞争条件和死锁。在本例中,线程1和线程2都在访问和修改共享对象sharedObject的count变量,需要确保线程安全性。由于volatile关键字确保了可见性,但不保证原子性,因此可能会存在并发问题。

2.2 使用synchronized关键字和锁机制

synchronized关键字和锁机制可以保证多个线程之间的同步和互斥访问,避免数据冲突问题。在使用synchronized关键字时,需要注意锁的范围和粒度,避免锁定过多的代码。

2.2.1 synchronized关键字和锁机制实现线程同步的示例代码

public class SynchronizedDemo {
    private int num = 0;
    
    /**
     * 定义一个同步方法,用于加1操作
     */
    public synchronized void increment() {
        num++;
        
        System.out.println(Thread.currentThread().getName() + "输出:" + num);
        
        // 通知另一个线程继续执行
        notifyAll();
    }
    
    /**
     * 定义一个同步方法,用于减1操作
     */
    public synchronized void decrement() {
        while (num == 0) { // 只有当num不等于0时,才执行下面的代码
            try {
                // 当前线程进入等待状态
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        num--;
        
        System.out.println(Thread.currentThread().getName() + "输出:" + num);

        // 通知另一个线程继续执行
        notifyAll();
    }
}

public class SynchronizedTest {
    public static void main(String[] args) {
        SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
        
        // 定义一个线程,用于执行递增操作
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                synchronizedDemo.increment();
            }
        }, "线程1").start();

        // 定义一个线程,用于执行递减操作
        new Thread(() -> {
            for (int j = 0; j < 5; j++) {
                synchronizedDemo.decrement();
            }
        }, "线程2").start();
    }
}

代码说明:

在上面的代码中,SynchronizedDemo类中的increment()和decrement()方法都被声明为synchronized方法,并使用关键字synchronized实现了线程同步。其中,使用wait()方法和notifyAll()方法实现线程间的通信,当num为0时,当前线程会进入等待状态,直到被其他线程唤醒。在主函数中,创建两个线程分别执行加1和减1操作,通过synchronized关键字和锁机制保证了两个线程的输出结果的正确性。

2.3 使用Atomic类

Java提供了Atomic类来保证多个线程之间对共享变量的原子性访问。Atomic类提供了一系列的原子操作,包括增加、减少、比较和交换等操作,可以避免数据冲突问题,并且具有更好的性能表现。

2.3.1 Atomic类实现线程同步的示例代码

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicDemo {
    private AtomicInteger num = new AtomicInteger(0);
    
    /**
     * 定义一个方法,用于加1操作
     */
    public void increment() {
        num.incrementAndGet();
        
        System.out.println(Thread.currentThread().getName() + "输出:" + num.get());
    }
    
    /**
     * 定义一个方法,用于减1操作
     */
    public void decrement() {
        num.decrementAndGet();
        
        System.out.println(Thread.currentThread().getName() + "输出:" + num.get());
    }
}

public class AtomicTest {
    public static void main(String[] args) {
        AtomicDemo atomicDemo = new AtomicDemo();
        
        // 定义一个线程,用于执行递增操作
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                atomicDemo.increment();
            }
        }, "线程1").start();

        // 定义一个线程,用于执行递减操作
        new Thread(() -> {
            for (int j = 0; j < 5; j++) {
                atomicDemo.decrement();
            }
        }, "线程2").start();
    }
}

代码说明:

在上面的代码中,AtomicDemo类中的num字段也被定义为常用的AtomicInteger类型,它可以实现对整数类型的原子更新,避免多线程操作的安全问题。在increment()和decrement()方法中,使用num.incrementAndGet()和num.decrementAndGet()方法实现对num字段的原子操作。在主函数中,创建两个线程分别执行加1和减1操作,通过Atomic类实现线程的安全同步,并保证输出结果的正确性。

2.4 使用线程池

在Java中,使用线程池可以避免线程的创建和销毁造成的性能损失。线程池可以根据需要调整线程的数量和优先级,提高程序的并发性能。在使用线程池时,需要注意线程池的大小和任务队列的大小,避免线程池的饱和和任务队列的溢出。

2.4.1 Java线程池实现多线程的示例代码

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolExample {
    public static void main(String[] args) {
        // 创建线程池,其中包含10个线程
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        // 提交任务给线程池执行
        for (int i = 0; i < 100; i++) {
            Runnable task = new Task(i);
            executorService.submit(task);
        }

        // 关闭线程池
        executorService.shutdown();
    }

    static class Task implements Runnable {
        private int taskId;

        public Task(int id) {
            this.taskId = id;
        }

        @Override
        public void run() {
            System.out.println("Task " + taskId + " is running.");
        }
    }
}

代码说明

该示例代码创建一个包含10个线程的线程池,并提交100个任务给线程池执行。每个任务都是一个实现了Runnable接口的Task对象,当线程池执行该任务时,打印该任务的ID。最后,线程池被关闭。

2.5 避免死锁

死锁是指多个线程之间互相等待对方释放资源的情况,从而导致程序无法继续执行的问题。在Java中,可以使用锁的顺序来避免死锁问题。锁的顺序要保持一致,并且尽量减少锁的粒度和范围,避免锁定过多的代码。

2.5.1 如何避免死锁示例代码

public class DeadlockAvoidanceExample {
    private static Object lock1 = new Object();
    private static Object lock2 = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("Thread 1 acquired lock 1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2) {
                    System.out.println("Thread 1 acquired lock 2");
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("Thread 2 acquired lock 1");
                synchronized (lock2) {
                    System.out.println("Thread 2 acquired lock 2");
                }
            }
        });

        // 设置线程优先级,使其更有可能出现死锁
        thread1.setPriority(Thread.MAX_PRIORITY);
        thread2.setPriority(Thread.MIN_PRIORITY);

        thread1.start();
        thread2.start();
    }
}

代码说明

在上面的示例代码中,有两个线程,它们尝试获取两个不同的锁(lock1和lock2)。如果两个线程在执行时交替获取锁,就不会发生死锁。但是,如果两个线程分别获取一个锁,然后尝试获取另一个锁,就会出现死锁

三、使用ThreadLocal类

在Java中,ThreadLocal类可以用来实现线程间的数据隔离,避免线程间的数据冲突问题。ThreadLocal类可以将数据绑定到线程上,保证每个线程都有自己独立的数据副本,避免多个线程之间共享数据造成的问题。

3.1 使用Concurrent包

Java的Concurrent包提供了多种并发容器和工具类,可以方便地实现多线程编程。例如,使用ConcurrentHashMap类可以实现多线程安全的HashMap,使用ConcurrentLinkedQueue类可以实现多线程安全的队列等。

3.1.1 代码示例

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

public class ConcurrentExample {
    public static void main(String[] args) {
        ConcurrentMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();

        // 将键值对添加到ConcurrentMap中
        concurrentMap.put("A", 1);
        concurrentMap.put("B", 2);
        concurrentMap.put("C", 3);

        // 获取并打印所有键值对
        for (Map.Entry<String, Integer> entry : concurrentMap.entrySet()) {
            String key = entry.getKey();
            Integer value = entry.getValue();
            System.out.println(key + " : " + value);
        }

        // 如果键“A”对应的值为1,则将它的值改为100
        concurrentMap.replace("A", 1, 100);

        // 获取键“A”对应的值,并打印输出
        Integer value = concurrentMap.get("A");
        System.out.println("Value of key A is: " + value);
    }
}

代码说明

该示例代码创建了一个ConcurrentMap,并将几个键值对添加到其中。然后遍历并打印所有的键值对。接着,如果键“A”的值为1,则将其值改为100。最后,获取键“A”的值并打印输出。

ConcurrentHashMap是ConcurrentMap接口的一个实现类,它是线程安全的,可以被多个线程同时访问和修改。在并发环境下,使用ConcurrentHashMap可以避免由于并发修改导致的数据不一致的问题。ConcurrentHashMap支持基本的Map操作,如put、get、remove等,并且还提供了一些增强功能,如replace、putIfAbsent等。

3.2 使用信号量和倒计时门闩(chuan)

在Java中,可以使用信号量和倒计时门闩来实现线程间的同步和互斥操作。信号量可以用来控制线程的访问数量,倒计时门闩可以用来控制线程的等待时间。这些机制可以用来协调多个线程的执行顺序,避免数据冲突和死锁问题。

3.1.2 Java中使用信号量和倒计时门闩的示例代码

3.1.2.2 信号量代码示例
import java.util.concurrent.Semaphore;

public class SemaphoreExample {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(2); // 初始化信号量数量为2
        for (int i = 1; i <= 5; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire(); // 获取信号量
                    System.out.println("Thread " + Thread.currentThread().getId() + " acquired semaphore");
                    Thread.sleep(1000);
                    System.out.println("Thread " + Thread.currentThread().getId() + " releasing semaphore");
                    semaphore.release(); // 释放信号量
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

代码说明

在上面的示例代码中,创建了一个Semaphore对象,初始值为2,表示最多有两个线程可以同时执行。然后,启动了5个线程,每个线程获取信号量,执行一些任务,然后释放信号量。

3.1.2.2 倒计时门闩代码示例
import java.util.concurrent.CountDownLatch;

public class CountdownLatchExample {
    public static void main(String[] args) throws InterruptedException {
        // 创建倒计时门闩,计数器初始值为3
        CountDownLatch latch = new CountDownLatch(3);

        // 创建3个线程并启动
        for (int i = 0; i < 3; i++) {
            Thread thread = new Thread(new Worker(i, latch));
            thread.start();
        }

        // 等待倒计时门闩的计数器为0
        latch.await();

        // 所有线程都执行完毕,打印提示信息
        System.out.println("All workers have finished.");
    }

    static class Worker implements Runnable {
        private int id;
        private CountDownLatch latch;

        public Worker(int id, CountDownLatch latch) {
            this.id = id;
            this.latch = latch;
        }

        @Override
        public void run() {
            System.out.println("Worker " + id + " is working...");
            try {
                // 模拟每个线程工作的时间
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Worker " + id + " has finished.");
            // 计数器减1
            latch.countDown();
        }
    }
}

代码说明

该示例代码创建了一个倒计时门闩,并将计数器初始值设置为3。然后,创建3个工作者线程并启动,每个线程会睡眠1秒钟以模拟工作的时间。在每个工作者线程执行完毕后,会调用latch.countDown()将计数器减1。当计数器减为0时,主线程才能继续执行。
CountDownLatch是Java中的一个工具类,它可以让一个或多个线程等待一个或多个事件的发生。CountDownLatch有一个计数器,当计数器为0时,会释放所有等待线程。可以通过countDown()方法将计数器的值减1,也可以通过await()方法等待计数器的值为0。

四、总结

Java并发编程是一项非常重要的技术,在现代计算机系统中广泛应用。掌握Java并发编程的基本原理和实践技巧,可以帮助我们更好地编写高性能的多线程程序。本文介绍了Java并发编程的基本原理和实践技巧,包括线程和进程、同步和互斥、线程间的通信、线程池等方面的内容。希望读者通过本文的介绍,可以更好地理解Java并发编程,并能够在实践中应用这些知识。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

陈书予

孩子快饿死了 求求打赏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值