[java] JUC包下的常用类

1、atomic包

atomic下运用了CAS的AtomicBoolean、AtomicInteger、AtomicReference等原子变量类

AtomicInteger count = new AtomicInteger();
//获取值
count.get();
//自增
count.incrementAndGet();

CAS

CAS: 全称Compare and swap,字面意思:”比较并交换“,一个 CAS 涉及到以下操作:

  • 我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。

    1. 比较 A 与 V 是否相等。(比较)
    2. 如果比较相等,将 B 写入 V。(交换)
    3. 返回操作是否成功。

总结一下 JAVA 的 cas 是怎么实现的:

  1. java 的 cas 利用的的是 unsafe 这个类提供的 cas 操作。
  2. unsafe 的cas 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg
  3. Atomic::cmpxchg 的实现使用了汇编的 cas 操作,并使用 cpu 硬件提供的 lock信号保证其原子性

CAS中的ABA问题

如果一开始位置V得到的旧值是A,当进行赋值操作时再次读取发现仍然是A,并不能说明变量没有被其它线程改变过。有可能是其它线程将变量改为了B,后来又改回了A。

解决ABA问题

  1. 在变量前面追加版本号:每次变量更新就把版本号加1,则A-B-A就变成1A-2B-3A。

  2. atomic包下的AtomicStampedReference类

    • 第一个参数expectedReference:表示预期值。
    • 第二个参数newReference:表示要更新的值。
    • 第三个参数expectedStamp:表示预期的时间戳。
    • 第四个参数newStamp:表示要更新的时间戳。
public class Main {
    public static void main(String[] args) {
        AtomicStampedReference<Integer> reference = new AtomicStampedReference<Integer>(10,1);
        reference.compareAndSet(10,11,reference.getStamp(),reference.getStamp()+1);
        System.out.println(reference.getReference());//输出11
        reference.compareAndSet(11,12,1,reference.getStamp()+1);
        System.out.println(reference.getReference());//修改失败,输出11
    }
}

作者:keyuan0214 链接:https://www.jianshu.com/p/6d0f135a89cd 来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

2、locks包

AbstractQueuedSynchronizer(AQS)

AQS就是一个并发包的基础组件,用来实现各种锁,各种同步组件的。它包含了state变量、加锁线程、等待队列等并发中的核心组件。

看一下ReentrantLock和AQS之间的关系。

我们看上图,说白了,ReentrantLock内部包含了一个AQS对象,也就是AbstractQueuedSynchronizer类型的对象。

这个AQS对象就是ReentrantLock可以实现加锁和释放锁的关键性的核心组件。

ReentrantLock加锁和释放锁的底层原理

好了,现在如果有一个线程过来尝试用ReentrantLock的lock()方法进行加锁,会发生什么事情?

很简单,这个AQS对象内部有一个核心的变量叫做state,是int类型的,代表了加锁的状态。

初始状态下,这个state的值是0。

另外,这个AQS内部还有一个关键变量,用来记录当前加锁的是哪个线程,初始化状态下,这个变量是null。

接着线程跑过来调用ReentrantLock的lock()方法尝试进行加锁,这个加锁的过程,直接就是用CAS操作将state值从0变为1。

(关于CAS,之前专门有文章做过详细阐述,大家可以自行阅读了解)

如果之前没人加过锁,那么state的值肯定是0,此时线程1就可以加锁成功。

一旦线程1加锁成功了之后,就可以设置当前加锁线程是自己。所以大家看下面的图,就是线程1跑过来加锁的一个过程。

其实看到这儿,大家应该对所谓的AQS有感觉了。说白了,就是并发包里的一个核心组件,里面有state变量、加锁线程变量等核心的东西,维护了加锁状态。

你会发现,ReentrantLock这种东西只是一个外层的API,内核中的锁机制实现都是依赖AQS组件的。

这个ReentrantLock之所以用Reentrant打头,意思就是他是一个可重入锁。

可重入锁的意思,就是你可以对一个ReentrantLock对象多次执行lock()加锁和unlock()释放锁,也就是可以对一个锁加多次,叫做可重入加锁。

大家看明白了那个state变量之后,就知道了如何进行可重入加锁!

其实每次线程1可重入加锁一次,会判断一下当前加锁线程就是自己,那么他自己就可以可重入多次加锁,每次加锁就是把state的值给累加1,别的没啥变化。

接着,如果线程1加锁了之后,线程2跑过来加锁会怎么样呢?

我们来看看锁的互斥是如何实现的?

线程2跑过来一下看到,哎呀!state的值不是0啊?所以CAS操作将state从0变为1的过程会失败,因为state的值当前为1,说明已经有人加锁了!

