第13章 线程安全与锁优化

xiaoxiao2021-02-28  97

1、概述

如何实现“高效并发”,首先需要保证并发的正确性,然后在此基础上来实现高效。

2、线程安全

定义:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。

2.1 java语言中的线程安全

按照线程安全的“安全程度”由强至弱来排序,我们可以将java语言中各种操作共享的数据分为以下5类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。

2.1.1 不可变(Immutable)

最简单就是把对象中带有状态的变量都声明为final

2.1.2 绝对线程安全

在Java API中标注自己是线程安全的类,大多数都不是绝对的线程安全。例如java.util.Vector是一个线程安全的容器,即使所有方法都被修饰成同步,也不意味着调用它的时候永远都不再需要同步手段。

import java.util.Vector; public class Main { private static Vector<Integer> vector = new Vector<Integer>(); public static void main(String[] args) throws Throwable{ while(true){ for (int i=0; i<10; i++){ vector.add(i); } Thread removeThread = new Thread(new Runnable(){ public void run() { for (int i=0; i<vector.size(); i++){ vector.remove(i); } } }); Thread printThread = new Thread(new Runnable(){ public void run() { for (int i=0; i<vector.size(); i++){ System.out.println(vector.get(i)); } } }); removeThread.start(); printThread.start(); //不要同时产生过多的线程,否则会导致操作系统假死 while (Thread.activeCount()>20); } } }

日志:

它只保证某个方法是同步,并不能保证所有方法同步。有可能一线程在执行remove()方法,刚好有一个线程在get()方法,获取的元素刚好是remove 的元素。

修改一下

import java.util.Vector; public class Main { private static Vector<Integer> vector = new Vector<Integer>(); public static void main(String[] args) throws Throwable{ while(true){ for (int i=0; i<10; i++){ vector.add(i); } Thread removeThread = new Thread(new Runnable(){ public void run() { synchronized (vector) { for (int i=0; i<vector.size(); i++){ vector.remove(i); } } } }); Thread printThread = new Thread(new Runnable(){ public void run() { synchronized (vector) { for (int i=0; i<vector.size(); i++){ System.out.println(vector.get(i)); } } } }); removeThread.start(); printThread.start(); //不要同时产生过多的线程,否则会导致操作系统假死 while (Thread.activeCount()>20); } } }

2.1.3、相对线程安全

对象单独的操作是线程安全的,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。如上例子

2.1.4、线程兼容

本身不是线程安全,但是可以通过同步手段实现线程安全的线程的java类,例如ArrayList HashMap等

2.1.5、线程对立

不管采用什么同步措施都不能保证线程安全。例如 Thread类 suspend() 和resume()方法 这里已经被jdk废弃了。

2.2、线程安全的实现方法

2.2.1.互斥同步(Mutual Exclusion & Synchronization)又称为 阻塞同步 悲观锁

保证共享数据在同一时刻只有一条(或者一些,使用信号量)线程使用 (类似停车场)

临界区(Critical Section)、互斥量(Mutex)、和信息量(Semaphore)都是主要的互斥实现方式。互斥是因,同步是果,互斥是方法,同步是目的

最基本的互斥同步手段就是synchronized关键字,synchronized关键字经过编译后,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码指令,这个两个字节码需要一个reference类型的参数来指明要锁定和解锁的对象,获取对象的锁,锁计数器加1,反之减一,当计数器为0时,锁就被释放了。

重入锁(ReentrantLock)来实现同步。代码写法上有点区别,一个表现为API层面的互斥锁(lock())和unlock()方法配合try/finally语句块来完成,一个表现为原生语法层面的互斥锁,不过ReentrantLock比synchronized增加了一些高级功能,主要有以下:等待可中断、可实现公平锁,以及锁可以绑定多个条件。

2.2.2 、非阻塞同步 (乐观锁),

没有线程竞争资源,如果有刚好有线程占用,那就再进行其他的补偿措施(最常见的补偿措施就是不断地重试,直到试成功为止)(坚持不懈直到成功),它不需要线程挂起,对于硬件如何实现设置

测试并设置(Test-and-Set)获取并增加(Fetch-and-Increment)交换(Swap)比较并交换(Compare-and-Swap CAS)加载链接/条件存储(Load-Linked/Store-Conditional LL/SC)

import java.util.concurrent.atomic.AtomicInteger; public class AtomicTest { public static AtomicInteger race = new AtomicInteger(0); public static void increase(){ race.incrementAndGet(); System.out.println(race); } private static final int THREADS_COUNT= 20; public static void main(String[] args) { Thread[] threads= new Thread[THREADS_COUNT]; for (int i=0; i<THREADS_COUNT; i++){ threads[i] = new Thread(new Runnable(){ public void run() { for(int i=0; i<10000; i++){ increase(); } } }); threads[i].start(); } } } 以上就是AtomicInteger CAS  操作, 

运行结果为:200000

但是会出现ABA问题,虽然一线程访问前是A,访问后还是A,但是在此期间可能有线程改为B,然后又改成了A, 为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”。

2.2.3、无同步方案

不需要同步操作

2.2.3.1、可重入代码(Reentrant Code) : 就是无论何时暂停,何时开始都不会出现错误

2.2.3.2、线程本地存储(Thread Local Storage): java.lang.ThreadLocal, 就是线程内部使用的变量

3、锁优化

技术: 适应性自旋(Adaptive Spinning)、锁消除(Lock Elimination)、锁粗化(Lock Coarsening)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)

3.1、 自旋锁与自适应自旋

-XX:+UseSpinning 参数开启,自旋就是一直在运行(等待线程释放锁,这样避免唤醒和休眠的性能损耗),只是在空转,自旋次数的默认是10次,如果自旋10次没有等待线程释放锁就挂起线程,用户使用参数-XX:PreBlockSpin设置自旋次数。自适应自旋实事求是,根据实际情况进行自旋。

3.2 锁消除

不能存在竞争的数据进行锁的消除

3.3、 锁粗化

就是同一个线程对于一个对象进行重复加锁与解锁(在没有线程竞争的情况下)。锁粗化,就是在开始加一次锁就行了。

3.4、轻量级锁

相对传统同步(重量级锁)来说,它是通过对象头部有标记信息(Mark Word) 来标记当前的锁情况,主要分为五种

未锁定(01)、轻量级锁(00)、重量级锁(10)、GC标记(11) 、可偏向(01)

类似于:刚开始是道德约束(对应轻量级锁)、如果道德约束不行,就法律约束(重量级锁),刚开始获取这个对象资源采用轻量级锁,当有线程来竞争,而当前线程占用,这时候就改变标记为10(重量级锁),你必须等着我执行完才能执行。

3.5、偏向锁

启用了偏向锁-XX:+UseBiaseLocking,就是第一个线程获取执行,如果有线程来竞争,就取消标记,改为轻量级锁,一直到重量级锁。

转载请注明原文地址: https://www.6miu.com/read-48324.html

最新回复(0)