线程1

Posted by Solace Blog on March 20, 2019

线程

马士兵老师线程示例代码;concurrent文件为源码

前言

  • 什么是线程
  • 如何创建一个线程
  • 线程同步的概念

synchronized关键字

  • 当要保证一个代码块在多线程执行时,使用synchronized关键字加锁来实现,一般的方法可以对成员变量加锁,或者也可以对this自身来加锁。

    public void m() {
      synchronized(this) { //任何线程要执行下面的代码,必须先拿到this的锁
        count--;
        System.out.println(Thread.currentThread().getName() + " count = " + count);
      }
    }
    
  • 如果是静态方法时,可以对方法加锁,或者对class加锁

    public synchronized static void m() { //这里等同于synchronized(yxxy.c_004.T.class)
      count--;
      System.out.println(Thread.currentThread().getName() + " count = " + count);
    }
    
    public static void mm() {
      synchronized(T.class) { //对this加锁不行,因为静态方法没有实例对象
        count --;
      }
    }
    
  • 实际业务如果对写加锁,那么在读的时候如果不加锁可能会出现脏读的现象,应结合具体业务来实现需求。

    /**
     * 对业务写方法加锁
     * 对业务读方法不加锁
     * 容易产生脏读问题(dirtyRead)
     */
      
    package yxxy.c_008;
      
    import java.util.concurrent.TimeUnit;
      
    public class Account {
      String name;
      double balance;
        
      public synchronized void set(String name, double balance) {
        this.name = name;
        try {
          Thread.sleep(2000);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      
        this.balance = balance;
      }
        
      public /*synchronized*/ double getBalance(String name) {
        return this.balance;
      }
        
        
      public static void main(String[] args) {
        Account a = new Account();
        new Thread(()->a.set("zhangsan", 100.0)).start();
          
        try {
          TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
          
        System.out.println(a.getBalance("zhangsan"));
          
        try {
          TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      
        System.out.println(a.getBalance("zhangsan"));
      }
    }
    
  • 使用synchronize获得的锁是可以重入的

    /**
     * 两个方法的锁都加在一个对象上,当其中一个同步方法调用另外一个同步方法时,再次申请的时候仍然会得到该对象的锁.
     * 也就是说synchronized获得的锁是可重入的
     * @author mashibing
     */
    package yxxy.c_009;
      
    import java.util.concurrent.TimeUnit;
      
    public class T {
      synchronized void m1() {
        System.out.println("m1 start");
        try {
          TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        m2();
      }
        
      synchronized void m2() {
        try {
          TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        System.out.println("m2");
      }
      
      public static void main(String[] args) {
        T t = new T();
        new Thread(() -> t.m1()).start();
      }
    }
    
  • 当线程抛出异常时,会释放锁,所以在线程中对待异常一定要小心。如果不想释放锁,一定要try catch捕获异常,然后进行操作,比如回滚事务

  • volatile和synchronize的区别

    每个线程都有自己的工作内存,每个线程的工作内存都从主内存有一份使用到的变量的拷贝,以此来作为缓冲。每个线程所使用到的变量都会从主内存缓存一份到缓冲区。当一个线程修改了变量的值之后,如果CPU很忙的话,它可能就没时间去主内存reload下最新的数据,所以有可能其他线程不会立即拿到最新的数据。

    volatile修饰的变量当被多个线程使用时,会保证此变量对于其他线程的可见性,即当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的,而普通变量不能做到这一点。

    所以下面的代码中,main方法修改running的变量,线程才会停止

    /**
     * volatile 关键字,使一个变量在多个线程间可见
     * A B线程都用到一个变量,java默认是A线程中保留一份copy,这样如果B线程修改了该变量,则A线程未必知道
     * 使用volatile关键字,会让所有线程都会读到变量的修改值
     * 
     * 在下面的代码中,running是存在于堆内存的t对象中
     * 当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区,在运行过程中直接使用这个copy,并不会每次都去
     * 读取堆内存,这样,当主线程修改running的值之后,t1线程感知不到,所以不会停止运行
     * 
     * 使用volatile,将会强制所有线程都去堆内存中读取running的值
     * 
     * 可以阅读这篇文章进行更深入的理解
     * http://www.cnblogs.com/nexiyi/p/java_memory_model_and_thread.html
     * 
     * volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized
     * @author mashibing
     */
    package yxxy.c_012;
      
    import java.util.concurrent.TimeUnit;
      
    public class T {
      /*volatile*/ boolean running = true; //对比一下有无volatile的情况下,整个程序运行结果的区别
      void m() {
        System.out.println("m start");
        while(running) {
          /*
          try {
            TimeUnit.MILLISECONDS.sleep(10);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }*/
        }
        System.out.println("m end!");
      }
        
      public static void main(String[] args) {
        T t = new T();
          
        new Thread(t::m, "t1").start();
          
        try {
          TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
          
        t.running = false;
          
          
      }
        
    }
    

    但由于volatile只能保证变量的可见性,但是并不能保证volatile的原子性(比如count++这种非原子性的操作)。而synchronize既可以保证可见性又保证原子性。但synchronize的效率比volatile的低很多。

    常见的基本数据类型都有对应的原子操作的对象,如:AtomicInteger、AtomicLong,它们的方法都是原子性的,效率也高得多。

  • 采用更细粒度的锁,可以使线程争取时间变短,从而提高效率

  • 锁是加在堆内存中的对象上,而不是栈里面的引用。当对象的引用发生改变,锁的对象也会发生变化。

  • java的字符串会有一个常量池,当锁的是字符串并且内容相等,那么会发生诡异的死锁现象

    /**
     * 不要以字符串常量作为锁定对象
     * 在下面的例子中,m1和m2其实锁定的是同一个对象
     * 这种情况还会发生比较诡异的现象,比如你用到了一个类库,在该类库中代码锁定了字符串“Hello”,
     * 但是你读不到源码,所以你在自己的代码中也锁定了"Hello",这时候就有可能发生非常诡异的死锁阻塞,
     * 因为你的程序和你用到的类库不经意间使用了同一把锁
     *
     * jetty
     *
     * @author mashibing
     */
    package yxxy.c_018;
      
    public class T {
      String s1 = "Hello";
      String s2 = "Hello";
      
      void m1() {
        synchronized(s1) {
      
        }
      }
      
      void m2() {
        synchronized(s2) {
      
        }
      }
    }
    
  • wait线程等待时会释放锁;notify唤醒一个线程但不会释放锁

  • ``` /**

    • 曾经的面试题:(淘宝?)
    • 实现一个容器,提供两个方法,add,size
    • 写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个时,线程2给出提示并结束
    • 分析下面这个程序,能完成这个功能吗?
    • @author mashibing */ ```
    /**
     * 曾经的面试题:(淘宝?)
     * 实现一个容器,提供两个方法,add,size
     * 写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个时,线程2给出提示并结束
     * 
     * 给lists添加volatile之后,t2能够接到通知,但是,t2线程的死循环很浪费cpu,如果不用死循环,该怎么做呢?
     * 
     * 这里使用wait和notify做到,wait会释放锁,而notify不会释放锁
     * 需要注意的是,运用这种方法,必须要保证t2先执行,也就是首先让t2监听才可以
     * 
     * 阅读下面的程序,并分析输出结果
     * 可以读到输出结果并不是size=5时t2退出,而是t1结束时t2才接收到通知而退出
     * 想想这是为什么?
     * 
     * notify之后,t1必须释放锁,t2退出后,也必须notify,通知t1继续执行
     * 整个通信过程比较繁琐
     * 
     * 使用Latch(门闩)替代wait notify来进行通知
     * 好处是通信方式简单,同时也可以指定等待时间
     * 使用await和countdown方法替代wait和notify
     * CountDownLatch不涉及锁定,当count的值为零时当前线程继续运行
     * 当不涉及同步,只是涉及线程通信的时候,用synchronized + wait/notify就显得太重了
     * 这时应该考虑countdownlatch/cyclicbarrier/semaphore
     * @author mashibing
     */
    package yxxy.c_019;
      
    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.TimeUnit;
      
    public class MyContainer5 {
      
      // 添加volatile,使t2能够得到通知
      volatile List lists = new ArrayList();
      
      public void add(Object o) {
        lists.add(o);
      }
      
      public int size() {
        return lists.size();
      }
      
      public static void main(String[] args) {
        MyContainer5 c = new MyContainer5();
      
        CountDownLatch latch = new CountDownLatch(1);
      
        new Thread(() -> {
          System.out.println("t2启动");
          if (c.size() != 5) {
            try {
              latch.await();
                
              //也可以指定等待时间
              //latch.await(5000, TimeUnit.MILLISECONDS);
            } catch (InterruptedException e) {
              e.printStackTrace();
            }
          }
          System.out.println("t2 结束");
      
        }, "t2").start();
      
        try {
          TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e1) {
          e1.printStackTrace();
        }
      
        new Thread(() -> {
          System.out.println("t1启动");
          for (int i = 0; i < 10; i++) {
            c.add(new Object());
            System.out.println("add " + i);
      
            if (c.size() == 5) {
              // 打开门闩,让t2得以执行
              latch.countDown();
            }
      
            try {
              TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
              e.printStackTrace();
            }
          }
      
        }, "t1").start();
      
      }
    }
    

多线程知识点列表

  • synchronize关键字用来指定对象加锁,锁的对象可以是变量、this对象、T.class。当锁定对象是this时等同于synchronize加在方法上;静态方法锁的对象只能是T.class
  • 同步方法中调用其他同步方法是没问题的。当锁对象一样时synchronized获得的锁是可重入的
  • 当业务的读写分离时容易出现脏读的出现,如果要避免脏读应该读写都加锁
  • 当锁的方法抛出异常后,会释放锁,一定要注意。业务代码可以在捕获异常后回滚数据库
  • volatile关键字可以保证各线程使用的变量被一个线程修改后,及时通知其他线程reload主内存的数据来获取最新数据,被修饰的变量在线程之间是可见的透明的
  • volatile不能保证修改变量时不会发生的不一致问题,比如++操作并不是原子性的所以会出现不一致的问题,synchronize可以避免。如果变量时基本数据类型时可以考虑使用Atomic***来操作数据,但依然不能保证多个方法连续调用是原子性的
  • synchronize优化,同步代码块中语句越少并发效果越好
  • 当synchronize锁定o对象,如果o变成另一个对象,锁的对象会发生改变,此时在等待的其他线程会得到同步代码块的执行机会。所以应当避免锁对象的改变
  • 使用synchronize关键字时不要使用字符串对象作为锁对象,因为字符串有缓冲池概念
  • wait()方法等待时会释放锁,notify()方法唤醒其他线程但是不会释放锁
  • 门闩CountDownLatch latch = new CountDownLatch(1);,使用latch.countDown();来把计数器-1,当计数器为0时,latch.await();后面的代码就会得到执行