并发—多线程

简介

线程简介

  • 程序:是指令和数据的有序集合,是一个静态的概念,程序内有进程;

  • 进程(Process):是程序的一次执行过程,是一个动态的概念,是系统资源分配的单位,一个进程可有多个线程,进程内有线程;

  • 线程(Thread):线程是 CPU 调度和执行的单位,一个进程中至少有一个线程,真正执行的是线程。

线程相关概念

线程就是独立的执行路径,

在程序运行时,一定会有一个主线程,如:主线程、GC线程,

main() 方法称为主线程,是系统的入口,用于执行整个程序,

在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序不可人为干预,

对同一份资源操作时,会存在资源抢夺问题,需要引入并发控制,

线程会带来额外的开销,如:CPU 调度时间,并发控制的开销等,

每个线程在自己的工作内存交互,内存控制不当会造成数据不一致,

线程开启不一定立即执行,由 CPU 调度执行,

进程间通信的方式

数据传输、资源共享、通知事件、进程控制

每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1 把数据从用户空间拷到内核缓冲区,进程2 再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信机制。

  • 管道pipe(匿名管道):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。

  • 命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。

  • 高级管道(popen):将另一个程序当做一个新的进程在当前程序进程中启动,则它算是当前程序的子进程,这种方式我们成为高级管道方式

  • 消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

  • 共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。

  • 信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。

  • 套接字Socket:套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。

  • 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

线程间通信的方式

线程通信就是当多个线程共同操作共享的资源时,互相告知自己的状态以避免资源争夺导致的数据不一致问题。

  1. 共享内存:线程之间共享程序的公共状态,线程之间通过读-写内存中的公共状态来隐式通信。volatile共享内存。

  2. 消息传递:线程之间没有公共的状态,线程之间必须通过明确的发送信息来显示的进行通信。wait/notify等待通知方式、join方式。

  3. 管道流:用于以管道为媒介的数据传输管道。输入/输出流的形式。

创建线程的方式

继承(extends) Thread类

Thread类继承了 Object类,实现了 Runnable接口。

但是不推荐使用,为了避免面向对象编程(OOP)中的单继承局限性。

创建步骤

  1. 自定义线程类继承 Thread类

  2. 重写 run()方法,编写线程执行体

  3. 创建自定义线程对象,调用 start()方法启动线程

实现(implements) Runnable接口,无返回值

推荐使用,因为避免了单继承局限性,灵活方便,方便同一个对象被多个线程使用。

创建步骤

  1. 自定义类实现 Runnable接口

  2. 重写 run()方法,编写线程执行体

  3. 创建线程对象,创建线程对象时将自定义类的对象作为线程对象的参数,调用 start()方法启动线程

实现(implements) Callable接口,有返回值

创建步骤

  1. 自定义类实现Callable接口,Callable需要指定泛型,即call方法的返回值类型,假定为 Boolean

  2. 重写 call方法,编写执行体,有返回值

线程的生命周期

新建状态(NEW)、就绪(可运行)状态(RUNNABLE)、死亡状态(TERMINATED)、有期限等待状态(TIMED_WAITING)、无期限等待状态(WAITING )、锁阻塞状态(BLOCKED)。

线程方法

方法说明
setPrioriry(int new Priorirt)改变线程的优先级。<br>记住优先级只是提高可能,不是说高优先级的一定比低优先级的线程先获得资源。
static void sleep(long millis)让正在执行的线程休眠指定毫秒数
void join()即插队,在 B线程执行时,让 A线程插队,B线程立即停止然后等待 A线程结束才可进入就绪状态
static void yield()即重新竞争,暂停正在执行的线程,所有线程重新竞争。<br>只是重新竞争,得看CPU的调度程序。
void interrupt()中断线程,最好别使用此方法
boolean isAlive()判断线程是否处于活动状态
getState()获取线程状态
getPrioriry()获取线程的优先级

线程停止

  • 最好别使用 JDK 提供的 stop()、destroy()方法,已经废弃

  • 推荐让线程自己停止下来,即建议使用标志位,

线程休眠 sleep

  • sleep(毫秒数)可以当当前运行线程休眠指定毫秒数

  • sleep 存在 InterruptedException异常

  • sleep 结束就会从有期限等待状态变成可运行状态

  • sleep 可以模拟网络延时、倒计时等

  • sleep 不会释放锁

  • 推荐使用 TimeUnit.SECONDS.sleep(10),JUC包下的。

守护线程

  • 线程分为 用户线程 和 守护线程;

  • JVM 必须保证主线程必须执行完毕,但 JVM 不关心守护线程;

  • 主线程结束,守护线程也必须结束;但守护线程结束,主线程照旧;

  • 守护线程比如:后台记录操作日志、监控内存、垃圾回收等。

线程同步相关问题

  • 发生在多个线程操作同一个资源时,即线程并发、多线程时

  • 线程同步就相当于一种等待机制,需要 队列 + 锁,保证安全性,即数据一致性,

  • 一个线程持有锁会导致其他所有需要该锁的线程挂起

  • 多线程竞争时,加锁、释放锁会导致比较多的 上下文切换、调度延时,引起性能问题

synchronized

  • 我们对 synchronized 不可见其开始和结束,

  • synchronized 用来修饰在 方法和对象 上,

  • 被修饰的代码块称为同步语句块

public synchronized void talk(){}
​
synchronized(锁资源){}
  • synchronized修饰方法,此方法可以被继承,但是子类的方法并不是线程安全的,需要重新加上 synchronized

synchronized 锁的区分

虽然 synchronized 只能放在两个地方,但是 synchronized 可以修饰:类、方法、静态方法、引用对象。

  1. 锁一个类:锁的是类,即类模板,如果 A 抢到了类锁然后 sleep,B 调用类对象的方法不受影响。因为 B 调用的是类对象的方法,即使对这个类对象加了锁,和类锁也是不同的锁资源,然后这个类的锁资源只有一个;

  2. 锁一个非静态的方法:锁的是类对象,即调用该方法的对象,被修饰的方法称为同步方法,多个类对象是不同的锁资源;

  3. 锁一个静态的方法:锁的是类,即类锁,类模板锁,和第一种锁资源是同一个;

  4. 锁一个引用对象(即任意一个非基本类型的对象):

  • 假如你锁的引用对象能使用常量池,即 String、Integer 这种,那不管多少个对象,只有一个锁资源,就算它被放在方法里面,

  • 如果你锁的是不能使用常量池的引用对象,只要它没被放在方法区里面,即只要不是 static修饰的,就是不同的锁资源,就算它被 final 修饰;

public void method() {
    //锁类模板
    synchronized(ClassName.class) {}
}
​
public void method() {
    Student s1 = new Student();
    //锁这个类对象
    synchronized(s1) {}
}
​
//锁类对象
public synchronized void talk(){}
​
//锁类模板
public synchronized static void talk(){}

