Synchronized使用及其原理

1、线程安全问题

1.1、问题分析

两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?

public static void main(String[] args) throws InterruptedException {
    Room room = new Room();
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 5000; i++) {
            room.increment();
        }
    }, "t1");
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 5000; i++) {
            room.decrement();
        }
    }, "t2");
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    log.info("{}", room.getCounter());
}

static class Room {

    private int counter = 0;

    public void increment() {
        counter++;
    }

    public void decrement() {
        counter--;
    }

    public int getCounter() {
        return counter;
    }
}

image-20220207155745178

我们的理想情况,counter 应该是等于 0 才对,之所以会出现非 0 的情况,是因为 JAVA 中对静态变量的自增,自减并不是原子操作,要彻底理解,可以从字节码的角度入手:

  • i++
getstatic i 		// 获取静态变量i的值
iconst_1 			// 线程内部准备一个常量1,用于接收静态变量i
iadd 				// 对常量1自增
putstatic i 		// 将修改后的常量1存入静态变量i
  • i–
getstatic i 		// 获取静态变量i的值
iconst_1 			// 线程内部准备一个常量1,用于接收静态变量i
isub 				// 对常量1自减
putstatic i 		// 将修改后的常量1存入静态变量i

可以发现,静态变量的自增自减,是需要在工作内存和主存之间进行数据交换的,而非直接在主存进行修改

所以就可能出现如下两种情况:

线程1 线程2 static i getstatic i 读取0 iconst_1 准备常数 1 isub 减法,线程内 i = -1 上下文切换 getstatic i 读取 0 iconst_1 准备常数 1 iadd 加法,线程内 i=1 putstatic i 写入 1 上下文切换 putstatic i 写入 -1 线程1 线程2 static i
线程1 线程2 static i getstatic i 读取 0 iconst_1 准备常数 1 iadd 加法,线程内 i = 1 上下文切换 getstatic i 读取 0 iconst_1 准备常数 1 isub 减法,线程内 i = -1 putstatic i 写入 -1 上下文切换 putstatic i 写入 1 线程1 线程2 static i

1.2、概念归纳

1.2.1、临界区

一段代码块中如果存在对共享资源的多线程读写操作,称这段代码为临界区

如:

static int counter = 0;
static void increment() 
// 临界区
{ 
    counter++;
}
static void decrement() 
// 临界区
{ 
    counter--;
}

1.2.2、竞态条件

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

2、synchronized

俗称对象锁,它采用一种互斥的方式让同一时刻至多只有一个线程能持有【对象锁】其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换

  • 相关语法如下
synchronized(对象) // 线程1, 线程2(blocked)
{
    临界区
}

class Test{
    public synchronized void test() {

    }
}
//等价于
class Test{
    public void test() {
        synchronized(this) {

        }
    }
}
class Test{
    public synchronized static void test() {
    }
}
//等价于
class Test{
    public static void test() {
        synchronized(Test.class) {

        }
    }
}
  • 所以我们可以使用 synchronized 来解决刚才出现的线程安全问题
static class Room {

    private int counter = 0;

    public void increment() {
        synchronized (this) {
            counter++;
        }
    }

    public void decrement() {
        synchronized (this) {
            counter--;
        }
    }

    public int getCounter() {
        synchronized (this) {
            return counter;
        }
    }
}

image-20220207165407858

注意一点就是,synchronized 锁被一个线程拿到后,如果这个线程时间片用完了,它是不会释放锁的,其他线程还是只能在外面等着

线程1 线程2 static i 锁对象 尝试获取锁 拥有锁 getstatic i 读取 0 iconst_1 准备常数 1 isub 减法,线程内 i = -1 上下文切换 被阻塞(BLOCKED) loop [尝试获取锁] 上下文切换 putstatic i 写入 -1 拥有锁 释放锁,并唤醒阻塞的线程 拥有锁 getstatic i 读取 -1 iconst_1 准备常数 1 iadd加法,线程内 i = 0 putstatic i 写入 0 拥有锁 释放锁,并唤醒阻塞的线程 线程1 线程2 static i 锁对象

1.4、线程八锁问题

针对下面几个问题判断 synchronized 锁住的是哪个对象

  • 情况1、
@Slf4j
class Number {
    public synchronized void a() {
        log.info("{},1", new Date().toString());
    }

