专栏首页Java深度编程【Java并发编程】线程安全与性能

【Java并发编程】线程安全与性能

类的线程安全表现为: 操作的原子性,类似数据库事务。 内存的可见性,当前线程修改后其他线程立马可看到。 不做正确的同步,在多个线程之间共享状态的时候,就会出现线程不安全。

安全策略有如下三种: 1. 栈封闭 栈封闭指的是变量都是在方法内部声明的,这些变量都处于栈封闭状态。在这个独立空间创使用则绝对是安全的,它会随方法的结束而结束。

2. 无状态

类没有任何成员变量,只有一堆成员函数,这样绝对是安全的。

3. 类不可变

Java中不管是String对象跟基本类型装箱后的对象都是不可变的,都带有final。让状态不可变,除了加final还可以不提供任何set方法也能做到(但这无法阻止反射机制)。

volatile

保证类的可见性,用volatile修饰的变量在get的时候多线程情况下不用加锁,保证可见性。但是在set的时候要加锁或者通过CAS操作进行变化。 比如ConcurrentHashMap

类中持有的成员变量,特别是对象的引用,如果这个成员对象不是线程安全的,通过get等方法发布出去(return出去),在并发情况下会造成这个成员对象本身持有的数据在多线程下不正确的修改,从而造成整个类线程不安全的问题。 解决方法:用concurrentLinkedQueue等线程安全容器或者返回一个副本:

public class UnsafePublishTest {
  //要么用线程的容器替换,要么发布出去的时候,提供副本,深度拷贝
  private List<Integer> list =  new ArrayList<>(3);
  public UnsafePublish() {
    list.add(1);
    list.add(2);
    list.add(3);
  }
  //将list不安全的发布出去了
  public List<Integer> getList() {
      return list;
  }

  //也是安全的, 加了锁
  public synchronized int getList(int index) {
    return list.get(index);
  }
  public synchronized void set(int index,int val) {
    list.set(index, val);
  }
}

死锁

竞争的资源一定是多于1个,同时小于等于竞争的线程数,资源只有一个,只会产生激烈的竞争。 死锁的根本成因:获取锁的顺序不一致,导致相互等待。

public class NormalDeadLockTest {
    private static Object valueFirst = new Object();//第一个锁
    private static Object valueSecond = new Object();//第二个锁

    //先拿第一个锁,再拿第二个锁
    private static void fisrtToSecond() throws InterruptedException {
        String threadName = Thread.currentThread().getName();
        synchronized (valueFirst) {
            System.out.println(threadName + " 获得第一个");
            TimeUnit.MILLISECONDS.sleep(100);
            synchronized (valueSecond) {
                System.out.println(threadName + " 获得第二个");
            }
        }
    }
    //先拿第二个锁,再拿第一个锁
    private static void SecondToFisrt() throws InterruptedException {
        String threadName = Thread.currentThread().getName();
        synchronized (valueSecond) {
            System.out.println(threadName + " 获得第二个");
            TimeUnit.MILLISECONDS.sleep(100);
            synchronized (valueFirst) {
                System.out.println(threadName + " 获得第一个");
            }
        }
    }