lock(锁)

  • java.util.concurrent.locks,lock接口是控制多个线程对共享资源进行访问的工具,

  • 锁提供了对共享资源的独占访问,每次只能有一个线程对 lock对象加锁,线程开始访问共享资源之前得先获得 lock对象,提供了更强大的同步机制,

  • ReentrantLock(可重入锁)类实现了 Lock,它拥有与 synchronized 相同的并发性和内存语义,ReentrantLock 更常用,因为可以显示地加锁、释放锁。

while (true) {
  try {
    lock.lock();
    
    if (ticketNums > 0) {
      try {
        Thread.sleep(100);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.println(ticketNums--);
    } else {
      break;
    }
  } finally {
    lock.unlock();
  }
}

synchronized 与 Lock 的对比

  • Lock 是显示锁(手动开启和关闭锁);而 synchronized 是隐式锁,自动释放锁;采用 synchronized 不需要用户去手动释放锁,当 synchronized方法或者 synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而 Lock 则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

  • Lock 只有代码块锁,即只能锁代码体;synchronized 可以锁类对象(即修饰方法),也可以锁代码块(即修饰对象);

  • Lock 不是 Java语言内置的,Lock 是一个类,通过这个类可以实现同步访问;synchronized 是 Java语言的关键字,因此是内置特性。

  • synchronized 是大粒度锁,lock 是细粒度锁,lock 因为是对象,所以还可分为读写锁,更灵活。所以 synchronized 适合锁小代码块使用,lock 适合在大代码块中使用。

生产者消费者问题

  • 仓库只能存放一件产品

  • 若仓库中没有产品,通知消费者等待,然后生产者生产产品然后放入仓库,并通知消费者来拿走;

  • 若仓库中有产品,则消费者拿走产品,并通知生产者继续生产;

分析

  • 若仅有 synchronized 是不足的,它只能实现 同步,但不可实现 通信

  • 所以需要 wait()、wait(long timeout)、notify()、notifyAll(),

  • 解决方法1:管程法(即使用缓冲区)

  • 解决方法2:信号灯法(即设置标志位)

简单实现

管程法

/**
 * 使用管程法解决生产者消费者问题
 *
 * @author 秋白、
 *
 */
public class TestProducerConsumer1 {
    public static void main(String[] args) {
        BufferArea bufferArea = new BufferArea();
        new Producer(bufferArea).start();
        new Consumer(bufferArea).start();
    }
​
    // 生产者
    static class Producer extends Thread {
        BufferArea bufferArea;
        public Producer(BufferArea bufferArea) {
            this.bufferArea = bufferArea;
        }
​
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                bufferArea.push(new Chicken(i));
                System.out.println("生产了第" + i + "个产品");
            }
        }
    }
​
    // 消费者
    static class Consumer extends Thread {
        BufferArea bufferArea;
        public Consumer(BufferArea bufferArea) {
            this.bufferArea = bufferArea;
        }
​
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println("消费了第" + bufferArea.pop().id + "个产品");
            }
        }
    }
