我觉得安全就是从两方面出发喽:
写
操作, 则这段代码是临界区
,需要考虑线程安全当然了,看看大佬对线程安全的定义:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的
但是呢,如果各家关起门里用各家自己的不向外露出的东西,在如此的法治社会,打手的安全是可以得到一定的保障的。
如果呀一个可变的、共享的东东(这个资源可以被多个线程所持有或者说多个线程都可以访问这个资源),那么就存在线程(打手)安全问题了。
那么下来,最重要的就是,怎样保证线程的安全。共有四个方式,先上图:
第一个方式:不可变
(最简单的方式就是把对象中带有状态的变量都声明为final,这样在构造函数结束之后这些变量就是不可变的)。当我们共享的东西贴上封条,谁都不能不按规矩随意的改变它,那么也能实现一定的线程安全。
这种没有任何成员变量的类也就是无状态的类是线程安全的
。因为成员变量保存的数据也可以称为状态信息,因此没有成员变量就称之为[无状态]不可变的对象一定是线程安全的
。只要一个不可变的对象被正确的构建出来(没有发生this引用逃逸之类的情况),那么这个对象的外部可见状态永远也不会改变,永远也不会看到这个对象在多个线程之中处于不一致的状态。public class ImmutableExample {
public static void main(String[] args){
Map<String, Integer> map = new HashMap<>();
Map<String, Integer> unmodifiableMap = Collections.unmodifiableMap(map);
unmodifiableMap.put("a", hu);
}
}
//执行结果:报错。因为人家都说不可变了你还想改不给你报个错才怪
Exception in thread "main" java.lang.UnsupportedOperationException
at java.util.Collections$UnmodifiableMap.put(Collections.java:1457)
at ImmutableExample.main(ImmutableExample.java:9)
第二个方式
:就是整天会听到别人念叨的 互斥同步【互斥是实现同步的一种手段罢了,临界区、互斥量、信号量都是常见的互斥实现方式,Java中实现互斥同步的手段主要有synchronized和ReentrantLock两个关键字或者两个锁
】 呀、加个锁呀…Synchronized+ReentrantLock
临界区
【//counter++:++操作既有读也有写,并且counter也是一个共享资源,所以这个++操作就是一个临界区】竞态条件Race Condition
:多个线程在临界区内执行,由于代码的执行序列不同
而导致结果无法预测,称之为发生了竞态条件或者说避免临界区的竞态条件发生有多种方法
:
synchronized实际是用对象锁保证了临界区内代码的原子性
,临界区内的代码对外是不可分割的,不会被线程切换所打断。互斥解决了并发进程/线程对临界区的使用问题。只要一个进程/线程进入了临界区,其他试图想进入临界区的进程/线程都会被阻塞着,直到第一个进程/线程离开了临界区
。】。互斥同步也叫阻塞同步,因为互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题并发进程/线程在一些关键点上可能需要互相等待与互通消息,这种相互制约的等待与互通信息称为进程/线程同步
,而互斥是实现同步的一种手段(Java中最基本的互斥同步手段就是synchronized关键字
),互斥是方法,同步才是互斥的目的
****。
由于多线程执行操作共享变量的这段代码可能会导致竞争状态,因此我们将此段代码称为临界区(critical section)
,它是访问共享资源的代码片段,一定不能给多线程同时执行。通过互斥信号量的方式,就能保证临界区任何时刻只有一个线程在执行,就达到了互斥的效果
。】,还可以线程间的事件同步。】,信号量表示资源的数量,对应的变量是一个整型(sem)变量
只要把进入临界区的操作置于 P(s) 和 V(s) 之间,即可实现进程/线程互斥
:此时,任何想进入临界区的线程,必先在互斥信号量上执行 P 操作,在完成对临界资源的访问后再执行 V 操作。由于互斥信号量的初始值为 1,故在第一个线程执行 P 操作后 s 值变为 0,表示临界资源为空闲,可分配给该线程,使之进入临界区。若此时又有第二个线程想进入临界区,也应先执行 P 操作,结果使 s 变为负值,这就意味着临界资源已被占用,因此,第二个线程被阻塞
。并且,直到第一个线程执行 V 操作,释放临界资源而恢复 s 值为 0 后,才唤醒第二个线程,使之进入临界区,待它完成临界资源的访问后,又执行 V 操作,使 s 恢复到初始值 1。synchronized使用就会导致上下文切换,很耗时。(Synchronized会引起线程上下文切换并带来线程调度开销)
Java中的每个对象都可以把这个对象当作一个同步锁来用
)都有自己的同步锁~加锁本身已经保证了内存可见性
它采用互斥的方式让同一时刻至多只有一个线程能持有[对象锁],其它线程再想获取这个对象锁时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
把在synchronized块内(锁内)使用到的变量从线程的私有工作内存中清除
**,这样在synchronized块内使用到该变量时就不会从线程的私有工作内存中获取,而是直接从主内存中获取把在synchronized块内对共享变量的修改刷新到主内存
**虽然synchronized锁是写在方法上,但是跟方法没半毛钱关系,此时锁的是this对象,控制使得多个线程来串行使用这个对象
。作用于当前对象实例,进入同步代码前要获得当前对象实例的锁static 静态方法和 synchronized(xxx.class)
**代码块上都是是给 Class 类上锁
类对象内存中只有一份
synchronized 关键字加到 static 静态方法和 synchronized(类.class) 代码块上都是是给 Class 类上锁
;对给定对象加锁,控制使得多个线程来串行使用这个对象
,进入同步代码库前要获得给定对象的锁synchronized 底层实现原理
:
本质都是对对象监视器 monitor 的获取
线程试图获取锁也就是获取 monitor
【monitor对象是OS提供的,存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因**】的持有权
。其内部包含一个计数器【在执行monitorenter时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1
。】
每个Java对象都可以关联一个Monitor对象
,如果使用synchronized给对象上锁(重量级)
之后,这个被上锁的对象的对象头中的Mark Word中就被设置指向Monitor对象的指针
每个对象中都内置了一个 ObjectMonitor对象
。另外,wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因Monitor是OS提供的,如果我们每次进入synchronized都要获取Monitor锁,这多影响性能呀【每次都要上Monitor锁很麻烦】,这不和上下文切换是一样的嘛
。所以锁升级就是Monitor的替代品。synchronized 属于重量级锁,效率低下
,锁是 cpu 一个总量级的资源,每次获取锁都要和 cpu 申请,非常消耗性能。因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间
,时间成本相对较高。synchronized 锁不仅能升级还能降级(不是所有的锁都能降级)
,具体的触发时机是在全局安全点(safepoint)中,执行清理任务的时候会触发尝试降级锁。当锁降级时,主要进行了以下操作:
插播一条新闻:JVM对synchronized的优化有哪些?从最近几个jdk版本中可以看出,Java的开发团队一直在对synchronized优化,其中最大的一次优化就是在jdk6的时候,新增了两个锁状态【锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态
,他们会随着竞争的激烈而逐渐升级】,通过锁消除、锁粗化、自旋锁等方法使用各种场景,给synchronized性能带来了很大的提升。
总是由同一线程多次获得
,因此如果每次都要竞争锁会付出很多没必要的代价,那么此时就是偏向锁
。【如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也就变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查Mark Word的锁标记位为偏向锁以及当前线程ID等于Mark Word的ThreadID即可,这样就省去了大量有关锁申请的操作。】记录
偏向的锁的threadID,因为偏向锁不会主动释放锁
,因此以后线程1再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致
,
使用CAS操作去消除同步使用的互斥量
)轻量级锁是由偏向锁升级而来,当存在第二个线程申请同一个锁对象时,偏向锁就会立即升级为轻量级锁
。注意这里的第二个线程只是申请锁,不存在两个线程同时竞争锁,可以是一前一后地交替执行同步块。也就是在没有多线程竞争的前提下减少传统的重量级锁使用OS互斥量产生的性能消耗。当同一时间有多个线程竞争锁时,锁就会被升级成重量级锁,此时其申请锁带来的开销也就变大
。重量级锁一般使用场景会在追求吞吐量,同步块或者同步方法执行时间较长的场景。public static String concatString(String s1, String s2, String s3){
return s1 + s2 + s3;
}
JDK1.5之前,编译器会对String的这个拼接方法concatString()进行自动优化,会转化成为StringBuffer对象的连续append()操作:优化完如下:
public static String concatString(String s1, String s2, String s3){
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
//每个append()方法中都有一个同步块,JVM虚拟机会观察变量sb然后也就会很快发现这个sb这个引用变量的动态作用域被限制在concatString()方法内部。也就是说,sb的所有引用永远不会逃逸到concatString()方法之外,其他线程无法访问到他,因此可以进行消除。
//综下所述,把不安全的线程代码改写成线程安全的代码--改写原来类+main方法中实例化改写后的类
public class AtomicExample{
private AtomicInteger cnt = new AtomicInteger();
public void add(){
cnt.incrementAndGet();//也可以用getAndIncrement(),俩方法一样
}
public int get(){
return cnt.get();
}
public static void main(String[] args) throws InterruptedException{
final int threadSize = 1000;
//main方法就改这一句就行
AtomicExample atomicExample = new AtomicExample();
final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
ExecutorService executorService = Executors.newCachedThreadPool();
for(int i = 0; i < threadSize; i++){
executorService.execute(() -> {
atomicExample.add();
countDownLatch.countDown ();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println(atomicExample.get());
}
}
执行结果:1000,不管执行多少次,都一直稳定是1000o
Java 中的线程是与操作系统中的线程 一对应的
,所以当一个线程在获取锁(比如独占锁)失败后,会被切换到内核状态而被挂起。当该线程获取到锁时又需要将其切换到内核状态而唤醒该线程,挂起线程和恢复或者叫唤醒线程的操作都需要转入内核态中完成
。而从用户状态切换到内核状态的开销是比较大的,在一定程度上会影响并发性能。并且很多应用上共享数据的锁定状态只会持续很短一段时间,为了这段时间去挂起和恢复并不值得许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得
,通过让线程执行循环等待锁的释放,不让出CPU。如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式。但是它也存在缺点:如果锁被其他线程长时间占用,一直不释放CPU,会带来许多的性能开销。自旋锁是通过 CPU 提供的 CAS 函数(Compare And Swap),在用户态完成加锁和解锁操作,不会主动产生线程上下文切换,所以相比互斥锁来说,会快一些,开销也小一些
。
也就是一直自旋,利用 CPU 周期,直到锁可用
】。这里的忙等待可以用 while 循环等待实现,不过最好是使用 CPU 提供的 PAUSE 指令来实现忙等待,因为可以减少循环等待时的耗电量
。
需要抢占式的调度器
(即不断通过时钟中断一个线程,运行其他线程)。否则,自旋锁在单 CPU 上无法使用,因为一个自旋的线程永远不会放弃 CPU。在多核系统下一般不会主动产生线程切换,适合异步、协程等在用户态切换请求的编程方式,但如果被锁住的代码执行时间过长,自旋的线程会长时间占用 CPU 资源,所以自旋的时间和被锁住的代码执行的时间是成正比的关系
CAS 函数就把这两个步骤合并成一条硬件级指令,形成原子指令,这样就保证了这两个步骤是不可分割的,要么一次性执行完两个步骤,要么两个步骤都不执行。
】
第二种方式中除了上面的Synchronized
,再就是JDK实现的JUC包中的重入锁ReentrantLock
(可重入锁,ReetrantLock实现依赖于AQS(AbstractQueuedSynchronizer),换句话说ReentrantLock最终还是使用AQS来实现的,ReetrantLock主要依靠AQS维护一个阻塞队列,多个线程对加锁时,失败则会进入阻塞队列。等待唤醒,重新尝试加锁
。),ReenTrantLock是Lock接口的一种子实现类(继承自AQS实现的独占锁ReentrantLock时,定义当 status为0表示锁空闲,为1表示锁己经被占用)独占锁ReentrantLock的实现是:当一个线程获取了ReentrantLock的锁后,在AQS内部会首先使用CAS操作把state状态值从0变为1,然后设置当前锁的持有者为当前线程,当该线程再次获取锁时发现他自己就是锁的持有者就会把状态值从1变为2(也就是设置可重入次数),而当另一个线程获取锁时发现自己并不是该锁的持有者,这个线程就会被放入AQS阻塞队列后挂起
因为ReentrantLock这个可重入锁的底层就是「使用 AQS 去实现」的
。释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念
。但要注意,获取多少次就要释放多少次,这样才能保证 state 是能回到零态的。当持有锁的线程长期不释放锁时正在等待的线程可选择放弃等待,改为处理其他事情
,可中断特性对处理执行时间非常长的同步块很有帮助
ReentrantLock有两种模式,一种是公平锁,一种是非公平锁
:支持公平锁和非公平锁:(ReentrantLock 提供了公平锁和非公平锁的实现),根据参数来决定其内部是一个公平锁还是非公平锁,默认是非公平锁。ReentrantLock 与synchronized 一样,都支持可重入
再次获取它自己己经获取的锁
时如果不被阻塞,那么我们说该锁是可重入的
**(也就是只要该线程获取了该锁,那么可以有限次数地进入被该锁锁住的代码)...
public void lock(){
sync.lock();//ReentrantLock的lock()委托给了sync类,根据创建ReentrantLock构造函数选择sync的实现是NonfairSync还是FairSync ,这个锁是一个非公平锁或者公平锁
}
...
/**
*sync的子类NonfairSync,也就是非公平锁
*/
final void lock() {
//(1)CAS设置状态值,,因为默认AQS的状态值为0,所以第一个调用Lock的线程会通过CAS设置状态值为1, CAS成功则表示当前线程获取到了锁
if (compareAndSetState(O, 1)){
setExclusiveOwnerThread(Thread.currentThread()); //setExclusiveOwnerThread设置该锁持有者是当前线程
} else{
//(2)调用AQS的acquire方法
acquire(1);//如果这时候有其他线程调用lock方法企图获取该锁,CAS会失败,然后会调用AQS的acquire方法。注意,传递参数为1,
}
}
public final void acquire(int arg) {
//(3)调用ReentrantLock重写的tryAcquire方法。AQS并没有提供可用的tryAcquire方法,tryAcquire方法需要子类自己定制化。所以这里会调用ReentrantLock重写的tryAcquire方法。
if(!tryAcquire(arg) &&
//tryAcquiref返回false会把当前线程放入AQS阻塞队列
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)){
selfinterrupt();
}
}
/**
*非公平锁的代码
*/
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current= Thread.currentThread() ;
int c = getState() ;
//(4)当前AQS状态值为0。查看当前锁的状态值是否为0,为0说明当前该锁空闲,那么就尝试CAS获取该锁,将AQS的状态值从0设置为1,并设置当前锁的持有者为当前线程然后返回true。如果当前状态值不为0说明该锁已经被某个线程持有
if (c == 0) {
if(compareAndSetState(O , acquires)) {
setExclusiveOwnerThread(current);
return true;
}//(5)当前线程是该锁持有者。查看当前线程是否是该锁的持有者,如果当前线程是该锁的持有者,则状态值加1,然后返回true 这里需要注意,nextc<0说明可重入次数溢出了。 如果当前线程不是锁的持有者则返回false,然后其会被放入AQS阻塞队列。
} else if (current== getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)//overflow
throw new Error (” Maximum lock count exceeded");
setState(nextc);
return true;
}//(6)
return false;
}
/**
*公平锁是怎么实现公平的。公平锁的话只需要看FairSync重写的Acquire方法。
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//(7)当前AQS状态值为0
if (c == 0) {
//(8)公平性策略,在设置CAS前添加了hasQueuedPredecessors 方法,该方法是实现公平性的核心代码
if (1hasQueuedPredecessors() &&
compareAndSetState(O , acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//(9)当前线程是该锁持有者
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}//(10)
return true;
}
}
/**
*如果当前线程节点有前驱节点则返回住时, 如果当前AQS队列为空或者当前线程节点是AQS的第一个节点则返回false。其中如果h==t则说明当前队列为空,直接返回false;如果h!=t并且s==null则说明有一个元素将要作为AQS的第一个节点入队列。之前enq函数的第一个元素入队列是两步操作:首先创建一个哨兵头节点,然后将第一个元素插入哨兵节点后,那么返回true,如果h!=t并且s!=null和s.thread != Thread.cunentThread()则说明队列里面的第一个素不是当前线程,那么返true
*/
public final boolean hasQueuedPredecessors() {
Node t = tail; //Read fields in reverse initial zation order
Node h = head;
Node s;
return h != t &&
((s = h.next) ==null || s.thread != Thread.currentThread()) ;
}
public class lockExample{
private Lock lock = new ReentrantLock();
public void func(){
lock.lock();
try{
for(int i = 0; i < 10; i++){
System.out.print(i + "");
}
} finally {
lock.unlock();//确保在finally中释放锁,从而避免发生死锁。
}
}
}
public static void main(String[] args){
LockExample lockExample = new LockExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> lockExample.func());
executorService.execute(() -> lockExample.func());
}
//执行结果:
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
那这两种方式有啥区别呢,做两个方面的比较瞅一瞅
可重入锁指的是在一个线程中可以多次获取同一把锁【自己可以再次获取自己的内部锁
】,比如: 一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁, 两者都是同一个线程每进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
synchronized是Java中的关键字,是JVM级别的锁
】 而 ReentrantLock 依赖于 API【ReentrantLock 就是Lock接口下的一个实现类,是API层面的锁
】
ReentrantLock 不是所有的 JDK 版本都支持
。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,也就说明Synchronized的使用比较方便简洁,它由编译器去保证锁的加锁和释放,因为 JVM 会确保锁的释放。ReenTrantLock需要手工声明来加锁和释放锁,最好在finally中声明释放锁。ReentrantLock等待可中断(可响应中断)
:通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。ReentrantLock可以指定是公平锁还是非公平锁:而synchronized只能是非公平锁
,所谓的公平锁就是先等待的线程先获得锁。 ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
选择性通知
(锁可以绑定多个条件):ReentrantLock类线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知
,在调度线程上更加灵活。 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知
”
synchronized控制同步的时候,可以配合Object的wait()、notify(),notifyAll() 系列方法可以实现等待/通知模式
。Lock提供了条件Condition接口,配合await(),signal(),signalAll() 等方法也可以实现等待/通知机制
。Condition可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活
。 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是 Condition 接口默认提供的。而 synchronized关键字就相当于整个 Lock 对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程
。synchronized 还会创建一个 内存屏障,内存屏障指令保证了所有 CPU 操作结果都会直接刷到主存中,从而保证操作的内存可见性
,同时也使得这个锁的线程的所有操作都 happens-before 于随后获得这个锁的线程的操作。
volatile性能肯定比synchronized关键字要好
两个或多个进程无限期的阻塞、相互等待
**的一种状态
另外,死锁说完了以后,加锁的几种情况,具体到代码中时:感觉有以下几点
TimeUnit.SECOND.sleep(...);
XXX dog = new Dog();
XXX cat = new Cat();
那么此时这两个方法哪怕分别被dog和cat两个调用而不是被同一个对象调用,这俩方法持有的锁都是一个锁,就是Class模板类那把锁而不是哪个对象的锁
第三种方式
:下面这种非阻塞同步。非阻塞同步类似一种乐观并发的策略,比如CAS除了上面第一种和第二种两种不可变和互斥同步,还有就是**第三种方式:下面这种非阻塞同步。**
**当一个线程没有获取到锁时这个线程会被阻塞挂起,这会导致线程上下文的切换和重新调度开销**,虽然Java 提供了非阻塞的volatile关键字来解决共享变量的可见性问题,这在一定程度上弥补了锁带来的开销问题,但是volatile只能保证共享变量的可见性,不能解决读->改->写等操作之间的原子性问题
),还有没有更好的方法呀**不用锁用能实现 value的内存可见性
**---------使用java.util.concurrent.atomic.AtomicInteger(JUC包下的原子性操作类),看下面:内部使用非阻塞 CAS算法实现的原子性操作类 AtomicLong
)类主要利用 CAS和 volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。 // 更新操作时提供“比较并替换”的作用
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try{
valueOffset = unsafe.objectFieldOffset(AutomicInteger.class.getDeclaredField("value"));
}catch(Exception ex){
throw new Error(ex);
}
}
private volatile int value;
private AtomicInteger cnt = new AtomicInteger();
//getAndIncrement()和incrementAndGet() 实现的是同一个作用,就是实现number++,底层用的是CAS
public void add(){
cnt.incrementAndGet();
}
public final int incrementAndGer(){
//getAndIncrement()方法的底层还是调用的是unsafe这个类的getAndAddInt(XX1, XX2, XX3)实现number++操作,那他是怎么实现的呢,咱看一下getAndAddInt(....)方法的三个形式参数代表什么不就行了
//XX1:代表哪个对象呀,一般常用this,就代表当前对象
//XX2:代表XX1对应对象的内存地址为XX2的那个地方
//XX3:每次加多少,你看例子程序中的var4=1,那就相当于期望值var5,var5加var4的话不就是var5+1嘛
//也不就对应上了咱们CAS(就是那个方法的使用规则嘛),当期望值为var5时,那就更新值为update,当前update=var5+var4=var5+1,不就实现了number++这个操作了嘛。只不过多了XX1和XX2两个形式参数嘛,指明的更具体更底层一点,不就是指到哪个对象的哪个内存地址上了嘛,没个啥。重要的就是compareAndSet(int expect, int update) 里面当前值=expect时,把这个当前值更新为update这个值
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
/**
*
*var1代表对象内存地址
*var2代表该字段相对对象内存地址的偏移
*var4代表操作需要加的数值,这里为1
*
*可以看到getAndAddInt()在一个循环中进行,发生冲突的做法是不断的进行重试
*/
public final int getAndAddInt(Object var1, long var2, int var4){
int var5;
do{
var5 = this.getIntVolatile(var1, var2);//得到旧的预期值
} while (!this.compareAndSwapInt(var1, var2, var5, var5 + var4));//通过调用compareAndSwapInt()来进行CAS比较,如果该字段内存地址中的值等于var5,那么就更新内存地址为var1+var2的变量为var4+var5
return var5;
}
CAS通过硬件保证了比较-更新操作的原子性
),针对多处理器操作而设计的一种特殊指令,用于管理对共享数据的并发访问。),CAS 是一种**无锁的非阻塞算法**的实现
JDK中的Unsafe类提供了一系列的compareAndSwap*方法
)。调用 Unsafe 类中的 CAS 方法, JVM 会帮助我们实现出 CAS 汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于 CAS是一种系统原语,原语属于操作系统用于范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,CAS 是一条 CPU 的原子指令,不会造成数据不一致问题。Unsafe 类使 Java 语言拥有了类似 C 语言指针一样操作内存空间
的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用 Unsafe 类会使得程序出错的概率变大,使得 Java 这种安全的语言变得不再“安全”,因此对 Unsafe 的使用一定要慎重。但是在 Unsafe 中,提供的下列接口可以直接进行内存操作
:堆外内存 ,是无法进行垃圾回收的
,需要我们把这些内存当做一种资源去手动调用freeMemory方法进行释放,否则会产生内存泄漏。通用的操作内存方式是在try中执行对内存的操作,最终在finally块中进行内存的释放。DirectByteBuffer 是 Java 用于实现堆外内存的一个重要类
,通常用在通信过程中做缓冲池,如在 Netty、MINA 等 NIO 框架中应用广泛。DirectByteBuffer 对于堆外内存的创建、使用、销毁等逻辑均由 Unsafe 提供的堆外内存 API 来实现。创建 DirectByteBuffer 的时候,通过 Unsafe.allocateMemory 分配内存、Unsafe.setMemory 进行内存初始化,而后构建 Cleaner 对象用于跟踪 DirectByteBuffer 对象的垃圾回收,以实现当 DirectByteBuffer 被垃圾回收时,分配的堆外内存一起被释放
。指令重排序可能会带来一个不好的结果,导致 CPU 的高速缓存和内存中数据的不一致
,而内存屏障(Memory Barrier)就是通过组织屏障两边的指令重排序从而避免编译器和硬件的不正确优化情况内存屏障是 CPU 为了防止代码进行重排序而提供的指令
,不同的硬件平台上实现内存屏障的方法可能并不相同。在 Java8 中,引入了 3 个内存屏障的函数,它屏蔽了操作系统底层的差异,允许在代码中定义、并统一由 JVM 来生成内存屏障指令,来实现内存屏障的功能
。StampedLock可以看成是读写锁的一个改进版本
。StampedLock 提供了一种乐观读锁的实现,这种乐观读锁类似于无锁的操作,完全不会阻塞写线程获取写锁,从而缓解读多写少时写线程“饥饿”现象。由于 StampedLock 提供的乐观读锁不阻塞写线程获取读锁,当线程共享变量从主内存 load 到线程工作内存时,会存在数据不一致问题。为了解决这个问题,StampedLock 的 validate 方法会通过 Unsafe 的 loadFence 方法加入一个 load 内存屏障
。指的就是Unsafe 中提供 allocateInstance 方法,仅通过 Class 对象就可以创建此类的实例对象,而且不需要调用其构造函数、初始化代码、JVM 安全检查等
。它抑制修饰符检测,也就是即使构造器是 private 修饰的也能通过此方法实例化,只需提类对象即可创建相应的对象。由于这种特性,allocateInstance 在 java.lang.invoke、Objenesis(提供绕过类构造器的对象生成方式)、Gson(反序列化时用到)中都有相应的应用使用该方法可以用来动态的创建一个匿名类,在Lambda表达式中就是使用 ASM 动态生成字节码,然后利用该方法定义实现相应的函数式接口的匿名类
。在 JDK 15 发布的新特性中,在隐藏类(Hidden classes)一条中,指出将在未来的版本中弃用 Unsafe 的defineAnonymousClass方法。当一个线程需要修改一个共享变量的值,完成这个操作需要先取出共享变量的值,赋给 A,基于 A 进行计算,得到新值 B,在用预期原值 A 和内存中的共享变量值进行比较,如果相同就认为其他线程没有进行修改,而将新值写入内存
)
这是因为有可能在线程1获取变量X的值A后,在执行CAS前,线程2使用 CAS修改了变量X的值为B,然后又使用CAS修改X变量的值为A
,所以虽然线程1执行CAS时变量X的值是A ,但是这个A己经不是线程1获取时的A了
。这就是ABA问题(ABA 问题:比如线程 A 去修改 1 这个值,修改成功了,但是中间 线程 B 也修改了这个值,但是修改后的结果还是 1,所以不影响 A 的操作,这就会有问题。可以用版本号来解决这个问题
。)
有个自旋次数的
,就是为了避开这个耗时问题
只能保证一个变量的原子操作
。很
除了上面三种,还有**第四种方式,那就是不使用同步
**,如下:
访问这个变量的每个线程都会有这个变量的一个本地拷贝
,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量
,从而起到 线程隔离的作用,避免了线程安全问题
。【//创建一个ThreadLocal变量:static ThreadLocal localVariable = new ThreadLocal<>()
;】ThreadLocal这个盒子中可以存储每个线程的私有数据,从而避免了线程安全问题
。
如果你创建了一个ThreadLocal变量,那么访问这个ThreadLocal变量的每个线程都会有这个ThreadLocal变量的本地副本
,这也是ThreadLocal变量名的由来。可以使用 get() 和 set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题
。public class ThreadLocal<T> {
//每当创建一个ThreadLocal对象,这个ThreadLocal.nextHashCode 这个值就会增长 0x61c88647。这个值很特殊,它是斐波那契数 也叫 黄金分割数。hash增量为 这个数字,带来的好处就是 hash 分布非常均匀。
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
static class ThreadLocalMap {
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
}
}
ThreadLocal的实现原理
:先看看ThreadLocal相关类图//Thread 类中有一个 threadLocals 和 一个 inheritableThreadLocals 变量,它们都是 ThreadLocalMap 类型的变量,可以把 ThreadLocalMap 理解为ThreadLocal 类实现的定制化的 HashMap。默认情况下这两个变量都是 null,只有当前线程调用 ThreadLocal 类的 set或get方法时才创建它们,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap类对应的 get()、set()方法
public class Thread implements Runnable {
//......
//与此线程有关的ThreadLocal值。由ThreadLocal类维护
ThreadLocal.ThreadLocalMap threadLocals = null;
//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
//......
}
往自己的ThreadLocalMap里存
,读也是以ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离
。Thread 类中有 threadLocals和inheritableThreadLocals,他们都是threadLocalMap类型的变量,设计为Map结构就是因为每个线程可以通过双列集合Map关联多个ThreadLocal变量了。
,threadLocalMap是一个定制化的HashMap。在默认情况下, 每个线程中的这两个变量都为 null ,只有当前线程第一次调用ThreadLocal的set方法或者get方法时才会创建它们俩)有一个类型为ThreadLocal的ThreadLocalMap的实例变量threadLocals,即 每个线程Thread都有一个属于自己的ThreadLocalMap
(Thread对象中持有一个ThreadLocal.ThreadLocalMap的成员变量,ThreadLocalMap是ThreadLocal的静态内部类
。)。ThreadLocal类型的本地变量存放在具体线程内存空间中
最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上
,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。 ThrealLocal 类中可以通过Thread.currentThread()获取到当前线程对象后,直接通过getMap(Thread t)可以访问到该线程的ThreadLocalMap对象。每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值【每个Thread中都具备一个ThreadLocalMap(Thread对象中持有一个ThreadLocal.ThreadLocalMap的成员变量),而ThreadLocalMap可以存储以ThreadLocal为 key ,Object 对象为 value 的键值对。】
。),每个线程在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//......
}
调用get方法时当前线程是thread 线程
,而这里调用set方法设置线程变量的是main线程
,两者是不同的线程,自然子线程访问时返回 null 。
访问共享变量时需要进行适当的同步
:同步的措施一般有:
//官方给了一个例子:
//这个类提供线程局部变量。 这些变量与其正常的对应方式不同,因为访问一个的每个线程(通过其get或set方法)都有自己独立初始化的变量副本。 ThreadLocal实例通常是希望将状态与线程关联的类中的私有静态字段(例如,用户ID或事务ID)。
//例如,下面的类生成每个线程本地的唯一标识符。 线程的ID在第一次调用ThreadId.get()时被分配,并在后续调用中保持不变。
import java.util.concurrent.atomic.AtomicInteger;
//只要线程存活并且ThreadLocal实例可以访问,每个线程都保存对其线程局部变量副本的隐式引用; 线程消失后,线程本地实例的所有副本都将被垃圾收集(除非存在对这些副本的其他引用)。
public class ThreadId {
// Atomic integer containing the next thread ID to be assigned
private static final AtomicInteger nextId = new AtomicInteger(0);
// Thread local variable containing each thread's ID
private static final ThreadLocal<Integer> threadId =
new ThreadLocal<Integer>() {
@Override protected Integer initialValue() {
return nextId.getAndIncrement();
}
};
// Returns the current thread's unique ID, assigning it if necessary
public static int get() {
return threadId.get();
}
}
// Integer不是线程安全的,所以每个线程都要有自己独立的副本
private static final ThreadLocal<Integer> threadId = ThreadLocal.withInitial(() -> new ThreadLocal<Integer>;
//创建一个ThreadLocal变量
static ThreadLocal<String> localVariable = new ThreadLocal<>();
public void set(T value){
//(1)获取当前线程
Thread t = Thread.currentThread();
//(2)将当前线程作为 key ,去查找对应的线程变量,找到则设置
ThreadLocalMap map = getMap(t);//使用当前线程作为参数 调用getMap(t)方法
if(map != null){//如果 getMap(t)的返回值不为空,则把 value 值设置到 threadLocals 中,也就是把当前
值放入当前线程的内存变量threadLocals中,key 就是当前 threadLocal 实例对象引用,value 是通过 set 方法传递的值
map.set(this, value);
}else{
(3)如果getMap(t)返回空值则说明是第一次调 set 方法,这时创建当前线程的threadLocals量(第一次调用就创建当前线程对应的HashMap)
createMap(t, value);
}
}
get():
public T get() {
//(4)获取当前线程
Thread t = Thread.currentThread();
//(5)获取当前线程的threadLocals变量
ThreadLocalMap map= getMap(t) ;
//(6)如果threadLocals不为null ,则返回对应本地变量的值
if(map != null) {
ThreadLocalMap.Entry e = map.getEntry(this) ;
if(e != null) (
@SuppressWarnings (”unchecked”)
T result= (T)e.value;
return result;
}
}
//(7)threadLocals 则初始化当前线程的 threadLocals 成员变量
return setInitialValue() ;
}
remove():
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread()) ;
if(m != null) {//如果当前线程的 threadLocals变量不为空,删除当前线程中指定的ThreadLocal 实例的本地变量
m.remove(this);
}
}
key 为 ThreadLocal 的弱引用(弱引用
:只要垃圾回收机制一运行,不管JVM的内存空间是否充足,都会回收该对象占用的内存
),弱引用比较容易被回收。因此,ThreadLocal(ThreadLocalMap的Key)被就有可能被垃圾回收器回收了,但是因为ThreadLocalMap生命周期和Thread是一样的,它这时候如果不被回收,就会出现这种情况:ThreadLocalMap的key没了,value还在
,这就会「造成了内存泄漏问题」。
这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露
。ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会 清理掉 key 为 null 的记录
。使用完 ThreadLocal方法后 最好手动调用remove()方法
巨人的肩膀:
深入理解Java虚拟机
狂神说视频
低并发编程
SpringForAll老师关于并发编程有三大核心问题:分工问题、同步问题、互斥问题的简单介绍