    public synchronized void b() {
        log.info("{},2", new Date().toString());
    }
}

public static void main(String[] args) {
    Number n1 = new Number();
    new Thread(() -> {
        n1.a();
    }).start();
    new Thread(() -> {
        n1.b();
    }).start();
}

锁住的是this,输出结果将会是 1->2 或 2->1

  • 情况2、
@Slf4j
class Number {
    @SneakyThrows
    public synchronized void a() {
        TimeUnit.SECONDS.sleep(1);
        log.info("{},1", new Date().toString());
    }

    public synchronized void b() {
        log.info("{},2", new Date().toString());
    }
}
public static void main(String[] args) {
    Number n1 = new Number();
    new Thread(() -> {
        n1.a();
    }).start();
    new Thread(() -> {
        n1.b();
    }).start();
}

锁住的是this,输出结果将会是 1s后->1->2 或 2->1s后->1

  • 情况3、
@Slf4j
class Number {

    @SneakyThrows
    public synchronized void a() {
        TimeUnit.SECONDS.sleep(1);
        log.info("{},1", new Date().toString());
    }

    public synchronized void b() {
        log.info("{},2", new Date().toString());
    }

    public void c() {
        log.info("{},3", new Date().toString());
    }
}

public static void main(String[] args) {
    Number n1 = new Number();
    new Thread(() -> {
        n1.a();
    }).start();
    new Thread(() -> {
        n1.b();
    }).start();
    new Thread(() -> {
        n1.c();
    }).start();
}

锁住的是this,输出结果将会是3->1s->1->2 或 2->3->1s->1 或 3->2->1s->1

  • 情况4、
@Slf4j
class Number {

    @SneakyThrows
    public synchronized void a() {
        TimeUnit.SECONDS.sleep(1);
        log.info("{},1", new Date().toString());
    }

    public synchronized void b() {
        log.info("{},2", new Date().toString());
    }
}

public static void main(String[] args) {
    Number n1 = new Number();
    Number n2 = new Number();
    new Thread(() -> {
        n1.a();
    }).start();
    new Thread(() -> {
        n2.b();
    }).start();
}

由于创建了两个 Number 对象,所以两个对象的方法都只会锁自己的那个对象,所以输出结果只会是 2->1s->1

  • 情况5、
@Slf4j
class Number {

    @SneakyThrows
    public static synchronized void a() {
        TimeUnit.SECONDS.sleep(1);
        log.info("{},1", new Date().toString());
    }

    public synchronized void b() {
        log.info("{},2", new Date().toString());
    }
}

public static void main(String[] args) {
    Number n = new Number();
    new Thread(() -> {
        n.a();
    }).start();
    new Thread(() -> {
        n.b();
    }).start();
}

静态方法的锁是锁的类 class,普通方法锁的是类对象,所以不是一把锁,故输出结果只会是 2->1s->1

  • 情况6、
@Slf4j
class Number {

    @SneakyThrows
    public static synchronized void a() {
        TimeUnit.SECONDS.sleep(1);
        log.info("{},1", new Date().toString());
    }

    public static synchronized void b() {
        log.info("{},2", new Date().toString());
    }
}

public static void main(String[] args) {
    Number n = new Number();
    new Thread(() -> {
        n.a();
    }).start();
    new Thread(() -> {
        n.b();
    }).start();
}

两个方法都是静态方法,所以锁住的是同一个类 class,故输出结果可能是 2->1s->1 或 1s->1->2

  • 情况7、
@Slf4j
class Number {

    @SneakyThrows
    public static synchronized void a() {
        TimeUnit.SECONDS.sleep(1);
        log.info("{},1", new Date().toString());
    }

    public synchronized void b() {
        log.info("{},2", new Date().toString());
    }
}

public static void main(String[] args) {
    Number n1 = new Number();
    Number n2 = new Number();
    new Thread(() -> {
        n1.a();
    }).start();
    new Thread(() -> {
        n2.b();
    }).start();
}

同理,不是一把锁,输出结果只会是 2->1s->1

  • 情况8、
@Slf4j
class Number {

    @SneakyThrows
    public static synchronized void a() {
        TimeUnit.SECONDS.sleep(1);
        log.info("{},1", new Date().toString());
    }

    public static synchronized void b() {
        log.info("{},2", new Date().toString());
    }
}