​
    // 产品
    static class Chicken {
        int id;// 产品编号
        public Chicken(int id) {
            this.id = id;
        }
    }
    // 缓冲区
    static class BufferArea {
        // 容器大小
        Chicken[] chickens = new Chicken[10];
        // 容器计数器
        int count = 0;
        // 生产者放入产品
        public synchronized void push(Chicken chicken) {
            // 如果容器满了,就得等待消费者消费
            if (count == chickens.length) {
                // 通知消费者消费,生产者等待
                try {
                    this.wait();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            // 如果容器没有满,就放入产品
            chickens[count++] = chicken;
            // 通知消费者消费
            this.notifyAll();
        }
​
        // 消费者拿走产品
        public synchronized Chicken pop() {
            // 判断可否消费
            if (count == 0) {
                // 等待生产者生产,消费者等待
                try {
                    this.wait();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            // 如果可以消费,就消费产品
            // 整理--count是因为当拿到count是10时,但没有count[10]
            Chicken chicken = chickens[--count];
            // 通知生产者生产
            this.notifyAll();
            return chicken;
        }
    }
}

信号灯法

/**
 * 使用信号灯法解决生产者消费者问题
 *
 * @author 秋白、
 *
 */
public class TestProducerConsumer2 {
    public static void main(String[] args) {
        TV tv = new TV();
        new Player(tv).start();
        new Watcher(tv).start();
    }
​
    // 生产者
    static class Player extends Thread {
        TV tv;
        public Player(TV tv) {
            this.tv = tv;
        }
​
        @Override
        public void run() {
            for (int i = 0; i < 20; i++) {
                if (i % 2 == 0) {
                    tv.play("快乐大本营播放中");
                } else {
                    tv.play("抖音记录美好生活");
                }
            }
        }
    }
​
    // 消费者
    static class Watcher extends Thread {
        TV tv;
        public Watcher(TV tv) {
            this.tv = tv;
        }
        @Override
        public void run() {
            for (int i = 0; i < 20; i++) {
                tv.watch();
            }
        }
    }
​
    // 产品
    static class TV {
        // 演员表演,观众等待
        // 观众观看,演员等待
        String show;
        // flag代表是否有产品
        boolean flag = false;
        // 表演
        public synchronized void play(String show) {
            if (flag) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("演员表演了" + show);
            // 通知观众观看
            this.notifyAll();
            // 更新节目
            this.show = show;
            this.flag = !this.flag;
        }
​
        // 观看
        public synchronized void watch() {
            if (!flag) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("观众观看了" + show);
            // 通知演员表演
            this.notifyAll();
            this.flag = !this.flag;
        }
    }
}

Java 其实开启不了线程

  • 线程的 start() 方法,group.add(this) 会把当前线程加入线程组,

  • 然后将 started 标志位设为 true,即调用 start0() 方法去启动这个线程,但是 start0 是本地方法,Java 无法直接操作硬件。

public synchronized void start() {
  if (threadStatus != 0)
    throw new IllegalThreadStateException();
​
  group.add(this);
​
  boolean started = false;
  try {
    start0();
    started = true;
  } finally {
    try {
      if (!started) {
      group.threadStartFailed(this);
    }
  } catch (Throwable ignore) {
      /* do nothing. If start0 threw a Throwable then
      it will be passed up the call stack */
    }
  }
}
// 本地方法,底层的C++ ,Java 无法直接操作硬件
private native void start0();

wait 与 sleep 的区别

  • 来自不同的类

    • wait 来自 Object类

    • sleep 来自 Thread类

  • 是否会释放锁

    • wait 会释放锁

    • sleep 不会释放锁

  • 使用的范围不同

    • wait 必须在同步块中使用

    • sleep 可以在任何地方使用

  • 是否需要捕获异常

    • wait 不需要捕获异常,因为没抛出编译时异常

    • sleep 必须捕获异常

线程的三大特性

原子性(Atomicity)

原子性是指在一个任务操作中,CPU 不可以在中途暂停然后再调度,即不可被中断操作,要么执行完成,要么就不执行。原子性指的就是一个操作是不可分割,不可中断,即使有多个线程执行,一个操作开始也不会受其他线程影响。

可以理解为线程的最小执行单元,不可被分割。当然这个最小执行单元可以只是一个操作也可以是一段代码。就比如:num++ 就不是原子性操作,它分为3步:1、获取值,2、+1,3、赋值。

在线程中实现原子性的操作可以为 synchronized 修饰或通过 lock 实现。

可见性(Visibility)

可见性就是指当一个线程修改了线程共享变量的值,其它线程能够立即得知这个修改。

通过:volatile关键字、synchronized 或 lock 实现均可实现变量的可见性。

final 关键字的可见性:被 final 修饰的字段在构造前中一旦初始化完成, 并且构造器没有把“this”的引用传递出去(this引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象),那在其他线程中就能看见 final字段的值。

有序性(Ordering)

有序性即指令的顺序有序,未经过重排序。

JUC线程并发(java.util.concurrent)

即java.util.concurrent、java.util.concurrent.atomic(原子性)、java.util.concurrent.locks这三个包。

Thread 只是一个普通的线程类、Runnable 没有返回值,效率也比 Callable 低,所以Callable 用的更多,并且现在用的大多的有返回值的线程,都是基于 Callable 的。

ReentrantLock

创建公平锁或非公平锁

若 new ReentrantLock 时没有参数,就默认创建一个 非公平锁,如果传参 true,就是创建一个公平锁,false 就是创建一个非公平锁。

public ReentrantLock() {
    sync = new NonfairSync();
}
​
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

Condition

Condition 取代了对象监视器方法的使用,如同 Lock 代替 synchronized,

Condition 可以精准地通知和唤醒线程,使用同一个 Lock对象,创建多个 Condition对象去监听每一个同步块,A业务中使用 condition2.signal() 唤醒业务B,以此类推,最后业务n 使用 condition1.signal() 唤醒业务A,精准唤醒。

基本使用

创建

使用 Lock对象创建,lock.newCondition();

等待、唤醒

condition.await(); 使线程等待,代替 wait():

condition.signal(); condition.signalAll(); 唤醒等待该锁的线程,代替 notify()、notifyAll();

虚假唤醒(虚假等待)问题

等待代码块应该出现在 while 循环中,才可以防止虚假唤醒。

虚假唤醒详解:两个线程请求同一个同步块,然后同步块中使用的是 if 判断去 wait,而不是 while 判断,假如 A 进去了,然后 wait 等待;然后 B 也进去了,wait 等待。然后当条件发生改变,notifyAll 它们,因为使用的是 if,所以 A,B 都会直接去执行同步块下面的代码,然后此时线程A 已经改变了锁资源,按我们想的 B应该等待的,但是因为使用的是 if,B 已经执行过 if 代码了,所以 B 就会跟 A 一样继续执行,这就导致了多线程中的错误。

关于锁的一些问题

其实就是说,static 同步方法是锁类模板,普通同步方法是锁类对象。

两线程调用同一类对象的同步方法

  • 问题1:A、B 线程都调用同步方法,不加睡眠,两线程谁先执行?

  • 问题2:A、B 线程都调用同步方法,加上任意睡眠,两线程谁先执行?

分析

因为 synchronized 锁方法锁的是类对象,所以其实哪个线程先拿到 类对象 锁资源,谁就会先执行,不管被休眠了多久。所以就看 main方法中,哪个线程在前面,那就是它先执行。

代码与结果

  • 不管怎么睡眠,main方法中都是A线程先被定义,先拿到phone这个锁

public class Test1 {
  public static void main(String[] args) {
    Phone phone = new Phone();
    //锁的存在
    new Thread(()->{phone.sendSms();},"A").start();
​
    try {TimeUnit.SECONDS.sleep(1);} 
    catch (InterruptedException e) {e.printStackTrace();}
  
    new Thread(()->{phone.call();},"B").start();
  }
}
​
class Phone{
  // synchronized 锁的对象是方法的调用者
  // 两个方法用的是同一个锁,谁先拿到谁执行
  public synchronized void sendSms(){
    try {TimeUnit.SECONDS.sleep(4);} 
    catch (InterruptedException e) {e.printStackTrace();}
  
    System.out.println("发短信");
  }
  
  public synchronized void call(){
    System.out.println("打电话");
  }
}

同一个类对象,一线程调用同步方法,另一线程调用普通方法

  • 问题3:A线程调用同步方法,B线程调用普通方法,两线程谁先执行?

分析

A 需要获取锁资源,B 不需要,那就看代码顺序和睡眠时间长短了,假如都不睡,那一定是A 先执行。

两个类对象,两个线程调用不同类对象的同步方法

  • 问题4:A线程调用类对象1 的同步方法,B线程调用类对象2 的同步方法,两线程谁先执行?

分析

两个类对象,那么就是两份不同的锁资源,然后 A、B 使用不同的锁去调用同步方法,两者锁都不一样,所以 A、B 之间不影响,只看它们的先后顺序和睡眠时间。

两个类对象,两个线程调用不同类对象的 static 同步方法

  • 问题5:A线程调用类对象1 的 static 同步方法,B线程调用类对象2 的 static 同步方法,两线程谁先执行?

分析

因为是对 static 加锁,那么锁的就是类模板了,只有一个锁资源,那么 A、B 线程不管是通过同一个类对象还是不同的类对象去调用这个 static 同步方法,都需要获取同一个锁资源。那就是 A 先拿到锁先执行。

不安全的集合类

List 不安全

会报 ConCurrentModificationException 异常

List<String> list = Arrays.asList("1", "2", "3");  //也可以创建List
​
public class UnsafeConllection {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0, 5));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
    }
}

解决方法

COW 用的是 Lock,而 Vector 用的是 synchronized,所以 Vector 效率更低。

  • 使用Vector

List<String> list = new Vector<String>();
  • 使用 Collections 工具类,将的 new 的集合 synchronized 一下

List<String> list = Collections.synchronizedList(new ArrayList<>());
  • 使用 JUC,COW

List<String> list = new CopyOnWriteArrayList<String>();

CopyOnWrite(COW)

CopyOnWrite:要写入时复制一份,即 COW,是计算机程序设计领域的一种优化策略,读写分离思想。

多个线程调用 list 的时候,读取 list 时是固定的,但是写入的时候我们不允许一起写入,因为会产生覆盖;所以使用 COW,可以避免写入时覆盖,即多个线程如果只是读,那么读的都是原文件,如果某个线程要去修改,那么会将原文件复制一份出来给此线程去修改,此线程修改的是专用的复制文件,其他线程继续读取原文件,这个过程对其他调用者是透明的,并且原文件会加锁,即同一时刻只允许一个线程去修改,当修改完成之后,会使用这个复制文件(副本)去更新原文件。

