volatile
- 关于上述程序的说明:
main方法中开启了两个线程,一个共享数据flag。myThread线程在100毫秒后将flag修改为true,主线程循环读取flag,如果为true则打印一句话。
但是执行结果发现,即使myThread将flag修改为true,主线程仍然没有打印任何值。 - 解释:
每个线程都会有自己的工作线程,且他们共享一块主内存。myThread线程从主内存读取数据后,在工作内存中修改数据并同步进主内存,但由于 while(true) 执行效率非常快,main线程从主内存获取数据之后根本来不及重新从主内存中读取数据,所以一直读取的是旧数据。解决方案:死循环中加入延时/加入同步锁/使用volatile修饰变量
volatile
使用内存栅栏技术(防止指令重排序),使得线程每次获取数据都是从主存中获取
相较于synchronized是一种更轻量级的同步策略
缺点
- 不具备互斥性,可以多线程同时读写
- 不保证操作原子性
原子变量
使用volatile能保证可见性,但是无法保证对变量修改的原子性(例如 i++ 问题),此时可以使用 java.util.concurrent.atomic 包下提供的常用原子变量
- 这些原子变量内部都使用了volatile保证可见先
- 对这些变量的修改都使用了 CAS 保证数据的原子性
private AtomicInteger num = new AtomicInteger();
num.getAndIncreasment(); // 自增
CAS
包含了三个操作数 内存值V,预估值A,更新值B。当且仅当 V== A 时,才会将B赋给V。这三步是同步的
会出现ABA问题
synchronized
synchronized 默认使用当前对象作为锁,如果是静态方法则使用当前class对象作为锁。
在某时刻内,只能有一个线程能够获取锁
ConcurrentHashMap/CountDownLatch
线程安全的Hash表
- HashMap 和 HashTable
HashMap线程不安全
HashTable线程安全,但效率低,每个方法都使用同步锁,并且复合操作(例:如果存在即修改)也是线程不安全
ConcurrentHashMap 锁分段机制
并发级别(concurrentLevel)
默认分为16段(segment),每个段中又有一个16单位长度的数组,数组的每个元素下又挂着一个链表
这样的话每个段都有一个独立的锁,所以可以多线程同时访问多个不同的段
jdk1.8之后每个段也修改为使用cas操作
- 此包下还提供了很多其他的集合,例如 CopyOnWriteArrayList,该类也是一个集合,用法同 List,但是每次向其中添加元素时,它都会重新复制一个新的List并指向原集合。所以对集合有很多的添加操作时效率较低,开销大。通常可以用于多线程使用迭代器遍历,并在遍历过程中添加元素(注:普通的list即使在迭代器遍历过程中添加元素也会报错)
CountDownLatch 闭锁
在进行某些运算时,只有其他所有线程的运算全部完成,当前运算才继续执行
// 这里的 2 代表这个闭锁可以监控2个线程是否结束,内部维护一个整型变量=2,每次子线程结束就-1,直到为0
CountDownLatch latch = new CountDownLatch(2)
new Thread(new MyThread(latch)).start();
new Thread(new MyThread(latch)).start();
latch.wait(); // 会一直等到闭锁值为0
class MyThread implements Runable{
private CountDownLatch latch;
public void run() {
// do something
this.latch.countDown(); // 子线程结束后,需手动将闭锁减一
}
}
Callable接口
创建线程的方式有四种
- 继承Thread类
- 实现Runable接口
- 实现Callable接口
- 使用线程池
- Callable接口和实现Runable接口区别
- Callable接口使用call()方法,Runable使用run()
- Callable接口带泛型,call方法有返回值
- call方法会抛异常
- 执行Callable需要FutureTask实现类的支持,用于接收运算结果,FutureTask是Future接口的实现类
同步锁Lock
- 解决多线程安全问题的方式:
- 同步代码块 synchronized
- 同步方法 synchronized
- 同步锁 Lock(jdk1.5之后)
注:Lock是一种显式锁,需要手动通过 lock() 和 unlock() 方法进行加锁和放锁
MyThread implements Runable{
private Integer source = new Integer(); // 多线程共享的变量
private Lock lock = new ReentrantLock(); // 多线程共享的锁
public void run() {
lock.lock(); // 手动加锁
// do something
lock.unlock(); // 手动放锁
}
}
等待唤醒机制
即Object的wait和notify方法实现的效果
注:wait和notify会释放锁
-
生产者消费者模式会产生的问题
生产者生产速度过快,或者消费者消费速度过慢都可能导致数据丢失。或者生产者生产速度过慢,消费者一直尝试获取数据而浪费资源
解决方案:生产者没有生产数据时使用wait等待,此时因为消费者使用同一把锁,故也会等待,就不会产生空轮询的问题 -
使用wait和notify的问题
会产生虚假唤醒的问题:假如有两个消费者线程处于wait状态(即资源数为0),此时一个生产者将资源数+1并尝试唤醒消费者线程,则这两个消费者都会被唤醒并连续将资源数-1,此时资源数就成了-1,发生异常
解决:为了解决虚假唤醒问题,wait() 和 notify()应该总是放在循环语句中使用(即使被虚假唤醒之后也会再次进行判断)
或者使用Lock中Condition类的方法
Condition condition = lock.getCondition(); // 需要从Lock实例中获取
condition.await() // 等同于obj.wait
condition.signalAll // 等同于 obj.notifyAll()
读写锁
多线程同时读写需要互斥操作,多线程读不需要互斥
private int resource = 0; // 共享资源
private ReadWriteLock lock = new ReentrantReadWriteLock(); // 需多线程共享
// 读
public void get() {
lock.readLock().lock();
print(resource);
lock.readLock().unloock(); // 释放锁,通常放在funally块中
}
// 写
public void set(int num){
lock.writeLock().lock();
this.resource = num;
lock.writeLock().unlock();
}
线程池
频繁创建销毁线程耗费性能
线程池中维护了一个线程队列,队列中保存着所有等待状态的线程。
线程的体系结构
- 工具类
ExecutorService newFixedThreadPool(): 创建固定大小的线程池
ExecutorService newCachedThreadPool(): 缓存线程池,线程池数量不固定,可以根据需求自动更改数量
ExecutorService newSingleThreadExecutor(): 创建单个线程池,线程池只有一个线程
ScheduledExecutorService newScheduledThreadPool(): 创建固定大小的线程,可以延迟或定时执行任务
使用
public static void main(Stringp[] args) {
// 创建含有5个线程的线程池
ExecutorService pool = Executors.newFixedThreadPool(5);
// 向线程池提交任务
pool.submit(new Runable() {
// do something.
});
// 关闭线程池
pool.shutdown() // 等待所有任务结束之后才会自动关闭
pool.shutdownNow() //立即关闭
}
Fork/Join框架
在必要的情况下,将一个大任务拆分成若干个小任务,再将一个个小任务运算结果进行join汇总(类比map/reduce,其实质是递归调用)
AQS
JUC的基础就是AQS,AQS基础时可重入锁和LockSupport
LockSupport类
可以理解为:线程等待唤醒机制的加强版(即:使用park/unpark代替/wait/notify)
限制:wait/notify必须在同步代码块中执行
await/signal必须在Lock.lock()和Lock.unlock()中执行
-
使用LockSupport
-
基本原理
LockSupport中所有的方法都是静态方法
unpark(Thread t)方法会为传入的线程发放一张许可证,在该线程内调用的park()方法时会检查是否存在许可证。故可以在park()方法调用前就调用unpark()方法
一个线程只能有一个许可证,故多次调用unpark方法只会对一个park方法生效,其他的park方法仍然会阻塞
AQS(抽象同步队列)
多线程获取同一个锁,必然会有一些线程需要等待,AQS使用了一个双向队列(CHL队列)来维护每个锁所需要等待的线程,并使用一个volatile的int类型的变量维护锁状态。例如,当该状态为0时,则表示没有线程占用锁,等于1则说明当前锁需要等待。该状态通过CAS、LockSupport.park()的方式进行维护
-
所有被维护的等待线程都会封装称为一个
Node
类型的内部类 -
Node类中也维护了一个名为
waitState
的int类型的变量和被维护的线程及前后Node的指针 -
waitState表示当前维护线程的状态,例如0表示正常等待,1表示放弃获取锁
源码解读(详细过程)
任何一个Lock的实现类都是在内部封装了一个AQS的实现类,调用Lock的方法本质上都是调用该AQS实现类的方法
例如:当调用Lock lock = new RenntrantLock()时,其本质上是 创建了一个NonfairSync(非公平锁)对象,该对象继承自AQS,当调用 lock.await()时,本质是调用该AQS对象的wait方法。所以在此需详细解读AQS的锁方法
注:
- 公平锁:先到先获取锁,如果这个锁的等待队列中已经有线程在等待,则新来的线程将进入该队列等待。非公平锁:不管是否有等待队列,如果可以获取锁,则立刻占有锁对象
- 公平锁和非公平锁底层代码基本一致,唯一的区别是公平锁会判断等待队列中是否存在有效节点
详细过程模拟
假设有A、B、C三个线程使用非公平锁同时抢占锁L,则会发生如下情况
- 初始状态时,AQS中的state=0,即锁L处于空闲状态
- A尝试获取锁,使用CAS将state设为1,同时,将AQS中的变量currentThread设置为A(即当前持有锁的线程,初始为null)
- 此时B尝试获取锁,发现当前stat为1,则使用CAS尝试将自己加入等待队列:创建一个
空的Node
,将其加入到队列头节点,并设置其waitState值为-1(该空节点实际上是A线程的代表,表示正在运行的节点)。再创建一个封装了B线程的Node
,将其加入到头节点之后,完成入队操作 - B加入到队列之后,会使用一个死循环尝试获取锁(此时线程阻塞在lock()方法上)。但是在第一次循环时,
如果没能获取到锁,即调用LockSupport.park()方法阻塞线程,直到A线程调用unlock(),本质上调用AQS的release()方法,该方法会去队列寻找下一个排队的线程B并设置state=0,然后调用unpark(B)方法后循环继续,判断state的值,设置state=1,直到获取到锁
,当B获取锁后,将B设置为等待队列的头指针,并将其waitState设置为-1,Thread设置为null(一个新的头节点)。将最开始创建的空的头节点设置为null,等待GC回收 - 如果B还在等待的情况下,C也需要获取锁,则C会直接加到等待队列尾部