【JavaEE】多线程进阶(2)

【JavaEE】多线程进阶(2)

一、JUC(java.util.concurrent) 的常⻅类

博客结尾附有此篇博客的全部代码!!!

1.1 Callable 接⼝

Callable 接口是 Java 中用于定义可以返回结果的任务的接口,它位于 java.util.concurrent 包中。

java 复制代码
public interface Callable<V> {
    V call() throws Exception;
}

实例应用:计算1+2+...+100的值,使用Callable接口

java 复制代码
   public static void main(String[] args) throws InterruptedException, ExecutionException {
        Callable<Integer> callable = new Callable<Integer>() {
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 0; i <= 100; i++) {
                    sum += i;
                }
                return sum;
            }
        };
        Thread thread = new Thread(callable);
        thread.start();
    }

原因:Thread本身不提供接受结果的方法,需要FutureTask对象来拿到结果(Thread不提供接受结果是为了更好的解耦合,将任务和线程分离开)

  • FutureTask:FutureTask 实现了 Runnable 接口,因此可以被 Thread 接受。
  • Thread类的构造函数可以接受一个 Runnable 对象,但不能接受其他类型的对象,因为 Thread 的内部逻辑是基于 Runnable 的 run() 方法实现的。

修改:

java 复制代码
public class CallableDemo {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        Callable<Integer> callable=new Callable<Integer>() {
            public Integer call() throws Exception {
                int sum=0;
                for (int i = 0; i <= 100; i++) {
                    sum+=i;
                }
                return sum;
            }
        };
        FutureTask<Integer> futureTask=new FutureTask<>(callable);
        Thread thread=new Thread(futureTask);
        thread.start();
        System.out.println(futureTask.get());
    }
}

通过Runnable接口计算1+2+...+100的值:

java 复制代码
public class RunnableDemo {
    private static int total=0;
    public static void main(String[] args) throws InterruptedException {
        Runnable r = new Runnable(){
            int sum=0;
            public void run() {
                for (int i = 0; i <=100 ; i++) {
                    sum+=i;
                }
                total=sum;
            }
        };
        Thread t1 = new Thread(r);
        t1.start();
        t1.join();
        System.out.println(total);
    }
}

1.2 ReentrantLock

可重⼊互斥锁. 和 synchronized 定位类似, 都是⽤来实现互斥效果, 保证线程安全

ReentrantLock 的核心功能是通过 Lock 接口实现的,它提供了以下方法:

  • lock():获取锁,如果锁已经被其他线程占用,则当前线程会阻塞,直到获取锁。
  • unlock():释放锁。
  • tryLock():尝试获取锁,如果锁可用则立即获取,否则返回 false,不会阻塞。
  • tryLock(long timeout, TimeUnit unit):尝试获取锁,如果在指定时间内无法获取锁,则返回 false。
  • isHeldByCurrentThread():判断当前线程是否持有该锁。
  • isLocked():判断锁是否被任何线程持有。
java 复制代码
public class ReentrantLockDemo1 {
    private static int total = 0;
    public static void main(String[] args) throws InterruptedException {
        ReentrantLock locker = new ReentrantLock();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                locker.lock();
                total++;
                locker.unlock();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                locker.lock();
                total++;
                locker.unlock();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(total);
    }
}

运行结果:total=100000

这里需要注意的:

因为这里解锁需要自己手动解锁,但是不可避免的抛出异常而导致代码运行终止,有可能就执行不到 locker.lock();

改进:将unlocker.lock();放入finally代码块中

ReentrantLock和synchronized对比:

  1. synchronized是关键字,ReentrantLock是Java的标准库中的类
  2. synchronized是通过代码块执行加锁解锁,而ReentrantLock是通过lock()和unlock()加锁解锁,需要注意的是unlock()不调用问题
  3. ReentrantLock提供的tryLock(),如果成功加锁,返回true;反之,加锁失败,返回false,不会出现阻塞;而且还可以设置等待时长,在这段时间后再尝试加锁,返回true/false。
  4. synchronized是非公平锁,ReentrantLock默认是非公平锁,但是可以设置为公平锁
java 复制代码
ReentrantLock lock = new ReentrantLock(true);
  1. 更强⼤的唤醒机制. synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是⼀个随机等待的线程. ReentrantLock 搭配Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定线程。

1.3 原子类

原子类通过提供一系列线程安全的变量操作方法,确保在多线程环境下对变量的读写操作是不可分割的(即原子的)。它们利用了底层硬件的原子操作指令(如 CAS),从而避免了锁的开销,提高了性能。

