多线程编程技巧

  1. 线程 操作 资源类
  2. 操作里面分为三步:判断 干活 通知
  3. 当多线程交互(多线程通信)的情况下,为了防止虚假唤醒的情况,使用wait()方法时,一定要使用while进行判断,而不是if。 ```java As in the one argument version, interrupts and spurious wakeups are possible, and this method should always be used in a loop:

synchronized (obj) { while () obj.wait(); … // Perform action appropriate to condition }

  1. 4. `JUC`中使用`Lock``Condition`,替代了`synchronized`,`wait()`,`notify()`等方法,`Lock``Condition`联合使用可以实现精准唤醒
  2. ```java
  3. public synchronized void increment(){
  4. //防止虚假唤醒
  5. while( num == 1){
  6. try {
  7. this.wait();
  8. } catch (InterruptedException e) {
  9. e.printStackTrace();
  10. }
  11. }
  12. System.out.println("操作者:"+Thread.currentThread().getName() + "\t当前num = " + ++num);
  13. this.notifyAll();
  14. }
  15. public synchronized void decrement(){
  16. while( num == 0){
  17. try {
  18. this.wait();
  19. } catch (InterruptedException e) {
  20. e.printStackTrace();
  21. }
  22. }
  23. System.out.println("操作者:"+Thread.currentThread().getName() + "\t当前num = " + --num);
  24. this.notifyAll();
  25. }
  1. public void increment(){
  2. //加锁
  3. lock.lock(); // block until condition holds
  4. try {
  5. // ... method body
  6. //判断
  7. //防止虚假唤醒
  8. while (num == 1){
  9. condition.await();
  10. }
  11. //干活
  12. System.out.println("操作者:"+Thread.currentThread().getName()+" 当前num:" + num++ + " 执行后num:" + num);
  13. //唤醒
  14. condition.signalAll();
  15. } catch (InterruptedException e) {
  16. e.printStackTrace();
  17. } finally {
  18. //释放锁
  19. lock.unlock();
  20. }
  21. //干活
  22. //唤醒
  23. }
  24. public void decrement(){
  25. //加锁
  26. lock.lock(); // block until condition holds
  27. try {
  28. // ... method body
  29. //判断
  30. while(num == 0){
  31. condition.await();
  32. }
  33. //干活
  34. System.out.println("操作者:"+Thread.currentThread().getName()+" 当前num:" + num-- + " 执行后num:" + num);
  35. //唤醒
  36. condition.signalAll();
  37. } catch (InterruptedException e) {
  38. e.printStackTrace();
  39. } finally {
  40. lock.unlock(); //释放锁
  41. }
  42. }
  1. 8锁问题的实质是锁对象问题
    • synchronized修饰静态方法或class文件,是类锁,当前资源类的所有对象共用一把锁
    • synchronized修饰普通方法和常量,是对象锁
  2. ArrayList/Set/Map线程不安全
    1. List<String> list = new ArrayList<>();
    2. for (int i = 0; i < 3; i++) {
    3. new Thread(() -> {
    4. //method operation...
    5. list.add(UUID.randomUUID().toString().substring(0,8));
    6. System.out.println(list);
    7. },String.valueOf(i)).start();
    8. }
  • 故障现象

出现异常:java.util.ConcurrentModificationException

  • 故障原因

当多个线程同时对ArrayList进行读写操作时,可能结果错误

  • 解决办法
    • List集合可以将ArrayList替换成Vector不推荐
    • 使用Collections._synchronizedList_(List)线程安全包装类
    • 使用CopyOnWriteArrayList/CopyOnWriteArraySet,写时复制技术一种读写分离的思想
    • 如果是Map集合,使用ConcurrentHashMap

写时复制技术
当多个线程对一个资源进行访问(读操作)时,如果某个线程需要对当期资源进行修改(写操作),系统会将当前资源拷贝一个副本,线程(写线程)会对副本进行修改,其他线程(读线程)对当前资源的访问不变,线程(写线程)完成修改之后,将修改后的资源合并到当前资源,是一种读写分离的思想。

  1. Callable接口

    1. public class MyCallable implements Callable<T>{
    2. @Override
    3. public String call() throws Exception {
    4. return null;
    5. }
    6. }

    CallableRunnable的区别

  • Callable接口核心方法是call()方法,Runnable接口的核心方法是run()方法
  • Callable接口可以有返回值,Runnable接口没有返回值
  • Callable接口可以抛出异常,Runnable接口不能抛出异常

