【多线程】常用的接口和类(Callable,ReentrantLock,原子类,信号量,CountDownLatch)

文章目录

    • 1. Callable接口
      • 1.1 Callable使用
      • 1.2 对比Runnable
    • 2. ReentrantLock类
      • 2.1 ReentrantLock概念
      • 2.2 ReentrantLock的用法
      • 2.3 对比synchronized
      • 2.4 如何选择使用锁
    • 3. 原子类
      • 3.1 常见的原子类
      • 3.2 AtomicInteger方法及使用
    • 4. 信号量
      • 4.1 信号量的概念
      • 4.2 信号量的方法及使用
      • 4.3 信号量的作用
    • 5. CountDownLatch

1. Callable接口

1.1 Callable使用

Callable和Runnable是一个类似的接口,但是Callable中的call方法可以返回任意一个类型结果,而Runable中的run方法并不能返回结果。所以引入Callable接口可以方便在多线程中的计算结果。

利用Callable接口计算1加到100:

import java.util.concurrent.*;

public class Test2 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
//      计算1加到100
        Callable callable = new Callable() {
            @Override
            public Object call() throws Exception {
                int sum = 0;
                for (int i = 1; i <= 100; i++) {
                    sum += i;
                }
                return sum;
            }
        };
		//Callable常要搭配FutureTask来使用,接受执行结果
        FutureTask<Integer> futureTask = new FutureTask<>(callable);

        Thread t = new Thread(futureTask);
        t.start();
        //调用get()会进行堵塞,直至获得结果
        int result = futureTask.get();
        System.out.println(result);
    }
}

我们并不能直接获得call方法返回的结果,所以Callable常搭配FutureTask来使用,FutureTask相当于一个执行任务的容器,使用get()可以获得执行任务的结果,并且调用get()会进行堵塞,直至获得结果。
Callable也可以搭配线程池使用,充分提供多线程性能优势:

import java.util.concurrent.*;

public class Test2 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(1);
//      计算1加到100
        Callable callable = new Callable() {
            @Override
            public Object call() throws Exception {
                int sum = 0;
                for (int i = 1; i <= 100; i++) {
                    sum += i;
                }
                return sum;
            }
        };

        FutureTask<Integer> futureTask = new FutureTask<>(callable);

//        Thread t = new Thread(futureTask);
//        t.start();
        executorService.submit(futureTask);

        int result = futureTask.get();
        System.out.println(result);

        executorService.shutdown();
    }
}

1.2 对比Runnable

  1. 两者都是执行一个任务;
  2. Callable可以返回一个任何类型的值,而Runnable不可以;
  3. Callable需要搭配FutureTask来使用,而Runnable不需要;

2. ReentrantLock类

2.1 ReentrantLock概念

RenntrantLock(可重入锁)是Java中的一个并发锁类它和synchronized关键字有相似的功能,但是相比更加的灵活,具有更多的功能,但操作起来应该更加的谨慎,防止死锁。
例如:

  1. 都具有可重入性:一个线程一次可以获得多个锁,不会发生死锁。
  2. 具有公平性:ReentrantLock默认是非公平锁,但是可以构造锁是传入true,开启公平锁模式,确定等待时间最长的锁优先获得锁。
    // ReentrantLock 的构造方法 public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
  3. 条件变量支持:它支持条件变量,可以在某些条件下等待或唤醒线程。
  4. 可中断性:线程可以在等待过程中被中断,可用于处理取消操作。
  5. 超时获得锁:线程在一定时间内尝试获得锁,如果超时仍然没有获得,可以进行其他操作。
    它属于Java的java.util.concurrent.locks包。

2.2 ReentrantLock的用法

  • lock():加锁,如果获不得锁就死等。
  • trylock(超时时间):加锁,在超时时间内等待获得锁,超过则放弃加锁。
  • unlock():解锁。

格式:

ReentrantLock lock = new ReentrantLock(); 

lock.lock();   
try {    
 // working    
} finally {  
 //可以防止锁没被释放  
 lock.unlock()    
}

举例,两个线程并发执行,分别对数字count加1000:

public class Test1 {
    //全局变量count
    static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        //锁
        ReentrantLock lock = new ReentrantLock();
        //线程t1
        Thread t1 = new Thread(() -> {
            lock.lock();
            try{
                for (int i = 0; i < 1000; i++) {
                    count++;
                }
            }finally {
                lock.unlock();
            }
        });
        //线程t2
        Thread t2 = new Thread(() -> {
           lock.lock();
           try{
               for (int i = 0; i < 1000; i++) {
                   count++;
               }
           }finally{
               lock.unlock();
           }
        });
        t1.start();
        t2.start();
        Thread.sleep(1000);
        System.out.println(count);
    }
}

2.3 对比synchronized

  1. synchronized是一个关键字,基于JVM内部实现,ReentrantLock是一个类,基于JVM外部实现(Java实现)。
  2. 两者都是可重入锁,一个线程可以多次加锁。
  3. synchronized一开始是非公平锁,在锁竞争激烈的情况下转换为公平锁,ReentrantLock默认也是一个非公平锁,但是也可以构造出公平锁。
  4. synchronized需要不手动释放锁,ReentrantLock需要手动获得锁。
  5. synchronized申请锁失败后,会死等,而ReentrantLock可以使用trylock(超时时间),一定时间放弃锁。
  6. synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是一个随机等待的线程. ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程。

