Java学习之==> 多线程

末鹿安然 提交于 2020-01-07 14:13:20

一、创建线程的三种方式

第一种

public class App {

  public static void main(String[] args) {

    Thread thread = new Thread(() -> {
      while (true) {
        System.out.println("testThread");
      }
    });

    thread.start();
  }
}
new Thread 创建线程

第二种

public class App {

  public static void main(String[] args) {

    Thread thread = new Thread(new Runnable() {
      @Override
      public void run() {
        while (true) {
          System.out.println("testThread");
        }
      }
    });

    thread.start();
  }
}
new Thread 创建线程

第二种和第一种本质上是同一种方法,只不过第一种方法用的是 lambda表达式的写法。

第三种

public class App extends Thread{

  public static void main(String[] args) {

    App app = new App();
    app.run();
  }

  @Override
  public void run() {
    System.out.println("testThread");
  }
}
继承Thread重写run()方法

二、synchronized关键字与锁

  为了解决多个线程修改同一数据而发生数据覆盖或丢失的问题,Java提供了synchronized关键字来加保护伞,以保证数据的安全。synchronized关键字对共享数据的保护有两种方式,一种是用于修饰代码块,一种是用于修饰方法。 

1、修饰代码块

  修饰代码块是把线程体内执行的方法中会涉及到修改共享数据时的操作,通过{}封装起来,然后用synchronized关键字修饰这个代码块。我们先来看一下修饰方法和代码块的情况下的线程安全问题:

public class App {

  private int i;

  public static void main(String[] args) {

    App app = new App();

    new Thread(() -> {
      app.produce();
    }).start();

    new Thread(() -> {
      app.consume();
    }).start();

  }

  public void produce() {
    while (i < 5) {
      i++;
      System.out.println("produce = " + i);
    }

  }

  public void consume() {
    while (i > 0) {
      i--;
      System.out.println("consume = " + i);
    }
  }
}

// 输出结果
produce = 1
consume = 0
produce = 1
produce = 1
consume = 0
consume = 1
consume = 0
produce = 2
produce = 1
produce = 2
produce = 3
produce = 4
produce = 5
没加synchronized关键字时线程安全问题

从输出结果来看,显然两个线程对变量 i 的操作出现了混乱,如果我们使用 synchronized 关键字来修饰,是不是问题就能解决呢?我们来看看下面这段代码:

public class App {

  private int i;

  public static void main(String[] args) {

    App app = new App();

    new Thread(() -> {
      app.produce();
    }).start();

    new Thread(() -> {
      app.consume();
    }).start();

  }

  public void produce() {
    synchronized (this){
      while (i < 5) {
        i++;
        System.out.println("produce = " + i);
      }
    }
  }

  public void consume() {
    synchronized (this){
      while (i > 0) {
        i--;
        System.out.println("consume = " + i);
      }
    }
  }
}

// 输出结果
produce = 1
produce = 2
produce = 3
produce = 4
produce = 5
consume = 4
consume = 3
consume = 2
consume = 1
consume = 0
synchronized关键字解决线程安全问题

从测试结果来看,增加 synchronized 关键字后,数据没有错乱了,下面来分析一下为什么没有加 synchronized 关键字时会出现线程安全问题,先来看下面这个图:

  我们知道,类的信息是存储在运行时数据区的方法区,而类属性(全局变量)自然也是存储在运行时数据区的方法区,属于线程共享的区域。我们使用两个线程同时执行 produce 和 consume 方法时,会在栈中复制一个 i 的副本并存储在栈中,两个线程在各自的栈中对 i 进行了操作并且更改了 i 的值(此时两个线程栈中 i 的值是不一样的),这时两个线程分别把栈中 i 的值刷回给主内存中,这时就造成了 i 的值的混乱。

  加上 synchronized 关键字后,对 i 的操作再主内存中进行,两个线程竞争锁资源,竞争到锁资源的线程对 i 进行操作,没竞争到锁资源的线程进入 BLOCKED 状态等待其他线程释放锁后再重新竞争,这样 i 的值就不会错乱。值得注意的是:synchronized 关键字的锁对象应该是同一个对象,否则,会有问题。

public class App {

  private static int i;

  public static void main(String[] args) {

    new Thread(()->{
      new App().produce();
    }).start();

    new Thread(()->{
      new App().consume();
    }).start();
  }

  public void produce() {
    synchronized (this){
      while (i < 5) {
        i++;
        System.out.println("produce = " + i);
      }
    }
  }

  public void consume() {
    synchronized (this){
      while (i > 0) {
        i--;
        System.out.println("consume = " + i);
      }
    }
  }
}

