并发编程基础

64 阅读3分钟

并发编程的问题

并发编程的目的是让程序运行更快,但并不是线程越多越好,要考虑以下几个方面

上下文切换

  • CPU通过分配时间片的方式,来执行多线程任务,当前任务执行一个时间片后会切换到下一个任务,任务在切换时保存当前状态,再次获取到时间片后,加载保存的状态继续执行。任务从保存到加载的过程就是一次上下文切换。
  • 过多的上下文切换会影响效率,所以多线程不一定快。在并发编程过程中,要考虑到上下文切换的影响,减少上下文切换

死锁

线程t1和线程t2互相等待对方释放锁

public class LockTest {
   public static String obj1 = "obj1";
   public static String obj2 = "obj2";
   public static void main(String[] args) {
      LockA la = new LockA();
      new Thread(la).start();
      LockB lb = new LockB();
      new Thread(lb).start();
   }
}
class LockA implements Runnable{
   public void run() {
      try {
         System.out.println(new Date().toString() + " LockA 开始执行");
         while(true){
            synchronized (LockTest.obj1) {
               System.out.println(new Date().toString() + " LockA 锁住 obj1");
               Thread.sleep(3000); // 此处等待是给B能锁住机会
               synchronized (LockTest.obj2) {
                  System.out.println(new Date().toString() + " LockA 锁住 obj2");
                  Thread.sleep(60 * 1000); // 为测试,占用了就不放
               }
            }
         }
      } catch (Exception e) {
         e.printStackTrace();
      }
   }
}
class LockB implements Runnable{
   public void run() {
      try {
         System.out.println(new Date().toString() + " LockB 开始执行");
         while(true){
            synchronized (LockTest.obj2) {
               System.out.println(new Date().toString() + " LockB 锁住 obj2");
               Thread.sleep(3000); // 此处等待是给A能锁住机会
               synchronized (LockTest.obj1) {
                  System.out.println(new Date().toString() + " LockB 锁住 obj1");
                  Thread.sleep(60 * 1000); // 为测试,占用了就不放
               }
            }
         }
      } catch (Exception e) {
         e.printStackTrace();
      }
   }
}

避免死锁的常见方法

  • 避免一个线程同时获取多个锁
  • 避免一个线程在锁内同时占有多个资源,尽量保证每个锁只占用一个资源
  • 使用定时锁lock.tryLock(timeout)

资源限制

硬件资源

  • 带宽
  • 硬盘读写速度
  • CPU处理速度

软件资源

  • 数据库连接数
  • socket连接数

volatile

  • 用于声明共享变量,当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。比synchronized的使用和执行成本低,不会引起上下文的切换
  • Java线程模型确保所有线程看到的volatile变量的值是一致的

synchronized

Java中每一个对象都可以作为锁

  • 对于普通同步方法,锁的是当前实例对象
  • 对于静态同步方法,锁的是当前类的Class对象
  • 对于同步方法块,锁是synchronized括号里配准的对象

当一个线程试图访问同步代码块时,他首先必须得到锁,退出或抛出异常时必须释放锁。

原子操作

原子操作是指不可被中断的一个或一系列操作,Java通过锁和循环CAS的方式实现原子操作。

  • AtomicBoolean
  • AtomicInteger

CAS的问题

  • ABA问题,如果一个值由A,变成B,然后再变成A,CAS检查会认为值没有变化
  • 循环时间长,开销大
  • 只能保证单个共享变量的原子操作

ThreadLocal

  • 线程变量,是一个以ThreadLocal对象为键,任意对象为值的存储结构
  • 一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值
  • 通过set(T)方法来设置一个值,在当前线程下再通过get()方法获取到值
public class ThreadLocalTest {
    private static final ThreadLocal<Long> TIME_THREADLOCAL =
            new ThreadLocal<Long>() {
                @Override
                protected Long initialValue() {
                    return System.currentTimeMillis();
                }
            };

    public static final void begin() {
        TIME_THREADLOCAL.set(System.currentTimeMillis());
    }

    public static final long end() {
        return System.currentTimeMillis() - TIME_THREADLOCAL.get();
    }

    public static void main(String[] args) throws InterruptedException {
        ThreadLocalTest.begin();
        TimeUnit.SECONDS.sleep(1);
        System.out.println("Cost: " + ThreadLocalTest.end() + " mills");
    }
}