一.synchronized关键字
1.线程安全问题
加上互斥锁以后,同一时间其他的线程就不能在访问这个资源了。
对java来说,synchronized关键字就满足互斥锁的特性,需要注意的是synchronized锁的是对象而不是方法。
2.synchronized获取的锁分类:对象锁和类锁
对象锁:同一个类的不同对象的对象锁是互不干扰的。
类锁:每个类只有一个class对象,所以每个类只有一个类锁。就算是不同的对象也使用同一个锁。
类锁和对象锁是互不干扰的:因为是二个不同的对象锁。
二.synchronized底层实现
1.Mark Word结构
一般而言synchronized使用的锁对象是存储在java对象头里面的,主要由Mark Word 和Class metadata address组成
Class metadata address:是对象指向它的类元数据的指针,JVM通过这个指针来确定这个对象是哪个类的实例,
Mark Word:存储对象自身的运行时数据,同时实现轻量锁和偏向锁的关键.
由于对象头的信息是与自身定义的数据没有关系的额外存储成本,所以Mark word被设计成为一个非固定的数据结构,以便存储跟多的有效数据。
从上图可以看到 Mark Word 包括重量级锁,重量级锁的指针指向 monitor 对象(监 视器锁)所以每一个对象都与一个 monitor 关联,当一个 monitor 被某个线程持有后, 它便处于锁定状态。谈及 monitor 来看一下 monitord 的实现,monitor 是由 ObjectMonitor 实现的
2.Monitor
在java的设计当中,每一个对象自创建出来,就自带一个看不见的锁,monitor锁,对象的“锁”一般我们也将其称为监视器(monitor);
当多个对象同时请求某个Monitor时,Monitor会设置几种状态来区分请求的线程。
3.自旋锁和自适应自旋锁
自旋锁:在许多的情况下,共享数据的锁定时间很短,挂起和恢复阻塞线程并不值得,在多CPU的情况下,完全可以让某个线程在门外等下,但不放弃cpu的时间片,等下但是不放弃CPU执行时间的行为即自旋锁,
在自旋的时候会占用CPU的时间片,如果太长,会影响性能。
自适应自旋锁:
如果在同一个锁对象上,自旋等待刚刚成功获取过锁,而且持有锁的线程正在运行当中,那么JVM就认为该锁自旋获取到锁的可能性很大,会自动增加等待时间,如果某个锁在以前自旋很少成功,那JVM就会减少自选的次数,以便浪费处理器资源。
4.消除锁
锁消除是JVM的另一种锁优化,
StringBuffer和append方法都是线程安全的,因为它是使用synchronized关键字修饰的。StringBuffer本身是加锁的,他在调用加锁的append方法时,JVM会消除和他没有竞争关系的append的锁。
5.锁粗化
对同一对象进行多次当量的加锁时可以考虑将锁粗化,只加一次锁就可以了
三.synchronized的四种状态
1.偏向锁
当一个线程访问同步块时,并获取锁的时候,头和栈帧当中的锁记录里面存储记录的ThreadID,以后该线程在进入和推出同步块的时候不需要进行加锁和解锁,从而提高了性能,对于不存在锁竞争的情况,偏向锁具有很好的优化性能,对与同一线程多次申请访问同一同步块的情况,是很多的
对于竞争比较激烈的情况,多个线程同时申请访问同一同步代码块的情况是很多的,此时使用偏向锁就得不偿失了。
2.轻量级锁
四.AQS(AbstractQueuedSynchronizer ) 队列同步器
参考:https://blog.csdn.net/weixin_41846500/article/details/86639640
AQS是java并发包的基础类,java并发包下很多API都是基于AQS来实现的加锁和释放锁等功能的,内核中的锁机制实现都是依赖AQS组件的。一般使用AQS的主要方式是继承.
1.AQS
这个AQS内部还有一个关键变量,用来记录当前加锁的是哪个线程,初始化状态下,这个变量是null。AQS对象内部还有一个核心的变量叫做state,是int类型的,代表了加锁的状态。初始状态下,这个state的值是0
线程1跑过来调用ReentrantLock的lock()方法尝试进行加锁,这个加锁的过程,直接就是用CAS操作将state值从0变为1。
如果之前没人加过锁,那么state的值肯定是0,此时线程1就可以加锁成功。
线程2会要进行加锁时会将自己放入AQS中的一个等待队列,因为自己尝试加锁失败了,此时就要将自己放入队列中来等待,等待线程1释放锁之后,自己就可以重新尝试加锁了,
AQS是如此的核心!AQS内部还有一个等待队列,专门放那些加锁失败的线程!
接着,线程1在执行完自己的业务逻辑代码之后,就会释放锁!他释放锁的过程非常的简单,就是将AQS内的state变量的值递减1,如果state值为0,则彻底释放锁,会将“加锁线程”变量也设置为null!
2.ReentrantLock再入锁
package com.imooc.basic.thread;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo implements Runnable{
/**
* 创建一个公平锁
*/
private static ReentrantLock rtLock=new ReentrantLock(true);
public void run() {
while(true){
//获取锁
rtLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"get Lock...");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//公平锁必须要显示的释放
rtLock.unlock();
}
}
}
public static void main(String[] args) {
ReentrantLockDemo reentrantLockDemo=new ReentrantLockDemo();
/**
* 创建同一个对象实例,此时这二个线程就会争夺同一把锁
*/
Thread thread1=new Thread(reentrantLockDemo);
Thread thread2=new Thread(reentrantLockDemo);
/**
* 2个线程争夺同一把锁
*/
thread1.start();
thread2.start();
}
}
公平性是防止线程饥饿情况出现的一种方式
线程饥饿,某个线程长时间等待,但获取不到锁的情况
和synchronized相比,ReentrantLock可以和普通对象一样使用。可以利用它来提供各种遍历的方法,进行精细的同步操作,可以说ReentrantLock将synchronized变成了可控的对象。
带超时的获取锁尝试:在尝试获取锁的几秒之后,未得到锁几秒之后就会退出锁的尝试了。
Unsafe可以用来在任意内存地址处读写数据。
五.JMM(java memory model)
1.主内存和工作内存
JMM 定义了程序中各个共享变量的访问规则,即在虚拟机中将变量存储到内存和从内存读 取变量这样的底层细节.并提供了内置解决方案(happen-before 原则,解决了JMM的可见性问题,操作A在内存上做的操作对操作B是可见的)及其 外部可使用的同步手段(synchronized/volatile 等),确保了程序执行在多线程 环境中的应有的原子性,可视性及其有序性。 意在解决在并发编程可能出现的线 程安全问题
工作内存当中存储的是主内存当中的变量的拷贝,每一线程只能访问自己的工作内存
java内存模型规定所有的变量都存储在主内存当中。是共享内存区域,所有的线程共享的,对变量的操作,读取必须要在工作内存当中进行,
首先将变量从主内存拷贝到自己的工作内存当中,然后对变量进行操作,操作完毕以后在将变量写会主内存。不能直接操作主内存当中的变量(volatile 变量仍然有工作内存的拷贝,但是由于 它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般),工作内存是每个线程的私有区域,线程之间无法访问其他的工作内存,线程之间的通信只能通过主内存来完成,
2.JMM对共享变量的访问的内置结局方案happens-before
happens-before是用来判断数据是否存在竞争,线程是否安全的主要依据.
happens-before原则指的代码的执行结果是有序的(JVM会对程序的指令进行重排序,但是不会影响程序的执行结果)
3.volatile关键字
volatile运算操作在多线程环境当中并不能保证线程安全性。
1.volatile是如何解决可见性的
CAS算法可以说是完美的解决了volatile的可见性问题 。取出主内存当中的值,在工作内存当中修改以后可以调用CAS方法将修改后的值写入主内存当中。
2.volatile的禁止优化重排
内存屏障:是一个CPU指令,
4.volatile和synchronized的区别
六.CAS(Compare and sweep)比较并替换
参考:https://blog.csdn.net/u010862794/article/details/72892300
保证线程安全的方式:
1.synchronized关键字
2.使用原子类操作,原子操作类,顾名思义,就是保证某个操作的原子性,
1.volatile保证可见性
2.CAS保证原子性
CAS(Compare and Swap),即比较并替换,实现并发算法时常用到的一种技术,是一种实现线程安全的高效算法。在JAVA中,sun.misc.Unsafe 类提供了硬件级别的原子操作来实现这个CAS。 java.util.concurrent 包下的大量类都使用了这个 Unsafe.java 类的CAS操作。
CAS 的思想很简单:三个参数,一个当前内存值 V、旧的预期值 A、即将更新的值 B,当 且仅当预期值 A 和内存值 V 相同时,将内存值修改为 B 并返回 true,否则什么都不做,并 返回 false。
实现原理:CAS 中有 Unsafe 类中的 compareAndSwapInt 方法,Unsafe 类中的 compareAndSwapInt,是一个本地方法,该方法的实现位于 unsafe.cpp 中 ,java.util.concurrent.atomic 包下的类大多是使用CAS操作来实现的
ABA问题解决的方案:在Compare阶段不仅比较预期值和此时内存中的值,还比较两个比较变量的版本号是否一致,只有当版本号一致才进行后续操作,这样就完美的解决了ABA问题!
七.线程池
在web开发当中服务器需要接收并处理请求,所以会为一个请求来分派一个线程来进行处理,如果并发的请求数量很多,
但每个线程执行的时间很短,这样就会频繁的创建和销毁线程,这是十分消耗吸能的,开发者一般会利用Excutors提供的通用线程池创建方法,去创建的不同配置的线程池,
1.JUC包下面的Executors提供了5种创建线程池的方法
注意:JUC下的Executors类当中的下面几个静态方法都可以创建线程池,即调用
JUC包下面的Executors类的以下几个静态方法即可创建线程池连接
1.线程池的工作原理:
线程池的工作原理:
线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于 corePoolSize;如果当前线程数为 corePoolSize,继续提交
的任务被保存到阻塞队列中,等待被执行; 如果阻塞队列满了,那就创建新的线程执行当前任务;直到线程池中的线程数达到 maxPoolSize,这 时再有任务来,只能执行 reject()处理该任务。
2.线程池的底层实现
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
由此可见,线程池的底层都是通过ThreadPoolExecutor来实现的。
ThreadPoolExecutor(corePoolSize,maxPoolSize,keepAliveTime,timeUnit,workQueue,threadFactory,handle);
corePoolSize:
线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于 corePoolSize;如果当前线程数为 corePoolSize:继续提交的任务被保存到阻塞队列中,等待被执行; 如果执行了线程池的 prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。
maximumPoolSize:
线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务, 前提是当前线程数小于 maximumPoolSize;
keepAliveTime
线程空闲时的存活时间,即当线程没有任务执行时,继续存活的时间;默认情况下,该参数只在线程 数大于 corePoolSize 时才有用;
unit:keepAliveTime 的单位;
workQueue
用来保存等待被执行的任务的阻塞队列,且任务必须实现 Runable 接口,在 JDK 中提供了如下阻塞 队列:
1、ArrayBlockingQueue:基于数组结构的有界阻塞队列,按 FIFO 排序任务;
2、LinkedBlockingQuene:基于链表结构的阻塞队列,按 FIFO 排序任务,吞吐量通常要高于 Array BlockingQuene;
3、SynchronousQuene:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除 操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于 LinkedBlockingQuene;
4、priorityBlockingQuene:具有优先级的无界阻塞队列;
threadFactory:
创建线程的工厂,通过自定义的线程工厂可以给每个新建的线程设置一个具有识别度的线程名。
handler:
线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策 略处理该任务,线程池提供了 4 种策略:
1、AbortPolicy:直接抛出异常,默认策略;
2、CallerRunsPolicy:用调用者所在的线程来执行任务;
3、DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
4、DiscardPolicy:直接丢弃任务;