Java并发编程读书笔记——线程安全与锁优化

Java并发实现的三个特征:原子性、可见性、顺序性。

原子性操作的实现原理
(1)总线锁实现原子性:所谓的总线锁定就是使用处理器提供的一个LOCK#信号,当处理器在总线上输出此信号的时候,其他处理器的请求将会被阻塞住,那么该处理器就独占总线,其他的所有处理器都将被阻塞。显然总线锁定的代价是比较大的不管是不是指定共享变量相关的总线操作都会被阻塞等待。
(2)缓存行锁定实现原子性:缓存锁定是指,在内存区域如果被缓存在处理器的缓存行中,并且在LOCK操作期间被锁定,那么当他执行锁操作回写到内存的时候不需要再总线上声言LOCK#信号,而是修改内部的内存地址,并允许他的缓存一致性机制来保证操作的原子性和正确性,显然缓存比总线锁定的效率会高出很多。
注意:不使用缓存锁定的几种情况:
(1)当数据不是被存储在处理器的内部,或者是数据跨了多个缓存行(cache line)时,处理器会调用总线锁定。
(2)某些早期的处理器是不支持缓存锁定的。
针对以上两种特殊的情况,我们通过Intel处理器提供了很多Lock前缀指令来实现处理。

重排序包括Java虚拟机编译重排序和处理器重排序。

Java虚拟机中支持的8中原子性操作:read和load、use、assign、store和write、lock和unlock

注意:在某些32位机上对于像long、double这样的64位数的读写可能是非原子性的,可能是分为两次32位的读写来操作的,所以在这样的机器上可能对于这样的数据读写可能会出现问题,但是现在的机器上基本上这样的数据都是支持64位原子读写的。

hanpens-before规则:
程序次序规则、管程锁定规则、volatile规则、线程启动规则、线程终止规则、线程终端规则、对象终结规则、传递性规则。

操作系统中线程的实现:内核实现(kernel 线程)(KLT)、轻量级进程(LWP)、用户线程(UT)

线程状态:
NEW:

Runnable:包括了操作相同里面的running和ready两种状态。所以处于这个状态的线程可能正在运行也可能在等待cup给他分配时间片。

Waiting:等待某个事件发生(等待别的线程将它唤醒),否则他会一直等待下去。以下的方法可能会造成这种状态:没有设置参数的Object.wait、没有设置参数的Thread.join、LockSupport.park

Timed Waiting:处于这种状态的线程也没有被CPU分配时间,但是他在过了指定的时间后就能进入Runable状态,以下的方法可能导致这种状态:Thread.sleep、有参数的wait、有参数的join、LockSupport.parkNanos、LockSupport.parkUntil

Blocked:阻塞,和等待(Wait)的区别就是,他是他在等待获取一个排它锁,获取了他就能进入running、而Wait是等待被其他线程唤醒或者等到他的wait时间完。

Terminated:终止态。
这里写图片描述

线程安全与锁的优化

Brian Goetz对线程安全的严谨定义:当多个线程访问一个对象时,如果不考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行其他的协调操作,调用这个对象的行为都可以获取正确的结果,那么这个对象就是线程安全的。
但是就上面这个定义来说太过于严谨大多数情况下的线程安全不必做到那么复杂,而只需要满足一个特征即可:代码本身封装了所有必要的正确性保障手段,而不需要调用者去操心线程安全问题。