接着线程2会看一下,是不是自己之前加的锁啊?当然不是了,“加锁线程”这个变量明确记录了是线程1占用了这个锁,所以线程2此时就是加锁失败。

给大家来一张图,一起来感受一下这个过程:

接着,线程2会将自己放入AQS中的一个等待队列,因为自己尝试加锁失败了,此时就要将自己放入队列中来等待,等待线程1释放锁之后,自己就可以重新尝试加锁了

所以大家可以看到,AQS是如此的核心!AQS内部还有一个等待队列,专门放那些加锁失败的线程!

同样,给大家来一张图,一起感受一下:

接着,线程1在执行完自己的业务逻辑代码之后,就会释放锁!他释放锁的过程非常的简单,就是将AQS内的state变量的值递减1,如果state值为0,则彻底释放锁,会将“加锁线程”变量也设置为null!

整个过程,参见下图:

接下来,会从等待队列的队头唤醒线程2重新尝试加锁。

好!线程2现在就重新尝试加锁,这时还是用CAS操作将state从0变为1,此时就会成功,成功之后代表加锁成功,就会将state设置为1。

此外,还要把“加锁线程”设置为线程2自己,同时线程2自己就从等待队列中出队了。

最后再来一张图,大家来看看这个过程。

作者:石杉的架构笔记
来源:慕课网
本文原创发布于慕课网 ,转载请注明出处,谢谢合作

链接:http://www.imooc.com/article/293135

ReentantLock

public class TestReentrantLock {
    // true为公平锁,false为非公平锁,默认为false
    private static ReentrantLock lock = new ReentrantLock(true);

    void run() {
        for (int i = 0; i < 100; i++) {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + "---");
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        TestReentrantLock t = new TestReentrantLock();
        Thread t1 = new Thread(t::run);
        Thread t2 = new Thread(t::run);
        t1.start();
        t2.start();
    }
}

Synchronized 和 lock区别如下:

  1. 来源:
    lock是一个接口,而synchronized是java的一个关键字,synchronized是内置的语言实现;

  2. 异常是否释放锁:
    synchronized在发生异常时候会自动释放占有的锁,因此不会出现死锁;而lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生。(所以最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。)

  3. 是否响应中断
    lock等待锁过程中可以用interrupt来中断等待,而synchronized只能等待锁的释放,不能响应中断;

  4. 是否知道获取锁
    Lock可以通过trylock来知道有没有获取锁,而synchronized不能;

  5. 在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。 所以说,在具体使用时要根据适当情况选择。

  6. 用法
    synchronized可以加在方法上,也可以加在特定代码块中;lock一般加在代码块上

  7. 锁类型
    Lock是通过CAS实现的乐观锁,而我synchronized则是使用操作系统互斥量实现的悲观锁

ReentrantReadWriteLock

  • ReetrantReadWriteLock读写锁的实现中,读锁使用共享模式;写锁使用独占模式,换句话说,读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的

  • ReetrantReadWriteLock读写锁的实现中,需要注意的,当有读锁时,写锁就不能获得;而当有写锁时,除了获得写锁的这个线程可以获得读锁外,其他线程不能获得读锁

  • 读写锁的实现必须确保写操作对读操作的内存影响。换句话说,一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容,读写锁之间为互斥

  • ReentrantReadWriteLock支持锁降级,不支持锁升级(同一个线程中,在没有释放读锁的情况下,就去申请写锁)

3、CountDownLatch

这个类是一个同步计数器,主要用于线程间的控制,当CountDownLatch的count计数>0时,await()会造成阻塞,直到count变为0,await()结束阻塞,使用countDown()会让count减1。CountDownLatch的构造函数可以设置count值,当count=1时,它的作用类似于wait()和notify()的作用。如果我想让其他线程执行完指定程序,其他所有程序都执行结束后我再执行,这时可以用CountDownLatch,但计数无法被重置,如果需要重置计数,请考虑使用 CyclicBarrier 。

public class TestCountdownLatch {
    public static void main(String[] args) {
        usingCountDownLatch();
    }

    private static void usingCountDownLatch() {
        Thread[] threads = new Thread[100];
        CountDownLatch latch = new CountDownLatch(threads.length);
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() -> {
                int result = 0;
                for (int j = 0; j < 10000; j++) result += j;
                latch.countDown();
            });
        }
        for (int i = 0; i < threads.length; i++) {
            threads[i].start();
        }
        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("end latch");
    }
}

4、CyclicBarrier