原子类的特性:
  • 无锁并发:原子类通过 CAS 机制实现线程安全,无需使用重量级的锁(如 synchronized 或 ReentrantLock)。
  • 高性能:由于避免了锁的开销,原子类在高并发场景下通常比传统同步机制性能更高。
  • 线程安全:原子类保证了对变量的操作是原子的,即使在多线程环境下也不会出现竞态条件。
常见原子类:

(1)基本类型原子类:

AtomicInteger:用于原子操作的整数。

AtomicLong:用于原子操作的长整型。

AtomicBoolean:用于原子操作的布尔值。

(2)引用类型原子类:

AtomicReference:用于原子操作的对象引用。

AtomicStampedReference:用于原子操作的对象引用,同时带有版本号(用于解决 ABA 问题)。

AtomicMarkableReference:用于原子操作的对象引用,同时带有布尔标记。

(3)数组类型原子类:

AtomicIntegerArray:用于原子操作整型数组。

AtomicLongArray:用于原子操作长整型数组。

AtomicReferenceArray:用于原子操作对象引用数组。

原子类的实例:

基本类型原子类:AtomicInteger:用于原子操作的整数

java 复制代码
public class AtomicIntegerArrayDemo1 {
    public static void main(String[] args) {
        AtomicInteger atomicInt = new AtomicInteger(2);
        atomicInt.incrementAndGet(); // 增加 1
        atomicInt.addAndGet(2);      // 增加 5
        atomicInt.compareAndSet(5, 10); // 如果当前值为 5,则设置为 10
        System.out.println(atomicInt.get());//这里获取的是10
    }
}
java 复制代码
public class AtomicIntegerArrayDemo {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger atomicInt = new AtomicInteger(0);

        Thread t1 = new Thread(() -> {
            for(int i = 0; i < 5000;i++ ){
                atomicInt.incrementAndGet();
            }
        });
        Thread t2 = new Thread(() -> {
            for(int i = 0; i < 5000;i++ ){
                atomicInt.incrementAndGet();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(atomicInt.get());//获取的是10000
    }
}

引用类型原子类:AtomicStampedReference:用于原子操作的对象引用,同时带有版本号。

java 复制代码
public class AtomicStampedReferenceDemo1 {
    public static void main(String[] args) {
        AtomicStampedReference<String> ref = new AtomicStampedReference<>("Hello", 0);
        ref.compareAndSet("Hello", "World",
                0, 1); // 更新引用和版本号
        System.out.println(ref.getReference());//expectedStamp和initialStamp相等,
                                               // 则更新initialRef引用值为newReference,并且更新版本号
    }
}

compareAndSet 方法的作用:

  • 检查当前引用值是否为 "Hello"。
  • 检查当前版本号是否为 0。
  • 如果两个条件都满足,则将引用值更新为 "World",版本号更新为 1

    数组类型原子类:AtomicReferenceArray:用于原子操作对象引用数组。
java 复制代码
public class AtomicReferenceArrayDemo {
    public static void main(String[] args) {
        AtomicReferenceArray<String> array = new AtomicReferenceArray<>(new String[]{"Hello", "World"});
        array.set(1, "Java");//将索引为1的引用改为Java
        System.out.println(array.get(1));
    }
}

1.4 线程池

线程池

1.5 信号量 Semaphore

Semaphore 的核心思想是通过一组许可证(permits)来控制对资源的访问。每个线程在访问资源之前,必须先获取一个许可证;访问完成后,释放许可证。许可证的数量是有限的,当许可证用完时,后续的线程将被阻塞,直到有许可证被释放。

代码实例
java 复制代码
public class SemaphoreDemo {
    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(5);
        System.out.println("使用第一个许可证");
        semaphore.acquire();
        System.out.println("使用第二个许可证");
        semaphore.acquire();
        System.out.println("使用第三个许可证");
        semaphore.acquire();
        System.out.println("使用第四个许可证");
        semaphore.acquire();
//        semaphore.release();
        semaphore.acquire();
        System.out.println("使用第五个许可证");
    }
}

将许可证改为4张,任务还是5个:

这里可以通过jconsole.exe来调试看下运行结果:

还是四张许可证,但是这里释放了一张许可证:

1.6 CountDownLatch

使用多线程,经常将一个大的任务分成多个子任务,使用多线程执行子任务,提高执行效率。

怎么判断子任务全部执行完毕呢?

这里就可以用CountDownLatch来记录各个任务完成。

