目录
1、多线程基础
1.1、线程生命周期
1.1.1、新建
1.1.2、就绪
1.1.3、运行
1.1.4、阻塞
1.1.5、死亡
1.2、终止线程的方式
1.2.1、正常运行结束
1.2.2、使用退出标志
1.2.3、使用interrupt()方法来中断线程
1.2.3、stop方法强制结束
1.3、sleep与wait方法区别
1.4、守护线程daemon
1.5、线程基本方法
1.5.1、wait
1.5.2、线程睡眠(sleep)
1.5.3、线程让步(yield)
1.5.4、notify与notifyAll
1.5.5、为什么wait, notify 和 notifyAll这些方法不在thread类里面
1.5.6、interrupted和 isInterrupted方法的区别
1.5.7、 有三个线程T1,T2,T3,怎么确保它们按顺序执行
1.6、多线程最佳实践
1.6.1、给你的线程起个有意义的名字
1.6.2、避免锁定和缩小同步的范围
1.6.3、多用同步类少用wait 和 notify
1.6.4、多用并发集合少用同步集合
2、线程池
2.1、线程池原理
2.2、线程复用原理
2.3、自定义线程池
2.3.1、线程池类
2.3.2、工作线程类
2.3.3、任务类
2.3.4、测试类
2.4、线程池组成
2.4.1、类关系图
2.4.2、ThreadPoolExecutor
2.4.3、拒绝策略
2.4.4、工作过程
2.5、线程池的作用
2.6、不建议使用 Executors静态工厂构建现成的线程池
2.7、线程池如何处理异常
2.8、线程池的工作队列
3、阻塞队列
3.1、阻塞队列API
3.2、阻塞队列家族
3.2.1、ArrayBlockingQueue
3.2.2、LinkedBlockingQueue
3.2.3、DelayQueue
3.2.4、SynchronousQueue
3.2.5、PriorityBlockingQueue
3.2.6、LinkedTransferQueue
3.2.7、LinkedBlockingDeque
4、线程工具类
4.1、FutureTask
4.1.1、构造方法
4.1.2、示例
4.2、AQS(AbstractQueuedSynchronizer抽象队列同步器)
4.2.1、AQS原理
4.2.2、自定义同步器
4.3、Condition(更高效)
4.4、Semaphore(信号量-控制同时访问的线程个数)
4.5、CountDownLatch(线程计数器)
5、java锁
5.1、synchronized关键字
5.1.1、使用方式
5.1.2、底层原理
5.1.3、关于synchronized的其它知识点
5.2、Lock接口
5.2.1、synchronized与lock的区别
5.2.2、可重入锁ReentrantLock
5.2.3、读写锁ReadWriteLock
5.3、锁的思想(非实际锁)
5.3.1、公平锁、非公平锁
5.3.2、分段锁
5.3.3、可重入锁
5.3.4、乐观锁与悲观锁
5.3.5、共享锁和独占锁
5.3.6、自旋锁
5.4、锁状态(针对synchronized)
5.5、锁优化
5.5.1、减少锁持有时间
5.5.2、减小锁粒度
5.5.3、锁分离
5.5.4、锁粗化
5.5.5、锁消除
对应线程状态为new
当线程对象调用了start()方法之后,该线程处于就绪状态。Java虚拟机会为其创建方法调用栈和程序计数器,等待调度运行
对应线程状态为RUNNABLE
等待阻塞:WAITING
超时阻塞:TIMED_WAITING,调用下列方法之一会导致超时阻塞
阻塞:BLOCKED,等待监视器锁
while(!exit){
//do something
}
这里的exit是自定义的变量
public class ThreadStopTest {
public static boolean exit = false;
public static void main(String[] args) {
//这个线程的作用是每3秒打印出:helloworld
new Thread(new Runnable() {
@Override
public void run() {
while (!exit){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("helloworld");
}
}
}).start();
//这个线程的作用是,10秒后停止上面那个线程
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
exit = true;
}
}).start();
}
}
分两种情况:
a)线程处于阻塞状态:如使用了sleep,同步锁的wait,socket中的receiver,accept等方法时,会使线程处于阻塞状态。当调用线程的interrupt()方法时,会抛出InterruptException异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后break跳出循环状态,从而让我们有机会结束这个线程的执行。通常很多人认为只要调用interrupt方法线程就会结束,实际上是错的, 一定要先捕获InterruptedException异常之后通过break来跳出循环,才能正常结束run方法。
public class StopThreadTest implements Runnable{
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new StopThreadTest());
t.start();
Thread.sleep(3000);
t.interrupt();
}
@Override
public void run() {
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
System.out.println("hello");
}
}
}
b)线程未处于阻塞状态:使用isInterrupted()判断线程的中断标志来退出循环。当使用interrupt()方法时,中断标志就会置true,和使用自定义的标志来控制循环是一样的道理
public class StopThreadTest implements Runnable{
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new StopThreadTest());
t.start();
Thread.sleep(3000);
t.interrupt();
}
@Override
public void run() {
while(!Thread.currentThread().isInterrupted()){
System.out.println("hello");
}
}
}
程序中可以直接使用thread.stop()来强行终止线程,但是stop方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果
不安全主要是:thread.stop()调用之后,创建子线程的线程就会抛出ThreadDeatherror的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因此,并不推荐使用stop方法来终止线程
可能会造成死锁
public class StopThreadTest implements Runnable{
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new StopThreadTest());
Thread t2 = new Thread(new StopThreadTest());
t1.setDaemon(true);
t2.setDaemon(true);
t1.start();
t2.start();
System.out.println(t1.isDaemon());
System.out.println(t2.isDaemon());
System.out.println(Thread.currentThread().isDaemon());
}
@Override
public void run() {
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getId());
}
}
}
输出结果:
true
true
false
如果t1和t2都设置为守护线程,那么当main线程结束后(main线程是非守护线程),系统中就没有非守护线程了,所以t1和t2线程都会自动退出,不会一直打印各自的线程ID
调用该方法的线程进入WAITING状态,只有等待另外线程的通知或被中断才会返回,需要注意的是调用wait()方法后,会释放对象的锁。因此,wait方法一般用在同步方法或同步代码块中
sleep导致当前线程休眠,与wait方法不同的是sleep不会释放当前占有的锁,sleep(long)会导致线程进入TIMED-WATING(超时等待)状态,而wait()方法会导致当前线程进入WATING状态
yield会使当前线程让出CPU执行时间片,与其他线程一起重新竞争CPU时间片。一般情况下,优先级高的线程有更大的可能性成功竞争得到CPU时间片,但这又不是绝对的,有的操作系统对线程优先级并不敏感
Object 类中的 notify() 方法,唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。类似的方法还有 notifyAll() ,唤醒再次监视器上等待的所有线程
这是个设计相关的问题,一个很明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁 就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象
首先,打断一个线程调用interrupt方法,interrupted和 isInterrupted都返回boolean值(是否中断),interrupted和 isInterrupted的主要区别是前者会将中断状态清除而后者不会
任何抛出了InterruptedException异常的方法都会将中断状态清零,所以我们测试两者区别的时候,不要用Thread.sleep
测试代码:
Thread t = new Thread(() -> {
//不要用Thread.sleep
/* try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
int i = 0;
for(;;){
i++;
if(i>99999){
break;
}
}
//System.out.println(Thread.interrupted());
//System.out.println(Thread.interrupted());
});
t.start();
t.interrupt();
System.out.println(t.isInterrupted());
System.out.println(t.isInterrupted());
利用Thread对象的join方法
这样可以方便找bug或追踪。OrderProcessor, QuoteProcessor or TradeProcessor 这种名字比 Thread-1. Thread-2 and Thread-3 好多了,给线程起一个和它要完成的任务相关的名字,所有的主要框架甚至JDK都遵循这个最佳实践
锁花费的代价高昂且上下文切换更耗费时间空间,试试最低限度的使用同步和锁,缩小临界区。因此相对于同步方法我更喜欢同步块,它给我拥有对锁的绝对控制权
首先,CountDownLatch, Semaphore, CyclicBarrier 和 Exchanger 这些同步类简化了编码操作,而用wait和notify很难实现对复杂控制流的控制。其次,这些类是由最好的企业编写和维护在后续的JDK中它们还会不断 优化和完善,使用这些更高等级的同步工具你的程序可以不费吹灰之力获得优化
这是另外一个容易遵循且受益巨大的最佳实践,并发集合比同步集合的可扩展性更好,所以在并发编程时使用并发集合效果更好。如果下一次你需要用到map,你应该首先想到用ConcurrentHashMap
ConcurrentHashMap,它内部细分了若干个小的HashMap,称之为段(Segment)。默认情况下一个ConcurrentHashMap被进一步细分为16个段,既就是锁的并发度
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行
他的主要特点为:线程复用;控制最大并发数;管理线程
每一个 Thread 的类都有一个 start 方法。 当调用start启动线程时Java虚拟机会调用该类的 run 方法。 那么该类的 run() 方法中就是调用了 Runnable 对象的 run() 方法。 我们可以继承重写 Thread 类,在其 start 方法中添加不断循环调用传递过来的 Runnable 对象。 这就是线程池的实现原理。循环方法中不断获取 Runnable 是用 Queue 实现的,在获取下一个 Runnable 之前可以是阻塞的
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
/**
* 自定义定长线程池
*/
public class MyThreadPool {
/**
* 线程池数组
*/
private Thread[] threadArr;
private int threadNum;
/**
* 线程队列
*/
private BlockingQueue runnableQueue = new ArrayBlockingQueue(100);
/**
* 定长线程池构造方法
*/
public MyThreadPool(int threadNum){
this.threadNum = threadNum;
initThreadPool();
}
/**
* 初始化线程池
*/
private void initThreadPool(){
threadArr = new WorkThread[threadNum];
for (int i = 0; i < threadNum; i++) {
threadArr[i] = new WorkThread(this);
threadArr[i].start();
}
}
/**
* 线程池执行器
*/
public void execute(Runnable r){
runnableQueue.add(r);
}
public BlockingQueue getRunnableQueue() {
return runnableQueue;
}
public void setRunnableQueue(BlockingQueue runnableQueue) {
this.runnableQueue = runnableQueue;
}
}
public class WorkThread extends Thread{
private MyThreadPool myThreadPool;
public WorkThread(MyThreadPool myThreadPool){
this.myThreadPool = myThreadPool;
}
@Override
public void run() {
while(true){
try {
Runnable r = myThreadPool.getRunnableQueue().take();
r.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class MyTask implements Runnable{
private String name;
public MyTask(String name) {
this.name = name;
}
@Override
public void run() {
while (true){
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name);
}
}
}
public class MyThreadPoolTest {
public static void main(String[] args) throws InterruptedException {
MyThreadPool myThreadPool = new MyThreadPool(5);
MyTask task1 = new MyTask("name1");
MyTask task2 = new MyTask("name2");
MyTask task3 = new MyTask("name3");
MyTask task4 = new MyTask("name4");
MyTask task5 = new MyTask("name5");
MyTask task6 = new MyTask("name6");
MyTask task7 = new MyTask("name7");
myThreadPool.execute(task1);
myThreadPool.execute(task2);
myThreadPool.execute(task3);
myThreadPool.execute(task4);
myThreadPool.execute(task5);
myThreadPool.execute(task6);
myThreadPool.execute(task7);
}
}
一般的线程池主要分为以下4个组成部分:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
int corePoolSize = 5;
int maximumPoolSize = 10;
long keepAliveTime = 60;
TimeUnit unit = TimeUnit.SECONDS;
BlockingQueue workQueue = new ArrayBlockingQueue<>(100,false);
ThreadFactory threadFactory = r -> new Thread(r,"bobo");
ThreadPoolExecutor.CallerRunsPolicy callerRunsPolicy = new ThreadPoolExecutor.CallerRunsPolicy();
ExecutorService es = new ThreadPoolExecutor(corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
threadFactory,
callerRunsPolicy);
for (int i = 0; i < 1000; i++) {
final int a =i;
es.execute(()->{
System.out.println(a);
});
}
线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也塞不下新任务了。这时候我们就需要拒绝策略机制合理的处理这个问题
JDK内置的拒绝策略如下:
1、线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们
2、当调用 execute() 方法添加一个任务时,线程池会做如下判断:
a)如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务
b)如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列
c)如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务
d)如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异RejectExecutionException
3、当一个线程完成任务时,它会从队列中取下一个任务来执行。
4、当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小
假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间。
如果:T1 + T3 远大于 T2,则可以采用线程池,以提高服务器性能
阿里巴巴Java开发手册,明确指出不允许使用Executors静态工厂构建线程池
原因如下:
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
Executors返回的线程池对象的弊端如下:
注意:使用无界队列的线程池会导致内存飙升
队列 | 有界性 | 锁 | 数据结构 |
---|---|---|---|
ArrayBlockingQueue | bounded(有界) | 加锁 | arrayList |
LinkedBlockingQueue | optionally-bounded | 加锁 | linkedList |
PriorityBlockingQueue | unbounded | 加锁 | heap |
DelayQueue | unbounded | 加锁 | heap |
SynchronousQueue | bounded | 加锁 | 无 |
LinkedTransferQueue | unbounded | 加锁 | heap |
LinkedBlockingDeque | unbounded | 无锁 | heap |
是一个用数组实现的有界阻塞队列,此队列按照先进先出(FIFO)的原则对元素进行排序。支持公平锁和非公平锁。【注:每一个线程在获取锁的时候可能都会排队等待,如果在等待时间上,先申请获取锁的线程先得到锁,那么这个锁就是公平的。反之,这个锁就是不公平的】
ArrayBlockingQueue的构造方法:
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
capacity:队列大小,即队列只能容纳capacity个元素
fail:是否公平
一个由链表结构组成的有界队列,此队列的默认长度为Integer.MAX_VALUE。此队列按照先进先出的顺序进行排序
LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能
LinkedBlockingQueue构造方法:
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node(null);
}
一个实现延迟获取的无界队列,在创建元素时,可以指定多久才能从队列中获取当前元素。只有延时期满后才能从队列中获取元素。DelayQueue内部使用非线程安全的优先队列(PriorityQueue)
DelayQueue可以运用在以下应用场景:
public static void main(String[] args) {
BlockingQueue blockingQueue = new DelayQueue();
new Thread(new Runnable() {
@Override
public void run() {
for (;;) {
try {
long begin = System.currentTimeMillis();
Task element = blockingQueue.take();
System.out.println("get----"+element.getTaskId()+"----"+(System.currentTimeMillis()-begin));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
Random r = new Random();
Task t1 = new Task(r.nextInt(20),r.nextInt(30)+1,TimeUnit.SECONDS);
Task t2 = new Task(r.nextInt(20),r.nextInt(30)+1,TimeUnit.SECONDS);
Task t3 = new Task(r.nextInt(20),r.nextInt(30)+1,TimeUnit.SECONDS);
Task t4 = new Task(r.nextInt(20),r.nextInt(30)+1,TimeUnit.SECONDS);
Task t5 = new Task(r.nextInt(20),r.nextInt(30)+1,TimeUnit.SECONDS);
try {
blockingQueue.put(t1);
blockingQueue.put(t2);
blockingQueue.put(t3);
blockingQueue.put(t4);
blockingQueue.put(t5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("完成");
}
class Task implements Delayed{
private Integer taskId;
/**
* 触发时间
*/
private long triggerTime;
/**
*
* @param delayTime 延迟时间
* @param unit 时间单位
*/
public Task(Integer taskId,long delayTime,TimeUnit unit){
System.out.println(taskId+"----"+delayTime);
this.taskId = taskId;
//计算触发时间
this.triggerTime = System.currentTimeMillis()+unit.toMillis(delayTime);
}
/**
* 获取延迟时间,该方法由阻塞队列周期执行
* @param unit
* @return
*/
@Override
public long getDelay(TimeUnit unit) {
return triggerTime-System.currentTimeMillis();
}
@Override
public int compareTo(Delayed o) {
Task t = (Task)o;
return this.taskId.compareTo(t.getTaskId());
}
public Integer getTaskId() {
return taskId;
}
public void setTaskId(Integer taskId) {
this.taskId = taskId;
}
}
输出:
3----13
5----15
1----3
17----20
0----16
完成
get----0----16008
get----1----0
get----3----0
get----5----0
get----17----4000
一个不存储元素的阻塞队列,每一个put操作必须等待take操作,否则不能添加元素。支持公平锁和非公平锁。SynchronousQueue的一个使用场景是在线程池里。Executors.newCachedThreadPool()就使用了SynchronousQueue,这个线程池根据需要(新任务到来时)创建新的线程,如果有空闲线程则会重复使用,线程空闲了60秒后会被回收
一个支持元素优先级排序的无界队列,默认使用元素的compareTo方法进行比较来确定元素的优先级,也可以自定义实现compareTo()方法来指定元素排序规则,不能保证同优先级元素的顺序
BlockingQueue blockingQueue = new PriorityBlockingQueue<>(10,(a,b)->{return a.compareTo(b);});
new Thread(new Runnable() {
@Override
public void run() {
for (;;) {
try {
Integer element = blockingQueue.take();
System.out.println(element);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
Random r = new Random();
for (int i = 0; i < 50; i++) {
int a = r.nextInt(100);
try {
blockingQueue.put(a);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("完成");
一个由链表结构组成的无界阻塞队列,相当于其它队列,LinkedTransferQueue实现TransferQueue接口,而TransferQueue
继承BlockingQueue接口
TransferQueue接口的方法:
一个由链表结构组成的双向阻塞队列。队列头部和尾部都可以添加和移除元素,多线程并发时,可以将锁的竞争最多降到一半。
public FutureTask(Callable callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
public FutureTask(Runnable runnable, V result) {
this.callable = Executors.callable(runnable, result);
this.state = NEW; // ensure visibility of callable
}
FutureTask ft = new FutureTask<>(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务完成了");
}, "success");
ft.run();
try {
//此方法会阻塞
String s = ft.get();
System.out.println(s);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch
AQS只是一个框架,具体资源的获取/释放方式交由自定义同步器去实现,AQS这里只定义了一个接口
ReentrantLock 内部有两个内部类,分别是 FairSync 和 NoFairSync,对应公平锁和非公平锁。他们都继承自 Sync。Sync 又继承自AQS
AQS 中有两个重要的成员:
AQS定义两种资源共享方式
请求锁时有三种可能:
a)自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了
b)自定义同步器实现时主要实现以下几种方法:
c)同步器的实现是AQS核心(state资源状态计数):
d)ReentrantReadWriteLock实现独占和共享两种方式:
自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire+tryRelease或tryAcquireShared+tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock
通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可
Semaphore类中比较重要的几个方法:
上面4个方法都会被阻塞,如果想立即得到执行结果,可以使用下面几个方法:
例子:
若一个工厂有5台机器,但是有8个工人,一台机器同时只能被一个工人使用,只有使用完了,其他工人才能继续使用。那么我们就可以通过Semaphore来实现
await
方法的另一个重载,传入等待的超时时间,可以不用一直阻塞
作用于静态方法:用类的class对象作为锁
作用于实例方法:用this对象作为锁
作用于实例代码块:用this对象作为锁
每个对象都有个monitor对象,加锁就是在竞争monitor对象
代码块加锁是在前后分别加上monitorenter和monitorexit指令来实现的
方法加锁是通过一个标记位ACC_SYNCHRONIZED 来判断的
monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因
monitor 对象由 C++ 实现。其中有三个关键字段:
_owner 记录当前持有锁的线程
_EntryList 是一个队列,记录所有阻塞等待锁的线程
_WaitSet 也是一个队列,记录调用 wait() 方法并还未被通知的线程。
Monitor的操作机制如下:
多个线程竞争锁时,会先进入 EntryList 队列。竞争成功的线程被标记为 Owner。其他线程继续在此队列中阻塞等待
如果 Owner 线程调用 wait() 方法,则其释放对象锁并进入 WaitSet 中等待被唤醒。Owner 被置空,EntryList 中的线程再次竞争锁
如果 Owner 线程执行完了,便会释放锁,Owner 被置空,EntryList 中的线程再次竞争锁
ReentrantLock是Lock接口的实现类,Lock接口和ReadWriteLock接口无继承关系
读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,多个写锁互斥。这是由jvm自己控制的,你只要上好相应的锁即可
ReadWriteLock是接口,ReentrantReadWriteLock是具体的实现类
公平锁是指多个线程按照申请锁的顺序来获取锁
非公平锁性能比公平锁高5~10倍,因为公平锁需要在多核的情况下维护一个队列
synchronized是非公平锁,ReentrantLock 默认的lock()方法采用的是非公平锁
分段锁也并非一种实际的锁,而是一种思想,ConcurrentHashMap是学习分段锁的最好实践
分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作
当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在哪一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入
假如一把锁锁了n个地方,那么只要得到这把锁,那n个地方都可以访问
同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。ReentrantLock 和synchronized 都是 可重入锁
乐观锁适用于读比较多的场景,悲观锁适用于写比较多的场景,不加锁会带来大量的性能提升
乐观锁常见的两种实现方式:版本号机制或CAS算法实现
a)版本号机制
b)CAS算法
a)独占锁
独占锁模式下,每次只能有一个线程能持有锁,ReentrantLock就是以独占方式实现的互斥锁。独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性
b)共享锁
共享锁则允许多个线程同时获取锁,并发访问 共享资源,如:ReadWriteLock的读锁是共享锁。共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源
锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁
只用在有线程安全要求的程序上加锁
将大对象(这个对象可能会被很多线程访问),拆成小对象,大大增加并行度,降低锁竞争。降低了锁的竞争,偏向锁,轻量级锁成功率才会提高。最最典型的减小锁粒度的案例就是ConcurrentHashMap
最常见的锁分离就是读写锁ReadWriteLock,根据功能进行分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,即保证了线程安全,又提高了性能,具体也请查看[高并发Java 五] JDK并发包1。读写分离思想可以延伸,只要操作互不影响,锁就可以分离。比如LinkedBlockingQueue 从头部取出,从尾部放数据
通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。但是,凡事都有一个度,如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化
锁消除是在编译器级别的事情。在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作,多数是因为程序员编码不规范引起