一.synchronize关键字
使用方式:
1)修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
2)修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
3)修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁
举例单例对象双重校验锁
public class Singleton{
private volatile static Singleton singleton;
private Singleton(){}
public static Singleton getSingleton(){
//先判断对象是否已经实例过,没有实例化过才能进入加锁代码
if (singleton==null){
//类对象加锁
synchronized (Singleton.class){
if (singleton==null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
注意
singleton采用volatile关键字 singleton = new singleton() 这段代码分三步执行
为singleton分配内存空间
初始化singleton
将singleton指向分配的内存地址
但是由于JVM具有指令重排
的特性执行顺序可能会发生改变,使用volatile
可以禁止JVM的指令重排,保证在多线程的环境下也能正常运行
synchronize关键字底层原理
1)同步语句块情况
public class synchronizeDemo {
public void method() {
synchronize(this){
System.out.println("synchronize 代码块");
}
}
}
通过JDK自带的命令查看类相关字节码信息
synchronize同步语句块实现使用的是monitorenter
和monitorexit
指令
monitorenter指令指向同步代码块的开始位置,monitorexit指令指明同步代码块的结束位置
当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权
当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1
相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放
如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止
2)synchronize修饰方法的情况
public class synchronizeDemo2 {
public synchronize void method() {
System.out.println("synchronize 方法");
}
}
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是
ACC_SYNCHRONIZED
标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED
访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用
synchronize和volatile
volatile关键字
是线程同步的轻量级实现,所以volatile性能比synchronize关键字要好
volatile
关键字只能用于变量而synchronize
关键字可以修饰方法以及代码块
多线程访问volatile
关键字不会发生阻塞,而synchronize
关键字会发生阻塞
volatile
关键字能保证数据的可见性,但不能保证数据的原子性。synchronize
皆可保证
volatile
关键字主要用于解决变量在多个线程之间的可见性,而synchronize
关键字解决的是多个线程之间访问资源的同步性
二.线程池
线程池提供了一种限制和管理资源。每个线程池还维护一些基本统计信息,例如已完成任务的数量
使用线程池好处:
1)降低资源消耗
,通过重复利用已创建的线程降低线程创建和销毁造成的消耗
2)提高响应速度
,当任务到达时,任务可以不需要等到线程创建就能立即执行
3)提高线程的可管理性
,线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统稳定性,使用线程池可以进行统一的分配,调优与监控
实现Runnable接口和Callable接口的区别
实现的runnable接口和callable接口实现类都可以被ThreadPoolExecutor或ScheduleadThreadPoolExecutor执行
两者的区别在于Runnable接口不会返回结果但是callable接口可以返回结果
工具类Executors可以实现Runnable对象和Callable对象之间的相互转换
(Executors.callable(Runnable task)或Executors.callable(Runnable task,Object resule) )
execute()方法与submit()方法
1)execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否
2)submit()方法用于提交需要返回值的任务,线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout ,Timeout unit)方法则会阻塞当前线程一段时间后立即返回,这时候任务有可能没有执行完
如何创建线程池
阿里巴巴Java开发手册
中强制建议线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor方式
Executor返回线程池对象的弊端:
FixedThreadPool和SingleThreadExecutor:允许请求的队列长度为Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM
(全称“Out Of Memory”,翻译成中文就是“内存用完了”)
CachedThreadPool和ScheduleadThreadPool:允许创建的线程数据为Integer.MAX_VALUE,可能创建大量线程,从而
导致OOM
(全称“Out Of Memory”,翻译成中文就是“内存用完了”)
建议创建方式:通过构造方法实现
三.Atomic原子类
Atomic 翻译成中文是原子的意思,这里 Atomic 是指一个操作是不可中断的
即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰
所以,所谓原子类说简单点就是具有原子/原子操作特征的类
并发包 java.util.concurrent 的原子类都存放在 java.util.concurrent.atomic 下
JUC包中的原子类四种类型
基本类型
使用原子的方式更新基本类型
AtomicInteger | 整形原子类 |
AtomicLong | 长整型原子类 |
AtomicBoolean | 布尔型原子类 |
数组类型
使用原子的方式更新数组里的某个元素
AtomicIntegerArray | 整形数组原子类 |
AtomicLongArray | 长整形数组原子类 |
AtomicReferenceArray | 引用类型数组原子类 |
引用类型
AtomicReference: | 引用类型原子类 |
AtomicStampedRerence | 原子更新引用类型里的字段原子类 |
AtomicMarkableReference | 原子更新带有标记位的引用类型 |
对象的属性修改类型
AtomicIntegerFieldUpdater | 原子更新整形字段的更新器 |
AtomicLongFieldUpdater | 原子更新长整形字段的更新器 |
AtomicStampedReference | 原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新可能出现的ABA问题 |
AtomicInteger使用
AtomicInteger类常用方法
public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程 在之后的一小段时间内还是可以读到旧的值
使用AtomicInteger之后,不用对increment()方法加锁也可保证线程安全
class AtomicIntegerTest {
private AtomicInteger count = new AtomicInteger();
//使用AtomicInteger之后,不需要对该方法加锁,也可以实现线程安全
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
简单描述下AtomicInteger类的原理
// setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替换”的作用)
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
AtomicInteger类主要利用CAS(compare and swap)+volatile和native方法
来保证原子操作,从而避免synchronize的高开销,执行效率大为提升
CAS
的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值
UnSafe 类的 objectFieldOffffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffffset
另外 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值
四.AQS
AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面
AQS是一个用来构建锁和同步器的框架
,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如ReentrantLock,Semaphore,ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的
原理:
AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资设
置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中
CLH
(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列
(虚拟的双向队列即不存在队列实例,仅存在结
点之间的关联关系
AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配
AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作
AQS使用CAS对该同步状态进行原子操作实现对其值的修改
//共享变量,使用volatile修饰保证线程可见性
private volatile int state;
状态信息通过protected类型的getState,setState,compareAndSetState进行操作
//返回同步状态的当前值
protected final int getState() {
return state;
}
// 设置同步状态的值
protected final void setState(int newState) {
state = newState;
}
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
AQS定义两种资源共享方式
1)Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁与非公平锁
公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
非公平锁:当线程要获取锁时,无视队列顺序直接抢锁,谁抢到谁拿到锁
2)Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch,Semaphore,
CountDownLatCh,CyclicBarrier,ReadWriteLock
ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读
不同的自定义同步器争用共享资源的方式也不同
自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了
AQS底层使用模板方法模式
同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应
用):
1)使用者继承AbstractQueuedSynchronizer并重写指定的方法(这些重写方法很简单,无非是对于共享资源state的获取和释放)
2)将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法,这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用
自定义同步器需重写下面几个AQS提供的模板方法
isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它
tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false
tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false
默认情况下,每个方法都抛出 UnsupportedOperationException
这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞
AQS类中的其他方法都是fifinal ,所以无法被其他类使用,只有这几个方法可以被其他类使用
AQS组件总结:
Semaphore(信号量)-允许多个线程同时访问
:synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源
CountDownLatch(倒计时器)
:CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行
CyclicBarrier(循环栅栏)
:CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活
CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