Callable接口需要和FutureTask结合使用,因为Thread类并没有接收Callable接口的构造方法,jdk底层使用适配器模式,通过FutureTask接收Callable参数。
Tips:同一个FutureTask只能执行一次,在FutureTask底层有一个记录Task执行次数的属性state,该属性可能包含六个状态。

  1. /**
  2. * Possible state transitions:
  3. * NEW -> COMPLETING -> NORMAL
  4. * NEW -> COMPLETING -> EXCEPTIONAL
  5. * NEW -> CANCELLED
  6. * NEW -> INTERRUPTING -> INTERRUPTED
  7. */
  8. private volatile int state;
  9. private static final int NEW = 0;
  10. private static final int COMPLETING = 1;
  11. private static final int NORMAL = 2;
  12. private static final int EXCEPTIONAL = 3;
  13. private static final int CANCELLED = 4;
  14. private static final int INTERRUPTING = 5;
  15. private static final int INTERRUPTED = 6;

因为FutureTask实现了Runnable接口,所以当Task调用run()方法之后,会调用set()方法,使用CAS方法更新state状态值,当下一次再想调用run()方法时,会检查state状态值是否为NEW。

  1. CountDownLatch& CycleBarrier

相同点

  1. 功能相似:
    • 都可以实现经过若干个线程线程之后,执行特定的功能;

不同点

  1. 构造方法不同:
    • CountDownLatch只有一个有参构造方法,需要传入一个count,当count = 0时,阻塞的线程可以执行
    • CycleBarrier有两个构造方法,一个构造方法需要传入一个count参数,count个线程等待之后程序继续执行;第二个构造方法有两个参数,一个是count参数,另一个是Runnable接口,count个线程进入等待之后由最后一个进入barrier的线程执行参数中提供的目标任务。
  2. count线程不同:
    • CountDownLatch的执行线程在执行之后正常结束,进入Terminal状态
    • CycleBarrier的执行线程在执行结束后被挂起,等待最后的线程执行目标任务 ```java CountDownLatch count = new CountDownLatch(6);

for (int i = 0; i < 6; i++) { new Thread(() -> { //method operation… System.out.println(Thread.currentThread().getName() + “ 离开”); count.countDown(); //减少计数器,当count计数器数字为0时,唤醒等待线程 },”学生:”+String.valueOf(i+1)).start(); }

count.await(); //除非计数器为0,否则等待 System.out.println(Thread.currentThread().getName() + “ 关门”);

  1. ```java
  2. CyclicBarrier barrier = new CyclicBarrier(7);
  3. for (int i = 0; i < 6; i++) {
  4. new Thread(() -> {
  5. //method operation...
  6. System.out.println("学生:"+Thread.currentThread().getName()+"离开自习室");
  7. barrier.await();
  8. },String.valueOf(i+1)).start();
  9. }
  10. barrier.await();//通过调整await()执行顺序和CountDownLatch执行效果相同
  11. System.out.println(Thread.currentThread().getName() + " 给自习室关门");
  1. CyclicBarrier barrier = new CyclicBarrier(7,()->{
  2. System.out.println("学生:"+Thread.currentThread().getName()+"给自习室关门");
  3. //最后进入barrier的线程执行
  4. });
  5. for (int i = 0; i < 7; i++) {
  6. new Thread(() -> {
  7. //method operation...
  8. System.out.println("学生:"+Thread.currentThread().getName()+"离开自习室");
  9. barrier.await();
  10. },String.valueOf(i+1)).start();
  11. }

多线程相关问题

  1. sleep()wait()区别

sleep()不会释放锁资源
wait()会释放锁资源

  1. Java线程状态

NEW, //新建状态,还没启动RUNNABLE_, //可运行(就绪状态)等待cpu时间片,BLOCKED, //阻塞状态,等待获得锁__WAITING, //等待状态,执行wait(),join(),park()方法进入,等待其他线程通知(notify())之后可以继续执
行(线程通信),不见不散型TIMEDWAITING, //即时等待,线程等待一个固定的时间,使用wait(long),join(long)进入TERMINATED_; //线程执行结束

  1. Lambda表达式

    1. 接口是函数式接口,即只含有一个抽象方法,可以使用Lambda表达式,
      1. 口诀:抄写大括号,写死右箭头,落地大括号
    2. default 关键字,jdk 1.8之后接口允许有默认实现,使用default关键字进行标注的方法可以实现方法
    3. 静态实现方法
    4. @FunctionalInterface 使用注解标注的接口,即为函数式接口,注解会检查接口内的方法时候符合函数式接口的定义规范
  2. HashMap相关知识点

    • HashMap底层是Node节点数组+单向链表+红黑树的数据结构
    • HashMap默认初始值是16,负载因子0.75,即当HashMap容量达到12时,会发生扩容
    • HashMap扩容会扩充到原来的一倍,ArrayList会扩充为原来的一半,HashMap每次扩容都是2的幂数
    • 当发生哈希冲突时,后添加的Node节点会通过尾插法添加到原链表的尾部
    • 当哈希冲突的Node节点数过多(超过8时),链表就会转化成红黑树