public static void main(String[] args) {
    Number n1 = new Number();
    Number n2 = new Number();
    new Thread(() -> {
        n1.a();
    }).start();
    new Thread(() -> {
        n2.b();
    }).start();
}

class 锁只有一把,不会因为实例化的对象不同而不同,所以两者是一把锁,可能出现 2->1s->1 或 1s->1->2

3、变量的线程安全分析

如何判断成员变量和静态变量是否线程安全

  • 如果它们没有共享,则线程安全
  • 如果它们被共享了,又分两种情况
    • 如果只有读操作,则线程安全
    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

局部变量是否线程安全

  • 局部变量是线程安全的
  • 但局部变量引用的对象不一定
    • 如果该对象没有逃离方法的作用访问,它是线程安全的
    • 如果该对象逃离方法的作用范围(return 出去给其他地方使用),需要考虑线程安全

3.1、常见线程安全类

  • String
  • Integer
  • StringBuffer
  • Random
  • Vector
  • Hashtable
  • java.util.concurrent.*

它们的每个方法是原子的,但它们多个方法的组合不是原子的

Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
    table.put("key", value);
}
线程1 线程2 table get("key") == null get("key") == null put("key", v1) put("key", v2) 线程1 线程2 table

3.2、不可变线程安全类

StringInteger 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的

特别地,String 有 replace,substring 等方法,看起来是改变了原始的值,但是实际上底层都是直接 new 的新字符串,原始字符串是一个 final 不可变的

3.3、案例分析

  • 1、
public class MyServlet extends HttpServlet {
    // 是否安全?
    Map<String,Object> map = new HashMap<>();
    // 是否安全?
    String S1 = "...";
    // 是否安全?
    final String S2 = "...";
    // 是否安全?
    Date D1 = new Date();
    // 是否安全?
    final Date D2 = new Date();

    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        // 使用上述变量
    }
}
  • map 很明显不是一个线程安全的

  • S1 是不可变类,所以线程安全的

  • S2 本身就是不可变类,再加一个 final 同样线程安全的

  • D1 是可变类,其中的一些属性是可以修改的,所以不是线程安全的

  • D2 加了 final 也不能保证,也不是线程安全的

  • 2、

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 int count = 0;

    public void update() {
        // ...
        count++;
    }
}
  • userService 可能会被多个线程所共享使用,会同时操作其中的 count 属性,所以不是线程安全的

  • 3、

@Aspect
@Component
public class MyAspect {
    // 是否安全?
    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));
    }
}
  • Spring AOP 中,一个切面正常情况都是一个单例,可能会存在多个线程同时执行 beforeafter 方法,导致 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 = ?";
        // 是否安全
        try (Connection conn = DriverManager.getConnection("","","")){
            // ...
        } catch (Exception e) {
            // ...
        }
    }
}
  • 多个线程同时调用 userService,其又直接调用 userDaoImpl.update方法,可以很明显看出其中的 Connection 放在了方法内部,是一个局部变量,每个线程都会创建一份,各自隔离,所以线程安全的

  • 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 {
    // 是否安全
    private Connection conn = null;
    public void update() throws SQLException {
        String sql = "update user set password = ? where username = ?";
        conn = DriverManager.getConnection("","","");
        // ...
        conn.close();
    }
}
  • 与上一个例子的区别就是,Connection 被放到了类中作为成员变量,多个线程访问就可能会出现,前一个线程刚将拿到的 Connection 赋值给成员变量 conn,但是后一个线程已经执行到 close 方法了,导致前一个线程创建的变量直接被关闭,所以不是线程安全的

  • 6、

public class MyServlet extends HttpServlet {
    // 是否安全
    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 {
    // 是否安全
    private Connection = null;
    public void update() throws SQLException {
        String sql = "update user set password = ? where username = ?";
        conn = DriverManager.getConnection("","","");
        // ...
        conn.close();
    }
}
  • 因为每个线程都会对 userDaoImpl 进行 new ,所以其中的 Connection 成员变量只对应一个线程,所以线程安全的

  • 7、

public abstract class Test {

    public void bar() {
        // 是否安全
        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();
    }
}
  • 其中 foo 方法是一个未知的方法,其实现说不定就会创建新的线程去操作 sdf 对象,也就是说,可能将 sdf 对象暴露给其他线程,所以不是线程安全的
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();
    }
}

