资讯专栏INFORMATION COLUMN

java高并发之从零到放弃(四)

Alex / 1324人阅读

摘要:前言本篇主要讲解如何去优化锁机制或者克服多线程因为锁可导致性能下降的问题线程变量有这样一个场景,前面是一大桶水,个人去喝水,为了保证线程安全,我们要在杯子上加锁导致大家轮着排队喝水,因为加了锁的杯子是同步的,只能有一个人拿着这个唯一的杯子喝

前言

本篇主要讲解如何去优化锁机制
或者克服多线程因为锁可导致性能下降的问题

ThreadLocal线程变量

有这样一个场景,前面是一大桶水,10个人去喝水,为了保证线程安全,我们要在杯子上加锁
导致大家轮着排队喝水,因为加了锁的杯子是同步的,只能有一个人拿着这个唯一的杯子喝水
这样子大家都喝完一杯水需要很长的时间
如果我们给每个人分发一个杯子呢?是不是每人喝到水的时间缩小到了十分之一

多线程并发也是一个道理
在每个Thread中都有自己的数据存放空间(ThreadLocalMap)
而ThreadLocal就是在当前线程的存放空间中存放数据
下面这个例子,在每个线程中存放一个arraylist,而不是大家去公用一个arraylist

public class ThreadLocalTest {
    public static ThreadLocal threadLocal = new ThreadLocal();
    public static ArrayList list = new ArrayList();
    public static class Demo implements Runnable {
        private int i;

        public Demo(int i) {
            this.i = i;
        }
        @Override
        public void run() {
            list.add(i);
            threadLocal.set(list);
            System.out.println(threadLocal.get());
        }
    }
    public static void main(String[] args) throws InterruptedException {
        ExecutorService es = Executors.newFixedThreadPool(5);
        for (int j = 0; j < 200; j++) {
            es.execute(new Demo(j));
        }
        Thread.sleep(3000);
        System.out.println(list.size());
        es.shutdown();
    }
}

在每个线程内部有一块存储区域叫做ThreadLocalMap
可以看到,ThreadLocal采用set,get存取值方式
只有线程完全关闭时,在ThreadLocalMap中的数据才会被GC回收

这时有一个值得考虑的问题
我们使用线程池来开发的时候,线程池中的线程并不会关闭,它只是处于空闲状态
也就是说,我们如果把过大的数据存储在当前线程的ThreadLocalMap中,线程不断的调用,被空闲...
最后会导致内存溢出
解决方法是当不需要这些数据时
使用ThreadLocal.remove()方法将变量给移除

CAS操作

还有一种脱离锁的机制,那就是CAS
CAS带着三个变量,分别是:
V更新变量:需要返回的变量
E预期值:原来的值
N新值,传进来的新变量

只有当预期值和新值相等时,才会把V=N,如果不相等,说明该操作会让数据无法同步
根据上面的解释,大概就能知道CAS其实也是在保护数据的同步性

当多个线程进行CAS操作时,可想只有一个线程能成功更新,之后其它线程的E和V会不地进行断比较
所以CAS的同步锁的实现是一样的

CAS操作的并发包在Atomic包中,atomic实现了很多类型
不管是AtomicInteger还是AtomicReference,都有相同点,请观察它们的源码:

private volatile V value;
private static final long valueOffset;

以上是AtomicReferenc

private volatile int value;
private static final long valueOffset;

以上是AtomicIntege

都有value,这是它们的当前实际值
valueOffset保存的是value的偏移量

下面给出一个简单的AtomicIntege例子:

public class AtomicTest {
    public static AtomicInteger atomicInteger = new AtomicInteger();
    //public static AtomicReference atomicReference = new AtomicReference();
    public static class Demo implements Runnable{

        @Override
        public void run() {
            for (int j=0;j<1000;j++){
                atomicInteger.incrementAndGet();        //当前值加1并且返回当前值
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService es = Executors.newFixedThreadPool(10);
        for (int i =0;i<10;i++){
            es.submit(new Demo());
        }
        Thread.sleep(5000);
        System.out.println(atomicInteger);
    }
}

你试着执行一下,如果打印出10000说明线程安全
使用CAS操作比同步锁拥有更好的性能

我们来看下incrementAndGet()的源码:

public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }

来看下getAndAddInt()源码:

public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

这里有一个循环,再细看源码发现是native的,虽然看不到原生代码,但是可以看出它这里做了一个CAS操作,不断地进行多个变量的比较,只有预设值和新值相等时,才跳出循环
var5就是需要更新的变量,var1和var2是预设值和新值

死锁

讲了那么多无锁的操作,我们来看一下一个死锁的现象
两个线程互相占着对方想得到的锁,就会出现死锁状况

public class DeadLock extends Thread{
    protected String suo;
    public static String zuo = new String();
    public static String you = new String();

