Java并发编程

109 阅读6分钟

synchronized关键字

锁的状态有:

  1. 无锁
  2. 偏向锁(JDK15默认关闭,JDK18废弃)
  3. 轻量级锁
  4. 重量级锁

锁升级

锁升级是指根据对共享资源竞争的激烈程度,锁状态从无锁->偏向锁->轻量级锁->重量级锁的一个过程.

锁升级的条件

  1. 无锁到偏向锁:当一个线程对共享资源进行访问的时候,会开启偏向锁
  2. 偏向锁到轻量级锁:在偏向锁状态,一个线程对共享资源进行访问的时候,判断对象头中mark word中对象锁标志位为偏向锁(101)时,再判断mark word中偏向的线程id是当前要访问的线程那么直接获取,如果mark word中的线程ID不是当前要访问的线程,那么就是进行锁升级到轻量级锁。在轻量级锁状态下,第二个线程会通过CAS的方式来尝试获取锁。
  3. 轻量级锁到重量级锁:在轻量级锁状态下,如果共享资源被某线程锁持有,第二个线程会通过CAS的方式来尝试获取锁,当尝试次数达到限制10次或者此时有第三个线程来获取锁时,此时锁状态会再次进行升级,升级到重量级锁。

轻量级锁:轻量级锁会在当前线程的栈帧创建一个锁记录(lock record)空间,将共享资源的mark word拷贝到这个锁记录空间,然后使用CAS将Mkar word中锁记录指针指向该锁记录,并将Lock record中的Owner指针指向对象头Mark word。

重量级锁:重量级锁底层是通过操作系统的互斥锁方式实现,通过monitorenter进入临界区,然后在同步代码结束处和异常处通过monitorexit退出。

Java内存模型(JMM)

Java内存模型是Java的并发编程相关的概念,抽象了内存模型(线程和主内存之间的关系),其主要目的是为了简化多线程编程,增强程序可移植性的。

并发编程的三个重要特征

  1. 可见性:
  2. 有序性
  3. 原子性

在JAVA内存模型(JMM)中,每个线程都会有自己的本地内存用于存储共享变量的副本,线程会操作本地内存中共享变量的副本然后再从本地内存写入主内存中,所以要保证可见性(两个线程之间通信)要有两个步骤:

  • 线程一将操作后的共享变量副本值同步到主存中;
  • 线程二从主存中读取共享变量。

本地内存是对CPU缓存,寄存器等的一个抽象概念。

volatile

volatile是线程同步轻量级实现,性能比synchronized好,但是volatile只能用于修饰变量。

volatile能保证可见性和有序性,但是不能保证操作的原子性,synchronized和ReentrantLock全都能保证。

volatile的使用

双重校验锁实现对象单例

public class Singleton { 
    private volatile static Singleton uniqueInstance; 
    private Singleton() { } 
    public static Singleton getUniqueInstance() { 
        //第一次判断
        if (uniqueInstance == null) { 
            //对象加锁 
            synchronized (Singleton.class) { 
                //第二次判断
                if (uniqueInstance == null) { 
                    uniqueInstance = new Singleton(); 
                } 
            } 
        } 
        return uniqueInstance; } }

这里对单例对象uniqueInstance使用了volatile进行修饰,这样做的目的是防止指令重排。 对象实例化的三个步骤为:

  1. 分配内存空间
  2. 初始化对象数据
  3. 分配对象地址给变量

由于步骤1和步骤2不满足happens-before原则,所以jvm进行指令重排时可能会按顺序1,3,2来执行代码,如果不加volatile,在单线程环境下这个顺序执行没有问题,但是如果在多线程环境下,在步骤3执行完之后判断uniqueInstance != null此时会可能将一个没有初始化的对象返回。

ReentrantLock

ReentrantLock是一个类,它实现了接口Lock,是一个可重入的独占锁。它比synchronized更灵活、强大,增加了轮询、超时、中断、公平和非公平等功能。

