1 基本概念
案例:采用2个无关联的线程对同一变量进行操作,一个进行5000次自增操作,另外一个进行5000次自减操作。
最终变量的结果是不确定的(2个算数操作的操作指令由于多线程原因会交错在一起)。
临界区(critical section):对共享资源进行多线程读写操作的代码块。
竞态条件(Race Condition):多个线程在临界区内执行,由于代码执行序列不同而导致结果无法预测,称之为发生了竞态条件
Java中如何避免发生竞态条件?
- 阻塞式解决方案:synchronized, Lock
- synchronized俗称“对象锁”,采用互斥的方式使得同一时刻最多只有一个线程能拥有这个“对象锁”。
- 非阻塞式:原子变量
2 Java中synchronized的使用与理解
synchronized (对象){ // 申请对象锁
临界区;
}
2-1 基本的使用
package chapter3;
//这个程序要运行必须在IDEA中装好lombok插件,并有lombok和slf4j-simple包
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test1")
public class Test1 {
static int counter = 0;
static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for(int i = 0;i < 5000;++i){
synchronized (lock){ // 申请对象锁
counter++;
}
}
},"t1");
Thread t2 = new Thread(()->{
for(int i = 0;i < 5000;++i){
synchronized (lock){ // 申请对象锁
counter--;
}
}
},"t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.warn("{}",counter); // 通过synchronized实现了对共享变量的互斥操作
}
}
结果
[main] WARN c.Test1 - 0
总结:Java中使用synchronized以对象锁的形式保证了临界区的原子性,避免竞态条件的发生。
上面代码引申:
-
代码中2次synchronized必须是同一对象
-
代码中仅仅进行一次synchronized无法保证竞态条件不发生。
对共享变量进行封装:
package chapter3;
//这个程序要运行必须在IDEA中装好lombok插件,并有lombok和slf4j-simple包
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test1")
public class Test2 {
static Room room = new Room();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for(int i = 0;i < 5000;++i){
synchronized (room){ // 申请对象锁
room.increment();
}
}
},"t1");
Thread t2 = new Thread(()->{
for(int i = 0;i < 5000;++i){
synchronized (room){ // 申请对象锁
room.decrement();
}
}
},"t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.warn("{}",room.getCounter()); // 通过synchronized实现了对共享变量的互斥操作
}
}
class Room{
private static int counter = 0;
public void increment(){
synchronized (this){
counter++;
}
}
public void decrement(){
synchronized (this){
counter--;
}
}
public int getCounter(){
synchronized (this){
return counter;
}
}
}
2-2 方法上的synchronized
2种等价写法:
class Test{
public void test(){
synchronized (this){ // this表示当前类的实例,也叫做qualified this
counter++;
}
}
}
等价于
class Test{
public synchronized void test(){
counter++;
}
}
//静态方法
class Test{
public static void test(){
synchronized (Test.class){
counter++;
}
}
}
等价于
class Test{
public synchronized static void test(){
counter++;
}
}
静态方法的synchronized与普通成员方法synchronized的区别:
- 静态方法上锁的是这个class。
- 普通成员方法,锁的是该对象的实例this。
- 一个class可以多个this实例
2-3 变量的线程安全分析
线程安全:多个线程执行同一段代码,所得到的最终结果是否符合预期。
局部变量:
- 局部变量是线程安全的
- 实例:栈帧中每一个frame存储的变量都是相互独立的。
- 局部变量引用的对象未必:
- 线程安全的判断依旧:引用的对象是否脱离方法的作用范围
静态变量:
- 静态变量没有被多个线程共享,或者被多个线程共享但只进行读操作,那么该静态变量就是线程安全的。
实例1:局部变量引用带来的线程不安全
package chapter3;
//这个程序要运行必须在IDEA中装好lombok插件,并有lombok和slf4j-simple包
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
@Slf4j(topic = "c.Test4")
public class Test4 {
static final int LOOP_NUMBER = 200;
static final int THREAD_NUMBER = 2;
public static void main(String[] args){
ThreadUnsafeExample tmp = new ThreadUnsafeExample();
for(int i = 0;i < THREAD_NUMBER;++i){
new Thread(()->{
tmp.method1(LOOP_NUMBER);
},"Thread"+i).start();
}
}
}
// 这里定义了一个线程不安全的类
class ThreadUnsafeExample{
ArrayList<String> list = new ArrayList<>();
public void method1(int loopnumber){
for(int i = 0;i < loopnumber;++i){
method2();
method3();
}
}
private void method2(){
list.add("1");
}
private void method3(){
list.remove(0);
}
}
运行结果:
Exception in thread "Thread0" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
at java.util.ArrayList.rangeCheck(ArrayList.java:653)
at java.util.ArrayList.remove(ArrayList.java:492)
at chapter3.ThreadUnsafeExample.method3(Test4.java:35)
at chapter3.ThreadUnsafeExample.method1(Test4.java:27)
at chapter3.Test4.lambda$main$0(Test4.java:15)
at java.lang.Thread.run(Thread.java:748)
分析:
- 多个线程通过成员变量共享了堆中的list对象。
实例2:局部变量的引用暴露带来的线程不安全
package chapter3;
//这个程序要运行必须在IDEA中装好lombok插件,并有lombok和slf4j-simple包
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
@Slf4j(topic = "c.Test5")
public class Test5 {
static final int LOOP_NUMBER = 10000;
static final int THREAD_NUMBER = 2;
public static void main(String[] args){
ThreadSafeExampelSubclass tmp = new ThreadSafeExampelSubclass();
for(int i = 0;i < THREAD_NUMBER;++i){
new Thread(()->{
tmp.method1(LOOP_NUMBER);
},"Thread"+i).start();
}
}
}
class ThreadsafeExample{
public void method1(int loopnumber){
ArrayList<String> list = new ArrayList<>(); //方法中new了一个对象,每个线程调用该方法都会new一个对象,因此不存在线程之间共享的成员,所以是安全的。
for(int i = 0;i < loopnumber;++i){
method2(list);
method3(list);
}
}
public void method2(ArrayList<String> list){
list.add("1");
}
public void method3(ArrayList<String> list){
list.remove(0);
}
}
class ThreadSafeExampelSubclass extends ThreadsafeExample{
@Override
public void method3(ArrayList<String> list){ // 方法内部创建的对象的引用通过继承被暴露了
new Thread(()->{
list.remove(0);
}).start();
}
}
运行结果会出现2种:
- 没有任何问题,程序正常退出(循环次数比较小的情况下)
- 出现如下错误:
Exception in thread "Thread-1446" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
at java.util.ArrayList.rangeCheck(ArrayList.java:653)
at java.util.ArrayList.remove(ArrayList.java:492)
at chapter3.ThreadSafeExampelSubclass.lambda$method3$0(Test5.java:41)
at java.lang.Thread.run(Thread.java:748)
Process finished with exit code 0
分析:由于父类使用public修饰list的操作方法,因此对于list的引用被暴露给子类。
- 子类通过重载将局部变量引用的对象被多个线程共享,引发问题
线程安全问题实际挺难发现的可以通过一些良好的编程习惯避免。
通过private,final等关键词保证安全,遵循面向对象编程的开闭原则的闭。
class ThreadsafeExample{
public final void method(int loopnumber){
ArrayList<String> list = new ArrayList<>();
for(int i = 0;i < loopnumber;++i){
method2(list);
method3(list);
}
}
private void method2(ArrayList<String> list){
list.add("1");
}
private void method3(ArrayList<String> list){
list.remove(0);
}
}
2-4 常用的线程安全类
基本理解
Java中常用的线程安全类:String, Integer, StringBuffer, Random, Vector, HashTable, java.util.concurrent(juc包)
线程安全类的理解:多个线程调用同一实例的某个方法时,是线程安全的。
- 可以理解为线程安全的类的方法是原子的(查看源码会发现有synchronized)
- 注意多个方法的组合未必是原子的。
HashTable table = new HashTable();
//线程1,线程2都会执行的代码
if(table.get("key") == null){
table.put("key",value);
}
分析: 虽然HashTable是线程安全的,但是上面的代码并不是线程安全的,在实际调度时可以出现下面的情况:
线程1.get --> 线程2.get --> 线程1.put ---> 线程2.put
即无法保证同一线程中get与put同时执行。想要保证可以另外synchronized。
不可变类的线程安全
包括:String, Integer
由于不可变性,所以这个类别是线程安全的。
String中replace,substring方法如何保证线程安全?
这些方法并没有改变原有的字符串,而是直接创建了一个新的字符串。
实例:String中substring源码(最后return是一个新的实例)
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
2-5 线程安全分析实例(重点)
案例1
public class MyServlet extends HttpServlet {
// 是否安全? 不是线程安全的,可以用线程安全的类HashMap去替代。
Map<String,Object> map = new HashMap<>();
// 是否安全? 线程安全,String是不可变类
String S1 = "...";
// 是否安全? 线程安全,String是不可变类
final String S2 = "...";
// 是否安全? 线程不安全,Data()不是线程安全类,其成员可能会引发安全问题。
Date D1 = new Date();
// 是否安全? 线程不安全,利用同上
final Date D2 = new Date();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
// 使用上述变量
}
}
Servlet是运行在tomcat环境下,只有一个实例,所以servlet必定会被tomcat多个线程共享使用‘
重点:分析成员变量在多线程环境下的安全性。
案例2
public class MyServlet extends HttpServlet {
// 是否安全? 不是线程安全的,成员变量count并不安全,UserServiceImpl实例受Servlet限制一般也只有
// 一个。
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 记录调用次数
private int count = 0;
public void update() {
// ...
count++;
}
}
案例3
这里利用AOP监测程序运行时间,可以采用环绕通知保证线程安全。
@Aspect
@Component
public class MyAspect {
// 是否安全? 不是线程安全的,变量start可以被同一实例的多个线程调用共享
private long start = 0L;
@Before("execution(* *(..))")
public void before() {
start = System.nanoTime();
}
@After("execution(* *(..))")
public void after() {
long end = System.nanoTime();
System.out.println("cost time:" + (end-start));
}
}
案例4
public class MyServlet extends HttpServlet {
// 是否安全 是线程安全的
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 是否安全 是线程安全的,没有对成员的修改操作
private UserDao userDao = new UserDaoImpl();
public void update() {
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
public void update() {
String sql = "update user set password = ? where username = ?";
// 是否安全 是线程安全,每个新的线程都会新建一个connection
try (Connection conn = DriverManager.getConnection("","","")){
// ...
} catch (Exception e) {
// ...
}
}
}
案例5
public class MyServlet extends HttpServlet {
// 是否安全 不安全
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 是否安全 不安全
private UserDao userDao = new UserDaoImpl();
public void update() {
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
// 是否安全 , 不是线程安全的,成员变量conn不安全。被多个线程共享
// 需要将conn设为局部变量
private Connection conn = null;
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
conn = DriverManager.getConnection("","","");
// ...
conn.close();
}
}
案例6
public class MyServlet extends HttpServlet {
// 是否安全 安全
// UserDao userDao = new UserDaoImpl();确保了线程安全,每有一个新的链接都重新new一个,
// 但是这种写法不推荐,浪费资源。
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
public void update() {
UserDao userDao = new UserDaoImpl();
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
// 是否安全 不是线程安全的,成员变量conn不安全。可以被同一实例多个线程共享
private Connection = null;
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
conn = DriverManager.getConnection("","","");
// ...
conn.close();
}
}
案例7:判断对象的引用是否泄露,警惕抽象方法引入的外星方法。
// 定义了一个抽象类
public abstract class Test {
public void bar() {
// 是否安全
// 不安全,
// 子类对foo方法定义并在foo中启动新的线程访问sdf对象,造成sdf在多个线程中出现共享,sdf并不是
// 这个案例与之前引用暴露带来的不安全问题如出一辙。
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
foo(sdf);
}
public abstract foo(SimpleDateFormat sdf);
public static void main(String[] args) {
new Test().bar();
}
}
//
public void foo(SimpleDateFormat sdf) {
String dateStr = "1999-10-11 00:00:00";
for (int i = 0; i < 20; i++) {
new Thread(() -> {
try {
sdf.parse(dateStr);
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}
foo在子类中的定义(这里对变量进行了修改)
public void foo(SimpleDateFormat sdf) {
String dateStr = "1999-10-11 00:00:00";
for (int i = 0; i < 20; i++) {
new Thread(() -> {
try {
sdf.parse(dateStr);
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}
案例8: String的源代码中对String类定义为何加上final这个关键词?
Final用于修饰类、成员变量和成员方法。final修饰的类,不能被继承(String、StringBuilder、StringBuffer、Math,不可变类),其中所有的方法都不能被重写(这里需要注意的是不能被重写,但是可以被重载,这里很多人会弄混),所以不能同时用abstract和final修饰类(abstract修饰的类是抽象类,抽象类是用于被子类继承的,和final起相反的作用);Final修饰的方法不能被重写,但是子类可以用父类中final修饰的方法;Final修饰的成员变量是不可变的,如果成员变量是基本数据类型,初始化之后成员变量的值不能被改变,如果成员变量是引用类型,那么它只能指向初始化时指向的那个对象,不能再指向别的对象,但是对象当中的内容是允许改变的。
- final修饰的类,不能被继承(String、StringBuilder、StringBuffer、Math,不可变类)
- 避免用户定义的String中的子类破坏其原有方法的安全性。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
/**
* Class String is special cased within the Serialization Stream Protocol.
*
* A String instance is written into an ObjectOutputStream according to
* <a href="{@docRoot}/../platform/serialization/spec/output.html">
* Object Serialization Specification, Section 6.2, "Stream Elements"</a>
*/
private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];
/**
* Initializes a newly created {@code String} object so that it represents
* an empty character sequence. Note that use of this constructor is
* unnecessary since Strings are immutable.
*/
public String() {
this.value = "".value;
}
/**
* Initializes a newly created {@code String} object so that it represents
* the same sequence of characters as the argument; in other words, the
* newly created string is a copy of the argument string. Unless an
* explicit copy of {@code original} is needed, use of this constructor is
* unnecessary since Strings are immutable.
*
* @param original
* A {@code String}
*/
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
2-6 多线程卖票实例分析
错误并行代码:
package chapter3.exericse;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Vector;
@Slf4j(topic = "c.Ticket")
public class Ticket {
public static void main(String[] args) throws InterruptedException {
TicketWindow ticketWindow = new TicketWindow(10000);
List<Thread> threadList = new ArrayList<>(); // 用于同步所有线程,让所有线程都结束
List<Integer> amountList = new Vector<>(); // Vector是线程安全的,可以在多线程环境使用
for(int i = 0;i < 1000; ++i){
Thread thread = new Thread(()->{
// 加个随机睡眠,确保出现问题
try {
Thread.sleep(randomAmount());
} catch (InterruptedException e) {
e.printStackTrace();
}
int tmp = ticketWindow.sell(randomAmount());
amountList.add(tmp);
});
threadList.add(thread);
thread.start();
}
// 等待所有线程运行完毕
for (Thread thread : threadList) {
thread.join();
}
// 统计余票
log.warn("余票:{}",ticketWindow.getCount());
//统计实际卖出的票,求和
log.warn("卖出的票: {}",amountList.stream().mapToInt(i->i).sum());
}
// random是线程安全的
static Random random = new Random();
public static int randomAmount(){
//随机范围1-5
return random.nextInt(5)+1;
}
}
// 定义售票窗口,提供查看余票并售票的功能
// 这个类会在多线程环境下运行
class TicketWindow{
// 统计剩余的票数
private int count;
public TicketWindow(int count){
this.count = count;
}
public int getCount(){
return count;
}
// 售票方法,返回售出票的数量
public int sell(int amount){
if(this.count >= amount){
this.count -= amount;
return amount;
}else{
return 0;
}
}
}
运行结果:
[main] WARN c.Ticket - 余票:7033
[main] WARN c.Ticket - 卖出的票: 2983
总结:
- 可以看到定义的TicketWindow在多线程环境下出现票数的统计错误。说明这个类是线程不安全的。
- 多线程问题难以复现:实际运行时发现多次运行有时候票数统计是正确的,有时候不正确,说明多线程问题比较难以排查。
买票的多线程问题分析:
可以发现多线程共享的成员有TicketWindow以及Vector对象的实例,Vector用到了add方法的,由于本身就是线程安全类,因此相关部分没有线程安全问题。
而TicketWindow的sell方法中count在多线程环境下会被修改,相关联的代码就是临界区。因此可以加一个对象锁。修改代码如下所示。
// 售票方法,返回售出票的数量
public synchronized int sell(int amount){
if(this.count >= amount){
this.count -= amount;
return amount;
}else{
return 0;
}
}
2-7 Monitor对象头以及synchronized工作原理(重要)
Java对象头的概念(32虚拟机情况):
- 普通对象:object header由mark word和Klass word,Kclass word是一个指针,指向对象所从属的class。
- mark word中存储了对象丰富的信息,注意mark word有5种状态表示,当给对象加上synchronized后,如果state是Heavyweight locked,此时加锁的对象通过mark word关联monitor对象。
- 数组对象:对象头除了包含mark word以及Kclass word还有数组长度
实例:32位虚拟机下,int类型只占用4字节,而Integer占用4+8字节,其中8字节是对象头
Monitor(管程)的基本概念:
- 管程:指的是管理共享变量以及对共享变量的操作过程,让他们支持并发。
- Java中的monitor:每个Java对象都可以关联一个monitor对象,如果对一个对象使用synchronized关键字,那个这个对象的对象头的mark word就被设置指向monitor对象的指针。
上图中原理讲解:
- 线程2执行synchronized(obj)会检查关联到Monitor对象中的owner为null,将owner设置为自己,每一个Monitor对象只能有一个owner。
- 线程1和线程3执行到临界区代码后,同样检查Monitor对象中的owner,由于Monitor对象存在owner,所以进入Entrylist (阻塞队列)进行等待。
Synchronized字节码层面理解
总结:
- 字节码第5行(monitor enter)就是代码执行到synchronized那里,然后将对象头中的mark word设置为Monitor指针。
- 字节码第11行(monitor exit)就是临界区代码执行完成,将对象头的的mark work重置,同时唤醒monitor对象中的EntryList,让其他线程进入临界区。
- 19-23行适用于处理临界区代码出现异常的情况。
2-8 synchronized进阶工作原理
Monitor(重量级锁)虽然能够解决不安全问题,但代价有点高(需要为加锁对象关联一个monitor对象),为了降低代价引入了下列机制:
基本概念:
- 轻量级锁
- 偏向锁
- 批量重刻名:一个类的偏向锁撤销达到20
- 不可偏向:某个类别被撤销的次数达到一定阀值(代价过高),设置为不可偏向。
轻量级锁
-
基本思想:利用线程中栈内存的锁记录结构作为轻量级锁,锁记录中存储锁定对象的mark word
-
使用场景:对象虽然有多线程访问,但多线程加锁的时间是错开的(没有竞争)
-
注意点:轻量级锁不需要用户指定,其使用是透明的,使用synchronized关键字。程序优先尝试轻量级锁。
2-8-1 轻量级锁的加锁过程
static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
上面的代码中进行了2次加锁。
step1:线程0首先在栈帧中创建锁记录对象
- 锁记录的Object reference指向加锁的对象
step2: 使用CAS(Compare and Swap)操作替换加锁对象中对象头的mark word,将mark word存储到所记录
- 替换成功,则加锁对象的mark word的锁记录地址和状态 00 ,表示light weight locked
- 替换失败,有2种情况;
- 一种是线程0以外的其他线程拥有这个线程的轻量锁,发生了竞争,此时进入锁膨胀阶段
- 线程0再次执行synchronized(锁重入,有点类似于函数内部调用另外一个函数),再添加一条 Lock Record 作为重入的计数(栈的结构)
- step3: 执行完临界区代码
- 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
- 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头
- CAS成功,则解锁成功
- CAS失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
2-8-2 锁膨胀的理解:将轻量级锁变为重量级锁(结合2-8-1)
发生场景实例:当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
step1: Thread1 为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址,自己进入 Monitor 的 EntryList 阻塞等待
step2:当 Thread-0 退出同步块解锁时,使用 CAS 将 Mark Word 的值恢复给对象头,必定失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程
2-8-3 自旋优化
定义:重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。(简单的理解为发现其他线程占着坑位,这个线程没有立刻阻塞而是多等了会)
注意点:
- 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势
- 自旋功能我们无需操作,Java 7 之后不能控制是否开启自旋功能
2-8-4 偏向锁
为什么需要偏向锁?
- 轻量级锁
- 优点:轻量级别锁通过线程栈帧中的锁记录结构替代重量级锁,不需要关联monitor对象。
- 缺点:单个线程(没有其他线程与其竞争)使用轻量级锁,在锁重入的时候仍然需要执行CAS操作(栈帧中添加一个新的lock record,见下图,会有资源浪费)。
偏向锁为了克服轻量级锁的缺点而提出的。
- 锁重入:同一线程多次对同一对象加锁。
会发生锁重入的代码:
static final Object obj = new Object();
public static void m1() {
synchronized( obj ) {
// 同步块 A
m2();
}
}
public static void m2() {
synchronized( obj ) {
// 同步块 B
m3();
}
}
public static void m3() {
synchronized( obj ) {
// 同步块 C
}
}
- 偏向锁:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有
- 注意点:由于第一次CAS将线程ID设置到加锁对象的对象头的mark word中,发生锁重入的后,就不会再额外产生锁记录。
2-8-5 偏向状态
偏向状态可以通过对象头的mark work反应出来,观察64位虚拟机的mark word,如下所示:
总体上有5种状态,可以通过mark word最后2位判断当前对象的状态。
state | 说明 |
---|---|
Normal(正常状态) | biased_lock为0表示没有被加偏向锁 |
Biased(偏向状态) | biased_lock为1表示被加偏向锁,thread用于存储线程id,注意该id时os层面(非jvm) |
Lightweight Locked(轻量级别的锁) | ptr_to_lock_record指向加锁线程栈帧中的锁记录 |
Heavyweight Locked(重量锁) | ptr_to_heavyweight_monitor指向加锁对象所关联的monitor对象 |
偏向锁的一些琐碎知识;
- 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的
thread、epoch、age 都为 0 (对象创建后的默认状态是偏向状态) - 偏向锁是默认是延迟的,不会在程序启动时立即生效(需要等一段时间,比如几s),如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟
- 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、
age 都为 0,第一次用到 hashcode 时才会赋值。
2-8-6 对象何时会撤销偏向状态(3种情况,待理解补充)
- 调用对象 hashCode 方法,由于偏向状态无法存储hash值
- 其他线程使用对象
- 当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
- 调用wait/notify
参考资料
20210224