文章目录
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;
}
}
我们的理想情况,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、概念归纳
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;
}
}
}
注意一点就是,synchronized 锁被一个线程拿到后,如果这个线程时间片用完了,它是不会释放锁的,其他线程还是只能在外面等着
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);
}
3.2、不可变线程安全类
String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的
特别地,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 中,一个切面正常情况都是一个单例,可能会存在多个线程同时执行 before 和 after 方法,导致 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;
}
}
}
- 不做任何处理,可以发现,会出现超卖的情况
- 要解决这种问题,就需要找出这个临界资源,在哪个地方进行的读写操作,本例中,自然而然就是 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 位虚拟机为例
- 普通对象
一个对象到底属于什么类型,可以由 Klass Word 指向的类 class 信息得到
- 数组对象
额外还有一个 4 字节的数组长度
- 其中 markword :
- 64 位的 markword :
其中有很多的信息我们后面会逐个了解,这里先不介绍
可以发现,对于一个对象,其对象头已经占用了不少的空间,如 Integer 除了其必须包含的 4 字节的 int 数据,还有 8 字节的对象头数据,所以一个 Integer 占用内存便是 int 类型的三倍
4.2、Monitor 原理
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针
- 刚开始 Monitor 中 Owner 为 null
- 当 Thread-2 执行 synchronized(obj) 就会将 obj 对象头中的 hashcode、age、biased_lock变为ptr_to_heavyweight_monitor ,也就是指向 monitor 的指针,而原始的信息都存入 monitor 中,最后的标志位 01 改为 10,共计 32 位
- 然后 Monitor 中将 Thread-2 保存到 Owner 中,其流程大致如下
- 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入 EntryList BLOCKED,直到Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的
- 图中 WaitSet 中的 Thread-0,Thread-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 ):
- 然后 Lock Record 指向锁对象,如果发现锁对象的 markword 状态为 Normal(01),会尝试 CAS 替换锁对象的 markword,将自身的 markword 替换到锁对象中
- 如果 CAS 替换成功,则表示由该线程给 Object 对象加锁
- 然后进入第二个 synchronized ,此时尝试替换锁对象 Object 的 markword ,发现锁对象的 markword 不再是 Normal 了,所以就替换失败,但是将锁对象的 markword 取出来发现,对象头保存的是自己线程中的 Lock Record,自己便将 markword 置为 null,表示锁重入
- 加锁完毕后,第二个 synchronized 代码块结束,把栈顶的锁记录取出,发现 markword 为 null,表示有重入,将会将当前锁记录清除掉,表示重入次数减一
- 同理,取出第一个锁记录时,发现 markword 不为 null,则将锁记录和锁对象的 markword CAS 替换回来,完成真正的解锁操作
在第二个 synchronized 的时候,如果发现该锁已经加过了,而又不是自己线程加的,就表明出现锁竞争,就会进入锁膨胀的过程,后面会接着讲
在第一个 synchronized 真正解锁操作的过程中,如果替换失败,则证明这个锁已经进行了锁膨胀,或者已经升级为重量级锁了,就会进入重量级锁解锁流程
4.5、锁膨胀
如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
- 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
- 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程,即为锁对象 Object 申请 Monitor 锁,让 Object 指向重量级锁地址,然后自己进入 Monitor 的 EntryList BLOCKED
- 当 Thread-0 退出同步块解锁时,使用 CAS 将 markword 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程
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 结构
- 也就是说 biased_lock 为 0 表示无锁,1 表示偏向锁,然后其头部保存到不再是 hashcode 等信息,而是线程 id,再是 epoch,其在批量重偏向和批量撤销会用到
一个对象创建时:
- 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的 thread、epoch、age 都为 0
- 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟
- 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、 age 都为 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();
}
5.2、检测死锁
检测死锁可以使用 jconsole 工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁:
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.
还可以使用 jconsle 工具,直接控制台输入 jconsole
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();
}
由于两个线程相互影响互相之间的结束条件,加上睡眠时间也是一致的,导致一直不停的执行,解决办法就是让睡眠时间分布随机