对象在内存中的内存布局
用sychronized
锁住对象后该对象的锁状态升级过程:new
- 无锁态 - 偏向锁 - 轻量级锁/自旋锁/无锁 (CAS)- 重量级锁 - GC标记信息
线程的几个状态
- NEW(新建状态)
- Runnable
- Ready(就绪状态,线程被放在等待队列中,等着被CPU执行)
- Running(运行状态,被扔到CPU中执行)
- Blocked
- Waiting
- TimedWaiting
- Terminated(终止态)
三种新建线程的方法
- 实现
Thread
类 - 实现
Runnable
接口 - 线程池
线程的常用方法:
sleep()
,沉睡一段时间(当前线程回到就绪状态),这段时间CPU执行其它线程yield()
,和sleep()
类似,让出CPU,当前线程回到就绪状态。使用很少见join()
,通知其它线程获得CPU执行,比如在t1
线程内运行t2.join()
,意思就是t1
线程通知t2
线程执行,自己回到就绪状态。
Synchronized
讲解
synchronized
实现过程:(不能禁止指令重排)
- Java代码:
synchronized
monitorenter
、moniterexit
- 执行过程中自动升级(偏向锁、自旋锁、重量级锁)
- 更底层的实现
lock comxchg
volatile
讲解:
- 保证变量的各线程可见性/数据一致性 (多个线程要用到变量时,重新去内存拿)
- 禁止CPU指令重排(在单线程没问题,多线程就会出现问题。为什么要指令重排,其实就是因为CPU太快了,而访问内存比访问缓存又慢了太多)
- 举个例子:对象的初始化三个步骤
Person p = new Person("zeng", 24);
- 申请对象
Person
的内存,这个时候给实例变量设置了默认值,比如name = null; age = 0;
- 调用该对象的构造函数进行真正的初始化实例变量
name = "zeng"; age = 24;
- 返回对象
Person
给p
- 申请对象
- 举个例子:对象的初始化三个步骤
volatile
不能实现synchronized
的原子性操作- 比如定义一个变量
volatile int count = 0;
10个线程分别count++
加1000次,最终的count
不一定会是10000,因为这里的count++
并不是一个原子性操作,它包含好几个指令,所以为了要实现整个的count++
原子性操作,也就是必须要使用sychronized
对count++
加锁。
- 比如定义一个变量
再注意一些问题:
- 在用
synchronized
锁住一个对象时,这个时候不能将这个引用去指向另一个对象 - 不要用
synchronized
去锁一个String、Integer
等基本数据类型的封装类的对象
ThreadLocal
讲解
- ThreadLocal可以作为每个线程存放属于自己变量的容器。
- 存放的容器是ThreadLocalMap。其中ThreadLocalMap属于每一个线程,也就是说每一个线程都有自己的ThreadLocalMap。
- ThreadLocalMap中的Entry的key为什么要设置为虚引用?(这个是重点)
- 首先ThreadLocalMap的底层存储格式为Entry的key-value数组,其中的key就是当前的引用型变量ThreadLocal(如下图所示)。试想,如果我们在线程运行过程中,将ThreadLocal置为null,那么这个时候Entry中的ThreadLocal理应被回收了,但如果Entry的key被设置成强引用则该ThreadLocal就不能被回收,所以需要设置为弱引用,设置为弱引用之后,垃圾回收线程只要发现有弱引用指向的对象,那么就会回收这个对象,这样在多线程使用中可以避免内存泄漏。
- 首先ThreadLocalMap的底层存储格式为Entry的key-value数组,其中的key就是当前的引用型变量ThreadLocal(如下图所示)。试想,如果我们在线程运行过程中,将ThreadLocal置为null,那么这个时候Entry中的ThreadLocal理应被回收了,但如果Entry的key被设置成强引用则该ThreadLocal就不能被回收,所以需要设置为弱引用,设置为弱引用之后,垃圾回收线程只要发现有弱引用指向的对象,那么就会回收这个对象,这样在多线程使用中可以避免内存泄漏。
CAS(无锁优化/自旋):
CompareAndSwap
Java
里面java.util.concurrnet.atomic.AtomicXXX
开头的类都是使用CAS自旋锁实现的。内部都是使用UnSafe
这个类的compareAndSet
等操作实现线程安全地修改值- 举个例子:
AtomicInteger count = new AtomicInteger(0);
在上面的volatile
的讨论中,count++
如果不加sychronized
锁会导致非原子性操作,但这里直接使用AtomicInteger
即可实现线程可见、原子性操作,将count++
到10000。并且不需要volatile、synchronized
。
- 举个例子:
- ABA问题(1变为2又被变为1),加版本号
version
- 所有的
Java
中CAS
的操作基本上都是用的UnSafe
这个类,这个UnSafe
使Java
语言有了像C++
的直接操作JVM
内存的能力。
ReentrantLock(可重入锁,公平锁(默认是非公平锁))本身底层也是CAS
- 可以替代
synchronized
,替换方法:lock.lock();
- 可以通过
lock.interupt
的方法将该锁设置为可以通过interup
方法唤醒正在wait
的线程 - 相比上个特点,
synchronized
的线程,wait
之后必须通过其它线程的notify()
才能唤醒 - 如果设置为公平锁,那么线程在抢一个资源时,会进入优先队列排队按先后顺序等待
synchronized
是非公平锁synchronized
自动加锁解锁,ReentrantLock
手动加锁解锁lock.lock()
- 底层实现:
ReentrantLock
是CAS的实现,synchronized
底层是有锁的升级过程(4种)
CountDownLatch锁(倒计时完了继续执行(门栓))
CyclicBarrier锁(当线程数目到达某个数目(栅栏值)时,继续执行后面的事物)
Phase锁(阶段锁,CyclicBarrier的升级版本,有多个阶段,比如结婚现场有7个人,先7人到达现场,再7人吃完饭,再xxxxx)
ReadWriteLock(共享锁、排他锁、多个线程可以一起执行)
Semaphore(信号量,用于限流(仅允许几个线程同时工作))
Exchanger(两个线程运行时交换值)
LockSupport(可以通过park()
方法随时将线程停止,并通过unpark()
方法随时让某线程就绪)
面试题1:定义两个线程,A线程往容器里放数据,B线程监测容器容量为5时,停止运行
- 有3种方法
- 使用
wait()
与notify()
方法的组合。这个很重要 - 使用门栓锁
CountDownLatch
- 使用
LockSupport
直接park()
与unpark()
面试题2:顺序打印A1B2C3……
面试题3:生产者消费者问题
版本1 通过synchronized、wait()、notify()
实现
package zr.thread;
import java.util.LinkedList;
import java.util.concurrent.TimeUnit;
/*
生产者与消费者实现1
写一个固定容量同步容器,拥有put和get方法, 以及getCount方法
能够支持2个生产者线程以及10个消费者线程的阻塞调用
使用wait()和notifyAll()来实现
这个方法是有瑕疵的,因为使用notifyAll()会唤醒所有的其它等待队列的线程,包括生产者、消费者
有没有办法只唤醒生产者,或者只唤醒消费者?
*/
/**
* @author ZR
* @Classname MyContainer1
* @Description 生产者消费者最简单写法
* @Date 2020/9/12 21:02
*/
public class MyContainer1<T> {
final private LinkedList<T> lists = new LinkedList<>();
// 最多10个元素
final private int MAX = 10;
private int count = 0;
// 因为++count所以要加synchronized
public synchronized void put(T t){
// 想想为什么用while而不是if
while(lists.size() == MAX){
try{
this.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
lists.add(t);
++count;
// 通知所有消费者线程消费
// 这个方法其实是有点小瑕疵的,因为notifyAll()会叫醒所有的其它wait()线程,也包括了另一个生产者
this.notifyAll();
}
// 因为--count所以要加synchronized
public synchronized T get(){
T t = null;
while(lists.size() == 0){
try{
this.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
t = lists.removeFirst();
--count;
// 通知生产者进行生产
// 这个方法其实是有点小瑕疵的,因为notifyAll()会叫醒所有的其它wait()线程,也包括了其它消费者
this.notifyAll();
return t;
}
public static void main(String[] args){
MyContainer1<String> c = new MyContainer1<>();
// 启动消费者线程
for(int i = 0; i < 10; i++){
new Thread(()->{
for(int j = 0; j < 5; j++)
System.out.println(c.get());
}, "customer" + i).start();
}
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 启动生产者线程
for(int i = 0; i < 2; i++){
new Thread(()->{
for(int j = 0; j < 25; j++)
c.put(Thread.currentThread().getName() + " " + j);
}, "producer" + i).start();
}
}
}
版本2 通过ReentrantLock
实现
package zr.thread;
import com.sun.org.glassfish.external.statistics.CountStatistic;
import java.util.LinkedList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author ZR
* @Classname MyContainer2
* @Description TODO
* @Date 2020/9/12 21:27
*/
public class MyContainer2<T> {
final private LinkedList<T> lists = new LinkedList<>();
// 最多10个元素
final private int MAX = 10;
private int count = 0;
private Lock lock = new ReentrantLock();
// Condition的本质就是等待队列,在这里生产者在生产者的队列,消费者在消费者的队列
// 在Container1例中,等待队列只有一个,生产者和消费者都在里边儿
private Condition producer = lock.newCondition();
private Condition customer = lock.newCondition();
public void put(T t){
try {
// 需要手动加锁
lock.lock();
while(lists.size() == MAX)
producer.await();
lists.add(t);
++count;
// 通知消费者线程进行消费
customer.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 手动解锁
lock.unlock();
}
}
public T get(){
T t = null;
try {
lock.lock();
while(lists.size() == 0)
customer.await();
t = lists.removeFirst();
--count;
// 通知生产者线程生产
producer.signalAll();
} catch (InterruptedException e){
e.printStackTrace();
} finally {
lock.unlock();
}
return t;
}
public static void main(String[] args){
MyContainer2<String> c = new MyContainer2<>();
// 启动消费者线程
for(int i = 0; i < 10; i++){
new Thread(()->{
for(int j = 0; j < 5; j++)
System.out.println(c.get());
}, "customer" + i).start();
}
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 启动生产者线程
for(int i = 0; i < 2; i++){
new Thread(()->{
for(int j = 0; j < 25; j++)
c.put(Thread.currentThread().getName() + " " + j);
}, "producer" + i).start();
}
}
}
AQS(CLH)队列
注意这里面的state
根据不同的同步锁取不同的意义,比如:
ReentrantLock
,state = 0
代表unlock
,state = 1
代表lock
CountDownLatch
,state = 5
代表需要倒计数5个数,才继续下面的操作
下面的Node
表示一个双向链表,这里面存储的就是线程Thread
!!!!!注意看下图!!!!!!