若文章内容或图片失效,请留言反馈。
部分素材来自网络,若不小心影响到您的利益,请联系博主删除。
- 视频链接:https://www.bilibili.com/video/av81461839
- 配套资料:https://pan.baidu.com/s/1lSDty6-hzCWTXFYuqThRPw( 提取码:5xiu)
写这篇博客旨在制作笔记,方便个人在线阅览,巩固知识。无他用。
博客的内容主要来自视频内容和资料中提供的学习笔记。当然,在此基础之上也增删了一些内容。
参考书籍
- 《实战 JAVA 高并发程序设计》 葛一鸣 著
- 《JAVA 并发编程实战》 Brian Goetz 等 著
- 《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明 著
参考博客
- Java并发编程(中上篇)从入门到深入 超详细笔记
同步和异步通常用来形容一次方法调用。
同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。
异步方法更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作。
并发和并行是两个非常容易被混淆的概念。它们都可以表示两个或者多个任务一起执行,但侧重点有所不同。
并发偏重于多个任务交替执行,而多个任务之间有可能还是串行的。并行是真正意义上的 “同时执行”。
从严格意义上来说,并行的多个任务是真的同时执行;
而对于并发来说,这个过程是交替的,一会儿执行任务 A,一会儿执行任务 B,系统会不停地在二者间切换。
但对于外部观察者来说,即使多个任务间是串行并发的,也会产生多任务并行执行的错觉。
临界区用来表示一种公共资源或者说共享数据,可以被多个线程使用。
但是每一次只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源就必须等待。
在并行程序中,临界区资源是保护的对象。
阻塞和非阻塞通常用来形容多线程间的相互影响。
比如一个线程占用了临界区资源,那么其他所有需要这个资源的线程就必须在临界区中等待。
等待会引起线程挂起,这种情况就是线程阻塞。
此时,如果占用资源的线程一直不愿意释放资源,那么其他所有阻塞在这个临界区的线程都不能工作。
非阻塞的意思与之相反,它强调没有一个线程可以妨碍其他线程执行,所有的线程都会尝试不断向前执行。
死锁、饥饿、活锁都属于多线程的活跃性问题。
死锁是最糟糕的一种情况。
当一个线程永远的持有一个锁,并且其他线程都尝试获得这个锁的时候,它们将永远被阻塞。
这种情况就是最简单的死锁形式,其中多个线程由于存在环路的锁依赖关系而永远地等待下去。
饥饿是指某一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行。
比如,它的优先级可能太低了,而高优先级的线程不断抢占它需要的资源,导致低优先级线程无法工作。
此外,某一个线程一直占着关键资源不放,导致其他需要这个资源的线程无法正常执行,这种情况也是饥饿的一种。
与死锁相比,饥饿还是有可能在未来一段时间内解决的(比如高优先级的线程已经完成任务,不再疯狂执行)。
活锁不会阻塞线程,但也不能继续执行,因为线程将不断重复执行相同的操作,而且总会失败。
活锁通常发生在处理事务消息的应用程序中:
如果不能成功地处理某个消息,那么消息处理机制将回滚整个事务,并将它重新放到队列的开头。
如果消息处理器在处理某种特定类型的消息时存在错误并导致它失败,那么每当这个消息从队列中取出并传递到存在错误的处理器时,都会发生事务回滚。由于这条消息又被放到队列开头,因此处理器将被反复调用,并返回相同的结果。虽然处理消息的线程没有阻塞,但也无法继续执行下去。这种形式的活锁通常是由过度的错误恢复代码造成的,因为它错误地将不可修复的错误作为可修复的错误。
当多个相互协作的线程都对彼此进行响应从而修改各自的状态,并使得任何一个线程都无法继续执行的时候,就发生了活锁。
Java 线程的状态转换
Java 语言定义了 6 种线程状态。
在任意一个时间点中,一个线程只能有且只有其中的一种状态,并且可以通过特定的方法在不同状态之间转换。
这 6 种状态分别是:New、Runnable、Waiting、Timed Waiting、Blocked、Terminated
/**
* 线程中的所有状态都在 Thread 中的 State 枚举中定义
*/
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
上述 6 种状态在遇到特定事件发生的时候将会互相转换,它们的转换关系如下图所示
从操作系统层面来看来线程状态则是五种:初始状态、可运行状态(就绪状态) 、运行状态、阻塞状态、终止状态。
共享模型之管程-共享问题
两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?
static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter++;
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter--;
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}", counter);
}
多次运行上方的代码块,发现控制台输出结果中多次出现了不为 0 的数
以上的结果可能是正数、负数、零。
这是因为 Java 中对静态变量的自增,自减并不是原子操作,要彻底理解,必须从字节码来进行分析
对于 i + + 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令
getstatic i // 获取静态变量 i 的值
iconst_1 // 准备常量 1
iadd // 自增
putstatic i // 将修改后的值存入静态变量 i
对于 i - - 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令
getstatic i // 获取静态变量 i 的值
iconst_1 // 准备常量 1
isub // 自减
putstatic i // 将修改后的值存入静态变量 i
Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换
如果是单线程以上 8 行代码是顺序执行(不会交错)没有问题
但多线程下这 8 行代码可能交错运行
具体情况如下图所示(左边是结果为 负数 的情况;右边是结果为 正数 的情况)
简言之,造成上述结果中出现正负数的原因是:指令交错 和 线程上下文切换
static int counter = 0;
static void increment() {
//临界区
counter++;
}
static void decrement() {
//临界区
counter--;
}
竞态条件(Race Condition)
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
共享模型之管程:synchronized
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
synchronized 即俗称的 对象锁
它采用互斥的方式让同一时刻至多只有一个线程能持有 对象锁,其它线程再想获取这个 对象锁 时就会阻塞住。
这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
synchronized(对象) // 线程 1 持有对象锁时,线程 2 再想获取对象锁时 就会处于 blocked 状态
{
临界区
}
加锁可解决之前的问题
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test4_3")
public class Test4_3 {
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.debug("{}", counter);
}
}
关于上述过程中 synchronized 关键字的图解
synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。
为了加深理解,请思考下面的三个问题
synchronized (lock) {
for (int i = 0; i < 5000; i++) {
counter--;
}
}
关键字 synchorinized
把需要保护的共享变量放入一个类
输出结果
17:41:00 [main] c.Test4_3_2 - 0
关键字 synchorinized
以下两处代码的效果等价
至于不加 synchorinized 的方法就好比不遵守规则的人,不去老实排队(好比翻窗户进去的)
其实就是考察 synchornized 锁住的是哪个对象
synchornized 关键字加在了 static 修饰的方法(即静态方法)上,效果等同于在当前类上加了锁;
synchornized 关键字加在了实例方法上,效果等同于在当前实例上加了锁;
这两者并不是同一对象。
共享模型之管程:线程安全分析
成员变量和静态变量是否线程安全?
局部变量是否是线程安全的?
public static void test1() {
int i = 10;
i++;
}
每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享
public static void test1();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=0
0: bipush 10
2: istore_0
3: iinc 0, 1
6: return
LineNumberTable:
line 10: 0
line 11: 3
line 12: 6
LocalVariableTable:
Start Length Slot Name Signature
3 4 0 i I
但是,局部变量的引用则稍有不同
下面是一个成员变量的例子
/*
* method2() 和 method3() 中都调用了共享资源 list。
* 当多个线程执行,发生指令交错时,可能会出现指针问题
*/
class ThreadUnsafe {
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); }
}
多运行几次下面的代码,就可能会出现控制台输出报错信息的情况。
public class TestThreadSafe {
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
ThreadUnsafe test = new ThreadUnsafe();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + (i+1)).start();
}
}
}
控制台输出报错信息
Exception in thread "Thread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
at java.util.ArrayList.rangeCheck(ArrayList.java:653)
at java.util.ArrayList.remove(ArrayList.java:492)
at cn.itcast.n4.ThreadUnsafe.method3(TestThreadSafe.java:33)
at cn.itcast.n4.ThreadUnsafe.method1(TestThreadSafe.java:24)
at cn.itcast.n4.TestThreadSafe.lambda$main$0(TestThreadSafe.java:14)
at java.lang.Thread.run(Thread.java:748)
- 这里的内容对应的正好是视频的 第 65 P:线程安全分析-局部变量引用
- 我把这个课程的老师在视频下方的评论截图给贴过来了。
将 list 修改为局部变量后,就不存在上面的问题了
更改一下之前的主方法即可测试(ThreadSafe test = new ThreadSafe();
)
class ThreadSafe {
public void method1(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); }
}
换言之便是 没有共享,便没有伤害。(即 局部变量没有暴露给外部的时候是线程安全的)
但是当我们把这个局部变量的引用暴露给外部的时候,还会是线程安全的吗?
方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public,此时会不会出现线程安全问题?
情况 1:有其它线程调用 method2 和 method3
这种情况依旧没有线程安全问题。
虽然其他线程调用了 method2 和 method3方法,但它们传进去的参数(list 对象)肯定是不一样的
情况 2:在 情况1 的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法
class ThreadSafe {
public void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
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 ThreadSafeSubClass extends ThreadSafe{
@Override
public void method3(ArrayList<String> list) {
new Thread(() -> { list.remove(0); }).start();
}
}
这种情况会出现线程安全问题。
子类继承了父类,并重写了父类中的 method3()。在这个方法中,list 变量是被共享的。
其他线程实可以通过子类中重写的方法来访问和改变父类中的方法的局部变量的。
如果指令重排使得 list.add() 发生在 list.remove() 之前
重写是多态性的体现,这里涉及到了动态分派的问题。动态分派会调用 invokevirtual
这条指令
《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 周志明 著 语
- 根据《Java 虚拟机规范》,
invokevirtual
指令的运行时(不包括特殊情况)解析过程大致分为以下几步:
- 找到操作数栈顶的第一个元素所指向的对象的实际类型,记作 C。
- 如果在类型 C 中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验。
如果通过权限校验,则返回这个方法的直接引用,查找过程结束;
如果不通过权限校验,则返回 java.lang.IllegalAccessError 异常。- 否则,按照继承关系从下往上依次对 C 的各个父类进行第二步的搜索和验证过程。
- 如果始终没有找到合适的方法,则抛出 java.lang.AbstractMethodError 异常。
- 因为
invokevirtual
指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用中的invokevirtual
指令并不是把常量池中方法的符号引用解析到直接引用上就结束了,还会根据方法接收者的实际类型来选择方法版本,这个过程就是 Java 语言中方法重写的本质。- 我们把这种在运行期根据实际类型确定方法执行版本的分派过程称为 动态分派。
如果不想子类影响父类中的公共方法 method1() 的话,可以添加 final(即 public final void method1(int loopNumber) { }
)
从这个例子可以看出 private 或 final 提供 安全 的意义所在,请体会开闭原则中的 闭。
String、Integer、StringBuffer、Random、Vector、Hashtable、java.util.concurrent
包下的类
这里的内容主要来自于微信公众号 程序员 cxuan 中提供的 Java 基础核心总结.pdf
java.util.concurrent
包下的类
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。
可以理解为如下代码块
Hashtable table = new Hashtable();
new Thread(()->{
table.put("key", "value1");
}).start();
new Thread(()->{
table.put("key", "value2");
}).start();
分析下面代码是否线程安全?
Hashtable table = new Hashtable();
// 线程 1,线程 2
if (table.get("key") == null) {
table.put("key", value);
}
线程 1 得到的数据为 null,之后释放锁。此后线程 2 得到的数据也为 null。两者都插入了数据,且其中必有一个数据被覆盖了。
所以上述的代码块不是线程安全的。
这里的问题就在于判断和动作不是一起的,if (table.get("key") == null)
这里的判断是没有加锁的。
String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的
有同学或许有疑问,String 有 replace,substring 等方法可以改变值啊,那么这些方法又是如何保证线程安全的呢?
java/lang/String.java
源码
这里以 String 的 substring 方法为例(replace 方法的原理也和这个差不多)
public String substring(int beginIndex) {
if (beginIndex < 0) { throw new StringIndexOutOfBoundsException(beginIndex); }
int subLen = value.length - beginIndex;
if (subLen < 0) { throw new StringIndexOutOfBoundsException(subLen); }
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
创建新字符串的时候会在原有的 value 基础进行一个复制。复制完成之后,再赋值给新字符串的 value 属性。
public String(char value[], int offset, int count) {
if (offset < 0) { throw new StringIndexOutOfBoundsException(offset); }
if (count <= 0) {
if (count < 0) { throw new StringIndexOutOfBoundsException(count); }
if (offset <= value.length) {
this.value = "".value;
return;
}
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) { throw new StringIndexOutOfBoundsException(offset + count); }
this.value = Arrays.copyOfRange(value, offset, offset + count);
}
不改动原有对象的属性,而是用新的对象来实现对象的不可变效果,这样保证了线程的安全性。(没有改变,也就没有线程安全问题)
Servlet 是在 Tomcat 环境下运行的,只有一个实例,所以它肯定会被 Tomcat 的多个线程共享使用。
public class MyServlet_1 extends HttpServlet {
// 是否安全?(否。Hashtable 才是线程安全的)
Map<String, Object> map = new HashMap<>();
// 是否安全?(是,字符串属于不可变类)
String S1 = "...";
// 是否安全?(是)
final String S2 = "...";
// 是否安全?(否)
Date D1 = new Date();
// 是否安全?(否)
// final 修饰引用类型时,表示对其初始化之后便不能再让其指向另一个对象。
// 但是日期里面的其他属性(诸如年月日)仍然可以被改变。
final Date D2 = new Date();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
// 使用上述变量
}
}
此处 Service 是 Servlet 的一个成员变量,而 Servlet 只有一个,所以这里 Sevice 也是会被多个线程共享的,其中的 count 也是共享资源。
public class MyServlet_2 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++;
/* ****************[临界区]**************** */
}
}
下方的代码块是 Spring Aop 的一个典型应用(做一个 切面,加 前置通知 和 后置通知)
Spring 中每一个对象都是默认单例的(除非是加了 @Scope
注解)
既然是单例就意味着其需要被共享,此处对象中的成员变量也是需要被共享的
@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));
}
}
可以将上述的代码做成 环绕通知,将 start 和 end 都做成环绕通知中的局部变量,这样可以避免线程安全问题。
// 该类中没有成员变量,一般也就意味着(在多个线程访问的情况下)没有其他线程可以更改其的属性、状态
// 一般没有成员变量的类都是线程安全的
public class UserDaoImpl implements UserDao {
public void update() {
String sql = "update user set password = ? where username = ?";
// 是否安全(是)
// 此处的 conn 属于方法内的局部变量
// 未来即使有多个线程同时访问它,也是互不干扰的。
// 例如:线程 1 创建 conn(1),线程 2 创建 conn(2),这两者都是互相独立的
try (Connection conn = DriverManager.getConnection("", "", "")) {
// ...
} catch (Exception e) {
// ...
}
}
}
public class UserServiceImpl implements UserService {
// 是否安全(是)
// 虽然该类中有成员变量 userDao,但 userDao 中没有可更改的属性(上面已经分析过了)
private UserDao userDao = new UserDaoImpl();
public void update() {
userDao.update();
}
}
public class MyServlet_3 extends HttpServlet {
// 是否安全(是)
// 该类中有成员变量 userService,UerService 类中有 成员变量
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserDaoImpl implements UserDao {
// 是否安全(否)
// 此处的 conn 是成员变量
// 因为 servlet 实例只有一个,导致了 service 实例也只有一个,进而导致了 dao 实例也只有一个
// 此时 dao 中的成员变量自然是可以被多个线程被共享的
// 线程 1 刚刚创建 conn 之后,(线程 1 还没有来得及使用 conn)线程 2 就更改了 conn,这样一来就导致了线程安全问题
private Connection conn = null;
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
conn = DriverManager.getConnection("", "", "");
// ...
conn.close();
}
}
public class UserServiceImpl implements UserService {
private UserDao userDao = new UserDaoImpl();
public void update() {
userDao.update();
}
}
public class MyServlet_4 extends HttpServlet {
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.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();
}
}
public class UserServiceImpl implements UserService {
public void update() {
// 这里是没有线程安全的问题的
// userDao 是作为 UserServieImpl 中的局部变量存在的
// 线程 1 调用它创建了一个 userDao,线程 2 调用它创建了一个新的 userDao,两者都不会互相干扰
// 但是不推荐这样写。
// 为了减少资源的占用、解决线程安全问题,还是应该把 connection 作为 Dao 中的局部变量来书写
UserDao userDao = new UserDaoImpl();
userDao.update();
}
}
public class MyServlet extends HttpServlet {
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public abstract class Test {
public void bar() {
// 是否安全(否)
// sdf 是局部变量。但是我们还要判断这个 局部变量的引用 是否暴露给了外界
// 该类是一个抽象类。仍然有机会把该类中的局部变量传递给子类中的方法。
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
foo(sdf);
}
// 子类中的方法就有可能被其他线程调用(比如直接在子类中创建一个新的线程),从而导致了多线程并发访问 sdf 的问题。
public abstract foo(SimpleDateFormat sdf);
public static void main(String[] args) {
new Test().bar();
}
}
其中 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();
}
}
即继承了父类的子类,子类中新创建了线程,这个线程和父类中的线程调用的对象是同一个,如此就会造成线程不安全问题
解决办法就是将父类子类中的 foo 方法设置为 private 或者 final
比较 JDK 中 String 类的实现
// String 类的修饰符就是 final,如此可阻止子类覆盖父类中的一些行为
public final class String{
// ... ...
}
测试下面代码是否存在线程安全问题,并尝试改正
// 售票窗口
public class TicketWindow {
private int count;
public TicketWindow(int count) { this.count = count; }
// 获取余票数量
public int getCount() { return count; }
// 售票
public synchronized int sell(int amount) {
if (this.count >= amount) {
this.count -= amount;
return amount;
} else {
return 0;
}
}
}
@Slf4j(topic = "c.ExerciseSell")
public class ExerciseSell {
public static void main(String[] args) throws InterruptedException {
// 模拟多人买票
TicketWindow window = new TicketWindow(1000);
// 所有线程的集合
// 这里的变量不会被多个线程所共享,所以使用普通的 ArrayList 即可
List<Thread> threadList = new ArrayList<>();
// 卖出的票数统计
// Vector 是线程安全的 List 实现类,底层是通过数组实现的
List<Integer> amountList = new Vector<>();
for (int i = 0; i < 2000; i++) {
Thread thread = new Thread(() -> {
/* ****************[临界区]**************** */
// 买票
int amount = window.sell(random(5));
// 统计买票数
amountList.add(amount);
// Vector 的 add() 是加了锁 synchronized 的
// amount 和 amountList 是属于两个不同的共享变量,不需要考虑上面两个方法的组合的线程安全
/* ****************[临界区]**************** */
});
threadList.add(thread);
thread.start();
}
for (Thread thread : threadList) {
thread.join();
}
// 统计卖出的票数和剩余票数
log.debug("余票:{}", window.getCount());
log.debug("卖出的票数:{}", amountList.stream().mapToInt(i -> i).sum());
}
// Random 为线程安全
static Random random = new Random();
// 随机 1~5
public static int random(int amount) {
return random.nextInt(amount) + 1;
}
}
首先要分析出临界区(多个线程对共享变量有读写操作的代码区)的代码位置。(已经在代码块中注释说明了)
显然,上述代码的临界区不是线程安全的。(可能会发生指令重排)
解决办法就是在 TicketWindow 类的 sell 方法上加上 synchronized 关键字。
public synchronized int sell(int amount){ }
测试下面代码是否存在线程安全问题,并尝试改正
// 账户
public class Account {
private int money;
public Account(int money) {
this.money = money;
}
public int getMoney() {
return money;
}
public void setMoney(int money) {
this.money = money;
}
// 转账
public void transfer(Account target, int amount) {
if (this.money >= amount) {
this.setMoney(this.getMoney() - amount);
target.setMoney(target.getMoney() + amount);
}
}
}
@Slf4j(topic = "c.ExerciseTransfer")
public class ExerciseTransfer {
public static void main(String[] args) throws InterruptedException {
Account a = new Account(1000);
Account b = new Account(1000);
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
a.transfer(b, randomAmount());
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
b.transfer(a, randomAmount());
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
// 查看转账 2000 次后的总金额
log.debug("total:{}", (a.getMoney() + b.getMoney()));
}
// Random 为线程安全
static Random random = new Random();
// 随机 1~100
public static int randomAmount() {
return random.nextInt(100) + 1;
}
}
启动主程序后发现控制台的打印结果并不是 2000(可能大于 2000,也可能小于 2000)
给 Account 类的 transfer 方法加上 synchronized
public void synchronized transfer(Account target, int amount) {
if (this.money >= amount) {
this.setMoney(this.getMoney() - amount);
target.setMoney(target.getMoney() + amount);
}
}
这样做是解决不了问题的。
将 synchronized 加在了成员方法上,即相当于把锁加在了当前类上(即 synchronized(this){ ... }
)
public void transfer(Account target, int amount) {
synchronized (this) {
if (this.money >= amount) {
this.setMoney(this.getMoney() - amount);
target.setMoney(target.getMoney() + amount);
}
}
}
此时只会保护自己的方法(this.setMoney() 和 this.getMoney()),但无法影响到另外一个对象上的方法
那么在此基础上再给 target 加锁呢?
这样确实可以解决当前问题,但是更容易产生死锁问题。
对于 this 和 target 来说,Account 类是它们所共享的,所以此处我们可以直接对 Account 加锁 synchronized (Account.class)
public void transfer(Account target, int amount) {
synchronized (Account.class) {
if (this.money >= amount) {
this.setMoney(this.getMoney() - amount);
target.setMoney(target.getMoney() + amount);
}
}
}
不过这样做的效果并不是很好(串行了,性能下降了),是有更好的方法的,这个之后再讲。
共享模型之管程:Monitor
参考资料:https://stackoverflow.com/questions/26357186/what-is-in-java-object-header
这里以 32 位虚拟机为例
普通对象
Object Header(64 bits) | |
---|---|
Mark Word(32 bits) | Klass Word(32 bits) |
数组对象
Object Header(96 bits) | ||
---|---|---|
Mark Word(32 bits) | Klass Word(32 bits) | array length(32 bits) |
其中 Mark Word 结构为
Mark Word(32 bits) | State | ||||
hashcode:25 | age:4 | biased_lock:0 | 01 | Normal | |
thread:23 | epoch:2 | age:4 | biased_lock:1 | 01 | Biased |
ptr_to_lock_record:30 | 00 | Lightweight Locked | |||
ptr_to_heavyweight_monitor:30 | 10 | Heavyweight Locked | |||
11 | Marked for GC |
64 位虚拟机 中的 Mark Word
Mark Word(64 bits) | State | |||||
unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | Normal |
thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 | Biased |
ptr_to_lock_record:62 | 00 | Lightweight Locked | ||||
ptr_to_heavyweight_monitor:62 | 10 | Heavyweight Locked | ||||
11 | Marked for GC |
以下内容参考自 《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 (周志明 著)
HotSpot 虚拟机,是 Sun/OracleJDK 和 OpenJDK 中的默认 Java 虚拟机,也是目前使用范围最广的 Java 虚拟机。
该虚拟机中, 对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data) 和 对齐填充(Padding)。
HotSpot 虚拟机对象的 对象头 部分包括两类信息:Mark Word 和 类型指针
第一类是用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,这部分数据的长度在 32 位和 64 位的虚拟机(未开启压缩指针)中分别为 32 个比特和 64 个比特,官方称它为 “Mark Word”。
对象需要存储的运行时数据很多,其实已经超出了 32、64 位 Bitmap 结构所能记录的最大限度,但对象头里的信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word 被设计成一个有着动态定义的数据结构,以便在极小的空间内存储尽量多的数据,根据对象的状态复用自己的存储空间。
例如在 32 位的 HotSpot 虚拟机中,如对象未被同步锁锁定的状态下,Mark Word 的 32 个比特存储空间中的 25 个比特用于存储对象哈希码,4 个比特用于存储对象分代年龄,2 个比特用于存储锁标志位,1 个比特固定为 0,在其他状态(轻量级锁定、重量级锁定、GC 标记、可偏向)下对象的存储内容如下表所示。
存储内容 | 标志位 | 状态 |
---|---|---|
对象哈希码、对象分代年龄 | 01 | 未锁定 |
指向锁记录的指针 | 00 | 轻量级锁定 |
指向重量级锁的指针 | 10 | 膨胀(重量级锁定) |
空,不需要记录信息 | 11 | GC 标记 |
偏向线程 ID、偏向时间戳、对象分代年龄 | 01 | 可偏向 |
对象头的另外一部分是类型指针,即对象指向它的类型元数据的指针。
Java 虚拟机通过这个指针来确定该对象是哪个类的实例。
并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据信息并不一定要经过对象本身。
此外,如果对象是一个 Java 数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通 Java 对象的元数据信息确定 Java 对象的大小,但是如果数组的长度是不确定的,将无法通过元数据中的信息推断出数组的大小。
接下来的 实例数据 部分是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。这部分的存储顺序会受到虚拟机分配策略参数(-XX:FieldsAllocationStyle
参数)和字段在Java 源码中定义顺序的影响。
HotSpot 虚拟机默认的分配顺序为 longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs),从以上默认的分配策略中可以看到,相同宽度的字段总是被分配到一起存放,在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。
如果 HotSpot 虚拟机的 +XX:CompactFields
参数值为 true(默认就为 true),那子类之中较窄的变量也允许插入父类变量的空隙之中,以节省出一点点空间。
对象的第三部分是 对齐填充,这并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。
由于 HotSpot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说就是任何对象的大小都必须是 8 字节的整数倍。
对象头部分已经被设计成正好是 8 字节的倍数(1 倍或 2 倍),因此如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。
Monitor 被翻译为监视器或管程
每个 Java 对象都可以关联一个 Monitor 对象
如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针
Monitor 的结构如下图所示
synchronized(obj){ //临界区代码 }
就会将 Monitor 的所有者 Owner 置为 Thread-2synchronized(obj){ //临界区代码 }
,static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
synchronized (lock) {
counter++;
}
}
上方的代码块对应的字节码是(javap -v Java文件名称.class
)
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 // Field lock:Ljava/lang/Object;
# <- lock 引用 (synchronized 的开始)
3: dup # dup:复制操作数栈栈顶的内容,并将复制内容压入栈顶
# 此处是复制 lock 对象的引用,现在这里就有 lock 对象的两个引用了
# 分别对应着 monitorenter 和 monitorexit 两个指令使用的阶段
4: astore_1 # lock 引用 -> slot 1
# 这里将操作数栈中的 lock 引用压入局部变量表的 1 号槽位
# 如此这般是为了之后的解锁
5: monitorenter # 将 lock 对象的 Mark Word 置为 Monitor 指针
# 该指令会消耗掉栈顶元素(lock 对象的引用)
# 并对 lock 对象加锁
# #####################################################################################################
# 下面的这一段字节码就是 counter++ 的意思
6: getstatic #3 // Field counter:I
# <- i
9: iconst_1 # 准备常数 1
10: iadd # +1(即自增)
11: putstatic #3 // Field counter:I
# -> i
# #####################################################################################################
14: aload_1 # <- lock 引用
# 加载之前存储在 1 号槽位中的 lock 引用(astore_1)
15: monitorexit # 将 lock 对象 Mark Word 重置, 唤醒 EntryList
# 上锁了之后,Mark Word 中存储的就是 Monitor 的指针了
# 原先 Mark Word 中存储的内容则存储在了 Monitor 对象中
# 解锁重置会从 Montor 对象中取出数据,将之前 Mark Word 的内容还原
16: goto 24 # 这里的意思是直接跳到 24: return
# #####################################################################################################
# 以上都是同步代码块正常执行(正常解锁)的情况
# 下面的字节码(19 ~ 23)则是异常情况下,释放同步代码块的锁的操作
19: astore_2 # e -> slot 2
# 将异常对象的引用抛到局部变量表中的 2 号槽位上
20: aload_1 # <- lock 引用
# 加载之前暂存在 1 号槽位的对象的引用(astore_1)
21: monitorexit # 将 lock 对象 MarkWord 重置, 唤醒 EntryList
22: aload_2 # <- slot 2 (e)
# 加载暂存在 2 号槽位上的异常对象
23: athrow # throw e
# #####################################################################################################
24: return
Exception table:
from to target type
6 16 19 any
19 22 19 any
LineNumberTable:
line 10: 0
line 11: 6
line 12: 14
line 13: 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
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: new #4 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."":()V
7: putstatic #2 // Field lock:Ljava/lang/Object;
10: iconst_0
11: putstatic #3 // Field counter:I
14: return
LineNumberTable:
line 6: 0
line 7: 10
使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是 synchronized
假设有两个方法同步块,利用同一个对象加锁
static final Object obj = new Object();
public static void method1() {
synchronized (obj) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized (obj) {
// 同步块 B
}
}
这里稍稍提一个概念:CAS(Compare and Swap),它体现的是一种乐观锁的思想
如果在尝试加轻量级锁的过程中,CAS 操作无法成功。
这时有一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),此时就需要进行锁膨胀,将轻量级锁变为重量级锁。
static Object obj = new Object();
public static void method1() {
synchronized (obj) {
// 同步块
}
}
重量级锁竞争的时候,还可以使用自旋来进行优化。
如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
线程阻塞意味着线程需要发生上下文切换,上下文切换是非常耗性能的。
线程 1 (cpu 1 上) | 对象 Mark | 线程 2 (cpu 2 上) |
---|---|---|
- | 10(重量锁) | - |
访问同步块,获取 monitor | 10(重量锁)重量锁指针 | - |
成功(加锁) | 10(重量锁)重量锁指针 | - |
执行同步块 | 10(重量锁)重量锁指针 | - |
执行同步块 | 10(重量锁)重量锁指针 | 访问同步块,获取 monitor |
执行同步块 | 10(重量锁)重量锁指针 | 自旋重试 |
执行完毕 10 | (重量锁)重量锁指针 | 自旋重试 |
成功(解锁) | 01(无锁) | 自旋重试 |
- | 10(重量锁)重量锁指针 | 成功(加锁) |
- | 10(重量锁)重量锁指针 | 执行同步块 |
- | … | … |
线程 1(cpu 1 上) | 对象 Mark | 线程 2(cpu 2 上) |
---|---|---|
- | 10(重量锁) | - |
访问同步块,获取 monitor | 10(重量锁)重量锁指针 | - |
成功(加锁) | 10(重量锁)重量锁指针 | - |
执行同步块 | 10(重量锁)重量锁指针 | - |
执行同步块 | 10(重量锁)重量锁指针 | 访问同步块,获取 monitor |
执行同步块 | 10(重量锁)重量锁指针 | 自旋重试 |
执行同步块 | 10(重量锁)重量锁指针 | 自旋重试 |
执行同步块 | 10(重量锁)重量锁指针 | 自旋重试 |
执行同步块 | 10(重量锁)重量锁指针 | 阻塞 |
- | … | … |
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
Java 6 中引入了偏向锁来做进一步优化:
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
}
}
先让我们回忆一下对象头格式(64 位)
Mark Word(64 bits) | State | |||||
unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | Normal |
thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 | Biased |
ptr_to_lock_record:62 | 00 | Lightweight Locked | ||||
ptr_to_heavyweight_monitor:62 | 10 | Heavyweight Locked | ||||
11 | Marked for GC |
一个对象创建时:
-XX:BiasedLockingStartupDelay=0
来禁用延迟测试延迟特性
测试偏向锁
class Dog {}
<dependency>
<groupId>org.openjdk.jolgroupId>
<artifactId>jol-coreartifactId>
<version>0.10-TESTversion>
dependency>
// 添加虚拟机参数 -XX:BiasedLockingStartupDelay=0
public static void main(String[] args) throws IOException {
Dog d = new Dog();
ClassLayout classLayout = ClassLayout.parseInstance(d);
new Thread(() -> {
log.debug("synchronized 前");
System.out.println(classLayout.toPrintableSimple(true));
synchronized (d) {
log.debug("synchronized 中");
System.out.println(classLayout.toPrintableSimple(true));
}
log.debug("synchronized 后");
System.out.println(classLayout.toPrintableSimple(true));
}, "t1").start();
}
11:08:58.117 c.TestBiased [t1] - synchronized 前
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101
11:08:58.121 c.TestBiased [t1] - synchronized 中
00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101
11:08:58.121 c.TestBiased [t1] - synchronized 后
00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101
测试禁用
-XX:-UseBiasedLocking
禁用偏向锁,控制台输出如下11:13:10.018 c.TestBiased [t1] - synchronized 前
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
11:13:10.021 c.TestBiased [t1] - synchronized 中
00000000 00000000 00000000 00000000 00100000 00010100 11110011 10001000
11:13:10.021 c.TestBiased [t1] - synchronized 后
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
禁用了偏向锁之后,其直接应用了轻量级锁。如果有竞争发生,轻量级锁则会膨胀为重量级锁。
所以得出加锁顺序的优先级:偏向锁 > 轻量级锁 > 重量级锁
调用了对象的 hashCode,但偏向锁的对象 Mark Word 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被撤销
当对象处于轻量级锁状态(Biased,101)时,Mark Word 中已经存不下 hashCode 了。
当一个可偏向的对象调用了 hashCode() 方法后,它就会撤销这个对象的偏向状态,变为 Normal 状态(001)。
测试 hashCode
-XX:-UseBiasedLocking
d.hashCode(); // 会禁用这个对象的偏向锁
输出
11:22:10.386 c.TestBiased [main] - 调用 hashCode:1778535015
11:22:10.391 c.TestBiased [t1] - synchronized 前
00000000 00000000 00000000 01101010 00000010 01001010 01100111 00000001
11:22:10.393 c.TestBiased [t1] - synchronized 中
00000000 00000000 00000000 00000000 00100000 11000011 11110011 01101000
11:22:10.393 c.TestBiased [t1] - synchronized 后
00000000 00000000 00000000 01101010 00000010 01001010 01100111 00000001
当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
想要达到上面的效果,需要注意:错开两个线程使用锁对象的时间。
如果这两个线程直接来竞争锁,那么很容易发生锁升级为重量级锁的情况。(这个在锁膨胀里已经讲过了)
private static void test2() throws InterruptedException {
Dog d = new Dog();
Thread t1 = new Thread(() -> {
synchronized (d) {
log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
}
synchronized (TestBiased.class) {
TestBiased.class.notify();
}
// 如果不用 wait/notify 使用 join 必须打开下面的注释
// 因为:t1 线程不能结束,否则底层线程可能被 jvm 重用作为 t2 线程,底层线程 id 是一样的
/*try {
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}*/
}, "t1");
t1.start();
Thread t2 = new Thread(() -> {
synchronized (TestBiased.class) {
try {
TestBiased.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
synchronized (d) {
log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
}
log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
}, "t2");
t2.start();
}
[t1] - 00000000 00000000 00000000 00000000 00011111 01000001 00010000 00000101
[t2] - 00000000 00000000 00000000 00000000 00011111 01000001 00010000 00000101
[t2] - 00000000 00000000 00000000 00000000 00011111 10110101 11110000 01000000
[t2] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
调用 wait/notify 会导致偏向状态被撤销
调用 wait 方法和 notify 方法会导致锁膨胀为重量级锁
public static void main(String[] args) throws InterruptedException {
Dog d = new Dog();
Thread t1 = new Thread(() -> {
log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
synchronized (d) {
log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
try {
d.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
}
}, "t1");
t1.start();
new Thread(() -> {
try {
Thread.sleep(6000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (d) {
log.debug("notify");
d.notify();
}
}, "t2").start();
}
[t1] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101
[t1] - 00000000 00000000 00000000 00000000 00011111 10110011 11111000 00000101
[t2] - notify
[t1] - 00000000 00000000 00000000 00000000 00011100 11010100 00001101 11001010
如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID
当撤销偏向锁阈值超过 20 次后,JVM 会这样觉得,我是不是偏向错了呢?
于是会在给这些对象加锁时重新偏向至加锁线程
private static void test3() throws InterruptedException {
Vector<Dog> list = new Vector<>();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 30; i++) {
Dog d = new Dog();
list.add(d);
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
}
synchronized (list) {
list.notify();
}
}, "t1");
t1.start();
Thread t2 = new Thread(() -> {
synchronized (list) {
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("===============> ");
for (int i = 0; i < 30; i++) {
Dog d = list.get(i);
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
}, "t2");
t2.start();
}
[t1] - 0 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 1 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 2 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 3 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 4 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 5 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 6 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 7 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 8 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 9 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 10 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 11 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 12 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 13 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 14 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 15 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 16 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 17 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 18 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 23 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 24 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t1] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - ===============> # 此处开始撤销偏向锁的操作
[t2] - 0 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 # 偏向锁
[t2] - 0 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 # 此处升级为了轻量级锁
[t2] - 0 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 # 不可偏向的正常状态
[t2] - 1 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 1 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 1 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 2 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 2 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 2 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 3 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 3 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 3 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 4 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 4 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 4 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 5 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 5 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 5 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 6 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 6 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 6 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 7 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 7 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 7 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 8 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 8 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 8 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 9 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 9 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 9 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 10 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 10 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 10 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 11 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 11 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 11 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 12 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 12 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 12 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 13 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 13 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 13 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 14 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 14 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 14 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 15 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 15 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 15 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 16 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 16 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 16 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 17 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 17 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 17 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 18 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 18 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 18 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 # 此处不再撤销偏向锁了
[t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 # 此处开始偏向了另一个线程
[t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 23 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 23 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 23 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 24 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 24 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 24 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
当撤销偏向锁阈值超过 40 次后,JVM 会这样觉得,自己确实偏向错了,根本就不该偏向。
于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的
static Thread t1, t2, t3;
private static void test4() throws InterruptedException {
Vector<Dog> list = new Vector<>();
int loopNumber = 39;
t1 = new Thread(() -> {
for (int i = 0; i < loopNumber; i++) {
Dog d = new Dog();
list.add(d);
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
}
LockSupport.unpark(t2);
}, "t1");
t1.start();
t2 = new Thread(() -> {
LockSupport.park();
log.debug("===============> ");
for (int i = 0; i < loopNumber; i++) {
Dog d = list.get(i);
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
LockSupport.unpark(t3);
}, "t2");
t2.start();
t3 = new Thread(() -> {
LockSupport.park();
log.debug("===============> ");
for (int i = 0; i < loopNumber; i++) {
Dog d = list.get(i);
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
}, "t3");
t3.start();
t3.join();
log.debug(ClassLayout.parseInstance(new Dog()).toPrintableSimple(true));
}
打印信息比较多,此处就不粘贴了。
「偏向锁」内容的参考资料
- https://github.com/farmerjohngit/myblog/issues/12
- https://www.cnblogs.com/LemonFive/p/11246086.html
- https://www.cnblogs.com/LemonFive/p/11248248.html
以下内容参考自 《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 (周志明 著)
锁消除是指虚拟机即时编译器(JIT)在运行时,对一些代码要求同步,但是会对被检测到的 不可能存在共享数据竞争的锁 进行消除。
锁消除的主要判定依据来源于逃逸分析的数据支持。
如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当作栈上数据对待,认为它们是线程私有的,同步加锁自然就无须再进行。
逃逸分析(Escape Analysis)是目前 Java 虚拟机中比较前沿的优化技术,它与类型继承关系分析一样,并不是直接优化代码的手段,而是为其他优化措施提供依据的分析技术。
逃逸分析的基本原理是:分析对象动态作用域,当一个对象在方法里面被定义后,它可能被外部方法所引用(例如作为调用参数传递到其他方法中),这种称为方法逃逸;甚至还有可能被外部线程访问到,譬如赋值给可以在其他线程中访问的实例变量,这种称为线程逃逸;从不逃逸、方法逃逸到线程逃逸,称为对象由低到高的不同逃逸程度。
如果能证明一个对象不会逃逸到方法或线程之外(换句话说是别的方法或线程无法通过任何途径访问到这个对象),或者逃逸程度比较低(只逃逸出方法而不会逃逸出线程),则可能为这个对象实例采取不同程度的优化。
@Fork(1)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3)
@Measurement(iterations = 5)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class MyBenchmark {
static int x = 0;
@Benchmark
public void a() throws Exception {
x++;
}
@Benchmark
public void b() throws Exception {
Object o = new Object();
synchronized (o) {
x++;
}
}
}
java -jar benchmarks.jar
Benchmark Mode Samples Score Score error Units
c.i.MyBenchmark.a avgt 5 1.542 0.056 ns/op
c.i.MyBenchmark.b avgt 5 1.518 0.091 ns/op
java -XX:-EliminateLocks -jar benchmarks.jar
(添加了 关闭锁消除优化 的参数)Benchmark Mode Samples Score Score error Units
c.i.MyBenchmark.a avgt 5 1.507 0.108 ns/op
c.i.MyBenchmark.b avgt 5 16.976 1.572 ns/op
锁粗化:对相同对象多次加锁,导致线程发生多次重入,可以使用锁粗化方式来优化,这不同于之前讲的细分锁的粒度。
以下内容参考自 《深入理解 JAVA 虚拟机 | JVM 高级特性与最佳实践》 (周志明 著)
原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小——只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变少,即使存在锁竞争,等待锁的线程也能尽可能快地拿到锁。
大多数情况下,上面的原则都是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体之中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。
共享模型之管程:wait / notify
Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
BLOCKED 和 WAITING 的线程都处于阻塞态(操作系统层面),不占用 CPU 时间片
BLOCKED 线程会在 Owner 线程释放锁时唤醒
WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入 EntryList 重新竞争
注意
BLOCKED 状态的线程是正在等待锁的释放的线程
而 WAITING 状态的线程是已经成功获得过锁,因为条件不满足,所以放弃了锁的线程
public final void wait() throws InterruptedExceptionpublic
public final native void notify()
obj.wait()
让进入 object 监视器的线程到 waitSet 等待obj.notify()
在 object 上正在 waitSet 等待的线程中挑一个唤醒obj.notifyAll()
让 object 上正在 waitSet 等待的线程全部唤醒wait()
方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到 notify 为止wait(long n)
方法是有时限的等待,到 n 毫秒后结束等待,或是被 notify它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法
@Slf4j(topic = "c.Test18")
public class Test18 {
static final Object lock = new Object();
public static void main(String[] args) {
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
如果不用 synchronized (lock)
获得对象的锁的话,是无法调用 wait() 和 notify() 的,会在控制台输出如下报错信息
Exception in thread "main" java.lang.IllegalMonitorStateException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
at thread.Test1.main(Test1.java:18)
可以用下面的代码块来观察 notify() 和 notifyAll() 的区别
@Slf4j(topic = "c.TestWaitNotify")
public class TestWaitNotify {
final static Object obj = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在 obj 上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
}, "t1").start();
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在 obj 上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
}, "t2").start();
// 主线程两秒后执行
sleep(0.5);
log.debug("唤醒 obj 上其它线程");
synchronized (obj) {
// obj.notify(); // 唤醒 obj 上一个线程
obj.notifyAll(); // 唤醒 obj 上所有等待线程
}
}
}
调用 notify() 的输出结果
20:00:53.096 [Thread-0] c.TestWaitNotify - 执行....
20:00:53.099 [Thread-1] c.TestWaitNotify - 执行....
20:00:55.096 [main] c.TestWaitNotify - 唤醒 obj 上其它线程
20:00:55.096 [Thread-0] c.TestWaitNotify - 其它代码....
调用 notifyAll() 的输出结果
19:58:15.457 [Thread-0] c.TestWaitNotify - 执行....
19:58:15.460 [Thread-1] c.TestWaitNotify - 执行....
19:58:17.456 [main] c.TestWaitNotify - 唤醒 obj 上其它线程
19:58:17.456 [Thread-1] c.TestWaitNotify - 其它代码....
19:58:17.456 [Thread-0] c.TestWaitNotify - 其它代码....
sleep(long n) 和 wait(long n)
注意:不带参数的 wait() 进入的是 WAITING 状态
@Slf4j(topic = "c.Test19")
public class Test19 {
static final Object lock = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (lock) {
log.debug("获得锁");
try {
// Thread.sleep(20000);
lock.wait(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t1").start();
Sleeper.sleep(1);
synchronized (lock) {
log.debug("获得锁");
}
}
}
如果使用的是 Thread.sleep(20000);
,则控制台输出如下内容(sleep(long n) 时间结束后,线程才获得了锁)
10:16:56.682 c.Test19 [t1] - 获得锁
10:17:16.697 c.Test19 [main] - 获得锁
如果使用的是 lock.wait(20000);
,则控制台输出如下内容(因为 Sleeper.sleep(1) ,所以 1 秒后才获得了锁)
10:17:42.555 c.Test19 [t1] - 获得锁
10:17:43.559 c.Test19 [main] - 获得锁
究其上述情况的原因便是:sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
这是下面 5 处代码块中的成员变量,都是一样的
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
public static void main(String[] args) {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
sleep(2);
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
}
}
}, "小南").start();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (room) {
log.debug("可以开始干活了");
}
}, "其它人").start();
}
sleep(1);
new Thread(() -> {
// 这里能不能加 synchronized (room)?
synchronized (room) {
hasCigarette = true;
log.debug("烟到了噢!");
}
}, "送烟的").start();
}
10:50:02.240 c.TestCorrectPosture [小南] - 有烟没?[false]
10:50:02.244 c.TestCorrectPosture [小南] - 没烟,先歇会!
10:50:04.250 c.TestCorrectPosture [小南] - 有烟没?[false]
10:50:04.250 c.TestCorrectPosture [送烟的] - 烟到了噢!
10:50:04.250 c.TestCorrectPosture [其它人] - 可以开始干活了
10:50:04.250 c.TestCorrectPosture [其它人] - 可以开始干活了
10:50:04.250 c.TestCorrectPosture [其它人] - 可以开始干活了
10:50:04.250 c.TestCorrectPosture [其它人] - 可以开始干活了
10:50:04.251 c.TestCorrectPosture [其它人] - 可以开始干活了
synchronized (room)
后,就好比小南在里面反锁了门睡觉,烟根本没法送进门public static void main(String[] args) {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
}
}
}, "小南").start();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (room) {
log.debug("可以开始干活了");
}
}, "其它人").start();
}
sleep(1);
new Thread(() -> {
synchronized (room) {
hasCigarette = true;
log.debug("烟到了噢!");
room.notify();
}
}, "送烟的").start();
}
10:56:50.686 c.TestCorrectPosture [小南] - 有烟没?[false]
10:56:50.690 c.TestCorrectPosture [小南] - 没烟,先歇会!
10:56:50.690 c.TestCorrectPosture [其它人] - 可以开始干活了
10:56:50.690 c.TestCorrectPosture [其它人] - 可以开始干活了
10:56:50.690 c.TestCorrectPosture [其它人] - 可以开始干活了
10:56:50.690 c.TestCorrectPosture [其它人] - 可以开始干活了
10:56:50.690 c.TestCorrectPosture [其它人] - 可以开始干活了
10:56:51.687 c.TestCorrectPosture [送烟的] - 烟到了噢!
10:56:51.687 c.TestCorrectPosture [小南] - 有烟没?[true]
10:56:51.687 c.TestCorrectPosture [小南] - 可以开始干活了
// 虚假唤醒
public static void main(String[] args) {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小南").start();
new Thread(() -> {
synchronized (room) {
Thread thread = Thread.currentThread();
log.debug("外卖送到没?[{}]", hasTakeout);
if (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("外卖送到没?[{}]", hasTakeout);
if (hasTakeout) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小女").start();
sleep(1);
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖到了噢!");
room.notifyAll();
}
}, "送外卖的").start();
}
10:59:59.532 c.TestCorrectPosture [小南] - 有烟没?[false]
10:59:59.534 c.TestCorrectPosture [小南] - 没烟,先歇会!
10:59:59.534 c.TestCorrectPosture [小女] - 外卖送到没?[false]
10:59:59.534 c.TestCorrectPosture [小女] - 没外卖,先歇会!
11:00:00.533 c.TestCorrectPosture [送外卖的] - 外卖到了噢!
11:00:00.533 c.TestCorrectPosture [小女] - 外卖送到没?[true]
11:00:00.533 c.TestCorrectPosture [小女] - 可以开始干活了
11:00:00.533 c.TestCorrectPosture [小南] - 有烟没?[false]
11:00:00.533 c.TestCorrectPosture [小南] - 没干成活...
public static void main(String[] args) {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
while (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小南").start();
new Thread(() -> {
synchronized (room) {
Thread thread = Thread.currentThread();
log.debug("外卖送到没?[{}]", hasTakeout);
if (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("外卖送到没?[{}]", hasTakeout);
if (hasTakeout) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小女").start();
sleep(1);
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖到了噢!");
room.notifyAll();
}
}, "送外卖的").start();
}
11:36:25.431 c.TestCorrectPosture [小南] - 有烟没?[false]
11:36:25.435 c.TestCorrectPosture [小南] - 没烟,先歇会!
11:36:25.435 c.TestCorrectPosture [小女] - 外卖送到没?[false]
11:36:25.435 c.TestCorrectPosture [小女] - 没外卖,先歇会!
11:36:26.432 c.TestCorrectPosture [送外卖的] - 外卖到了噢!
11:36:26.432 c.TestCorrectPosture [小女] - 外卖送到没?[true]
11:36:26.432 c.TestCorrectPosture [小女] - 可以开始干活了
11:36:26.433 c.TestCorrectPosture [小南] - 没烟,先歇会!
11:36:26.433 c.TestCorrectPosture [小南] - 没干成活...
public static void main(String[] args) {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
while (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小南").start();
new Thread(() -> {
synchronized (room) {
Thread thread = Thread.currentThread();
log.debug("外卖送到没?[{}]", hasTakeout);
while (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("外卖送到没?[{}]", hasTakeout);
if (hasTakeout) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小女").start();
sleep(1);
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖到了噢!");
room.notifyAll();
}
}, "送外卖的").start();
}
11:40:46.261 c.TestCorrectPosture [小南] - 有烟没?[false]
11:40:46.266 c.TestCorrectPosture [小南] - 没烟,先歇会!
11:40:46.266 c.TestCorrectPosture [小女] - 外卖送到没?[false]
11:40:46.266 c.TestCorrectPosture [小女] - 没外卖,先歇会!
11:40:47.271 c.TestCorrectPosture [送外卖的] - 外卖到了噢!
11:40:47.271 c.TestCorrectPosture [小女] - 外卖送到没?[true]
11:40:47.271 c.TestCorrectPosture [小女] - 可以开始干活了
11:40:47.271 c.TestCorrectPosture [小南] - 没烟,先歇会!
正确使用 wait 和 notify 的姿势
synchronized (lock) {
while (条件不成立) { // 一直重试,当条件不成立时,继续判断
lock.wait();
}
// 干活
}
//另一个线程
synchronized (lock) {
// 唤醒所有等待线程,避免 虚假唤醒 的情况发生
lock.notifyAll();
}
共享模型之管程:设计模式-1
即 Guarded Suspension,用在一个线程等待另一个线程的执行结果
要点
GuardedObject.java
public class GuardedObject_1 {
// 结果
private Object response;
// 获取结果
public Object get() {
synchronized (this) {
// 条件不满足时,继续等待
while (response == null) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return response;
}
}
// 产生结果
public void complete(Object response) {
synchronized (this) {
// 给结果成员变量赋值
this.response = response;
// 唤醒所有等待线程
this.notifyAll();
}
}
}
测试
ProtectivePause_Test1.java
@Slf4j(topic = "c.ProtectivePause_Test1")
public class ProtectivePause_Test1 {
// [线程 1] 等待 [线程 2] 的下载结果
public static void main(String[] args) {
GuardedObject_1 guardedObject1 = new GuardedObject_1();
new Thread(() -> {
log.debug("等待结果");
List<String> list = (List<String>) guardedObject1.get();
log.debug("结果大小:{}", list.size());
}, "t1").start();
new Thread(() -> {
log.debug("执行下载");
try {
List<String> list = Downloader.download();
guardedObject1.complete(list);
} catch (IOException e) {
e.printStackTrace();
}
}, "t2").start();
}
}
输出结果
11:57:55.242 [t2] c.ProtectivePause_Test1 - 执行下载
11:57:55.242 [t1] c.ProtectivePause_Test1 - 等待结果
11:57:56.521 [t1] c.ProtectivePause_Test1 - 结果大小:3
这个设计模式其实就是两个线程之间交互结果的模式
使用 join() 可以交互结果,但是有俩缺点:
保护性暂停设计模式只需要一个消息通知,通知了就可以继续工作;等待结果的变量可以设置为局部变量
这种模式适用于 线程中的一部分代码需要同步,而后续代码块不需要同步 的情况。
public class GuardedObject_2 {
// 结果
private Object response;
// 获取结果
// timeout:最大等待时间,表示需要等待多久
public Object get(long timeout) {
synchronized (this) {
// 记录开始等待时间
long beginTime = System.currentTimeMillis();
// 记录经历的时间
long passedTime = 0;
// 条件不满足时,继续等待
while (response == null) {
// waitTime:这一轮循环需要等待的时间
// 这样做的目的是为了避免虚假唤醒造成的等待时间变长的问题
long waitTime = timeout - passedTime;
// 经历时间 超过 最大等待时间,退出循环
if (waitTime <= 0) { break; }
try {
// 存在[虚假唤醒,没有结果返回]的情况,这个时候不应该继续等待 timeout 时长
// 举个例子,比如说 15:01 唤醒了线程,但没有结果返回
// 因为返回结果为空,故再次进入循环,但此时不应该是继续等待 2 秒,而是应该等待 1 秒了。
this.wait(waitTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
passedTime = System.currentTimeMillis() - beginTime; // 刷新等待过程的经历时间
}
return response;
}
}
// 产生结果
public void complete(Object response) {
synchronized (this) {
// 给结果成员变量赋值
this.response = response;
// 唤醒所有等待线程
this.notifyAll();
}
}
}
测试
@Slf4j(topic = "c.ProtectivePause_Test2")
public class ProtectivePause_Test2 {
// [线程 1] 等待 [线程 2] 的下载结果
public static void main(String[] args) {
GuardedObject_2 guardedObject2 = new GuardedObject_2();
new Thread(() -> {
log.debug("t1 线程开始");
Object response = guardedObject2.get(2000);
log.debug("结果是:{}", response);
}, "t1").start();
new Thread(() -> {
log.debug("t2 线程开始");
Sleeper.sleep(1);
guardedObject2.complete(new Object());
}, "t2").start();
}
}
输出结果
11:58:20.888 [t1] c.ProtectivePause_2 - t1 线程开始
11:58:20.888 [t2] c.ProtectivePause_2 - t2 线程开始
11:58:21.901 [t1] c.ProtectivePause_2 - 结果是:java.lang.Object@65c12e7e
更改一下 ProtectivePause_Test2.java 中的代码:Sleeper.sleep(3);
即模拟超时效果(最大等待时间内,没有来得及返回结果)
13:07:45.855 [t2] c.ProtectivePause_Test2 - t2 线程开始
13:07:45.855 [t1] c.ProtectivePause_Test2 - t1 线程开始
13:07:47.866 [t1] c.ProtectivePause_Test2 - 结果是:null
更改一下 ProtectivePause_Test2.java 中的代码:guardedObject2.complete(null);
即模拟虚假唤醒的效果
13:08:17.008 [t1] c.ProtectivePause_Test2 - t1 线程开始
13:08:17.008 [t2] c.ProtectivePause_Test2 - t2 线程开始
13:08:19.023 [t1] c.ProtectivePause_Test2 - 结果是:null
保护性暂停是一个线程等待另一个线程的结果,join() 是一个线程等待另一个线程的结束。
两者的实现方式都是一样的。
java/lang/Thread.java
public final void join() throws InterruptedException {
join(0);
}
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis(); // 记录等待的开始时间
long now = 0; // 记录等待过程中的经历时间
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now; // delay:这一轮循环需要的等待时间
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base; // 刷新等待的经历时间
}
}
}
join() 方法的本质是在当前线程对象实例上调用线程的 wait() 方法
图中 Futures 就好比居民楼一层的信箱(每个信箱有房间编号),左侧的 t0,t2,t4 就好比等待邮件的居民,右侧的 t1,t3,t5 就好比邮递员。如果需要在多个类之间使用 GuardedObject 对象,作为参数传递不是很方便,因此设计一个用来解耦的中间类, 这样不仅能够解耦 结果等待者 和 结果生产者,还能够同时支持多个任务的管理。
GuardedObject_3.java
public class GuardedObject_3 {
// GuardedObject 的唯一标识
private int id;
public GuardedObject_3(int id) {
this.id = id;
}
public int getId() {
return id;
}
// 结果
private Object response;
// 获取结果
// 此处的 timeout 表示需要等待多久
public Object get(long timeout) {
synchronized (this) {
// 记录开始等待时间
long beginTime = System.currentTimeMillis();
// 记录经历的时间
long passedTime = 0;
// 条件不满足时,继续等待
while (response == null) {
// 这一轮循环需要等待的时间
long waitTime = timeout - passedTime;
// 经历时间 超过 最大等待时间,退出循环
if (waitTime <= 0) {
break;
}
try {
// 存在 虚假唤醒,没有结果返回 的情况
// 这个时候不应该继续等待 timeout 时长
this.wait(waitTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
passedTime = System.currentTimeMillis() - beginTime;
}
return response;
}
}
// 产生结果
public void complete(Object response) {
synchronized (this) {
// 给结果成员变量赋值
this.response = response;
// 唤醒所有等待线程
this.notifyAll();
}
}
}
Mailboxes.java
public class Mailboxes {
private static Map<Integer, GuardedObject_3> boxes = new Hashtable<>();
private static int id = 1;
// 产生唯一性的 id
private static synchronized int generateId() {
return id++;
}
public static GuardedObject_3 createGuardedObject() {
GuardedObject_3 go = new GuardedObject_3(generateId());
boxes.put(go.getId(), go);
return go;
}
public static Set<Integer> getIds() {
return boxes.keySet();
}
public static GuardedObject_3 getGuardedObject(int id) {
return boxes.remove(id); // 返回 id 并删除该 id
}
}
People.java
@Slf4j(topic = "c.People")
public class People extends Thread {
@Override
public void run() {
// 收信
GuardedObject_3 guardedObject = Mailboxes.createGuardedObject();
log.debug("开始收信 id:{}", guardedObject.getId());
Object mail = guardedObject.get(5000);
log.debug("收到信 id:{}, 内容:{}", guardedObject.getId(), mail);
}
}
Postman.java
@Slf4j(topic = "c.Postman")
public class Postman extends Thread {
private int id;
private String mail;
public Postman(int id, String mail) {
this.id = id;
this.mail = mail;
}
@Override
public void run() {
GuardedObject_3 guardedObject = Mailboxes.getGuardedObject(id);
log.debug("送信 id:{}, 内容:{}", id, mail);
guardedObject.complete(mail);
}
}
ProtectivePause_Test3.java
@Slf4j(topic = "c.ProtectivePause_Test3")
public class ProtectivePause_Test3 {
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
new People().start();
}
Sleeper.sleep(1);
for (Integer id : Mailboxes.getIds()) {
new Postman(id, "内容" + id).start();
}
}
}
控制台输出
16:57:00.170 [Thread-2] c.People - 开始收信 id:2
16:57:00.170 [Thread-0] c.People - 开始收信 id:1
16:57:00.170 [Thread-1] c.People - 开始收信 id:3
16:57:01.179 [Thread-4] c.Postman - 送信 id:2, 内容:内容2
16:57:01.179 [Thread-3] c.Postman - 送信 id:3, 内容:内容3
16:57:01.180 [Thread-2] c.People - 收到信 id:2, 内容:内容2
16:57:01.180 [Thread-1] c.People - 收到信 id:3, 内容:内容3
16:57:01.180 [Thread-5] c.Postman - 送信 id:1, 内容:内容1
16:57:01.180 [Thread-0] c.People - 收到信 id:1, 内容:内容1
基本上可以理解为点对点投递,一一对应的模式。
线程之间通讯,一般是需要用到 id 的。可以于此创建一个用来存储 id 的封装类。
Message.java
final class Message {
private int id;
private Object value;
public Message(int id, Object value) {
this.id = id;
this.value = value;
}
public int getId() {
return id;
}
public Object getValue() {
return value;
}
@Override
public String toString() {
return "Message{" +
"id=" + id +
", value=" + value +
'}';
}
}
消息队列类,用于 Java 消息队列类。
MessageQueue.java
@Slf4j(topic = "c.MessageQueue")
class MessageQueue {
// 消息的队列集合
private LinkedList<Message> list = new LinkedList<>();
// 队列容量
private int capacity;
public MessageQueue(int capacity) {
this.capacity = capacity;
}
// 获取消息
public Message take() {
// 检查队列是否为空
synchronized (list) {
while (list.isEmpty()) {
try {
log.debug("队列为空,消费者线程需要等待 ... ");
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 从队列的头部获取消息并且返回(且删除消息)
Message message = list.removeFirst();
log.debug("已消费消息 {}", message);
list.notifyAll();
return message;
}
}
// 存入消息
public void put(Message message) {
synchronized (list) {
// 检查对象是否已经满了
while (list.size() == capacity) {
try {
log.debug("队列已满,生产者线程需要等待 ... ");
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 将消息加入队列的尾部
list.addLast(message);
log.debug("已生产消息 {}", message);
list.notifyAll();
}
}
}
测试类:TestProCon.java
@Slf4j(topic = "c.TestProCon")
public class TestProCon {
public static void main(String[] args) {
MessageQueue messageQueue = new MessageQueue(2);
for (int i = 0; i < 3; i++) {
int id = i;
new Thread(() -> {
messageQueue.put(new Message(id, "值" + id));
}, "生产者" + i).start();
}
new Thread(() -> {
while (true) {
Sleeper.sleep(1);
Message message = messageQueue.take();
}
}, "消费者").start();
}
}
控制台输出结果
16:29:32.607 [生产者0] c.MessageQueue - 已生产消息 Message{id=0, value=值0}
16:29:32.610 [生产者1] c.MessageQueue - 已生产消息 Message{id=1, value=值1}
16:29:32.610 [生产者2] c.MessageQueue - 队列已满,生产者线程需要等待 ...
16:29:33.611 [消费者] c.MessageQueue - 已消费消息 Message{id=0, value=值0}
16:29:33.611 [生产者2] c.MessageQueue - 已生产消息 Message{id=2, value=值2}
16:29:34.622 [消费者] c.MessageQueue - 已消费消息 Message{id=1, value=值1}
16:29:35.635 [消费者] c.MessageQueue - 已消费消息 Message{id=2, value=值2}
16:29:36.645 [消费者] c.MessageQueue - 队列为空,消费者线程需要等待 ...
共享模型之管程:Park / Unpark
Park、Unpark 都是 LockSupport 类中的方法
// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)
与 Object 的 wait & notify 相比 wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必
park & unpark 是以线程为单位来 阻塞 和 唤醒 线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么 精确
park & unpark 可以先 unpark,而 wait & notify 不能先 notify
每个线程都有自己的一个 Parker 对象(Java 中不可见),由三部分组成 _counter
,_cond
和 _mutex
打个比喻,线程就像一个旅人
_counter
就好比背包中 的备用干粮(0 为耗尽,1 为充足)_counter
,本情况为 0,这时,获得 _mutex
互斥锁_cond
条件变量阻塞_counter
为 1_cond
条件变量中的 Thread_0_counter
为 0_counter
为 1_counter
,本情况为 1,这时线程无需阻塞,继续运行_counter
为 0共享模型之管程:重新理解线程状态的转换
吐槽:其实这段我在第一章节也写过一些内容。但这个确实是重点,视频教程里也有,我也就照抄下来了。
可以回顾一下本博客中的第一章节的部分复习内容:1.6.Java 线程的状态转换
start()
方法start()
方法之后
假设有线程 Thread t
New 状态表示创建了一个 Java 的线程对象,但是还没有和操作系统的线程关联起来。
当调用 t.start()
方法时,NEW --> RUNNABLE,此时线程对象就和操作系统底层的线程关联起来了。
t 线程用 synchronized(obj)
获取了对象锁后
obj.wait()
方法时,t 线程从 RUNNABLE --> WAITINGobj.notify()
、obj.notifyAll()
、t.interrupt()
时
t.join()
方法时,当前线程从 RUNNABLE --> WAITING
interrupt()
时,当前线程从 WAITING --> RUNNABLE当前线程调用 LockSupport.park()
方法会让当前线程从 Runnable --> WAITING
调用 LockSupport.unpark(目标线程)
或调用了线程 的 interrupt()
,会让目标线程从 WAITING --> Runnable
t 线程用 synchronized(obj)
获取了对象锁后
obj.wait(long n)
方法时,t 线程从 RUNNABLE --> TIMED_WAITINGobj.notify()
、obj.notifyAll()
、t.interrupt()
时
t.join(long n)
方法时,当前线程从 RUNNABLE --> TIMED_WAITING
interrupt()
时,当前线程从 TIMED_WAITING --> RUNNABLEThread.sleep(long n)
,当前线程从 RUNNABLE --> TIMED_WAITINGLockSupport.parkNanos(long nanos)
或 LockSupport.parkUntil(long millis)
时,LockSupport.unpark(目标线程)
或调用了线程 的 interrupt()
,或是等待超时,t 线程用 synchronized(obj)
获取了对象锁时如果竞争失败,从 Runnable --> BLOCKED
持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,
如果其中 t 线程竞争 成功,从 BLOCKED --> Runnable,其它失败的线程仍然 BLOCKED
共享模型之管程:活跃性
使用多把锁可以将锁的粒度细分
举例
一间大屋子有两个功能:睡觉、学习,互不相干。
现在小南要学习,小雨要睡觉,但如果只用一间屋子(一个对象锁)的话,那么并发度很低。
解决方法是准备多个房间(多个对象锁)
Room.java
@Slf4j(topic = "c.Room")
public class Room {
public void sleep() {
synchronized (this) {
log.debug("sleeping 2 小时");
Sleeper.sleep(2);
}
}
public void study() {
synchronized (this) {
log.debug("study 1 小时");
Sleeper.sleep(1);
}
}
}
TestMultiLock.java
public class TestMultiLock {
public static void main(String[] args) {
Room room = new Room();
new Thread(() -> {
room.study();
}, "小南").start();
new Thread(() -> {
room.sleep();
}, "小雨").start();
}
}
控制台输出(显然这里的并发度太低了,小雨必须要等小南完学习完才可以睡觉)
18:44:36.712 [小南] c.Room - study 1 小时
18:44:37.720 [小雨] c.Room - sleeping 2 小时
此时我们可以改进一下代码(设置多个对象锁)来增强并发度
BigRoom.java
@Slf4j(topic = "c.BigRoom")
class BigRoom {
private final Object studyRoom = new Object();
private final Object bedRoom = new Object();
public void sleep() {
synchronized (bedRoom) {
log.debug("sleeping 2 小时");
Sleeper.sleep(2);
}
}
public void study() {
synchronized (studyRoom) {
log.debug("study 1 小时");
Sleeper.sleep(1);
}
}
}
TestMultiLock.java
public class TestMultiLock {
public static void main(String[] args) {
BigRoom bigRoom = new BigRoom();
Room room = new Room();
new Thread(() -> {
//room.study();
bigRoom.study();
}, "小南").start();
new Thread(() -> {
//room.sleep();
bigRoom.sleep();
}, "小雨").start();
}
}
控制台输出
18:52:45.909 [小雨] c.BigRoom - sleeping 2 小时
18:52:45.909 [小南] c.BigRoom - study 1 小时
存在这样一种情况:一个线程需要同时获取多把锁。这个时候是很容易发生死锁的
测试代码
@Slf4j(topic = "c.TestDeadLock")
public class TestDeadLock {
public static void main(String[] args) {
test1();
}
private static void test1() {
Object A = new Object();
Object B = new Object();
Thread t1 = new Thread(() -> {
synchronized (A) {
log.debug("lock A");
sleep(1);
synchronized (B) {
log.debug("lock B");
log.debug("操作...");
}
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (B) {
log.debug("lock B");
sleep(0.5);
synchronized (A) {
log.debug("lock A");
log.debug("操作...");
}
}
}, "t2");
t1.start();
t2.start();
}
}
控制台输出(由于多个线程由于存在环路的锁依赖关系而永远地等待下去)
19:04:55.361 c.TestDeadLock [t1] - lock A
19:04:55.361 c.TestDeadLock [t2] - lock B
// 俩线程都是一直在等待
检测死锁可以使用 jconsole 工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁
Found one Java-level deadlock:
=============================
"t2":
waiting to lock monitor 0x00000000264b1128 (object 0x000000071751f380, a java.lang.Object),
which is held by "t1"
"t1":
waiting to lock monitor 0x00000000264b3228 (object 0x000000071751f390, a java.lang.Object),
which is held by "t2"
Java stack information for the threads listed above:
===================================================
"t2":
at org.example.chapter04.activeness.TestDeadLock.lambda$test1$1(TestDeadLock.java:32)
- waiting to lock <0x000000071751f380> (a java.lang.Object)
- locked <0x000000071751f390> (a java.lang.Object)
at org.example.chapter04.activeness.TestDeadLock$$Lambda$2/1792845110.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"t1":
at org.example.chapter04.activeness.TestDeadLock.lambda$test1$0(TestDeadLock.java:21)
- waiting to lock <0x000000071751f390> (a java.lang.Object)
- locked <0x000000071751f380> (a java.lang.Object)
at org.example.chapter04.activeness.TestDeadLock$$Lambda$1/897913732.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.
打印出来的信息还是蛮多的,具体操作和分析见视频:【活跃性 - 定位死锁】
避免死锁要注意加锁顺序
另外如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 Linux 下可以通过 top
先定位到CPU 占用高的 Java 进程,再利用 top -Hp 进程id
来定位是哪个线程,最后再用 jstack
排查
有五位哲学家,围坐在圆桌旁。
筷子类
class Chopstick {
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "筷子{" + name + '}';
}
}
哲学家类
@Slf4j(topic = "c.Philosopher")
class Philosopher extends Thread {
Chopstick left;
Chopstick right;
public Philosopher(String name, Chopstick left, Chopstick right) {
super(name);
this.left = left;
this.right = right;
}
@Override
public void run() {
while (true) {
// 尝试获得左手筷子
synchronized (left) {
// 尝试获得右手筷子
synchronized (right) {
eat();
}
}
}
}
private void eat() {
log.debug("eating...");
Sleeper.sleep(1);
}
}
就餐(测试类)
public class TestDeadLock {
public static void main(String[] args) {
Chopstick c1 = new Chopstick("1");
Chopstick c2 = new Chopstick("2");
Chopstick c3 = new Chopstick("3");
Chopstick c4 = new Chopstick("4");
Chopstick c5 = new Chopstick("5");
new Philosopher("苏格拉底", c1, c2).start();
new Philosopher("柏拉图", c2, c3).start();
new Philosopher("亚里士多德", c3, c4).start();
new Philosopher("赫拉克利特", c4, c5).start();
new Philosopher("阿基米德", c1, c5).start();
}
}
执行不多会,就执行不下去了
12:33:15.575 [苏格拉底] c.Philosopher - eating...
12:33:15.575 [亚里士多德] c.Philosopher - eating...
12:33:16.580 [阿基米德] c.Philosopher - eating...
12:33:17.580 [阿基米德] c.Philosopher - eating...
// 卡在这里, 不向下运行
使用 jconsole
检测死锁,发现
-------------------------------------------------------------------------
名称: 阿基米德
状态: cn.itcast.Chopstick@1540e19d (筷子1) 上的BLOCKED, 拥有者: 苏格拉底
总阻止数: 2, 总等待数: 1
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
- 已锁定 cn.itcast.Chopstick@6d6f6e28 (筷子5)
-------------------------------------------------------------------------
名称: 苏格拉底
状态: cn.itcast.Chopstick@677327b6 (筷子2) 上的BLOCKED, 拥有者: 柏拉图
总阻止数: 2, 总等待数: 1
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
- 已锁定 cn.itcast.Chopstick@1540e19d (筷子1)
-------------------------------------------------------------------------
名称: 柏拉图
状态: cn.itcast.Chopstick@14ae5a5 (筷子3) 上的BLOCKED, 拥有者: 亚里士多德
总阻止数: 2, 总等待数: 0
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
- 已锁定 cn.itcast.Chopstick@677327b6 (筷子2)
-------------------------------------------------------------------------
名称: 亚里士多德
状态: cn.itcast.Chopstick@7f31245a (筷子4) 上的BLOCKED, 拥有者: 赫拉克利特
总阻止数: 1, 总等待数: 1
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
- 已锁定 cn.itcast.Chopstick@14ae5a5 (筷子3)
-------------------------------------------------------------------------
名称: 赫拉克利特
状态: cn.itcast.Chopstick@6d6f6e28 (筷子5) 上的BLOCKED, 拥有者: 阿基米德
总阻止数: 2, 总等待数: 0
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
- 已锁定 cn.itcast.Chopstick@7f31245a (筷子4)
这种线程没有按预期结束,执行不下去的情况,归类为 活跃性 问题,除了死锁以外,还有 活锁 和 饥饿者 两种情况
活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束
例
@Slf4j(topic = "c.TestLiveLock")
public class TestLiveLock {
static volatile int count = 10;
static final Object lock = new Object();
public static void main(String[] args) {
new Thread(() -> {
// 期望减到 0 退出循环
while (count > 0) {
sleep(0.2);
count--;
log.debug("count: {}", count);
}
}, "t1").start();
new Thread(() -> {
// 期望超过 20 退出循环
while (count < 20) {
sleep(0.2);
count++;
log.debug("count: {}", count);
}
}, "t2").start();
}
}
解决方法:要保证一方不能改变另一方的结束条件
很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束。
饥饿的情况不易演示,讲读写锁时会涉及饥饿问题
这里举一个线程饥饿的例子,先来看看使用顺序加锁的方式解决之前的死锁问题
顺序加锁的解决方案
共享模型之管程:ReentrantLock
重入锁可以完全替代 synchronized 关键字。
在 JDK 5.0 的早期版本中,重入锁的性能远远好于 synchronized。
但从 JDK 6.0 开始,JDK 在 synchronized 上做了大量的优化,使得两者的性能差距并不大。
重入锁使用 java.util.concurrent.locks.ReentrantLock
类来实现。
相对于 synchronized 它具备如下特点
基本语法
// 获取锁
reentrantLock.lock();
try {
// 临界区
} finally {
// 释放锁
reentrantLock.unlock();
}
对 ReentrantLock 的几个重要方法整理如下。
可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
示例代码:TestReentry.java
@Slf4j(topic = "c.TestReentry")
public class TestReentry {
private static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) {
reentrantLock.lock();
try {
log.debug("enter main");
method_1();
} finally {
reentrantLock.unlock();
}
}
public static void method_1() {
reentrantLock.lock();
try {
log.debug("enter method_1");
method_2();
} finally {
reentrantLock.unlock();
}
}
private static void method_2() {
reentrantLock.lock();
try {
log.debug("enter method_2");
} finally {
reentrantLock.unlock();
}
}
}
控制台输出
09:39:14.440 [main] c.TestReentry - enter main
09:39:14.441 [main] c.TestReentry - enter method_1
09:39:14.441 [main] c.TestReentry - enter method_2
以下内容参考自 《实战 JAVA 高并发程序设计》 葛一鸣 著
对于 synchronized 来说,如果一个线程在等待锁,那么结果只有两种情况,要么它获得这把锁继续执行,要么它就保持等待。
而使用重入锁,则提供另外一种可能,那就是线程可以被中断。也就是在等待锁的过程中,程序可以根据需要取消对锁的请求。
有些时候,这么做是非常有必要的。比如,如果你和朋友越好一起去打球。如果你等了半小时,朋友还未到。
突然接到了一个电话,说由于突发情况,不能如约了。那么你一定就扫兴地打道回府了。
中断正式提供了一套类似的机制。如果一个线程正在等待锁,那么它依然可以收到一个通知,被告知无须再等待,可以停止工作了。
lockInterruptibly() 方法,这是一个可以对中断进行响应的锁请求动作,即在等待锁的过程中,可以中断响应。
参考博客:https://blog.csdn.net/weixin_53142722/article/details/124566944
- 可打断指的是处于阻塞状态等待锁的线程可以被打断等待。
- 注意:lock.lockInterruptibly() 和 lock.trylock() 方法是可打断的,lock.lock() 不是。
- 可打断的意义在于避免得不到锁的线程无限制地等待下去,防止死锁的一种方式。
- 处于阻塞状态的线程,被打断了就不用阻塞了,我们可以在捕获打断异常后直接停止该线程的运行。
@Slf4j(topic = "c.TestInterrupt")
public class TestInterrupt {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
log.debug("启动...");
try {
// 如果没有竞争,那么此方法会获取 lock 对象锁
// 如果有竞争,则会进入阻塞队列,可以被其他线程用 interrupt 方法打断
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("等锁的过程中被打断");
return;
}
try {
log.debug("获得了锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
log.debug("获得了锁");
t1.start();
try {
sleep(1);
t1.interrupt();
log.debug("执行打断");
} finally {
lock.unlock();
}
}
}
控制台输出
19:58:06.971 [main] c.TestInterrupt - 获得了锁
19:58:06.974 [t1] c.TestInterrupt - 启动...
19:58:07.981 [main] c.TestInterrupt - 执行打断
19:58:07.982 [t1] c.TestInterrupt - 等锁的过程中被打断
java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
at org.example.chapter04.reentrantLock.TestInterrupt.lambda$main$0(TestInterrupt.java:19)
at java.lang.Thread.run(Thread.java:748)
注意:如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断
除了等待外部通知之外,要避免死锁还有另外一种方法,那就是限时等待。
我们可以使用 tryLock() 方法进行一次限时的等待。
原文链接:https://blog.csdn.net/weixin_53142722/article/details/124566944
- 使用 lock.tryLock() 方法会返回获取锁是否成功。返回一个布尔值,如果锁获取成功则返回 true,反之则返回 false。
- tryLock 方法还可以指定等待时间,参数为:tryLock(long timeout, TimeUnit unit),其中 timeout 为最长等待时间,TimeUnit 为时间单位。
- 如果 tryLock() 获取锁失败了、获取超时了或者被打断了,不再阻塞,线程直接停止运行。
- 也就是说在等待获取锁的过程中,这个方法也是支持可打断的。
示例代码(立即失败)
@Slf4j(topic = "c.TestTimeoutWaiting_1")
public class TestTimeoutWaiting_1 {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
log.debug("启动...");
if (!lock.tryLock()) {
log.debug("获取立刻失败,返回");
return;
}
try {
log.debug("获得了锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
log.debug("获得了锁");
t1.start();
try {
sleep(2);
} finally {
lock.unlock();
}
}
}
控制台输出
11:18:58.627 [main] c.TestTimeoutWaiting_1 - 获得了锁
11:18:58.630 [t1] c.TestTimeoutWaiting_1 - 启动...
11:18:58.631 [t1] c.TestTimeoutWaiting_1 - 获取立刻失败,返回
示例代码(超时失败)
@Slf4j(topic = "c.TestTimeoutWaitting_2")
public class TestTimeoutWaitting_2 {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
log.debug("启动...");
try {
if (!lock.tryLock(1, TimeUnit.SECONDS)) {
log.debug("获取等待 1s 后失败,返回");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
log.debug("获得了锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
log.debug("获得了锁");
t1.start();
try {
sleep(2);
} finally {
lock.unlock();
}
}
}
控制台输出
11:21:57.510 [main] c.TestTimeoutWaitting_2 - 获得了锁
11:21:57.512 [t1] c.TestTimeoutWaitting_2 - 启动...
11:21:58.525 [t1] c.TestTimeoutWaitting_2 - 获取等待 1s 后失败,返回
示例代码(解决哲学家就餐问题)
class Chopstick extends ReentrantLock {
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "筷子{" + name + '}';
}
}
@Slf4j(topic = "c.Philosopher")
class Philosopher extends Thread {
Chopstick left;
Chopstick right;
public Philosopher(String name, Chopstick left, Chopstick right) {
super(name);
this.left = left;
this.right = right;
}
@Override
public void run() {
while (true) {
// 尝试获得左手筷子
if (left.tryLock()) {
try {
// 尝试获得右手筷子
if (right.tryLock()) {
try {
eat();
} finally {
right.unlock();
}
}
} finally {
left.unlock(); // 释放自己手里的筷子
}
}
}
}
Random random = new Random();
private void eat() {
log.debug("eating...");
Sleeper.sleep(0.5);
}
}
@Slf4j(topic = "c.Test23")
public class Test23 {
public static void main(String[] args) {
Chopstick c1 = new Chopstick("1");
Chopstick c2 = new Chopstick("2");
Chopstick c3 = new Chopstick("3");
Chopstick c4 = new Chopstick("4");
Chopstick c5 = new Chopstick("5");
new Philosopher("苏格拉底", c1, c2).start();
new Philosopher("柏拉图", c2, c3).start();
new Philosopher("亚里士多德", c3, c4).start();
new Philosopher("赫拉克利特", c4, c5).start();
new Philosopher("阿基米德", c5, c1).start();
}
}
以下内容参考自 《实战 JAVA 高并发程序设计》 葛一鸣 著
公平锁会按照时间的先后顺序,保证先到者先得,后到者后得。
公平锁的一大特点是:它不会产生饥饿现象。
如果我们使用 synchronized 关键字进行锁控制,那么产生的锁就是非公平的。
而重入锁允许我们对其公平性进行设置。它有一个如下的构造函数
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
当参数 fair 为 truef 时,表示锁是公平的。
公平锁看起来很优美,但是要实现公平锁必然要求系统维护一个有序队列,因此公平锁的实现成本比较高,性能相对也非常低下。
在默认情况下,ReentrantLock 是非公平的。如果没有特别的需求,也不需要使用公平锁。
公平锁和非公平锁在线程调度表现上也是非常不一样的。
synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的
打个比方
使用要点:
相关方法
示例代码
@Slf4j(topic = "c.Test24")
public class Test24 {
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
static ReentrantLock ROOM = new ReentrantLock();
// 等待烟的休息室
static Condition waitCigaretteSet = ROOM.newCondition();
// 等外卖的休息室
static Condition waitTakeoutSet = ROOM.newCondition();
public static void main(String[] args) {
new Thread(() -> {
ROOM.lock();
try {
log.debug("有烟没?[{}]", hasCigarette);
while (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
waitCigaretteSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("可以开始干活了");
} finally {
ROOM.unlock();
}
}, "小南").start();
new Thread(() -> {
ROOM.lock();
try {
log.debug("外卖送到没?[{}]", hasTakeout);
while (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
waitTakeoutSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("可以开始干活了");
} finally {
ROOM.unlock();
}
}, "小女").start();
sleep(1);
new Thread(() -> {
ROOM.lock();
try {
hasTakeout = true;
waitTakeoutSet.signal();
} finally {
ROOM.unlock();
}
}, "送外卖的").start();
sleep(1);
new Thread(() -> {
ROOM.lock();
try {
hasCigarette = true;
waitCigaretteSet.signal();
} finally {
ROOM.unlock();
}
}, "送烟的").start();
}
}
控制台输出
20:08:58.861 c.Test24 [小南] - 有烟没?[false]
20:08:58.873 c.Test24 [小南] - 没烟,先歇会!
20:08:58.873 c.Test24 [小女] - 外卖送到没?[false]
20:08:58.873 c.Test24 [小女] - 没外卖,先歇会!
20:08:59.868 c.Test24 [小女] - 可以开始干活了
20:09:00.880 c.Test24 [小南] - 可以开始干活了
共享模型之管程:设计模式-2
比如,必须先 2 后 1 打印
示例代码
@Slf4j(topic = "c.Test25")
public class Test25 {
static final Object lock = new Object();
// 表示 t2 是否运行过
static boolean t2runned = false;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lock) {
while (!t2runned) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("1");
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (lock) {
log.debug("2");
t2runned = true;
lock.notify();
}
}, "t2");
t1.start();
t2.start();
}
}
控制台输出
21:43:14.751 c.Test25 [t2] - 2
21:43:14.754 c.Test25 [t1] - 1
public static ReentrantLock reentrantLock = new ReentrantLock();
public static Condition condition = reentrantLock.newCondition();
// 表示 t2 是否运行过
static boolean t2runned = false;
public static void main(String[] args) {
new Thread(() -> {
reentrantLock.lock();
try {
while (!t2runned) {
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
reentrantLock.unlock();
}
log.debug("t1 ...");
}, "t1").start();
new Thread(() -> {
reentrantLock.lock();
try {
log.debug("t2 ...");
t2runned = true;
condition.signal();
} finally {
reentrantLock.unlock();
}
}, "t2").start();
}
控制台输出
22:13:08.329 [t2] c.Test25 - t2 ...
22:13:08.332 [t1] c.Test25 - t1 ...
Thread t1 = new Thread(() -> {
LockSupport.park();
log.debug("1");
}, "t1");
t1.start();
new Thread(() -> {
log.debug("2");
LockSupport.unpark(t1);
}, "t2").start();
输出
22:15:11.632 c.Test26 [t2] - 2
22:15:11.635 c.Test26 [t1] - 1
线程 1 输出 a 5 次,线程 2 输出 b 5 次,线程 3 输出 c 5 次。
现在要求输出 abcabcabcabcabc 怎么实现
class WaitNotify {
public void print(String str, int waitFlag, int nextFlag) {
for (int i = 0; i < loopNumber; i++) {
synchronized (this) {
while(flag != waitFlag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.print(str);
flag = nextFlag;
this.notifyAll();
}
}
}
// 等待标记
private int flag;
// 循环次数
private int loopNumber;
public WaitNotify(int flag, int loopNumber) {
this.flag = flag;
this.loopNumber = loopNumber;
}
}
@Slf4j(topic = "c.Test27")
public class Test27 {
public static void main(String[] args) {
WaitNotify wn = new WaitNotify(1, 5);
new Thread(() -> {
wn.print("a", 1, 2);
}).start();
new Thread(() -> {
wn.print("b", 2, 3);
}).start();
new Thread(() -> {
wn.print("c", 3, 1);
}).start();
}
}
class AwaitSignal extends ReentrantLock {
private int loopNumber;
public AwaitSignal(int loopNumber) {
this.loopNumber = loopNumber;
}
//参数1 打印内容, 参数2 进入哪一间休息室, 参数3 下一间休息室
public void print(String str, Condition current, Condition next) {
for (int i = 0; i < loopNumber; i++) {
lock();
try {
current.await();
System.out.print(str);
next.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
unlock();
}
}
}
}
public class Test30 {
public static void main(String[] args) throws InterruptedException {
AwaitSignal awaitSignal = new AwaitSignal(5);
Condition a = awaitSignal.newCondition();
Condition b = awaitSignal.newCondition();
Condition c = awaitSignal.newCondition();
new Thread(() -> {
awaitSignal.print("a", a, b);
}).start();
new Thread(() -> {
awaitSignal.print("b", b, c);
}).start();
new Thread(() -> {
awaitSignal.print("c", c, a);
}).start();
Thread.sleep(1000);
awaitSignal.lock();
try {
System.out.println("开始...");
a.signal();
} finally {
awaitSignal.unlock();
}
}
}
class ParkUnpark {
public void print(String str, Thread next) {
for (int i = 0; i < loopNumber; i++) {
LockSupport.park();
System.out.print(str);
LockSupport.unpark(next);
}
}
private int loopNumber;
public ParkUnpark(int loopNumber) {
this.loopNumber = loopNumber;
}
}
@Slf4j(topic = "c.Test31")
public class Test31 {
static Thread t1;
static Thread t2;
static Thread t3;
public static void main(String[] args) {
ParkUnpark pu = new ParkUnpark(5);
t1 = new Thread(() -> {
pu.print("a", t2);
});
t2 = new Thread(() -> {
pu.print("b", t3);
});
t3 = new Thread(() -> {
pu.print("c", t1);
});
t1.start();
t2.start();
t3.start();
LockSupport.unpark(t1);
}
}
本章我们需要重点掌握的是