Set 不安全

会报 ConCurrenntModificationException 异常

public class UnsafeConllection2 {
  public static void main(String[] args) {
    Set<String> set = new HashSet<>();
    for (int i = 0; i < 10; i++) {
      new Thread(() -> {
          set.add(UUID.randomUUID().toString().substring(0, 5));
          System.out.println(set);
      }, String.valueOf(i)).start();
    }
  }
}

解决方法

  • 使用 Collections 工具类,将的 new 的集合 synchronized 一下

Set<String> set = Collections.synchronizedSet(new HashSet<String>());
  • 使用 JUC,COW

Set<String> set = new CopyOnWriteArraySet<String>();

HashSet 其实就是 HashMap 的 key

我们知道 ArrayList 的底层是数组,那么 HashSet 的底层其实就是 HashMap。

public HashSet() {
  map = new HashMap<>();
}
​
// set 本质就是 map 的 key,而key是无法重复的!
public boolean add(E e) {
  return map.put(e, PRESENT)==null;
}
​
//这是一个常量,不变的值
private static final Object PRESENT = new Object();

Map不安全

resize 扩容的时候会调用 transfer方法,就会产生环状链表。

然后一定记住,链表中分清栈区和堆区,比如 e.next=newTable[i],newTable[i]=e,e=next。

  • 这是头插法,e 和 newTable[i] 都是栈区指针对象,指向堆区的 eTrue 和 nTrue,而 e.next 则是在改变堆区,因为 e 已经在栈区了,e.next 是在堆区的。第一句就是将 nTrue 的地址赋给 eTrue.next,

  • 然后第二句是将 eTrue 的地址赋给 newTable[i]。因为 newTable 是指向 nTrue 的,如果是 newTable[i].XXX,那么就是在改变堆区,但是 newTable[i] 的话就是在改变栈区,

  • 第三句话则是将 e 这个栈区指针指向下一个,使得 while循环继续进行。 JDK1.7和JDK1.8中HashMap为什么是线程不安全的?-CSDN博客

解决方法

  • 使用 Collections工具类,将的 new 的集合 synchronized 一下

Map<String, String> map = Collections.synchronizedMap(new HashMap<String, String>());
  • 使用 JUC,ConcurrentHashMap

Map<String, String> map = new ConcurrentHashMap<>();

ConcurrentHashMap

ConcurrentHashMap 是线程安全的数组,是 Hashtable 的替代品,同为线程安全,其性能要比 Hashtable 更好

ConcurrentHashMap 诞生的原因:兼顾 HashMap 和 Hashtable
  • HashMap 线程不安全:在并发环境下,可能会形成环状链表(扩容时可能造成),导致 get 操作时,cpu 空转,所以,在并发环境中使用 HashMap 是非常危险的,

  • Hashtable 是线程安全的:Hashtable 和 HashMap 的实现原理几乎一样,

  • 差别:Hashtable 不允许 key 和 value 为 null;

  • Hashtable 线程安全的策略实现代价却比较大,get/put 所有相关操作都是 synchronized 的,这相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞。

ConcurrentHashMap 的构成
  • 数据结构:Synchronized + CAS +Node +红黑树

  • Node 的 val 和 next 都用 volatile 保证,保证可见性,查找、替换、赋值操作都使用 CAS

  • 读操作无锁。

为什么在有Synchronized 的情况下还要使用CAS
  • 因为 CAS 是乐观锁,在一些场景中(并发不激烈的情况下)它比 Synchronized 和 ReentrentLock 的效率要高

  • 当 CAS 保障不了线程安全的情况下(扩容或者 hash冲突的情况下)转成 Synchronized 来保证线程安全,大大提高了低并发下的性能。

Callable

可以有返回值、可以抛出异常、call()。

使用 Callable 创建线程

使用 new Thread(new FutureTask<Integer>(myThread)).start(),这个产生的结果会被缓存,效率高,

并且即使你 new 了另一个也以 myThread 为核心的线程去跑,其内核 Callable 的 call() 方法只会跑一次,

public class CallableTest1 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
​
        FutureTask<Integer> futureTask = new FutureTask<Integer>(myThread);
​
        //结果会被缓存,效率高,
        new Thread(futureTask).start();
​
        try {
            // 这个 get 方法会阻塞拿结果
            System.out.println(futureTask.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
​
    static class MyThread implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            System.out.println("call");
            return 123;
        }
    }
}

FutureTask

  • 是 Runnable 的实现类,所以可以直接 new Thread(new FutureTask(callable对象)) 来创建线程,

  • 可以将 Callable 作为构造器的参数,这样就可以让 Callable 去创建一个线程了

  • FutureTask 的 get() 方法可以得到 Callable 的 call() 方法的返回结果,会使当前线程阻塞等待至获得结果。

三大线程辅助类(三个线程计数器)

CountDownLatch(减法计数闭锁)

  • 先 new 一个 CountDownLatch 出来,在参数里面设定要等待的线程个数,我们可以把它想象成看门者;

  • 然后线程里面的 run() 方法或 call()方法的代码块的随便一行加上 countDownLatch.countDown(); 即每个线程执行了就将计数 -1,可以想象成孩子出教室门,然后看门人在计数,看门人只关心孩子有没有跨出门框,只要孩子一踏出门框,就将人数 -1,

  • 然后使用 countDownLatch.await(); 来等待,即这个减法计数器一直阻塞等待,直到计数 =0 的时候,然后执行这个方法下面的代码。

  • 但是 CountDownLatch 不可重置计数,如果需要重置计数的版本,请考虑使用 CyclicBarrier,

  • 闭锁是让线程每执行完然后将计数 -1,并不会让线程阻塞等待计数归零,即线程就只管执行自己的,

  • countDown():数量-1,该方法不会产生阻塞;写在线程里面,

  • await():等待计数器归零,然后向下执行,会让线程阻塞等待,一般用在 main 线程中,

public class CountDownLatchTest1 {
  public static void main(String[] args) {
    // 总数为6,即从6开始计数
    CountDownLatch countDownLatch = new CountDownLatch(6);
​
    for (int i = 0; i < 6; i++) {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "出门");
            // 数量-1
            countDownLatch.countDown();
        }).start();
    }
​
    try {
        // 等待计数器为0,然后再向下执行 关门,否则 main 线程就一直阻塞等待
        countDownLatch.await();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("关门");
  }
}

CyclicBarrier(循环栅栏)(加法计数器)

  • 有两个构造器,

    • public CyclicBarrier(int parties, Runnable barrierAction)

    • public CyclicBarrier(int parties)

  • 栅栏能阻塞 一组线程 直到某个事件的发生,

  • 栅栏与闭锁的关键区别在于,所有的线程必须同时到达栅栏位置,才能继续执行。而减法计数器就是计数,然后通过 await()方法来阻塞。

  • 所有线程都到达栅栏,就是说当每个线程执行到 CyclicBarrier.await() 后,该线程就会进入阻塞,一直等待至线程个数达到 CyclicBarrier 参数中设置的数量;然后再各自执行 CyclicBarrier.await() 后面剩下的代码,

  • 然后假如 CyclicBarrier 的构造器中有 Runnable,那么线程个数到达设置数量后,执行这个 Runnable,它执行完后,那些等待的线程才可以继续执 CyclicBarrier.await() 后面的代码。