Java中线程的安全:
为了理解线程的安全我们不把线程安全当做一个非真即假的二元排他选项来理解,而是将它看作是一种安全程度递减的来排序,所以我们可以将Java中共享数据分为以下五类:
不可变性绝对的线程安全相对的线程安全线程兼容线程对立
(1)不可变性:即用final修饰的变量只要在构造函数中没有出现this逃逸的情况的话就永远不会产生线程安全问题,所以她是最简单最纯粹的安全性实现。
**补充说明:**Java中常见的不可变对象包括了String,以及八种基本类型的包装类对象。(只要是被final修饰的都是不可变的,所以想要实现对象的不可变那么就要用final修饰类中的属性)
(2)绝对的线程安全:绝对的线程安全类完全满足Brian的定义,Java中也有很多声明为绝对安全的类,但是大多数都是飞绝对安全的。
比如Vector类中的add,get方法,他们都是synchronized修饰的同步方法,单个看起来是绝对线程安全的,但是如果我们将两个操作连起来就不是线程安全的啦!还得我们自己加同步块才能实现线程安全。
(3)相对的线程安全:这就是我们常认定的线程安全,比如HashTable、Vector等类就是相对线程安全的。
(4)线程兼容:线程兼容是指对象本身不是线程安全的,但是可以通过调用端正确的使用同步手段来保证在并发环境中可以安全的使用,我们常说的线程不安全就是指的这种情况。Java中大多数额API都是线程兼容的,比如像Vectorhe HashTable相对应的集合类ArrayList和HashMap等。
(5)线程对立:指的是无论调用端是否采取同步的操作都无法再多线程的环境下使用该代码。这种情况是绝对有害的应当避免。比如常见的suspend和resume方法,如果两个线程同时持有线程对象,一个尝试中断,一个尝试恢复,在并发进行时无论是否采用了同步都有死锁的风险。常见的还有System.setIn、System.setOut和System.runFinalizersOnExit等。

如何实现线程安全:
(一)互斥同步
采用临界区互斥量信号量、都是主要的实现线程互斥的方法。PS:实现线程的互斥是为了达到线程同步的目的。
最基本的实现互斥的方式是使用sysnchronized关键字,sysnchronized实现原理:
分类
(1)修饰普通方法:与sysnchronized(this)是差不多的效果,都是以当前对象为锁,
(2)使用sysnchronized同步代码块:比(1)更高效,因为缩小了加锁的范围。
(3)修饰静态方法:相当于sysnchronized(this.getClass()),即坐定当前对象的Class对象。
如果非要使用的而且三者都能实现的情况下建议使用(2)更高效。

根据虚拟机规范的要求,在执行monitorenter的时候,首先回去检查对象是否已经被加锁如果没有被锁定或者已经被当前线程获得锁那么对象的对象的锁计数器加1,相应的在调用moniterexit的时候,就将计数器减1,如果计数器值为0就释放线程。如果线程获取锁失败,那么就要阻塞等待。(以上的这段话红隐含了sysnchronized关键字隐式支持锁重入)
除了sysnchronized关键字以外还有Java.util.concurrent(J.U.C)中重入锁(ReentrantLock)实现同步,在ReentranLock中增加某些高级功能包括:等待可中断实现公平锁一个锁可绑定多个条件,另外ReentranLock是表现在API层面的互斥锁(通过lock、unlock配合try、finally语句块来实现)而sysnchronized则表现为原生语法层面的实现。

等待可中断:持有锁的线程长期不释放锁的时候,等待的线程可以选择不等待,而去处理其他的事情。但是又不同与sysnchronized中的定时等待。
公平锁:就是按照申请锁的顺序来一次获取锁,但是sysnchronized和ReentranLock默认情况下都是非公平锁,ReentranLock如果想要使用公平锁,可以通过带参数的构造函数来实现。
绑定多个条件:一个ReentranLock可以创建多个等待条件。

sysnchronized已经优化到和ReentrantLock差不多的性能啦,所以我们在选择的时候不要以性能来考虑是否选择哪一个。

(二)非阻塞同步
互斥同步主要的问题就是进行线程的阻塞和唤醒带来的性能上的损耗(这种实现也成为阻塞同步),所以随着硬件的发展我们使用基于冲突检测的乐观并发策略,通俗的将就是先进性操作,如果没有线程来争用共享内存的话那么就操作成功,如果有数据争用,产生了冲突,那就采取相应的措施,因为这种实现不需要吧线程挂起,因此这种操作称为非阻塞同步。
良好的硬件保证了一个从语义上看起来需要多次操作的行为只通过一条指令就能实现。常见的这类指令有:测试并设置(Test-and-Set)获取并增加(Fetch-and-Increment)交换(Swap)比较并交换(Compare-and-Swap:CAS)加载链接、条件存储(Load-Linked/Store-Conditional)