该类从字面理解为循环屏障,它可以协同多个线程,让多个线程在这个屏障前等到,直到所有线程都到达了这个屏障时,再一起执行后面的操作。假如每个线程各有一个await,任何一个线程运行到await方法时就阻塞,直到最后一个线程运行到await时才同时返回。和之前的CountDownLatch相比,它只有await方法,而CountDownLatch是使用countDown()方法将计数器减到0,它创建的参数就是countDown的数量;CyclicBarrier创建时的int参数是await的数量。 barrie在释放等待线程后可以重用,所以称它为循环 的barrier

public class TestCyclicBarrier {
    public static void main(String[] args) {
        CyclicBarrier barrier = new CyclicBarrier(20, () -> {
            System.out.println(Thread.currentThread().getName() + ":完成最后任务");
        });

        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + "到达");
                    Thread.sleep(100);
                    barrier.await();
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

5、Semaphore

该类用于控制信号量的个数,构造时传入个数。总数就是控制并发的数量。假如是5,程序执行前用acquire()方法获得信号,则可用信号变为4,程序执行完通过release()方法归还信号量,可用信号又变为5.如果可用信号为0,acquire就会造成阻塞,等待release释放信号。acquire和release方法可以不在同一个线程使用。Semaphore实现的功能就类似厕所有5个坑,假如有10个人要上厕所,那么同时只能有多少个人去上厕所呢?同时只能有5个人能够占用,当5个人中 的任何一个人让开后,其中等待的另外5个人中又有一个人可以占用了。另外等待的5个人中可以是随机获得优先机会,也可以是按照先来后到的顺序获得机会,这取决于构造Semaphore对象时传入的参数选项。单个信号量的Semaphore对象可以实现互斥锁的功能,并且可以是由一个线程获得了“锁”,再由另一个线程释放“锁”,这可应用于死锁恢复的一些场合。

public class TestSemaphore {
    public static void main(String[] args) {
        Semaphore s = new Semaphore(2, true);
        new Thread(() -> {
            try {
                s.acquire();
                System.out.println("t1.start");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                s.release();
            }
        }).start();
        new Thread(() -> {
            try {
                s.acquire();
                System.out.println("t2.start");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                s.release();
            }
        }).start();
    }
}

6、Future

Future是一个接口。Future模式可以这样来描述:我有一个任务,提交给了Future,Future替我完成这个任务。期间我自己可以去做任何想做的事情。一段时间之后,我就便可以从Future那儿取出结果。就相当于下了一张订货单,一段时间后可以拿着提订单来提货,这期间可以干别的任何事情。其中Future 接口就是订货单,真正处理订单的是Executor类,它根据Future接口的要求来生产产品。它经常配合线程池来一起工作,将任务交给线程池去处理。

FutureTask是一个实现类,它实现了Runnable接口,配合Callable接口创建线程。Callable接口的call()方法作为线程执行体,call可以有返回值,也可以抛出异常。Callable对象不能直接作为Thread的目标,但用Future可以完成。

7、Exchanger

这个类用于交换数据,只能用于两个线程。当一个线程运行到exchange()方法时会阻塞,另一个线程运行到exchange()时,二者交换数据,然后执行后面的程序。

import java.util.concurrent.Exchanger;
public class JUCTest implements Runnable{
	private  Exchanger<String> exchange ;
	private  String name;
	private	 String str;
	public JUCTest(Exchanger<String> exchange,String name,String str){
		this.exchange=exchange;
		this.name=name;
		this.str=str;
	}
	@Override  
	public void run(){  //线程同步用了synchronized否则无法保证s的正确性
		try{		
			System.out.println(name+"线程自己的数据时:"+str);
			String s=exchange.exchange(str);    //交换数据
			System.out.println(name+"获取另一个线程的数据:"+s);
		}
		catch(Exception e){
			e.printStackTrace();
		}
	}
	public static void main(String[] args) throws Exception {
		Exchanger<String> ex=new Exchanger<String>();
		new Thread(new JUCTest(ex,"zhou","Hello")).start();   
		new Thread(new JUCTest(ex,"yu","World")).start();   
	}
}

8、Phaser

JAVA 1.7引入了一个新的并发API:Phaser,一个可重用的同步barrier。用来解决控制多个线程分阶段共同完成任务的情景问题。

比如:5个学生一起参加考试,一共有三道题,要求所有学生到齐才能开始考试,全部同学都做完第一题,学生才能继续做第二题,全部学生做完了第二题,才能做第三题,所有学生都做完的第三题,考试才结束。分析这个题目:这是一个多线程(5个学生)分阶段问题(考试考试、第一题做完、第二题做完、第三题做完),所以很适合用Phaser解决这个问题。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值