Semaphore(信号量)(并发限流器)

  • 有两个构造器,permits 就是总的信号量,即资源数,

    • public Semaphore(int permits)

    • public Semaphore(int permits, boolean fair)

  • 使用 semaphore.acquire() 去请求获取资源,如果有资源,就得到,如果没有资源,就阻塞等待至有资源。就是说 acquire()方法一定会产生等待,因为你想要去获得资源,发送这个请求和获取响应的这段时间一定会产生,

  • acquire() 方法必须在 try块中,所以 release()方法一定要记得放在 finally块中。

读写锁 ReentrantReadWriteLock

简介

  • ReadWriteLock接口,其只有一个实现类:ReentrantReadWriteLock

  • 读的时候可以被多个线程一起读,写的时候只能有一个线程去写。写的时候不允许别人读,即保持数据是最新的,但是自己可以读。读锁就是共享锁,写锁就是独占排他锁。

  • 读锁可以在没有写锁的时候被多个线程同时持有;写锁是独占的(排他的)

  • 读写锁会比 Lock、ReentrantLock 有更加细粒度的控制

实践

实践1
public class ReadWriteLockTest1 {
  public static void main(String[] args) {
      MyLockedCache myLockedCache = new MyLockedCache();
      // 写
      for (int i = 0; i < 5; i++) {
          final int temp = i;
          new Thread(() -> {
              myLockedCache.put(String.valueOf(temp), String.valueOf(temp));
          }, String.valueOf(i)).start();
      }
      // 读
      for (int i = 0; i < 5; i++) {
          final int temp = i;
          new Thread(() -> {
              myLockedCache.get(String.valueOf(temp));
          }, String.valueOf(i)).start();
      }
  }
​
  static class MyLockedCache {
    private volatile Map<String, Object> map = new HashMap<String, Object>();
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
​
    // 写
    public void put(String key, Object value) {
      readWriteLock.writeLock().lock();
      try {
          System.out.println(Thread.currentThread().getName() + "写入");
          map.put(key, value);
          System.out.println(Thread.currentThread().getName() + "写入完毕");
      } catch (Exception e) {
          e.printStackTrace();
      } finally {
          readWriteLock.writeLock().unlock();
      }
    }
    // 读
    public void get(String key) {
      readWriteLock.readLock().lock();
      try {
          System.out.println(Thread.currentThread().getName() + "读取");
          System.out.println(Thread.currentThread().getName() + "读取到:" + map.get(key) + ",读取完毕");
      } catch (Exception e) {
          e.printStackTrace();
      } finally {
          readWriteLock.readLock().unlock();
      }
    }
  }
}

BlockingQueue<E>接口(阻塞队列)

  • 一个队列,一端放入,另一端拿走,FIFO

  • 如果队列是空的,拿走端就阻塞等待放入;如果队列被放满了,放入端就会阻塞

  • 使用场景:多线程并发处理、线程池

  • BlockingQueue<E>阻塞队列接口,其父类是 Collection<E>,其子类有:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue(同步队列)。一般使用 ArrayBlockingQueue 实现类,传的参数是队列大小

四组API

  • 如果队列满了或空了,以下不同的方法会产生不同的结果

方式抛出异常有返回值且<br>不抛出异常阻塞等待<br>(一直阻塞)超时等待
添加add()offer()put()offer(E e, long timeout, TimeUnit unit)
移除remove()poll()take()poll()
获取队首元素element()peek()--

SynchronousQueue同步队列

  • 队列大小只能 =1,即放入一个元素,必须得等拿走了才能继续放,

  • 两个线程,一个只管放,另一个只管取,那么当队列为空的时候,取的那个必须得等放的先放一个,然后队列有元素时,放的那个得等取的把元素取走才能继续放,所以全都是成对出现。

线程池

池化技术,比如说:线程池、数据库连接池、内存池、对象池。池化技术:事先准备好指定数量的资源,便于使用、复用和管理,降低资源消耗,提高响应速度。因为不停地 创建、销毁十分浪费资源。

经常创建和销毁线程,使用量大的资源且高并发的情况下的线程,对性能影响很大。使用线程池的话,就可以减少创建线程、销毁线程、调度线程等的开销,便于线程管理、提高响应速度、降低资源消耗;

ExecutorService、Executors 是一些线程工具类,线程池的工厂类,可以用于创建不同类型的线程池。

Executors工具类(不推荐使用)

线程池不推荐使用 Executors 去创建,而是推荐通过 ThreadPoolExecutor 的方式。因为这样的处理方式可以更明确线程池的运行规则,并且规避资源耗尽的风险。比如:

  1. FixedThreadPool 和 SingleThreadPool:允许请求队列的长度为 Integer.MAX_VALUE(约为21亿),可能会堆积大量请求,从而导致 JVM 的 OOM,

  2. CacheThreadPool 和 ScheduleThreadPool:允许创建线程的数度为 Integer.MAX_VALUE(约为21亿),可能会创建大量的线程,从而导致 JVM 的 OOM。

使用 Executors 的弊端

  • 不一定适合当前业务

  • 线程名字无法控制

  • 七大参数有 6个都无法修改控制

Executors 创建四种线程池

指定的参数都是核心线程数大小。

// 单个线程的线程池
ExecutorService threadPool = Executors.newSingleThreadExecutor();
// 固定线程数量的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(3);
// 可伸缩数量的线程池,会根据线程量需求量自动伸缩
ExecutorService threadPool = Executors.newCachedThreadPool();
// 定时任务线程池,延迟执行或周期循环执行
ExecutorService threadPool = Executors.newScheduledThreadPool(2);
​
try {
  for (int i = 0; i < 100; i++) {
    threadPool.execute(() -> {
      System.out.println(Thread.currentThread().getName() + " OK");
    });
  }
} catch (Exception e) {e.printStackTrace();}
finally {threadPool.shutdown();}

ThreadPoolExecutor(推荐使用的线程池)

简介

Executors 的几个创建线程池的方法的底层其实都是 new ThreadPoolExecutor。

拒绝策略推荐还是自己实现,比如去使用 消息中间件,防止遗漏。

推荐使用有界队列,并且合理长度。无界队列的话有可能全装满了然后出错。

  • 线程池的运行规则:

    • 核心线程池大小 < 需要的线程数量 <= 核心线程池大小+阻塞队列大小,就让没拿到的先进入阻塞队列;

    • 核心线程池大小+阻塞队列大小 < 需要的线程数量 <= 最大核心线程池大小+阻塞队列大小,就开启最大核心线程池大小,还不够就进入阻塞队列;

    • 如果 最大核心线程池大小+阻塞队列大小 也不够,就实施拒绝策略