// 输出结果
produce = 1
produce = 2
consume = 1
consume = 1
consume = 0
produce = 2
produce = 1
produce = 2
produce = 3
produce = 4
produce = 5
synchronized锁对象不同

  从以上代码上看,虽然 synchronized 关键字的锁对象都是 this,但执行 produce() 和 consume() 方法使用的是两个不同的对象,即它的锁对象不是同一个对象,虽然 i 定义为了静态变量,操作的是同一个 i ,但是又出现了线程安全问题,这时,我们需要把 synchronized 关键字的锁对象改为 App.class,如下:

public class App {

  private static int i;

  public static void main(String[] args) {

    new Thread(()->{
      new App().produce();
    }).start();

    new Thread(()->{
      new App().consume();
    }).start();
  }

  public void produce() {
    synchronized (App.class){
      while (i < 5) {
        i++;
        System.out.println("produce = " + i);
      }
    }
  }

  public void consume() {
    synchronized (App.class){
      while (i > 0) {
        i--;
        System.out.println("consume = " + i);
      }
    }
  }
}

// 输出结果
produce = 1
produce = 2
produce = 3
produce = 4
produce = 5
consume = 4
consume = 3
consume = 2
consume = 1
consume = 0
synchronized锁对象相同

2、修饰方法

synchronized 关键字修饰方法时,放在返回值的前面,如果该方法是静态方法,则锁对象是当前类的Class对象,如果不是静态方法,锁对象是 this

public class App {

  private static int i;

  public static void main(String[] args) {

    new Thread(()->{
      new App().produce();
    }).start();

    new Thread(()->{
      new App().consume();
    }).start();
  }

  public static synchronized void produce() {
    while (i < 5) {
      i++;
      System.out.println("produce = " + i);
    }
  }

  public static synchronized void consume() {
    while (i > 0) {
      i--;
      System.out.println("consume = " + i);
    }
  }
}

// 输出结果
produce = 1
produce = 2
produce = 3
produce = 4
produce = 5
consume = 4
consume = 3
consume = 2
consume = 1
consume = 0
synchronized 关键字修饰方法

下面我们来做一个小练习:多线程的生产者和消费者模式,两个线程循环打印 1 和 0

public class App {

  private int i;

  public static void main(String[] args) {

    App app = new App();

    new Thread(app::produce).start();
    new Thread(app::consume).start();
  }