  1. 构造 CountDownLatch 实例, 初始化 10 表⽰有 10 个任务需要完成.
  2. 每个任务执⾏完毕, 都调⽤ latch.countDown() . 在 CountDownLatch 内部的计数器同时⾃减.
  3. 主线程中使⽤ latch.await(); 阻塞等待所有任务执⾏完毕. 相当于计数器为 0 了
代码实例
java 复制代码
public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(3);
        Thread t1 = new Thread(()->{
            for(int i=0;i<3;i++){
                try {
                    Thread.sleep((long) (Math.random() * 2000));
                    latch.countDown();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t1.start();
        latch.await(); // 阻塞主线程,直到计数器为 0
        System.out.println("所有任务执行完毕");
    }
}

1.7 线程安全的集合类

Vector, Stack, HashTable, 是线程安全的(不建议⽤), 其他的集合类不是线程安全的

多线程环境使⽤ ArrayList

让ArrayList变成线程安全:

  1. ⾃⼰使⽤同步机制 (synchronized 或者 ReentrantLock)
  2. Collections.synchronizedList(new ArrayList);
    返回List的各种关键方法都带synchronized,这种做法类似于Vector, Stack
  3. 使⽤ CopyOnWriteArrayList
    读操作:读操作直接访问底层数组,不需要加锁,因此性能很高。
    写操作:
  • 创建底层数组的完整副本。
  • 在副本上进行修改操作。
  • 将副本替换为原始数组。
    这种操作的效率相对低效,因为每次都需要复制整个数组。
多线程环境使⽤哈希表

HashMap 本⾝不是线程安全的.

在多线程环境下使⽤哈希表可以使⽤:

• Hashtable

• ConcurrentHashMap

Hashtable
  • 使用全局锁(synchronized)保护整个哈希表(这意味着在任何时刻,只有一个线程可以修改哈希表,其他线程必须等待),所有操作(包括读写)都会锁住整个表。
  • 这种机制简单但效率低下,尤其是在高并发场景下,容易导致线程阻塞。

存在缺点:

  1. 如果多线程访问同⼀个 Hashtable 就会直接造成锁冲突.
  2. size 属性也是通过 synchronized 来控制同步, 也是⽐较慢的.
  3. ⼀旦触发扩容, 就由该线程完成整个扩容过程. 这个过程会涉及到⼤量的元素拷⻉, 效率会⾮常低.
ConcurrentHashMap
  • 使用分段锁(Segment)机制,将哈希表分为多个段,每个段有自己的锁。
  • JDK 1.8 以后,进一步优化为基于 CAS 和 synchronized 的锁机制,结合数组 + 链表 + 红黑树的数据结构。
  • 读操作通常不需要加锁,写操作的锁粒度更细,大大减少了锁竞争。

优化扩容:

  1. 发现需要扩容的线程, 只需要创建⼀个新的数组, 同时只搬⼏个元素过去.
  2. 扩容期间, 新⽼数组同时存在.
  3. 后续每个来操作 ConcurrentHashMap 的线程, 都会参与搬家的过程. 每个操作负责搬运⼀⼩部分元素.
  4. 搬完最后⼀个元素再把⽼数组删掉.
  5. 这个期间, 插⼊只往新数组加.
  6. 这个期间, 查找需要同时查新数组和⽼数组

1.8 死锁

线程安全

此篇博客的全部代码!!!

相关推荐
言慢行善28 分钟前
sqlserver模糊查询问题
java·数据库·sqlserver
专吃海绵宝宝菠萝屋的派大星34 分钟前
使用Dify对接自己开发的mcp
java·服务器·前端
大数据新鸟1 小时前
操作系统之虚拟内存
java·服务器·网络
Tong Z1 小时前
常见的限流算法和实现原理
java·开发语言
凭君语未可1 小时前
Java 中的实现类是什么
java·开发语言
He少年1 小时前
【基础知识、Skill、Rules和MCP案例介绍】
java·前端·python
克里斯蒂亚诺更新1 小时前
myeclipse的pojie
java·ide·myeclipse
迷藏4941 小时前
**eBPF实战进阶:从零构建网络流量监控与过滤系统**在现代云原生架构中,**网络可观测性**和**安全隔离**已成为
java·网络·python·云原生·架构
迷藏4941 小时前
**发散创新:基于Solid协议的Web3.0去中心化身份认证系统实战解析**在Web3.
java·python·web3·去中心化·区块链
qq_433502182 小时前
Codex cli 飞书文档创建进阶实用命令 + Skill 创建&使用 小白完整教程
java·前端·飞书