补充:CAS操作需要三个参数:内存位置(V)、旧的预期值(A)、新值(B)在CAS执行的时候当且仅当V中的值等于A的时候才将B的值写入V中,否则处理器不更新V中值,但是不管怎样都会返回V 中的旧值。上述的过程就是一个原子的操作。在jdk1.5以后才可以使用CAS操作,它由com.misc.UnSafe类里面的compareAndSwapInt和compareAndSwapLong等几个方法包装提供,虚拟机内部对这些方法都做了处理,编译出来以后就是一调相关处理器的CAS操作指令,而没有方法调用的过程,或者可以认为他被无条件的内联进去了。除此之外,UNSafe类不能被用户调用,UnSafe.getSafe中的代码限制只能启动类加载器加载的类才能调用。因此如果如果我们不通过反射手段的话只能通过其他JDK中提供的特定的API来间接的调用他们。比如J.U.C包中提供的整数原子类,其中的compareAndSet和getAndIncrement等方法都是用了UNSafe类中的CAS操作。所以原子类的一个很重要的方法就是基于CAS的线程安全的自增

CAS的弊端:
ABA问题:加入一个值有A变为B再变为A而后来的线程检测到了他的值为A,认为状态没有改变,但是实际上室友本质上的区别的。要避免的这样的情况就只有使用版本号来解决。比如1A 变2B变3A
自旋问题:因为CAS会通过不断的自旋来实现最终的set的目的,所以如果某个数据长期被争用的话就可能产生常时间的自旋操作这回给CPU带来很大的开销。但是如果JVM支持pause指令的话那么效率就会有一定的提升。
只能保证一个共享变量的原子性操作:比如有两个变量i=0;j=a想要实现他们的原子性操作,单单靠原本的CAS肯定是不行的。我们可以通过组合的方式来解决比如:ij=2a,这样就能奇妙的解决这个问题,但是这个解方案只适合特定的情形。

(三)无同步方案:
同步只是用来解决存在共享数据争用的情况下的线程安全问题,假如一个方法不存在线程争用那么也没有必要对他实现同步了,因为他本身就是线程安全的。一下两类就是本身就是线程安全的代码:
可重入代码:也成纯代码,就是在代码执行过程中转过去执行其他的代码,而在控制权返回后对结果没有影响(包括对自身的递归调用)。所以只要是可重入的代码一定是线程安全的,但是反过来就不一定了,还有一种说法就是可重入函数是线程安全的,如果一个方法的结果的返回值是可预测的,只要输入相同的数据返回值规定那么就满足可重入性的要求。
线程本地存储:如果一段代码所需的数据必须与其他代码共享,那么可以看看这些共享数据的代码能否在同一个线程里面完成,如果能那么无需通过同步也能实现线程安全。这样的例子非常常见。比如说经典的WEB交互模型中”一个请求对应一个线程”的处理方式,可以在服务端使用线程的本地存储来解决线程安全问题。

补充说明:如果一个变量是内存共享的可以将它申明为volatile(易变的),如果是线程独享的可以将它放在ThreadLocal中他可以实现线程的本地存储功能。每一个Thread对象中,都有一个ThreadLocalMap对象,存储一组以threadLocalHashCode为键,以本地变量为值得K-V值对,ThreadLocal对象就是当前线程的ThreadLocalMap的访问入口,每个ThreadLocal对象都包含了一个独一无二的threadLocalHashCode值,使用这个值就可以在线程的K-V值对中找到对应的本地线程变量。

锁的优化
锁的优化方法包括:锁消除(Lock Elimination)、锁粗化(Lock Coasening)、适应性自旋(Adaptive Spinning)、轻量级锁(Lightweight Lock)和偏向锁(Biased Lock )等,这些技术都是为了在线程间更高效的实现数据共享,以及解决竞争的问题。

自旋锁与适应性自旋锁互斥同步最大的影响就是阻塞的表现,挂起线程和恢复线程的操作都需要转入内核态完成,这回给操作系统带来很大的压力,事实上大多数的锁都只会持续很短的一段时间,如果我们通过实现让之后的线程稍微的等一小段时间,我们不放弃CPU执行时间,看看锁是否会很快的释放,为了实现这样的功能,我们可以添加一段忙循环(自旋),这就是所谓的自旋锁。自旋锁的引入本身避免了线程频繁切换的开销,但是他还要占用cpu时间,如果自旋的时间段效果当然不错,如果时间过长还不如使用排它锁的效率高。
而自适应锁就很智能的解决了这个问题,自适应意味着自旋的时间不是固定的了,而是由前一次在同一个锁上的自旋时间和锁的拥有着状态来决定,如果在同一个锁对象上,刚刚通过自旋获得锁并且持有锁的线程正在运行,那么虚拟机就会认为自旋会再次成功,进而他的自旋时间会更长,如果多次自旋都没有获得锁,那么可能就会省略自旋的过程。