    //执行先拿第二个锁,再拿第一个锁
    private static class TestThread extends Thread {
        private String name;
        public TestThread(String name) {
            this.name = name;
        }
        public void run() {
            Thread.currentThread().setName(name);
            try {
                SecondToFisrt();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        Thread.currentThread().setName("TestDeadLock");
        TestThread testThread = new TestThread("SubTestThread");
        testThread.start();
        try {
            fisrtToSecond();//先拿第一个锁,再拿第二个锁
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

如果怀疑发送死锁:

  1. 通过jps 查询应用的id
  2. 通过jstack id 查看应用的锁的持有情况

简单来说就是 甲拿着A锁,获取B锁,乙拿着B锁获取A锁,注意在甲乙获得第一个锁的时候休眠会儿,来制造死锁。 解决方法:保证加锁的顺序性。 1.先锁小的再锁大的。可以通过唯一ID活着通过System自带的获得ID函数System.identityHashCode():

public class SafeOperate implements ITransfer {
  //加时赛锁
  private static Object tieLock = new Object();

    @Override
    public void transfer(UserAccount from, UserAccount to, int amount)
            throws InterruptedException {
      int fromHash = System.identityHashCode(from);
      int toHash = System.identityHashCode(to);
      // 或者你可以保证 ID唯一可以用ID实现
      //始终先锁hash小的那个
      if(fromHash<toHash) {
            synchronized (from){
                System.out.println(Thread.currentThread().getName() +" get"+from.getName());
                Thread.sleep(100);
                synchronized (to){
                    System.out.println(Thread.currentThread().getName() +" get"+to.getName());
                    from.flyMoney(amount);
                    to.addMoney(amount);
                }
            }        
      }else if(toHash<fromHash) {
            synchronized (to){
                System.out.println(Thread.currentThread().getName() +" get"+to.getName());
                Thread.sleep(100);
                synchronized (from){
                    System.out.println(Thread.currentThread().getName() +" get"+from.getName());
                    from.flyMoney(amount);
                    to.addMoney(amount);
                }
            }        
      }else {//解决hash冲突的方法
        synchronized (tieLock) { //那个线程拿到再处理
        synchronized (from) {
          synchronized (to) {
                      from.flyMoney(amount);
                      to.addMoney(amount);            
          }
        }
      }
      }
    }
}

2.通过tryLock 核心思路就是while死循环获得两个锁,都获得才可以进行操作然后break。

 public void transfer(UserAccount from, UserAccount to, int amount)
            throws InterruptedException {
      Random r = new Random();
      while(true) {
        if(from.getLock().tryLock()) {
          try {
            System.out.println(Thread.currentThread().getName() +" get "+from.getName());
            if(to.getLock().tryLock()) {
              try {
                  System.out.println(Thread.currentThread().getName() +" get "+to.getName());
                          //两把锁都拿到了
                          from.flyMoney(amount);
                          to.addMoney(amount);
                          break;
              }finally {
                to.getLock().unlock();
              }
            }
          }finally {
            from.getLock().unlock();
          }
        }
        SleepTools.ms(r.nextInt(10)); // 防止发生活锁!
      }
    }

活锁

尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生拿锁,释放锁的过程。比如上面的while循环如果没有时间休眠的话,可能会导致获取和释放锁产生时间差,来不及去处理,就可能出现想回等待的死锁:

甲拿到A尝试拿B,拿B失败了再重新尝试拿A,再重新拿B,这样周而复始的尝试。
乙拿到B尝试拿A,拿A失败了再重新尝试拿B,再重新拿A,这样周而复始的尝试。

解决办法:把对象加锁顺序的不确定性变成确定性的顺序。 解决:

  1. 通过内在排序,保证加锁的顺序性
  2. 通过尝试拿锁配合休眠若干也可以。

线程饥饿 饥饿:线程因无法访问所需资源而无法执行下去的情况。 在CPU繁忙的情况下,优先级低的线程得到执行的机会很小,就可能发生线程饥饿;持有锁的线程,如果执行的时间过长,也可能导致饥饿问题。

性能 多线程是好但是要切记勿装逼强行使用,装逼必被打。使用多线程是为了提供系统的性能,充分利用系统资源。但是引入多线程后会引入额外的开销。衡量应用程序性能一般:服务时间、延迟时间、吞吐量、可伸缩性,深入了解性能优化。 做应用的时候:

先保证程序的正确性跟健壮性,确实达不到性能要求再想如何提速。 一定要以测试为基准。 一个程序中串行的部分永远是有的. 装逼利器:阿姆达尔定律 S=1/(1-a+a/n)

系统中某一部件因为采用更快的实现后,整个系统性能的提高与该部分的使用频率或者在总运行时间中比例有关。直观地,你把一个部件提升了很多,但是这个部件却不经常使用,因此这种提高看上去是提高其实并没有。所以Amdahl定律认为我们除了需要关注部件的加速比,还要关注该部件的使用频率/情况。

本文分享自微信公众号 - Java深度编程(JavaDeep),作者:SoWhat1412

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-04-03

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 多线程编程必备技术—— volatile,synchronized,lock

    volatile: volatile是一个类型修饰符(type specifier)。它是被设计用来修饰被不同线程访问和修改的变量。确保本条指令不会...

    Java深度编程
  • SpringBoot 集成 Dubbo + zookeeper全注解,无xml方式(+各种坑的说明))

    大家知道springBoot发明的初衷是为了减少xml的配置,而dubbo的传统方式就是xml配置,所以既然用了springBoot就应该遵循它的规则,在...

    Java深度编程
  • javaScript的Math数学对象 --用法大全

    Math是 JavaScript 的原生对象,提供各种数学功能。该对象不是构造函数,不能生成实例,所有的属性和方法都必须在Math对象上调用。简而言之就如...

    Java深度编程
  • 阻击外挂——《龙之谷手游》安全测试的那点事

    随着智能手机的全面普及和市场泛娱乐化,移动游戏行业发展迅猛,无论是市场收入还是用户规模,手游在游戏市场上已经占据了半壁江山。如此火热的市场吸引了大量外挂、辅助工...

    WeTest质量开放平台团队
  • 阻击外挂:《龙之谷手游》安全测试的那点事

    手游的使用场景与传统APP有着巨大的差异,不同的游戏玩法, 技术实现都不一样,因此手游安全测试团队需要对每一个游戏,都从零开始研究游戏内部实现架构。近期腾讯推出...

    WeTest质量开放平台团队
  • 详解CentOS7下PostgreSQL 11的安装和配置教程

    这一步初始化数据库命令会在 /var/lib/pgsql 目录下创建名称为11文件夹,11为数据库版本,如果安装的是其他版本,对应的是其版本号(9.4、9.5)...

    砸漏
  • 医学统计学:总体均数的估计与假设检验

    了解总体特征的最佳方法是对总体的每一个个体进行观察、试验,但这在医学研究实际中往往不可行。我们只能采用抽样研究,从总体中随机抽取一个或几个样本,通过样本信息了解...

    口仆
  • Golang语言-- 小技巧

    .前言 Golang 开发过程中的一些小技巧在这里记录下。 2.内容 1)包的引用 经常看到Golang代码中出现 _ "controller/home" 类似...

    李海彬
  • 当JavaScriptCore遇上多线程

    JSContext是native代码执行JS代码的上下文,native可以向JSContext中注入方法和属性以供JS调用,相当于在JS的window对象上挂属...

    forrestlin
  • Java设计模式(一)----单例模式

    单例模式 一、特点: 二.分类 (一)、懒汉式单例 (二)、双重检查锁定 ...

    汤高

扫码关注云+社区

领取腾讯云代金券