    public DeadLock(String suo){
        this.suo=suo;
    }

    @Override
    public void run(){
        if (suo==zuo){
            synchronized (zuo){
                System.out.println("拿到了左,正在拿右......");
                synchronized (you){
                    System.out.println("拿到了右,成功了");
                }
            }
        }
        if (suo==you){
            synchronized (you){
                System.out.println("拿到了右,正在拿左......");
                synchronized (zuo){
                    System.out.println("拿到了zuo,成功了");
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i=0;i<10000;i++){
            DeadLock t1 = new DeadLock(zuo);
            DeadLock t2 = new DeadLock(you);
            t1.start();t2.start();
        }

        Thread.sleep(50000);
    }
}

如图:


出现了两个线程的死锁现象,所以说去锁不仅能提升性能,也能防止死锁的产生

今天就先到这里,大家可以看看这些内容的拓展
记得点关注看更新,谢谢阅读

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/70676.html

相关文章

  • java并发之从零到放弃(一)

    摘要:今天就先到这里,大家可以看看这些内容的拓展记得点关注看更新,谢谢阅读 前言 这是一个长篇博客,希望大家关注我并且一起学习java高并发废话不多说,直接开始 并行和并发 并行:多个线程同时处理多个任务并发:多个线程处理同个任务,不一定要同时 下面用图来描述并行和并发的区别:(实现和虚线表示两个不同的线程) showImg(https://segmentfault.com/img/bVYT...

    luoyibu 评论0 收藏0
  • java并发之从零到放弃(二)

    摘要:可以用代替可以用代替定义的对象的值是不可变的今天就先到这里,大家可以看看这些内容的拓展记得点关注看更新,谢谢阅读 前言 java高并发第二篇讲的是java线程的基础依旧不多说废话 线程和进程 进程是操作系统运行的基础,是一个程序运行的实体,windows上打开任务管理器就能看到进程线程是轻量级的进程,是程序执行的最小单位,是在进程这个容器下进行的 线程基本操作 新建一个线程类有两种方式...

    cloud 评论0 收藏0
  • java并发之从零到放弃(三)

    摘要:前言今天讲的多线程的同步控制直接进入正题重入锁重入锁可以完全代替,它需要类来实现下面用一个简单的例子来实现重入锁以上代码打印出来的是,可以说明也实现了线程同步它相比更加灵活,因为重入锁实现了用户自己加锁,自己释放锁记得一定要释放,不然其他线 前言 今天讲的多线程的同步控制直接进入正题 ReentrantLock重入锁 重入锁可以完全代替synchronized,它需要java.util...

    FrozenMap 评论0 收藏0
  • java并发之从零到放弃(五)

    摘要:前言这篇主要来讲解多线程中一个非常经典的设计模式包括它的基础到拓展希望大家能够有所收获生产者消费者模式简述此设计模式中主要分两类线程生产者线程和消费者线程生产者提供数据和任务消费者处理数据和任务该模式的核心就是数据和任务的交互点共享内存缓 前言 这篇主要来讲解多线程中一个非常经典的设计模式包括它的基础到拓展希望大家能够有所收获 生产者-消费者模式简述 此设计模式中主要分两类线程:生产者...

    meislzhua 评论0 收藏0
  • 前端从零开始系列

    摘要:只有动手,你才能真的理解作者的构思的巧妙只有动手,你才能真正掌握一门技术持续更新中项目地址求求求源码系列跟一起学如何写函数库中高级前端面试手写代码无敌秘籍如何用不到行代码写一款属于自己的类库原理讲解实现一个对象遵循规范实战手摸手,带你用撸 Do it yourself!!! 只有动手,你才能真的理解作者的构思的巧妙 只有动手,你才能真正掌握一门技术 持续更新中…… 项目地址 https...

    Youngdze 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<