public ThreadPoolExecutor(
    // 核心线程数
    int corePoolSize,
    // 最大线程数
    int maximumPoolSize,
    // 最大保留(空闲)时间。使用最大线程数后若一直无人调用,就释放掉
    // 核心线程数的超时时间可另外自己去设置
    long keepAliveTime,
    // 时间单位
    TimeUnit unit,
    // 阻塞队列
    BlockingQueue<Runnable> workQueue,
    // 线程工厂。方便给线程指定名称
    ThreadFactory threadFactory,
    // 拒绝策略
    RejectedExecutionHandler handler)
ThreadPoolExecutor 提供的四种拒绝策略
//满了后,抛出异常。即当最大线程池大小+阻塞队列大小 < 线程连接请求时,抛出异常
new ThreadPoolExecutor.AbortPolicy()
​
//满了后,线程池不受理,退回给使用线程池的线程去处理线程请求里面的线程业务。哪来的回哪里
//即通过我写的就是,线程池不受理这个请求,然后main线程去处理线程业务,输出了main OK
new ThreadPoolExecutor.CallerRunsPolicy()
​
//满了后,丢弃任务,且不抛出异常。把线程的任务直接不要了,然后正常执行
new ThreadPoolExecutor.DiscardPolicy()
​
//满了后,尝试和最早的线程去竞争,且不会抛出异常。竞争成功就执行,竞争失败就被丢弃
new ThreadPoolExecutor.DiscardOldestPolicy()
线程池状态变化

实践

实践1
public static void main(String[] args) {
  //自定义线程池
  ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
      2, 5, 3,
      TimeUnit.SECONDS, new LinkedBlockingDeque<>(3),
      Executors.defaultThreadFactory(),
      new ThreadPoolExecutor.AbortPolicy());
  
  try {
    for (int i = 0; i < 7; i++) {
        threadPoolExecutor.execute(() -> {
            System.out.println(Thread.currentThread().getName() + " OK");
        });
    }
  } catch (Exception e) {
      e.printStackTrace();
  } finally {
      threadPoolExecutor.shutdown();
  }
}
实践2
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2, 4,
    10, TimeUnit.MILLISECONDS,
    new ArrayBlockingQueue<>(2),
    new ThreadFactory() {
        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r);
            t.setName("abc");
            return t;
        }
    },
    new ThreadPoolExecutor.AbortPolicy());
        
executor.execute(() -> {
  for (int i = 0; i < 10; i++) {
      System.out.println(i);
  }
});

线程池调优

如何设置定义 最大核心线程大小

要学会根据业务去分辨,线程池业务是 CPU 使用得多还 IO 使用得多。分为 CPU密集型和 IO密集型。CPU 的话,一般电脑的 CPU 是几核就定义为几。核心线程数要能满足平时的一般需求量,最大则是满足最高需求量。

// 获取CPU的核数
Runtime.getRuntime().availableProcessors();

Future 及相关异步回调

  • Future 设计的初衷:对将来的某个事件的结果进行建模

  • Future 的间接实现类:CompletableFuture、ForkJoinTask、FutureTask、RecursiveAction、RecursiveTask。

  • 异步调用就像服务器与客户端用的 Ajax 一样,有:异步执行、成功回调、异步回调。

ForkJoin(分支合并)

使用 ForkJoin 可以并行执行任务,提高效率,并且维护的队列是双端队列。

ForkJoinPool 与 ThreadPoolExecutor 一样,都是 Executor 的间接实现类,

核心思想就是任务拆解,可以把大任务拆分成小任务,如果子任务还大,就继续拆,然后拆成一颗树的样子,执行完结果后,再进行分支合并,一步步合成最终结果。

ForkJoin的特点:工作窃取。

* 工作窃取:A线程有 4个任务,此时刚执行到第2个,B线程有 5个任务,都执行完了,那么 B线程就会把 A线程剩下的两个没有执行过的任务拿过来一个去执行,提高效率。但是工作窃取会有弊端,可能会发生线程争抢任务的情况。

实践

实践1

通过 ForkJoinPool 的:forkjoinPool.execute(ForkJoinTask task) 来同步执行计算任务,那么我们就需要 ForkJoinTask 子对象。其实 ForkJoinPool 还有一个 submit 方法,这个方法有很多重载方法,可以异步执行并提交计算任务,

注意:execute 是直接执行,submit 是执行再提交;execute 是同步的,submit 是异步的;execute 没有返回值,submit 有返回值,返回值为 ForkJoinTask<T>,再通过返回值类型的 get()方法获取真正的返回值,get()方法会阻塞等待任务执行完,

而 ForkJoinTask 有三个实现类:CountedCompleter(技术完成者)、RecursiveAction(递归事件,没有返回值)、RecursiveTask(递归任务,有返回值),一般使用RecursiveTask:

  1. 自定义类 extends 继承 RecursiveTask<V>,重写其:V compute()方法,在方法里面进行任务拆分,

  2. 自定义类的对象创建对象,使其成为一个被拆分的子任务,然后该对象调用 fork()方法:将此任务,在当前任务正在运行的池中异步执行此任务,

  3. 然后你可以通过 idDone() 来判断是否已完成任务;还有 join()方法:当任务完成了,就会返回这个任务的计算结果

  4. 写完递归任务后,在方法里面 new ForkJoinPool,然后再 new 自定义的任务类对象,让forkJoinPool 去 execute 或 submit 这个任务,用 submit 的话可以获得自定义类对象的返回值。