可以将这种未知方法称为外星方法,所以为了避免不确定的第三方实现,JDK 便将 String 类设置为一个 final,读者可自行体会一下

3.4、卖票问题

public class Test {
    private static final Random RANDOM = new Random();

    private static int randomInt() {
        return RANDOM.nextInt(5) + 1;
    }

    public static void main(String[] args) throws InterruptedException {
        TicketWindow ticketWindow = new TicketWindow(100000);
        List<Integer> amount = new Vector<>();
        List<Thread> threads = new ArrayList<>();
        for (int i = 0; i < 40000; i++) {
            Thread thread = new Thread(() -> {
                int sell = ticketWindow.sell(randomInt());
                amount.add(sell);
            });
            threads.add(thread);
            thread.start();
        }
        for (Thread thread : threads) {
            thread.join();
        }
        System.out.println("卖出:" + amount.stream().mapToInt(i -> i).sum());
        System.out.println("剩余:" + ticketWindow.getCount());
    }
}

@Slf4j
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;
        }
    }
}
  • 不做任何处理,可以发现,会出现超卖的情况

image-20220208111125807

  • 要解决这种问题,就需要找出这个临界资源,在哪个地方进行的读写操作,本例中,自然而然就是 sell 方法了,只需要在其上方加上 synchronized 关键字就可以了
public synchronized int sell(int amount) {
    if (this.count >= amount) {
        this.count -= amount;
        return amount;
    } else {
        return 0;
    }
}

4、Monitor

Monitor 被翻译为监视器或管程

4.1、Java对象头

一个 Java 对象在内存中都是由两部分组成,其一便是对象头,其二才是对象中的一些成员属性,这里以 32 位虚拟机为例

  • 普通对象

image-20220208113241449

一个对象到底属于什么类型,可以由 Klass Word 指向的类 class 信息得到

  • 数组对象

image-20220208113344930

额外还有一个 4 字节的数组长度

  • 其中 markword

image-20220208114436666

  • 64 位的 markword :

image-20220208114851767

其中有很多的信息我们后面会逐个了解,这里先不介绍

可以发现,对于一个对象,其对象头已经占用了不少的空间,如 Integer 除了其必须包含的 4 字节的 int 数据,还有 8 字节的对象头数据,所以一个 Integer 占用内存便是 int 类型的三倍

4.2、Monitor 原理

每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针

image-20220208121255670

  • 刚开始 MonitorOwnernull
  • Thread-2 执行 synchronized(obj) 就会将 obj 对象头中的 hashcodeagebiased_lock变为ptr_to_heavyweight_monitor ,也就是指向 monitor 的指针,而原始的信息都存入 monitor 中,最后的标志位 01 改为 10,共计 32

image-20220208143547741

  • 然后 Monitor 中将 Thread-2 保存到 Owner 中,其流程大致如下

image-20220208144007754

  • Thread-2 上锁的过程中,如果 Thread-3Thread-4Thread-5 也来执行 synchronized(obj),就会进入 EntryList BLOCKED,直到Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的
  • 图中 WaitSet 中的 Thread-0Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲 wait-notify 时会分析

synchronized 必须是进入同一个对象的 monitor 才有上述的效果,不加 synchronized 的对象不会关联监视器,不遵从以上规则

4.3、synchronized原理

方法级别的 synchronized 不会在字节码指令中有所体现

static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
    synchronized (lock) {
        counter++;
    }
}