锁消除:主要依据来源于逃逸分析的数据支持。如果判断出一段代码中,对上所有的数据都不会逃逸出去从而被其他的线程访问到,那么就可以把他当作栈上的数据对待即可,认为他们是线程私有的,进而同步加锁的过程也不需要了。

这里写图片描述
这里写图片描述
这里写图片描述
锁粗化:原则上我们在编写代码的时候总是推荐将锁的作用范围限制的越小越好,但是如果需要频繁的对某个特定的锁获取释放,而获取释放锁涉及到线程的切换造成的开销很大,还不如将所粗化,将锁范围扩展。

这里写图片描述

轻量级锁:要理解轻量级锁,以及后面会讲到的偏向锁的原理和运作过程,必须从HotSpot虚拟机的对象(对象头部分)的内存布局开始介绍。 HotSpot虚拟机的对象头(Object Header)分为两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、 GC分代年龄(Generational GC Age)等,这部分数据的长度在32位和64位的虚拟机中分别为32bit和64bit,官方称它为“Mark Word”,它是实现轻量级锁和偏向锁的关键。 另外一部分用于存储指向方法区对象类型数据的指针,如果是数组对象的话,还会有一个额外的部分用于存储数组长度。
对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。 例如,在32位的HotSpot虚拟机中对象未被锁定的状态下,Mark Word的32bit空间中的25bit用于存储对象哈希码(HashCode),4bit用于存储对象分代年龄,2bit用于存储锁标志位,1bit固定为0,在其他状态(轻量级锁定、 重量级锁定、 GC标记、 可偏向)下对象的存储内容见表13-1。

这里写图片描述

Mark Word在不同的所状态下的具体表现:
这里写图片描述

轻量级锁的执行过程:在进入同步块的时候,如果此对象没有被锁定(此时的锁标志位为01),虚拟机现在栈中建立一个叫做“锁记录”空间,用于存储对象目前的Mark Word的拷贝(官方把这份拷贝加了一个Displaced前缀,即Displaced Mark Word),整个过程的栈、对象头表现如下:
这里写图片描述
然后虚拟机将采用CAS操作,尝试将对象的Mark Word更新为指向Lock Record的指针,如果这个更新成功了,那么线程就成功的获取到了锁,并且对象的Mark Word的锁标志位(Mark Word的最后2bit)将转变为“00”,即表示此对象处于轻量级锁的状态,这时候的线程栈和对象头表现为:
这里写图片描述
这里写图片描述

这里写图片描述
这里写图片描述
这里写图片描述

锁的状态位:
01:表示无锁状态或者偏向锁定状态;
00:轻量级锁状态;
10:重量级锁状态;
11:GC标记。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
嗨!很高兴回答你关于Java并发编程的问题。请问你想知道什么方面的内容呢?我可以分享一些学习笔记和建议给你。 1. 并发编程基础:了解并发编程的基本概念,如线程、进程、、同步等。学习Java中的并发编程模型以及相关的API,如Thread、Runnable、Lock、Condition等。 2. 线程安全性:学习如何保证多线程环境下的数据安全性,了解共享资源的问题以及如何使用同步机制来防止数据竞争和并发问题。 3. 线程间的通信:掌握线程间的通信方式,如使用wait/notify机制、Lock/Condition等来实现线程的协调与通信。 4. 并发容器:学习并发容器的使用,如ConcurrentHashMap、ConcurrentLinkedQueue等。了解它们的实现原理以及在多线程环境下的性能特点。 5. 并发工具类:熟悉Java提供的并发工具类,如CountDownLatch、CyclicBarrier、Semaphore等,它们可以帮助你更方便地实现线程间的协作。 6. 并发编程模式:学习一些常见的并发编程模式,如生产者-消费者模式、读者-写者模式、线程池模式等。了解这些模式的应用场景和实现方式。 7. 性能优化与调试:学习如何分析和调试多线程程序的性能问题,了解一些性能优化的技巧和工具,如使用线程池、减少竞争、避免死等。 这些只是一些基本的学习笔记和建议,Java并发编程是一个庞大而复杂的领域,需要不断的实践和深入学习才能掌握。希望对你有所帮助!如果你有更具体的问题,欢迎继续提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值