  public synchronized void produce() {
    while (true) {
      if (i < 5) {
        i++;
        System.out.println("produce = " + i);
        this.notify();
      }
      try {
        this.wait();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }

  public synchronized void consume() {
    while (true) {
      if (i > 0) {
        i--;
        System.out.println("consume = " + i);
        this.notify();
      }
      try {
        this.wait();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }
}
synchronized修饰实例方法
public class App {

  private static int i;

  public static void main(String[] args) {

    new Thread(()->{
      new App().produce();
    }).start();

    new Thread(()->{
      new App().consume();
    }).start();

  }

  public static synchronized void produce() {
    while (true) {
      if (i < 5) {
        i++;
        System.out.println("produce = " + i);
        App.class.notify();
      }
      try {
        App.class.wait();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }

  public static synchronized void consume() {
    while (true) {
      if (i > 0) {
        i--;
        System.out.println("consume = " + i);
        App.class.notify();
      }
      try {
        App.class.wait();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }
}
synchronized修饰静态方法

三、Lock接口

  在Lock接口出现之前,Java程序是靠 synchronized 关键字实现锁功能的。JDK1.5之后并发包中新增了Lock接口以及相关实现类来实现锁功能。虽然synchronized方法和语句的范围机制使得使用监视器锁更容易编程,并且有助于避免涉及锁的许多常见编程错误,但是有时我们需要以更灵活的方式处理锁。例如,用于遍历并发访问的数据结构的一些算法需要使用“手动”或“链锁定”:您获取节点A的锁定,然后获取节点B,然后释放A并获取C,然后释放B并获得D等。在这种场景中synchronized关键字就不那么容易实现了,使用Lock接口容易很多。

synchronized 关键字与 Lock 的区别:

  • Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;
  • Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

synchronized 的局限性与 Lock 的优点

  如果一个代码块被synchronized关键字修饰,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待直至占有锁的线程释放锁。事实上,占有锁的线程释放锁一般会是以下三种情况之一:

  • 占有锁的线程执行完了该代码块,然后释放对锁的占有;
  • 占有锁线程执行发生异常,此时JVM会让线程自动释放锁;
  • 占有锁线程进入 WAITING 状态从而释放锁,例如在该线程中调用wait()方法等。

那我们试图考虑以下三种情况:

  • 在使用synchronized关键字的情形下,假如占有锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,那么其他线程就只能一直等待,别无他法。这会极大影响程序执行效率。因此,就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间 (解决方案:tryLock(long time, TimeUnit unit)) 或者 能够响应中断 (解决方案:lockInterruptibly())),这种情况可以通过 Lock 解决;
  • 我们知道,当多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作也会发生冲突现象,但是读操作和读操作不会发生冲突现象。但是如果采用synchronized关键字实现同步的话,就会导致一个问题,即当多个线程都只是进行读操作时,也只有一个线程在可以进行读操作,其他线程只能等待锁的释放而无法进行读操作。因此,需要一种机制来使得当多个线程都只是进行读操作时,线程之间不会发生冲突。同样地,Lock也可以解决这种情况 (解决方案:ReentrantReadWriteLock) ;
  • 我们可以通过Lock得知线程有没有成功获取到锁 (解决方案:ReentrantLock) ,但这个是synchronized无法办到的。

上面提到的三种情形,我们都可以通过Lock来解决,但 synchronized 关键字却无能为力。事实上,Lock 是 java.util.concurrent.locks包 下的接口,Lock 实现提供了比 synchronized 关键字 更广泛的锁操作,它能以更优雅的方式处理线程同步问题。也就是说,Lock提供了比synchronized更多的功能。

Lock接口的简单使用

public class App {

  private int i;

  private Lock locker = new ReentrantLock();

  public static void main(String[] args) {
    App app = new App();

    new Thread(app::produce).start();
    new Thread(app::consume).start();
  }

  public void produce() {
    locker.lock();
    try {
      while (i < 5) {
        i++;
        System.out.println("produce = " + i);
      }
    } finally {
      locker.unlock();
    }
  }

  public void consume() {
    locker.lock();
    try {
      while (i > 0) {
        i--;
        System.out.println("consume = " + i);
      }
    } finally {
      locker.unlock();
    }
  }
}
使用Lock保证线程安全

为了防止 while 代码块中的代码执行报错而导致的Lock锁不释放,我们应该把 while 代码块放入 try...finally 中,finally中放入 lock.unlock(),无论 while 代码块执行是否报错都会释放锁。

使用Condition实现等待/通知机制

  synchronized关键字与wait()和notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。

  在使用notify/notifyAll()方法进行通知时,被通知的线程是由JVM选择的,使用ReentrantLock类结合Condition实例可以实现“选择性通知”,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。

public class App {

  private int i;

  private Lock locker = new ReentrantLock();
  private Condition produceCondition = locker.newCondition();
  private Condition consumeCondition = locker.newCondition();

  public static void main(String[] args) {
    App app = new App();

    new Thread(app::produce).start();
    new Thread(app::consume).start();
  }

  public void produce() {
    locker.lock();
    try {
      while (true) {
        while (i < 5) {
          i++;
          System.out.println("produce = " + i);
          consumeCondition.signalAll();
        }
        try {
          produceCondition.await();
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    } finally {
      locker.unlock();
    }
  }

  public void consume() {
    locker.lock();
    try {
      while (true) {
        while (i > 0) {
          i--;
          System.out.println("consume = " + i);
          produceCondition.signalAll();
        }
        try {
          consumeCondition.await();
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    } finally {
      locker.unlock();
    }
  }
}
使用Condition实现等待/通知机制

四、线程池

  Java中频繁的创建和销毁线程是非常消耗资源的,为了减少资源的开销,提高系统性能,Java 提供了线程池。Java 中创建线程池主要有以下两种方式:

  • Executors
  • ThreadPoolExecutor

Executors

public class App {

  public static void main(String[] args) {

    ExecutorService executors = Executors.newFixedThreadPool(5);

    Runnable runnable = () -> {
      System.out.println("testThread...");
    };

    for (int i = 0; i < 5; i++) {
      executors.submit(runnable);
    }
    executors.shutdown();
  }
}
使用Executors创建线程池

我们不建议使用这种方式创建线程池,先来看下以下这段代码:

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }


public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }

@Native public static final int   MAX_VALUE = 0x7fffffff;

使用 Executors.newFixedThreadPool() 来创建线程池,调用的是 ThreadPoolExecutor() 类的构造方法,其中有个参数是 new LinkedBlockingQueue<Runnable>() 队列,允许的请求队列长度为 Integer.MAX_VALUE,如果短时间接收的请求太多的话,队列中可能会堆积太多请求,最终导致内存溢出。

ThreadPoolExecutor

正确的创建线程池的方式如下:

public class App {

  public static void main(String[] args) {

    ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
            .setNameFormat("demo-pool-%d").build();
    ExecutorService executors = new ThreadPoolExecutor(
            5,
            200,
            10,
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(1024),
            namedThreadFactory,
            new ThreadPoolExecutor.AbortPolicy());

    Runnable runnable = () -> {
      System.out.println("testThread...");
    };

    for (int i = 0; i < 5; i++) {
      executors.submit(runnable);
    }
    executors.shutdown();
  }
}

指定线程的名称、核心线程数、最大线程数,阻塞队列长度以及拒绝策略,拒绝策略的意思是队列中的请求超过设定的值时,我们该如何操作,上面这段代码中的 new ThreadPoolExecutor.AbortPolicy() 代表拒绝,如果对于超出阻塞队列长度的请求不想拒绝的话,可以把请求存入其他介质,比如 redis 当中。我们先来看一下AbortPolicy的实现:

    public static class AbortPolicy implements RejectedExecutionHandler {
        /**
         * Creates an {@code AbortPolicy}.
         */
        public AbortPolicy() { }

        /**
         * Always throws RejectedExecutionException.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         * @throws RejectedExecutionException always
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
        }
    }
AbortPolicy

AbortPolicy 类实现自 RejectedExecutionHandler 接口,如果我们想要用其他的处理方式来处理超出队列长度的请求,可以自己来写一个策略实现 RejectedExecutionHandler接口的 rejectedExecution()方法。

五、JUC

  JUC就是 java.util .concurrent 工具包的简称,这是一个处理线程的工具包,这里我们挑选几个工具来介绍一下。

倒数计数器

  使用多线程来执行任务,如果需要等到所有线程执行完再来统计结果,这时可以用 countDownLatch.countDown()来倒数,countDownLatch.countDown() 设置的数字一般和线程数一致,如果不是所有线程都执行完了,则会使用 countDownLatch.await()进行等待,也可以设置超时时间结束等待,下面我们举个例子来看一下:

public class App{

  static ThreadFactory threadFactory = new ThreadFactoryBuilder()
          .setNameFormat("demo-pool-%d")
          .setDaemon(true)
          .build();

  public static void main(String[] args) throws InterruptedException, ExecutionException {

    // 创建线程池
    ExecutorService executorService = new ThreadPoolExecutor(
            5,
            200,
            5,
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(1024),
            threadFactory,
            new ThreadPoolExecutor.AbortPolicy());

    // 设置倒数计数器
    CountDownLatch countDownLatch = new CountDownLatch(11);

    int sum = 0;

    for (int i = 0; i < 10; i++) {

      Future<Integer> submit = executorService.submit(()->{
        try {
          return Integer.valueOf(RandomStringUtils.randomNumeric(3));
        }catch (Exception e){
          e.printStackTrace();
          return -1;
        }finally {
          countDownLatch.countDown();
        }
      });
      sum += submit.get();
    }

    // 如果不想一直等下去,则可设置超时时间
    countDownLatch.await(2, TimeUnit.SECONDS);

    // 持续等待
    // countDownLatch.await();

    System.out.println("sum = " + sum);

    executorService.shutdown();
  }
}
CountDownLatch

上面这个例子,每个线程都会返回一个随机数,目的是将这10个线程返回的随机数相加,所以就必须等到这10个线程都返回结果后才能进行计算,所以用 countDownLatch.countDown(11)进行倒数,又因为它设置的数字为 11,比线程数还多一个,所以在10个线程都返回结果后不会立即打印结果,而是等待 countDownLatch.await(2, TimeUnit.SECONDS) 中设置的超时时间再打印结果。

流控器

  流控器的作用是对线程的使用进行限流,比如:线程池的最大线程数为 10,程序中要启动 10个线程来执行任务,如果我不想线程池中的线程全部被用完,想要保留几个线程作为其他任务使用,这时我们可以对线程进行限流,使用Semaphore,如下:

public class App{

  static ThreadFactory threadFactory = new ThreadFactoryBuilder()
          .setNameFormat("demo-pool-%d")
          .setDaemon(true)
          .build();

  public static void main(String[] args) throws InterruptedException {

    // 创建线程池
    ExecutorService executorService = new ThreadPoolExecutor(
            5,
            200,
            5,
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(1024),
            threadFactory,
            new ThreadPoolExecutor.AbortPolicy());

    // 设置倒数计数器
    CountDownLatch countDownLatch = new CountDownLatch(11);

    // 设置流量控制器/信号量
    Semaphore semaphore = new Semaphore(2);

    for (int i = 0; i < 10; i++) {
      executorService.submit(() -> {
        try {
          semaphore.acquire();
          System.out.println("Hello,world...");
          Thread.sleep(2000);
        } catch (InterruptedException e) {
          e.printStackTrace();
        } finally {
          countDownLatch.countDown();
          semaphore.release();
        }
      });
    }
    countDownLatch.await();
    executorService.shutdown();
  }
}
Semaphore

值得注意的是:semaphore.acquire() 应该放在入口,程序最后都要使用 semaphore.release() 将线程进行释放,否则其他线程也进不来,为了保证每次线程执行完都会被释放,semaphore.release() 应该放在 finally 代码块中,无论 try 代码块中的代码是否报错,线程都会被释放。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!