2.4 如何选择使用锁

  1. 锁竞争不激烈,使用synchronized更加的方便,自动释放锁,加锁。
  2. 锁竞争激烈,使用ReentrantLock,搭配trylock(超时时间)更加方便,且避免死等。
  3. 需要公平锁 ,使用ReentrantLock。

3. 原子类

3.1 常见的原子类

原子类是 Java中用于多线程编程的一组类,它们提供了一种线程安全的方式来执行原子操作,这意味着这些操作不会被其他线程中断,也不会导致数据不一致或竞态条件。

一些常见的 Java 原子类包括:

  1. AtomicInteger: 用于原子操作整数类型,例如自增、自减、获取当前值等。
  2. AtomicLong: 用于原子操作长整数类型,同样支持自增、自减、获取当前值等操作。
  3. AtomicBoolean: 用于原子操作布尔类型,通常用于控制标志位的状态。
  4. AtomicReference: 用于原子操作引用类型,允许你原子性地设置和获取引用对象。
  5. AtomicReferenceArray: 用于原子操作引用类型的数组。
  6. AtomicIntegerFieldUpdater: 用于原子性地更新某个类的整数字段。

这些原子类的主要优势在于它们提供了一种无需显式使用锁(如synchronized关键字)就可以执行线程安全操作的方式。这可以提高多线程程序的性能,因为锁可能导致线程的阻塞和竞争。

3.2 AtomicInteger方法及使用

方法 作用
incrementAndGet() 前置自增1
decrementAndGet() 前置自减1
getAndDecrement() 后置自减1
getAndIncrement() 后置自增1
getAndAdd(int n) 自增n
public class Test2 {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger atomicInteger = new AtomicInteger(0);
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                atomicInteger.incrementAndGet();//++i
//                atomicInteger.decrementAndGet();//--i
//                atomicInteger.getAndDecrement();//i--
//                atomicInteger.getAndIncrement();//i++
//                atomicInteger.getAndAdd(n);//自增n
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                atomicInteger.incrementAndGet();
            }
        });
        t1.start();
        t2.start();
        Thread.sleep(1000);
        System.out.println(atomicInteger.get());//get方法获得数
    }
}

4. 信号量

4.1 信号量的概念

信号量(Semaphore)用于表示可用资源数量,本质上就是一个计数器。当执行P操作这个信号量就减一个,当执行V操作这个信号量就加一个。
如果信号量减为0,P操作就无法执行,等待V操作增加信号量,如果信号量为最大值了,V操作就无法执行,等待P操作减少信号量。
Semaphore中PV操作时原子性的,则可以多线程环境中直接使用。

4.2 信号量的方法及使用

方法 作用
acquire() P操作
release() V操作
public class Test3 {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(2);
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                try {
                    semaphore.acquire();
                    System.out.println("申请到一个资源");
                    Thread.sleep(2000);
                    semaphore.release();
                    System.out.println("释放一个资源");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        for (int i = 0; i < 10; i++) {
            new Thread(runnable).start();
        }
    }
}

4.3 信号量的作用

  1. 资源控制:信号量可以对共享资源进行保护,限制同时访问资源的线程数量。例如,数据库连接池可以使用信号量来限制同时连接到数据库的线程数量,以避免资源过度竞争。
  2. 线程的通信:信号量也可以用于线程之间的通信,例如一个线程可以等待另一个线程发出的信号来执行某些操作。这在多线程协作和同步的场景中非常有用。
  3. 控制执行顺序:通过适当设置信号量的初始值,可以控制线程的执行顺序。例如,可以设置一个信号量为0,然后一个线程在完成某个任务后释放信号量,另一个线程等待信号量被释放才能执行。
  4. 限流:信号量可以用于限制对某些资源或服务的请求速率,以防止资源过度消耗或过载。
  5. 解决死锁:在一些情况下,信号量可以用来解决死锁问题,通过适当的信号量设置,可以破坏死锁的条件,使得系统更加健壮。

总之,信号量是一种重要的同步工具,虽然在某些情况下可能看起来多此一举,但在复杂的多线程应用中,它们可以提供精确的控制和协调,帮助确保线程安全和程序的可靠性

5. CountDownLatch

同时等待N个任务执行结束。

方法 作用
countDown() 计数器减一
await() 堵塞等待所有任务执行结束
public class Test4 {
    static int count = 10;
    public static void main(String[] args) throws InterruptedException {
        //初始化10,表示10个任务需要完成
        CountDownLatch countDownLatch = new CountDownLatch(10);
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                try{
                    System.out.println(count--);
                    //每次调用countDown(),CountDownLatch内计数器减1
                    countDownLatch.countDown();
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        };
        for (int i = 0; i < 10; i++) {
            new Thread(runnable).start();
        }
        //堵塞等待所有任务执行结束
        countDownLatch.await();
        System.out.println("执行结束");
    }
}

你可能感兴趣的:(Java多线程编程,java,多线程,线程安全)