public static void main(java.lang.String[]);
 	descriptor: ([Ljava/lang/String;)V
	flags: ACC_PUBLIC, ACC_STATIC
	Code:
 	  stack=2, locals=3, args_size=1
 		 0: getstatic #2 					// <- lock引用 (synchronized开始)
 		 3: dup
 		 4: astore_1 						// lock引用 -> slot 1
 		 5: monitorenter 					// 将 lock对象 MarkWord 置为 Monitor 指针,这一步是c实现的,我们看不到
 		 6: getstatic #3 					// <- i
 		 9: iconst_1 						// 准备常数 1
 		10: iadd 							// +1
 		11: putstatic #3 					// -> i
 		14: aload_1 						// <- lock引用
 		15: monitorexit 					// 将 lock对象 MarkWord 重置, 唤醒 EntryList
 		16: goto 24
 		19: astore_2 						// e -> slot 2 
 		20: aload_1 						// <- lock引用
 		21: monitorexit 					// 将 lock对象 MarkWord 重置, 唤醒 EntryList
 		22: aload_2 						// <- slot 2 (e)
 		23: athrow 							// throw e
 		24: return
 	  Exception table:
 		 from to target type
 			6 16     19  any
 		   19 22     19  any
 	  LineNumberTable:
        line 8: 0
        line 9: 6
        line 10: 14
        line 11: 24
 	  LocalVariableTable:
 		Start Length Slot Name Signature
 			0     25    0 args [Ljava/lang/String;
 	  StackMapTable: number_of_entries = 2
 		frame_type = 255 /* full_frame */
 		  offset_delta = 19
 		  locals = [ class "[Ljava/lang/String;", class java/lang/Object ]
		  stack = [ class java/lang/Throwable ]
 		frame_type = 250 /* chop */
          offset_delta = 4
  • 注意 Exception table 中,虚拟机将会监测 6~16 行,如果出现异常,将会执行 19~24 行,将锁释放

4.4、轻量级锁

如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。 轻量级锁对使用者是透明的,即语法仍然是 synchronized

  • 假设有两个方法同步块,利用同一个对象加锁
static final Object obj = new Object();

public static void method1() {
    synchronized( obj ) {
        // 同步块 A
        method2();
    }
}
public static void method2() {
    synchronized( obj ) {
        // 同步块 B
    }
}
  • 首先执行到第一个 synchronized 后,会在当前线程的栈帧中创建一个锁记录( Lock Record ):

image-20220208153318586

  • 然后 Lock Record 指向锁对象,如果发现锁对象的 markword 状态为 Normal(01),会尝试 CAS 替换锁对象的 markword,将自身的 markword 替换到锁对象中

image-20220208153521055

  • 如果 CAS 替换成功,则表示由该线程给 Object 对象加锁

image-20220208153848566

  • 然后进入第二个 synchronized ,此时尝试替换锁对象 Objectmarkword ,发现锁对象的 markword 不再是 Normal 了,所以就替换失败,但是将锁对象的 markword 取出来发现,对象头保存的是自己线程中的 Lock Record,自己便将 markword 置为 null,表示锁重入

image-20220208154559707

  • 加锁完毕后,第二个 synchronized 代码块结束,把栈顶的锁记录取出,发现 markwordnull,表示有重入,将会将当前锁记录清除掉,表示重入次数减一
  • 同理,取出第一个锁记录时,发现 markword 不为 null,则将锁记录和锁对象的 markword CAS 替换回来,完成真正的解锁操作

在第二个 synchronized 的时候,如果发现该锁已经加过了,而又不是自己线程加的,就表明出现锁竞争,就会进入锁膨胀的过程,后面会接着讲

在第一个 synchronized 真正解锁操作的过程中,如果替换失败,则证明这个锁已经进行了锁膨胀,或者已经升级为重量级锁了,就会进入重量级锁解锁流程

4.5、锁膨胀

如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

  • Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁

image-20220208160011739

  • 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程,即为锁对象 Object 申请 Monitor 锁,让 Object 指向重量级锁地址,然后自己进入 MonitorEntryList BLOCKED

image-20220208160832592

  • Thread-0 退出同步块解锁时,使用 CASmarkword 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Ownernull,唤醒 EntryListBLOCKED 线程

4.6、自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞,也就是说,当第二个线程来尝试获取锁失败,不会立刻进入阻塞状态(会进行上下文切换),自己会进行几次循环,如果在循环过程中,锁已经被释放了,

那么自己便可以不用进入阻塞状态

  • 自旋也只针对于多核 CPU 有效,自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
  • Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
  • Java 7 之后不能控制是否开启自旋功能

4.7、偏向锁

在学习轻量级锁的时候,大家肯定发现了一个问题,轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。这会不会有点浪费时间?所以 Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有

  • 例如下面代码
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
    }
}
偏向锁
轻量级锁
用 锁记录替换 markword
生成锁记录
用 锁记录替换 markword
生成锁记录
用 锁记录替换 markword
生成锁记录
用 ThreadID 替换 markword
检查 ThreadID 是否是自己
检查 ThreadID 是否是自己
对象
m1内调用synchronized(obj)
m2内调用synchronized(obj)
m2内调用synchronized(obj)
对象
m1内调用synchronized(obj)
m2内调用synchronized(obj)
m2内调用synchronized(obj)