ReentrantLock与Synchronized的区别

  • 两个都是可重入锁
  • synchonized是依赖JVM实现的(是一个关键字),ReentrantLock依赖于API
  • ReentrantLock
    • 可等待中断:使用方法lock.lockInterruptible()使用可中断锁,当该线程被中断时便可停止等待获取锁。代码如下:
      public static void main(String[] args) throws InterruptedException {
          Lock lock = new ReentrantLock();
          Thread thread1 = new Thread(new Runnable() {
              @Override
              public void run() {
                  try {
                      lock.lock();
                      System.out.println(Thread.currentThread().getName() + "获取到了锁");
                  } catch (Exception ex) {
      
                  }
                  //不释放锁
              }
          }, "thread1");
          Thread thread2 = new Thread(new Runnable() {
              @Override
              public void run() {
                  try {
                      lock.lockInterruptibly();
                      System.out.println(Thread.currentThread().getName() + "获取到了锁");
                  } catch (InterruptedException ex) {
                      System.out.println(Thread.currentThread().getName() + "中断");
                  }
                  //不释放锁
              }
          }, "thread2");
          thread1.start();
          thread2.start();
          Thread.sleep(2000);
          if (thread2.isAlive()) {
              System.out.println("thread still is alive, interrupt it");
              thread2.interrupt();
          }
      }
      
    • 可实现公平锁:默认是非公平锁,构造器构造时可选择公平锁,synchronized是非公平锁
    • 可实现选择性通知:可以借助Condition接口与newCondition()方法
        public static void main(String[] args) {
            Lock lock = new ReentrantLock(true);
            Condition emptyCondition = lock.newCondition();
            Condition fullCondition = lock.newCondition();
            Stack<String> cup = new Stack<>();
            new Thread(() -> {
                while (true) {
                    lock.lock();
                    try {
                        while (cup.size() == 3) {
                            try {
                                fullCondition.await();
                            } catch (InterruptedException e) {
                                throw new RuntimeException(e);
                            }
                        }
                        Thread.sleep(1000);
                        String time = "" + System.currentTimeMillis();
                        cup.push(time + "");
                        System.out.println("product: " + time);
                        emptyCondition.signal();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    } finally {
                        lock.unlock();
                    }
                }
            }, "producer").start();
            new Thread(() -> {
                while (true) {
                    lock.lock();
                    try {
                        while (cup.size() == 0) {
                            try {
                                emptyCondition.await();
                            } catch (InterruptedException e) {
                                throw new RuntimeException(e);
                            }
                        }
                        String pop = cup.pop();
                        System.out.println("consumer: " + pop);
                        fullCondition.signal();
                    } finally {
                        lock.unlock();
                    }
                }
            }, "consumer").start();
        }
      

ThreadLocal

ThreadLocal作用是可以实现每个线程都有自己专属的本地变量。可以用该类来保存用户信息等,这样对于对于一个应用来说每个工作线程可以通过不同的用户信息处理业务逻辑。

原理

Thread类包含一个ThreadLocalMap类型的属性threadLocals,ThreadLocal每次都是对当前线程对象中的属性threadLocals进行操作。ThreadLocalMap类型的key是当前要操作的ThreadLocal实例对象,value是定义的ThreadLocal要保存的数据。可以看到每个线程都有一个自己的ThreadLocalMap实例用于保存属于自己线程的变量值,所以同一个ThreadLocal变量,每个线程访问的是自己的。 #ThreadLocal源码级别详解

线程池

使用线程池的优点:

  1. 降低资源消耗。重复利用已创建的线程降低创建和销毁造成的消耗。
  2. 提高响应速度。当任务到达时,不需要再创建线程。
  3. 提高线程的可管理性。使用线程池可以统一进行分配,调优和监控。

线程池创建的方式

  1. 通过Executors创建
  2. 使用ThreadPoolExecutor构造函数来创建(推荐)

线程池常见的参数

  1. CorePoolSize
  2. MaximumPoolSize
  3. KeepAliveTime
  4. TimeUnit
  5. BlockingQueue
  6. ThreadFactory
  7. RejectedExecutionHandler