并发线程安全

189 阅读8分钟

类的线程安全定义

如果多线程下使用这个类,不管多线程时如何调度和使用这个类,这个类总是表现出正确的行为,这个类是线程安全的。

线程安全的表现为:

  • 操作原子性
  • 内存可见性

不做正确的同步,在多个线程之间共享状态的时候,就会出现线程不安全。

怎样才能做到线程安全

  • 栈封闭:所有的变量都是在方法内部生明的,这些变量都是处于栈封闭状态

  • 无状态:没有任何成员变量的类,叫做无状态类

    public class StateLess {
    
        public int service(int a, int b) {
            return a * b;
        }
    }
    
  • 让类不可变

    • 加final关键字,对于一个类,所有的成员变量应该是私有的,同样,只要有可能,所有的成员变量都应该由final关键字来修饰

      如果成员变量是一个类的时候,这个变量所对应的类应该是不可变的,

      不可变类,final修饰成员变量

      public class ImmutetableToo {
      
          private final int a;
          private final int b;
      
          public ImmutetableToo(int a, int b) {
              this.a = a;
              this.b = b;
          }
      
          public int getA() {
              return a;
          }
      }
      

      表面上不可变,实际可变,final修饰为引用类型相关的成员变量

      public class ImmutetableFinalRef {
      
          private final int a;
          private final int b;
          private final User user;
      
          public ImmutetableFinalRef(int a, int b) {
              this.a = a;
              this.b = b;
              this.user = new User(23);
          }
      
          public int getA() {
              return a;
          }
      
          public int getB() {
              return b;
          }
      
          public User getUser() {
              return user;
          }
      
          private static class User {
              private int age;
      
              public User(int age) {
                  this.age = age;
              }
      
              public int getAge() {
                  return age;
              }
      
              public void setAge(int age) {
                  this.age = age;
              }
          }
      
          public static void main(String[] args) {
              ImmutetableFinalRef ref = new ImmutetableFinalRef(12, 23);
              User u = ref.getUser();
              u.setAge(35);
              System.out.println(ref.getUser().getAge());
          }
      
      }
      

      由上可看出还是可以通过应用修改age的值,若要解决,就要把age也用fianl修饰

    • 对象根本不提供任何修改成员变量的方法,同时成员变量也不作为方法的返回值

    public class ImmutetableToo {
        private List<Integer> list = new ArrayList<>(3);
    
        public ImmutetableToo() {
            list.add(1);
            list.add(2);
            list.add(3);
        }
    
        public boolean contains(int i) {
            return list.contains(i);
        }
    }
    
  • Volatile:保证类的可见性,最适合一个线程写,多个线程读的情况

  • 加锁或者CAS

  • 安全的发布:类中持有的成员变量,特别是引用类型。如果这个成员变量不是线程安全的,通过get()方法发布出去,会造成这个成员变量本身所持有的数据在多线程的情况下被不正确的修改,从而造成整个类的线程不安全

    线程不安全的类

    public class UnsafePublish {
       private List<Integer> list = new ArrayList<Integer>(3);
    
        public UnsafePublish() {
            list.add(1);
            list.add(2);
            list.add(3);
        }
    
        //这样是不安全的
        public List<Integer> getList() {
            return list;
        }
    }
    

    解决方法

    //若要线程安全
    private ConcurrentLinkedQueue<Integer> list = new ConcurrentLinkedQueue<Integer>(3);
    //或者
    public synchronized List<Integer> getList(){
        return list;
    }
    //或者
    public synchronized void setList(int index,int value){
        list.set(index,value)
    }
    
  • ThreadLocal

Servlet并不是线程安全的,我们平时感觉不到的原因,

  • 需求上,很少有共享的需求
  • 接受到请求,返回回答的时候都是由一个线程来负责的

死锁

资源一定多于1个,同时小于竞争线程的数量,资源只有一个只会导致激烈的竞争

