进程是操作系统进行资源分配的最小单位,线程是进行运算调度的最小单位。一个进程在执行过程中可产生多个线程,这些线程共享进程的堆和方法区资源。
Java创建线程的方式- 继承Thread类
- 实现Runnable接口
- 实现Callable接口(线程返回值/异常通过FutureTask封装)
- 两者都暂停了线程的执行,但wait()释放了锁,而sleep()没有。
- wait()的线程不会主动苏醒,需要其他线程调用同一对象的notify()或者notifyAll()来唤醒;而sleep()执行完毕后线程会自动苏醒。
- wait()常用于线程间通信,而sleep()常用于暂停执行。
定义:多个线程因等待无法释放的资源而无期限地阻塞。
必要条件:
①互斥:同一资源任意时刻只被一个线程占用;
②请求和保持:线程因请求资源而阻塞时,不会释放已有的资源;
③不剥夺:线程已有的资源在使用结束前不会被强行剥夺;
④循坏等待:若干线程间形成一种头尾相接的循坏等待资源关系。
synchronized关键字通过保证被修饰的代码块在任意时刻只有单个线程能执行,来解决多线程之间访问资源的同步性。
用法- 修饰实例方法(锁的是对象实例)
- 修饰静态方法(锁的是类)
- 修饰代码块(可指定锁的目标,灵活性高)
- 不能修饰构造方法(构造方法本身就是线程安全的)
- 不能被继承
每个对象都内置了一个对象监视器ObjectMonitor(C++实现),执行synchronized命令时,线程会尝试获取对象监视器。获得时,锁计数器+1;释放时,锁计数器-1。
synchronized 和 ReentrantLock- 两者都是可重入锁(即可再次重复获取自己已有的锁)
- synchronized关键字基于JVM层面,会自动解锁,而Lock接口基于API层面,需手动解锁
- ReentrantLock在保持和synchronized相同的并发性和内存语义基础上,新增了高级功能:
- 支持中断等待: 等待锁的线程可选择放弃,从而处理其他事情。
- 支持公平锁:先等待的线程先获得锁。synchronized只能非公平锁,ReentrantLock默认非公平锁。
- 支持选择性通知:结合Condition接口,同一个锁可以绑定多个条件,从而实现分组等待/通知。(synchronized的锁相当于只对一个Condition进行wait和notify)
volatile Java内存模型(JMM)针对读多写少的场景,为了允许多个线程同时读,而只当有线程写时才禁止其他线程读写,Java提供了ReentrantReadWriteLock读写锁。原理是维护了两个锁,读相关的称为共享锁,写相关的称为排他锁。
Java的所有变量都存储在主内存中,但每个线程会把主内存中的变量,复制一个副本存在自己独立的本地内存中。线程只能直接操作自己本地内存中的变量副本,这就可能导致同一变量在不同线程间的副本数据不一致。
为了解决这个问题,Java提供了volatile关键字,表示该变量是共享且不稳定的,每次使用它需要去主内存中读取。
Java 内存模型定义了 8 个操作来完成主内存和本地内存的交互操作:
• read:把一个变量的值从主内存传输到本地内存中
• load:在 read 之后执行,把 read 得到的值放入本地内存的变量副本中
• use:把本地内存中一个变量的值传递给执行引擎
• assign:把一个从执行引擎接收到的值赋给本地内存的变量
• store:把本地内存的一个变量的值传送到主内存中
• write:在 store 之后执行,把 store 得到的值放入主内存的变量中
• lock:作用于主内存的变量,标识为某一线程独占。
• unlock:释放锁定状态的变量。
- 原子性
操作的执行不会被中断。即要么操作完全成功,要么不执行操作。
JMM保证上述8个操作具有原子性。但单操作的原子性,无法保证线程安全。 - 可见性
当一个线程对共享变量进行修改,其他线程可以立马得知。
使用volatile就可以保证共享变量的可见性。 - 有序性
JMM允许编译器和处理器对指令进行重排序,且保证重排序不会影响单线程程序的执行。
但并发编程会受重排序的影响,使用volatile可以禁止指令进行重排序优化。
synchronized关键字和volatile关键字是两个互补的存在。
volatile用于保证变量的可见性和有序性,轻量级,性能好。
synchronized用于保证代码块的原子性、可见性和有序性。
Q:使用双重检验锁实现单例模式
A:
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton(){}
public static Singleton getInstance() {
if (uniqueInstance == null) {
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
Q1:为什么不直接对getInstance加synchronized?
A:直接同步方法的话,每次调用该方法都要进行同步,而实际上只有在创建对象实例前的调用,才有同步的必要。通过双重检查的方式,实例创建完毕后的调用,在synchronized块外的第一次检查就返回了,避免了性能损失。
Q2:为什么要将uniqueInstance申明为volatile变量?
A:uniqueInstance = new Singleton();这行代码其实分为三步执行:1.为uniqueInstance分配内存地址 2.执行构造方法 3. 将uniqueInstance指向分配的内存地址。由于JVM具有指令重排的特性,执行顺序可能变成1→3→2。假设有两个线程,T1执行了1和3,此时T2进入,判断uniqueInstance != null,于是返回了一个初始化还未完毕的对象。因此,需要使用volatile来禁止JVM重排指令。
通常情况下,我们创建的变量可以被任意线程访问并修改。如果想让每个线程都拥有自己专属的本地变量,则可以使用ThreadLocal类。
// SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本
private static final ThreadLocal formatter;
原理
每个Thread有一个ThreadLocalMap,存储着以ThreadLocal为key,Object对象为value的键值对。
这样每个线程都保存了一份变量的副本,避免了线程安全问题。
ThreadLocalMap的key是ThreadLocal的弱引用,而value是强引用。于是可能出现key被回收,而value永远无法回收的情况,造成内存泄漏。为了解决这个问题,ThreadLocal在调用set()、get()、remove()方法时,会自动清理key为null的Entry。
线程池创建方法池化技术(线程池、数据库连接池、Http 连接池)的思想就是减少每次获取资源的消耗,提高资源的利用率。
- 通过ThreadPoolExecutor构造方法创建(阿里巴巴规范推荐使用的方式)
- 通过Executors工具类创建(对上述构造方法的封装)
FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致 out of memory。
CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 out of memory。
- corePoolSize:核心线程数,即可同时运行的最小线程数量。
- maximumPoolSize:当队列存放的任务达到容量上限时,可同时运行的最大线程数量。
- workQueue:新任务到来时,会判断当前运行的线程数是否已达核心线程数,若已达到则将新任务放入队列中。
- keepAliveTime:当线程数大于核心线程数时,没有任务可执行的线程会等待keepAliveTime才会被回收销毁。
- unit:keepAliveTime参数的时间单位。
当线程数量达到maximumPoolSize且workQueue已满,ThreadPoolExecutor定义了一些策略:
- ThreadPoolExecutor.AbortPolicy:抛出RejectedExecutionException来拒绝新任务的处理。(默认策略)
- ThreadPoolExecutor.CallerRunsPolicy:在本线程(调用execute方法的线程)运行新任务。如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略
- ThreadPoolExecutor.DiscardPolicy:丢弃新任务。
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列中最早的未处理任务。
AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
CASCAS 的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个 volatile 变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。
AQSAbstractQueuedSynchronizer,是一个用来构建锁和同步器的框架,ReentrantLock、FutureTask等都是基于AQS。
原理AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。