public class ForkJoinTest1 extends RecursiveTask<Long> {
    private static final long serialVersionUID = 1L;
    private Long start; // 1
    private Long end; // 1990900000
    // 临界值
    private Long temp = 10000L;
    public ForkJoinTest1(Long start, Long end) {
        this.start = start;
        this.end = end;
    }
    @Override
    protected Long compute() {
        // 如果任务量不大,就不去使用ForkJoin
        if ((end - start) < temp) {
            Long sum = 0L;
            for (Long i = start; i <= end; i++) {
                sum += i;
            }
            System.out.println(sum);
            return sum;
        } else {
            // 中间值
            long middle = (start + end) / 2;
            // 拆分任务,我们这里拆分成了两个子任务
            ForkJoinTest1 task1 = new ForkJoinTest1(start, middle);
            // 把子任务压入线程队列
            task1.fork();
            ForkJoinTest1 task2 = new ForkJoinTest1(middle + 1, end);
            task2.fork();
            return task1.join() + task2.join();
        }
    }
​
    public static void main(String[] args) {
//        test1();// 15722ms
//        test2();// 9646ms
        test3();// 196ms
    }
​
    // 普通程序员,15722
    public static void test1() {
        Long sum = 0L;
        long start = System.currentTimeMillis();
        for (Long i = 1L; i <= 10_0000_0000; i++) {
            sum += i;
        }
        long end = System.currentTimeMillis();
        System.out.println("sum=" + sum + " 时间:" + (end - start));
    }
​
    // 使用ForkJoin
    public static void test2() {
        long start = System.currentTimeMillis();
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        ForkJoinTask<Long> task = new ForkJoinTest1(0L, 10_0000_0000L);
//        forkJoinPool.execute(task);// 直接执行任务,该方法没有返回值
        ForkJoinTask<Long> submit = forkJoinPool.submit(task);// 提交任务
        Long sum = null;
        try {
            sum = submit.get();
        } catch (Exception e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("sum=" + sum + " 时间:" + (end - start));
    }
​
    // Stream并行流
    public static void test3() {
        long start = System.currentTimeMillis();
        long sum = LongStream.rangeClosed(0L, 10_0000_0000L).parallel().reduce(0, Long::sum);
        long end = System.currentTimeMillis();
        System.out.println("sum=" + sum + "时间:" + (end - start));
    }
}

CompletableFuture

简介

supplyAsync 表示创建带返回值的异步任务的,相当于 ExecutorService submit(Callable<T> task) 方法,

runAsync 表示创建无返回值的异步任务,相当于 ExecutorService submit(Runnable task)方法,这两方法的效果跟 submit 是一样的。

  • 得到 CompletableFuture 的结果有两个方法,get() 和 join() 可以得到异步执行请求的返回结果,

  • 使用 completableFuture.whenComplete 这个方法来定义如果异步执行请求成功执行,会怎么样,方法里面要给的是 BiConsumer 函数式接口,里面的第一个参数t 是异步执行请求正常的返回结果,然后第二个参数u 是错误信息,

  • 然后在后面 .exceptionally 这个方法定义如果失败会怎么样,方法里面要给的是 Function函数式接口;

  • 最后 .get()类获取真正执行时的返回结果,如果正确执行,就 get 到异步执行请求的 return结果,如果发生错误,就 get 到 exceptionally 里面的 return结果

实践

实践1
public class FutureTest1 {
    public static void main(String[] args) {
//        CompletableFuture<Void> completableFuture1 = 
//          CompletableFuture.runAsync(() -> {
//            try {
//                TimeUnit.SECONDS.sleep(2);
//            } catch (Exception e) {
//                e.printStackTrace();
//            }
//            System.out.println(Thread.currentThread().getName() + "runAsync");
//        });
//        System.out.println("CompletableFuture1");
//        try {
//            System.out.println(completableFuture1.get());
//        } catch (Exception e) {
//            e.printStackTrace();
//        }
      
​
      CompletableFuture<Integer> completableFuture2 = 
        CompletableFuture.supplyAsync(() -> {
          System.out.println(Thread.currentThread().getName() + "supplyAsync");
          //int i = 10 / 0;
          return 1024;
      });
      try {
        System.out.println(
          completableFuture2.whenComplete((t, u) -> System.out.println("t:" + t + ",u:" + u))
            .exceptionally(e -> {
                System.out.println(e.getMessage());
                return 2333;
            }).get());
      } catch (Exception e) {
          e.printStackTrace();
      }
  }
}
实践2
@Override
public List<String> getResult() {
  // 有返回值的异步任务线程集合
  List<CompletableFuture<String>> completableFutureList = Lists.newArrayList();
  
  // requestList 就假设是入参集合,然后 100个为一批创建异步线程去处理
  Lists.partition(requestList, 100).forEach(it -> {
    // 创建异步任务线程
    CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
      // 其他数据处理
      return "ok了";
    });
    completableFutureList.add(completableFuture);
  });
  
  // 获取多个异步任务的返回值
  List<String> stringList = completableFutureList.stream().map(CompletableFuture::join).collect(Collectors.toList());
  return stringList;
}

CAS

CAS 基于乐观锁思想。

Unsafe类是很底层的类,里面全是 native方法。

在 Java 中,使用 atomic包下面的各种类,会有 compareAndSet方法,也是比较并交换,到达到期望值时更新值。compareAndSet 方法有两个参数,第一个是 期望值,第二个是 更新值。

compareAndSet方法的下面一层的方法是:compareAndSetInt方法,然后这个方法是native 本地方法,Java 无法调用内存,但 Java 可以通过C++ 的 native方法,操作内存。

  • 记住 Integer 有 byte 范围的常量池,而如果使用大于 byte 范围的数,就会 new 堆内存对象,所以比较的时候一定不同,因为引用地址不同。

// 参数为初始值
AtomicInteger atomicInteger = new AtomicInteger(21);
// 第一个参数是期望值,第二个参数是期望达到是更新成的值
​
System.out.println(atomicInteger.compareAndSet(21, 22));
// 获取此时的值
System.out.println(atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(21, 23));
System.out.println(atomicInteger.get());

自旋锁

自旋锁存在以下问题:

  • 循环会耗时

  • 一次性只能保证一个共享变量的原子性

  • 存在 ABA问题

ABA问题

  • CAS 可能会产生 ABA问题

  • 原始值1;A线程期望 1,更新成 2;B线程比 A 快,期望 1,更新成 3,然后又期望 3,更新成 1,

  • 虽然 A线程最后能正常执行,但其实过程中已经被 B 戏耍了,但是 A 只关心它去执行操作的时候的当前值是否为 1,然后执行更新操作,所以对 A 而言无感知。

解决ABA问题-版本号

用带版本号的原子类可以防止 ABA 问题:AtomicStampedReference、AtomicReference。

因为 AtomicStampedReference 可以携带版本号,即每次被更新,就会迭代版本号,那么就可以通过 期望值+版本号 判断是否被动过手脚,

new AtomicStampedReference<Integer>(10, 0); 这个泛型是第一个参数的类型,第二个参数是版本号,其是固定类型 int 。

通过 atomicStampedReference.getStamp() 可获取当前的版本号。

AtomicStampedReference 的 compareAndSet方法有四个参数:期望的值、更新后的值、期望的版本号、更新后的版本号,只要有一个不达到期望值,就返回 false。

public static void main(String[] args) {
  AtomicStampedReference<Integer> atomicStampedReference = 
    new AtomicStampedReference<Integer>(10, 0);
  new Thread(() -> {
      System.out.println(Thread.currentThread().getName() + "拿到版本号:"
         + atomicStampedReference.getStamp());
      System.out.println(atomicStampedReference.compareAndSet(10, 11,
atomicStampedReference.getStamp(),atomicStampedReference.getStamp() + 1));
​
      System.out.println(Thread.currentThread().getName() + "拿到版本号:"
         + atomicStampedReference.getStamp());
      System.out.println(atomicStampedReference.compareAndSet(11, 10,
atomicStampedReference.getStamp(),atomicStampedReference.getStamp() + 1));
​
      System.out.println(Thread.currentThread().getName() + "拿到版本号:"
         + atomicStampedReference.getStamp());
  }, "A").start();
  new Thread(() -> {
      // 获得版本号
      int stamp = atomicStampedReference.getStamp();
      System.out.println(Thread.currentThread().getName() + "拿到版本号:"
         + stamp);
      try {
          TimeUnit.SECONDS.sleep(2);
      } catch (InterruptedException e) {
          e.printStackTrace();
      }
      System.out.println(atomicStampedReference.compareAndSet(10, 11, 
        stamp, stamp + 1));
  }, "B").start();
}