回顾一下 markword 结构

image-20220208165131671

  • 也就是说 biased_lock 为 0 表示无锁,1 表示偏向锁,然后其头部保存到不再是 hashcode 等信息,而是线程 id,再是 epoch,其在批量重偏向和批量撤销会用到

一个对象创建时:

  • 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的 threadepochage 都为 0
  • 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟
  • 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcodeage 都为 0,第一次用到 hashcode 时才会赋值

4.7.1、撤销偏向锁

  • 调用了偏向锁对象的 hashCode,但 markword 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被撤销,轻量级锁会在锁记录中记录 hashCode 重量级锁会在 Monitor 中记录 hashCode
  • 当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁

4.7.2、批量重偏向

如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID(当撤销偏向锁阈值超过 20 次后,JVM 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程)

4.7.3、批量撤销

当撤销偏向锁阈值超过 40 次后,JVM 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的

5、死锁

5.1、现象

在程序设计的时候,往往我们会把一些锁的粒度设置的比较小,比如一间大屋子有两个功能:睡觉、学习,互不相干现在小南要学习,小女要睡觉,但如果只用一间屋子(一个对象锁)的话,那么并发度很低,解决方法是准备多个房间(多个对象锁),但是随着锁的增多,如果某一个线程需要同时获得多把锁,那么我们程序就可能出现死锁状态

@SneakyThrows
public static void main(String[] args) {
    Object a = new Object();
    Object b = new Object();
    Thread t1 = new Thread(() -> {
        synchronized (a) {
            log.info("lock A");
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (b) {
                log.info("lock b");
                log.info("操作...");
            }
        }
    }, "t1");
    Thread t2 = new Thread(() -> {
        synchronized (b) {
            log.info("lock b");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (a) {
                log.info("lock A");
                log.info("操作...");
            }
        }
    }, "t2");
    t1.start();
    t2.start();
}

image-20220209145130819

5.2、检测死锁

检测死锁可以使用 jconsole 工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁:

image-20220209145623019

D:\IDEA WorkSpace\JUCDemo>jstack 16396
2022-02-09 14:56:10
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.261-b12 mixed mode):

"DestroyJavaVM" #14 prio=5 os_prio=0 tid=0x000001e3266b2000 nid=0x57fc waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"t2" #13 prio=5 os_prio=0 tid=0x000001e34378a000 nid=0x55cc waiting for monitor entry [0x0000001541aff000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at com.phz.juc.Test.lambda$main$1(Test.java:41)
        - waiting to lock <0x000000076e6ab300> (a java.lang.Object)
        - locked <0x000000076e6ab310> (a java.lang.Object)
        at com.phz.juc.Test$$Lambda$3/1848402763.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

"t1" #12 prio=5 os_prio=0 tid=0x000001e343788000 nid=0x5390 waiting for monitor entry [0x00000015419ff000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at com.phz.juc.Test.lambda$main$0(Test.java:27)
        - waiting to lock <0x000000076e6ab310> (a java.lang.Object)
        - locked <0x000000076e6ab300> (a java.lang.Object)
        at com.phz.juc.Test$$Lambda$2/932172204.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

"Service Thread" #11 daemon prio=9 os_prio=0 tid=0x000001e3433be800 nid=0x60e8 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C1 CompilerThread3" #10 daemon prio=9 os_prio=2 tid=0x000001e3433a1000 nid=0x4d54 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread2" #9 daemon prio=9 os_prio=2 tid=0x000001e34339f000 nid=0x13e4 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread1" #8 daemon prio=9 os_prio=2 tid=0x000001e34339a000 nid=0x516c waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" #7 daemon prio=9 os_prio=2 tid=0x000001e343397800 nid=0x1d08 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Monitor Ctrl-Break" #6 daemon prio=5 os_prio=0 tid=0x000001e343393800 nid=0x4a2c runnable [0x00000015412ff000]
   java.lang.Thread.State: RUNNABLE
        at java.net.SocketInputStream.socketRead0(Native Method)
        at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
        at java.net.SocketInputStream.read(SocketInputStream.java:171)
        at java.net.SocketInputStream.read(SocketInputStream.java:141)
        at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
        at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
        at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
        - locked <0x000000076e2c5598> (a java.io.InputStreamReader)
        at java.io.InputStreamReader.read(InputStreamReader.java:184)
        at java.io.BufferedReader.fill(BufferedReader.java:161)
        at java.io.BufferedReader.readLine(BufferedReader.java:324)
        - locked <0x000000076e2c5598> (a java.io.InputStreamReader)
        at java.io.BufferedReader.readLine(BufferedReader.java:389)
        at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:47)

"Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x000001e340f19000 nid=0x3510 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x000001e340f03000 nid=0x2c34 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x000001e340ee6000 nid=0xab4 in Object.wait() [0x0000001540fff000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x000000076e008ee0> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)
        - locked <0x000000076e008ee0> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:165)
        at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:216)

"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x000001e340ede000 nid=0x2288 in Object.wait() [0x0000001540eff000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x000000076e006c00> (a java.lang.ref.Reference$Lock)
        at java.lang.Object.wait(Object.java:502)
        at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
        - locked <0x000000076e006c00> (a java.lang.ref.Reference$Lock)
        at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)

"VM Thread" os_prio=2 tid=0x000001e340eb1800 nid=0x779c runnable

"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x000001e3266c9000 nid=0x72f4 runnable

"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x000001e3266ca800 nid=0x1d84 runnable

"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x000001e3266cc000 nid=0x6b90 runnable

"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x000001e3266cd000 nid=0x6fb8 runnable

"GC task thread#4 (ParallelGC)" os_prio=0 tid=0x000001e3266cf000 nid=0x5a4c runnable

"GC task thread#5 (ParallelGC)" os_prio=0 tid=0x000001e3266d0000 nid=0x6c90 runnable

"GC task thread#6 (ParallelGC)" os_prio=0 tid=0x000001e3266d3000 nid=0x322c runnable

"GC task thread#7 (ParallelGC)" os_prio=0 tid=0x000001e3266d4000 nid=0x39a8 runnable

"GC task thread#8 (ParallelGC)" os_prio=0 tid=0x000001e3266d5000 nid=0x2fac runnable

"GC task thread#9 (ParallelGC)" os_prio=0 tid=0x000001e3266d6000 nid=0x22e0 runnable

"VM Periodic Task Thread" os_prio=2 tid=0x000001e3433c2800 nid=0x5910 waiting on condition

JNI global references: 319


Found one Java-level deadlock:
=============================
"t2":
  waiting to lock monitor 0x000001e340ee4f98 (object 0x000000076e6ab300, a java.lang.Object),
  which is held by "t1"
"t1":
  waiting to lock monitor 0x000001e340ee3578 (object 0x000000076e6ab310, a java.lang.Object),
  which is held by "t2"

Java stack information for the threads listed above:
===================================================
"t2":
        at com.phz.juc.Test.lambda$main$1(Test.java:41)
        - waiting to lock <0x000000076e6ab300> (a java.lang.Object)
        - locked <0x000000076e6ab310> (a java.lang.Object)
        at com.phz.juc.Test$$Lambda$3/1848402763.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)
"t1":
        at com.phz.juc.Test.lambda$main$0(Test.java:27)
        - waiting to lock <0x000000076e6ab310> (a java.lang.Object)
        - locked <0x000000076e6ab300> (a java.lang.Object)
        at com.phz.juc.Test$$Lambda$2/932172204.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

image-20220209145724342

image-20220209145744060

还可以使用 jconsle 工具,直接控制台输入 jconsole

image-20220209145923241

image-20220209150020523

image-20220209150036303

5.3、活锁

活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束,例如

static volatile int count = 10;

public static void main(String[] args) {
    new Thread(() -> {
        // 期望减到 0 退出循环
        while (count > 0) {
            try {
                TimeUnit.MILLISECONDS.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count--;
            log.info("count: {}", count);
        }
    }, "t1").start();
    new Thread(() -> {
        // 期望超过 20 退出循环
        while (count < 20) {
            try {
                TimeUnit.MILLISECONDS.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count++;
            log.info("count: {}", count);
        }
    }, "t2").start();
}

image-20220209151501703

由于两个线程相互影响互相之间的结束条件,加上睡眠时间也是一致的,导致一直不停的执行,解决办法就是让睡眠时间分布随机

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值