死锁的根本原因:获取锁的顺序不一致导致的,例子以及解决方法如下

  • 简单的静态死锁:

    public class UseNormalDeadLock {
        private static final Object firstLock = new Object();
        private static final Object secondLock = new Object();
    
        private static void firstToSecond() {
            synchronized (firstLock) {
                System.out.println(Thread.currentThread().getName() + " get first");
                SleepTools.ms(100);
                synchronized (secondLock) {
                    System.out.println(Thread.currentThread().getName() + " get second");
                }
            }
        }
    
        private static void secondToFirst() {
            synchronized (secondLock) {
                System.out.println(Thread.currentThread().getName() + " get second");
                SleepTools.ms(100);
                synchronized (firstLock) {
                    System.out.println(Thread.currentThread().getName() + " get first");
                }
            }
        }
    
        private static class Worker extends Thread {
            private String name;
    
            public Worker(String name) {
                this.name = name;
            }
    
            @Override
            public void run() {
                Thread.currentThread().setName(name);
                secondToFirst();
            }
        }
    
        public static void main(String[] args) {
            Thread.currentThread().setName("TestDeadLock");
            Worker worker = new Worker("SubTestThread");
            worker.start();
            firstToSecond();//先拿第一个锁,再拿第二个锁
    
        }
    
    
    }
    

    解决办法:

    • jps查看发生死锁的id
    • 在通过jstack id来查看应用的锁的持有情况
    • 然后调整获取首的顺序
  • 动态的锁

    定义一个账户类

    public class UserAccount {
    
        private int money;
        private final String name;
    
        public UserAccount(int money, String name) {
            this.money = money;
            this.name = name;
        }
    
        private Lock lock = new ReentrantLock();
    
        @Override
        public String toString() {
            return "UserAccount{" +
                    "name='" + name + '\'' +
                    ", money=" + money +
                    '}';
        }
    
        public Lock getLock() {
            return lock;
        }
    
        public int getMoney() {
            return money;
        }
    
        public String getName() {
            return name;
        }
    
        public void addMoney(int amount) {
            money = money + amount;
        }
    
        public void flyMoney(int amount) {
            money = money - amount;
        }
    
    }
    

    定义一个转账接口

    public interface ITransfSev {
        void transferMoney(UserAccount from,UserAccount to ,int amount) throws InterruptedException;
    }
    

    定义一个转账类来实现转账接口

    public class Transfer implements ITransfSev {
        @Override
        public void transferMoney(UserAccount from, UserAccount to, int amount) throws InterruptedException {
            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);
                }
            }
        }
    }
    

    定一个个启动类(包含转账线程)

    public class Company {
        public static class TransferThread extends Thread {
            private String name;
            private UserAccount from;
            private UserAccount to;
            private int amount;
            private ITransfSev transfSev;
    
            public TransferThread(String name, UserAccount from, UserAccount to,
                                  int amount, ITransfSev transfSev) {
                this.name = name;
                this.from = from;
                this.to = to;
                this.amount = amount;
                this.transfSev = transfSev;
            }
    
            @Override
            public void run() {
                try {
                    Thread.currentThread().setName(name);
                    transfSev.transferMoney(from, to, amount);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        public static void main(String[] args) {
            Company company = new Company();
            UserAccount zhangsan = new UserAccount(1000, "zhangsan");
            UserAccount lisi = new UserAccount(1000, "lisi");
            int amount = 100;
            ITransfSev transfSev = new Transfer();
    //        ITransfSev transfSev = new TransferSafe();
    //        ITransfSev transfSev = new TransferSafeToo();
    
    
            TransferThread zToL = new TransferThread("zToL", zhangsan, lisi, amount, transfSev);
            TransferThread lToZ = new TransferThread("lToZ", lisi, zhangsan, amount, transfSev);
            zToL.start();
            lToZ.start();
    
        }
    

    这样是会有死锁,应为from和to是传入的,就算我在Transfer类中定义先获取from,在获取to这样,但是from和to实际上是由外面控制的。

    解决办法由两种

    • 把外界传人的from和to进行比价,规定获取锁的顺序

      public class TransferSafe implements ITransfSev {
      
          private final Object lock = new Object();
      
          @Override
          public void transferMoney(UserAccount from, UserAccount to, int amount) throws InterruptedException {
              int fromHash = System.identityHashCode(from);
              int toHash = System.identityHashCode(to);
      
              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 (fromHash < toHash) {
                  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 {
                  synchronized (lock) {
                      synchronized (from) {
                          synchronized (to) {
                              from.flyMoney(amount);
                              to.flyMoney(amount);
                          }
                      }
                  }
              }
          }
      }
      
    • 在账户类里买定义一个锁,使用tryLock

      public class TransferSafeToo implements ITransfSev {
      
          @Override
          public void transferMoney(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));
              }
          }
      }
      

      这样会有活锁的问题

活锁

尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生拿锁,释放锁的过程。

就以上一段tryLock为例子,若两个线程都在tryLock,当各自都拿到一个锁,都拿第二个锁失败就会放弃各自的锁,然后又重新拿,又各自只拿到一个锁,这样一直循环下

解决方法,每个线程休眠随机数,错开拿锁的时间。

线程饥饿

指低优先级的线程一直无法获取到cpu执行权

性能和思考

影响性能的因素
  • 上下文切换:5000-10000个时钟周期约为几微秒
  • 内存同步:加锁等操作会有额外的指令
  • 阻塞:挂起会额外增加两次上下文切换
思考
  • 减少锁的粒度:使用锁的时候,锁保护的对象是多个,多个对象其实也是独立变化的时候,不如使用多个锁来一一保护这些对象。但是要注意避免发生死锁。

    public class FennessLock {
        private List<String> users = new ArrayList<String>();
        private List<String> queries = new ArrayList<String>();
        
        public synchronized void setUsers(List<String> users) {
            this.users = users;
        }
    
        public  void setUsers(List<String> users) {
            synchronized (users){
                this.users = users;
            }
            
        }
    	 ...
    
    	 //可以变为
        public synchronized void setQueries(List<String> queries) {
            this.queries = queries;
        }
        public  void setQueries(List<String> queries) {
            synchronized(queries){
                this.queries = queries;
            }
            
        }
    
    
        ...
    
        
    }
    
  • 减少锁的竞争

  • 缩小锁的范围:快进快出,尽量减少对锁的持有的时间

    public class ReduceLock {
    
        private Map<String, String> map = new HashMap<String, String>(16);
    
        public synchronized boolean isMathc(String name, String regexp) {
            String key = "user." + name;
            String job = map.get(key);
            if (null == job) {
                return false;
            } else {
                return Pattern.matches(regexp, job);//很花费时间的话
            }
    
        }
    
        //可以修改为
        public boolean isMathcReduce(String name, String regexp) {
            String key = "user." + name;
            String job;
            synchronized (this) {
                job = map.get(key);
            }
            if (null == job) {
                return false;
            } else {
                return Pattern.matches(regexp, job);
            }
    
        }
    }
    

    避免多余缩减锁的范围 锁粗化(JVM也提供)

    synchronized (this) {
       job = map.get(key);
     }
    job = job+"s";
    synchronized (this) {
      job = map.get(key);
    }
    //可粗化为
     synchronized (this) {
       job = map.get(key);
       job = job+"s";
       job = map.get(key);
     }
    
  • 锁分段 ConcurrentHashMap

  • 替换独占锁

    • 使用读些锁
    • 使用CAS
    • 使用系统自带的并发容器

线程安全的单例模式

双重检查锁的单例
public class SingleDcl {
    private static SingleDcl singleDcl;

    public SingleDcl() {

    }

    public static SingleDcl getInstance() {
        if (null == singleDcl) {
            synchronized (SingleDcl.class) {
                if (null == singleDcl) {
                    return new SingleDcl();
                }
            }
        }
        return singleDcl;
    }
}

表上看这个单例模式是线程安全的,其实并不是,因为我们的类的加载并不是原子性的,可能出现Java中的另一个线程看到个初始化了一半的SingleDlc的情况

image-20200423140056343

解决办法有以下几种:

  • 使用volatile修饰singleDcl

  • 在类加载的时候,把实例new出来,在JVM中,对类的加载和类的初始化,有虚拟机保证线程安全

    • 饥饿式:

      public class SingleDclSafe {
          private static SingleDcl singleDcl = new SingleDcl();
      
          public SingleDclSafe() {
          }
      
      }
      
    • 懒汉式:

      public class SingleDclSafeToo {
      
          public SingleDclSafeToo() {
      
          }
      
          private static class SingleSclSafeTooHolder {
              private static SingleDclSafeToo singleDclSafeToo = new SingleDclSafeToo();
          }
      
          public SingleDclSafeToo getInstace() {
              return SingleSclSafeTooHolder.singleDclSafeToo;
          }
      }
      

      这里有一个延时加载模式: 当我们一个类里买某写成员变量在初始化的时候并不想加载,而是由我们自己决定

      public class InitLazy {
      
          private Integer a;
          private Integer b;
      
      
          public InitLazy(Integer a) {
              this.a = a;
          }
      
          public Integer getA() {
              return a;
          }
      
          private static class BHolder {
              private static Integer b = 1000000;
          }
      
          public Integer getB() {
              return BHolder.b;
          }
      }