volatile

  • volatile 是 Java虚拟机提供的轻量级的同步机制,

  • volatile 只能修饰变量,

  • synchronized 能保证:可见性、原子性,但无法保证有序性,即无法禁止指令重排序,volatile 能保证:可见性、有序性(禁止指令重排序),但不保证原子性,

volatile原理

指令重排序

指令重排只是一种存在的现象,可能性很小,但是逻辑上存在,只要大量尝试,一定会出现一次指令重排。

什么是指令重排序?

首先要讲一下 as-if-serial语义,不管怎么重排序,(单线程)程序的执行结果不能被改变。

为了使指令更加符合 CPU 的执行特性,最大限度的发挥机器的性能,提高程序的执行效率,只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码逻辑顺序不一致,这个过程就叫做指令的重排序。

三种重排序:编译器重排序、指令级并行的重排序、内存系统重排序。

源代码 -> 编译器优化的重排 -> 指令并行也可能会重排 -> 内存系统也会重排 -> 最终执行的指令顺序。

内存屏障

  • volatile 的底层就是 Lock 和内存屏障。

  • 内存屏障是CPU指令,可以保证:

    • 保证特定操作的执行顺序

    • 保证某些变量的内存可见性

四类内存屏障

  • LoadLoad屏障:对于这样的语句 Load1,LoadLoad,Load2。在 Load2 及后续读取操作要读取的数据被访问前,保证 Load1 要读取的数据被读取完毕。

  • StoreStore屏障:对于这样的语句 Store1, StoreStore, Store2,在 Store2 及后续写入操作执行前,保证 Store1 的写入操作对其它处理器可见。

  • LoadStore屏障:对于这样的语句 Load1, LoadStore,Store2,在 Store2 及后续写入操作被刷出前,保证 Load1 要读取的数据被读取完毕。

  • StoreLoad屏障:对于这样的语句 Store1, StoreLoad,Load2,在 Load2 及后续所有读取操作执行前,保证 Store1 的写入对所有处理器可见。

内存屏障的生效简介

  • 在每个 volatile读操作后插入 LoadLoad屏障,在读操作后插入 LoadStore屏障。

  • 在每个 volatile写操作的前面插入一个 StoreStore屏障,后面插入一个 SotreLoad屏障。

各种锁

各种锁

非公平锁、公平锁

  • 非公平锁(默认的锁):可以插队,不公平,因为假如 3h 的线程在 3s 的之前,应该让 3s 的先执行,所以一般使用非公平锁,更灵活(默认都是非公平锁)。

  • 公平锁:先来后到,必须排队,很公平,得加 true,Lock 锁那块

共享锁、排他锁(读写锁)

  • 共享锁:多个线程可以同时占有,比如读锁

  • 排他锁:只能被一个线程占有,比如写锁

悲观锁、乐观锁

  • 悲观锁:坏事一定会发生,所以先做预防(上锁)。就比如改值,会先加锁,防止别人动。

  • 乐观锁:坏事不一定会发生,所以事后补偿。就比如改值,会先不加锁,把值拿到线程栈的工作空间后先做自己的事,到真要改回主存的时候,进行判断,如果真变了,再做进一步的操作

  • 自旋锁:是乐观锁的一种实现。把值拿到线程栈的工作空间,做完修改后自旋比较,若被改动过,就一直循环修改,直到成功。

    • 存在 ABA问题解决方法:加版本或是否被改动标记:

      1. version:AtomicStampedReference

      2. boolean:AtomicMarkableReference

统一锁、分段锁

  • 统一锁:大粒度的锁。用于解决死锁问题,比如锁A等B,锁B等A,那么把A+B做成一把统一锁,就能解决死锁。但是粒度太大,并发度小

  • 分段锁:小粒度的锁。比如CHM(ConcurrentHashMap)。粒度小了,并发大了,但是容易出现死锁

可重入锁(递归锁)、不可重入锁

可重入锁

可重入锁:可重复可递归调用的锁(即锁资源知道自己被谁拿了,下次这个线程想调用另一个同步块,再来请求锁资源,本来就是它拿了锁资源,所以不会发生死锁,它可以直接入)。请求了两次,都是请求同一个锁资源

所以说,锁是一个虚拟的概念,但锁资源是真实存在的,锁其实就是将线程ID 放在锁资源的 markword 里面,这就算是被占用了,被锁住了,

可重入锁有:synchronized、ReentrantLock。

不同级别的锁的重入策略
  1. 偏向锁:单线程独占,重入只用检查 threadId 等于该线程;

  2. 轻量级锁:重入:将栈帧中 lock record 的 header 设置为 null,重入退出:只用弹出栈帧,直到最后一个重入退出 CAS 写回数据释放锁;

  3. 重量级锁:重入 recursions++,重入退出 _recursions--,recursions=0 时释放锁。

synchronized 的可重入锁实践
public static void main(String[] args) {
    Phone phone = new Phone();
    new Thread(() -> {
        phone.send();
    }, "A").start();
    new Thread(() -> {
        phone.send();
    }, "B").start();
}
static class Phone {
    public synchronized void send() {
        System.out.println(Thread.currentThread().getName() + "send");
        call();
    }
    public synchronized void call() {
        System.out.println(Thread.currentThread().getName() + "call");
    }
}
ReentrantLock 的可重入锁实践
public static void main(String[] args) {
    Phone phone = new Phone();
    new Thread(() -> {
        phone.send();
    }, "A").start();
    new Thread(() -> {
        phone.send();
    }, "B").start();
}
static class Phone {
    Lock lock = new ReentrantLock();
    public void send() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "send");
            call();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
​
    public void call() {
        lock.lock();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            System.out.println(Thread.currentThread().getName() + "call");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

不可重入锁

使用自旋锁来创建不可重入锁,第二次 lock,就会死锁,

AtomicReference<Thread> atomicReference = new AtomicReference<Thread>();
// 加锁
public void lock() {
  Thread thread = Thread.currentThread();
  System.out.println(thread.getName() + " lock");
  // 自旋锁,再次lock就死锁了
  while (!atomicReference.compareAndSet(null, thread)) {
  }
}
​
// 解锁
public void unlock() {
  Thread thread = Thread.currentThread();
  System.out.println(thread.getName() + " unlock");
  atomicReference.compareAndSet(thread, null);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值