共享模型之管程
- 3.1.线程共享变量问题分析
- 3.2.synchronized 解决方案
- 3.3.方法上的synchronized
- 3.4.线程八锁
- 3.5.变量的线程安全分析
- 3.6.Monitor 概念
- 3.7.monitor原理
- 3.8.synchronized 原理进阶
- 3.9.wait notify
- 3.10 .同步模式-保护性暂停
- 3.11.异步模式-消费者/生产者模式
- 3.12.park&unpark
- 3.13.线程状态转换
- 3.13.1 NEW --> RUNNABLE
- 3.13.2 RUNNABLE <--> WAITING
- 3.13.3RUNNABLE <--> WAITING
- 3.13.4RUNNABLE <--> WAITING
- 3.13.5 RUNNABLE <--> TIMED_WAITING
- 3.13.6 RUNNABLE <--> TIMED_WAITING
- 3.13.7 RUNNABLE <--> TIMED_WAITING
- 3.13.8 RUNNABLE <--> TIMED_WAITING
- 3.13.9 RUNNABLE <--> BLOCKED
- 3.13.10 RUNNABLE <--> TERMINATED
- 3.14. 多把锁
- 3.15 线程活跃性
- 3.16 ReentrantLock
- 3.17.同步模式-顺序控制
- 3.18本章小结
- 4.共享模型之内存
3.1.线程共享变量问题分析
两个线程对初始值为 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);
}
问题分析
以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作,要彻底理 解,必须从字节码来进行分析
例如对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i
而对应 i-- 也是类似:
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i
而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:
如果是单线程以上 8 行代码是顺序执行(不会交错)没有问题:
多线程情况下出现负数的情况:
多线程情况下出现正数的情况:
临界区 Critical Section
-
一个程序运行多个线程本身是没有问题的
-
问题出在多个线程访问共享资源
-
多个线程读共享资源其实也没有问题
-
在多个线程对共享资源读写操作时发生指令交错,就会出现问题
-
-
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
例如,下面代码中的临界区
static int counter = 0;
static void increment()
// 临界区
{
counter++;
}
static void decrement()
// 临界区
{
counter--;
}
竞态条件 Race Condition
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
3.2.synchronized 解决方案
应用之互斥
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
-
阻塞式的解决方案:synchronized,Lock
-
非阻塞式的解决方案:原子变量 本次课使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一 时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁 的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
注意
虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:
互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
同步是由于线程执行的先后、顺序不同、需要
一个线程等待其它线程运行到某个点
synchronized 语法
synchronized(对象) // 线程1, 线程2(blocked)
{
临界区
}
使用synchronized解决上面代码数据共享安全问题:
@Slf4j
public class Test1 {
static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
// 不要错误理解为锁住了对象就能一直执行下去
synchronized (Test1.class){
counter++;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (Test1.class){
counter--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}",counter);
}
}
思考
synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切 换所打断。 为了加深理解,请思考下面的问题
- 如果把 synchronized(obj) 放在 for 循环的外面,如何理解? for循环不执行完,对象锁不会释放,其他线程无法访问
- 原子性 如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎样运作? 锁的对象不一致,无法保证原子性
- 锁对象 如果 t1 synchronized(obj) 而 t2 没有加会怎么样?如何理解? t2不会获取对象锁,不会进行阻塞,照常执行
使用面向对象思想进行改进:
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.debug("{}",room.get());
}
-------------------------------------------------------------------------------------------------------------------
public class Room {
int value = 0;
public void increment() {
synchronized (this) {
value++;
}
}
public void decrement() {
synchronized (this) {
value--;
}
}
public int get() {
synchronized (this) {
return value;
}
}
}
3.3.方法上的synchronized
成员方法上: 锁 this
class Test{
public synchronized void test() {
}
}
//等价于
class Test{
public void test() {
synchronized(this) {
}
}
}
静态方法上:锁当前类的Class对象
class Test{
public synchronized static void test() {
}
}
//等价于
class Test{
public static void test() {
synchronized(Test.class) {
}
}
}
不加synchronized的方法,无需获取对象锁,异步执行,不能保证原子性
3.4.线程八锁
其实就是考察 synchronized 锁住的是哪个对象,是否是锁的同一个对象
以下的情况都要考虑是那个线程先抢到cpu执行权
情况1:输出:12或21
class Number{
public synchronized void a() {
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
}
情况2:1秒后 12 或 2 1秒后1
class Number{
public synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
}
// 锁住的都是this,需要进行同步等待,就看谁先获取到cpu执行权
情况3:输出:3 1秒后12 或 23 1秒后 1 或 32 1秒后 1
class Number{
public synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
public void c() {
log.debug("3");
}
}
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();
}
// n1.a();和n1.b();锁的都是 this对象,n1.c();未加锁无需进行同步等待
情况4:输出:2 1秒后 1
class Number{
public synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n2.b(); }).start();
}
// 锁的不是同一个对象,不会产生互斥
情况5:输出:2 1秒后 1
class Number{
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
}
// 锁的不是同一个对象,不会产生互斥,一个锁的this,一个锁的Number.class
情况6:输出:1秒后 12, 或 2 1秒后 1
class Number{
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public static synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
}
// 锁的都是Number.class对象
情况7::2 1秒后 1
class Number{
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n2.b(); }).start();
}
// 锁的对象不一致,一个锁的是this,一个锁住的是Number.class
情况8::1秒后12, 或 2 1秒后 1
class Number{
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public static synchronized void b() {
log.debug("2");
}
}
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.class,就看谁先拿到cpu执行权
3.5.变量的线程安全分析
成员变量和静态变量是否线程安全?
-
如果它们没有共享,则线程安全
-
如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
- 如果只有读操作,则线程安全
- 如果有读写操作,则这段代码是临界区,需要考虑线程安全
局部变量是否线程安全?
局部变量是线程安全的
但局部变量引用的对象则未必
-
如果该对象没有逃离方法的作用访问,它是线程安全的
-
如果该对象逃离方法的作用范围,需要考虑线程安全
局部变量线程安全分析
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
成员变量的例子
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);
}
}
执行
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).start();
}
}
其中一种情况是,如果线程2 还未 add,线程1 remove 就会报错:
Exception in thread "Thread0" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
at java.util.ArrayList.rangeCheck(ArrayList.java:657)
at java.util.ArrayList.remove(ArrayList.java:496)
at com.compass.test.ThreadUnsafe.method3(ThreadUnsafe.java:30)
at com.compass.test.ThreadUnsafe.method1(ThreadUnsafe.java:22)
at com.compass.test.ThreadUnsafe.lambda$main$0(ThreadUnsafe.java:41)
at java.lang.Thread.run(Thread.java:748)
分析:
- 无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量
- method3 与 method2 分析相同
- list是一个不安全的集合,内部对add,remove方法都没有实现线程安全机制,有的线程还没有来得及add,而别的线程又进行remove,就会出现一个IndexOutOfBoundsException异常
将 list 修改为局部变量
class ThreadSafe {
public final 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);
}
}
那么就不会有上述问题了,因为每个线程都有自己的栈,list集合由每个线程自己的栈帧去创建,list集合不再是共享变量,也就不存在多线程安全问题。
分析:
- list 是局部变量,每个线程调用时会创建其不同实例,没有共享
- 而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象
- method3 的参数分析与 method2 相同
方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public 会不会代理线程安全问题?
情况1:有其它线程调用 method2 和 method3
情况2:在 情况1 的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法
class ThreadSafe {
public final 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{
// 新建一个线程去执行remove操作,
@Override
public void method3(ArrayList<String> list) {
new Thread(() -> {
list.remove(0);
}).start();
}
}
可以看出 private 或 final 提供【安全】的意义所在,避免子类对父类的一些方法进行重写,导致可能出现多线程安全问题
常见线程安全类
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- java.util.concurrent 包下的类
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为
Hashtable table = new Hashtable();
new Thread(()->{
table.put("key", "value1");
}).start();
new Thread(()->{
table.put("key", "value2");
}).start();
- 它们的每个方法是原子的
- 但注意它们多个方法的组合不是原子的,见后面分析
线程安全类方法的组合
分析下面代码是否线程安全?
回答:不安全,会受到线程上下文切换的影响,有可能两个线程都同时进入了get方法获取到的都是null,后续线程1设置put了一个值,线程2又put一个值将线程1put的值进行了覆盖。
public void setVale(){
Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
table.put("key", value);
}
}
不可变类线程安全性
String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的
有同学或许有疑问,String 有 replace,substring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安 全的呢?
**String类安全的原因:**只能读不能修改,所以是线程安全的,在执行substring()时并没有该有String对象的属性而是新建一个新的字符串来进行操作。
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);
}
public class Immutable{
private int value = 0;
public Immutable(int value){
this.value = value;
}
public int getValue(){
return this.value;
}
}
如果想新增一个方法呢?
public class Immutable{
private int value = 0;
public Immutable(int value){
this.value = value;
}
public int getValue(){
return this.value;
}
public Immutable add(int v){
return new Immutable(this.value + v);
}
}
实例分析
例1:Servlet单实例,会被Tomcat的多个线程所共享使用,所以他的成员变量都可能会有线程安全问题。
public class MyServlet extends HttpServlet {
// 是否安全? 不是线程安全的,因为HashMap是线程不安全的
Map<String,Object> map = new HashMap<>();
// 是否安全? 是的,是不可变类
String S1 = "...";
// 是否安全?是的,是不可变类
final String S2 = "...";
// 是否安全? 不是线程安全的
Date D1 = new Date();
// 是否安全? 不是线程安全的,虽然不能对D2引用值进行修改,但是可以对他的属性修改
final Date D2 = new Date();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
// 使用上述变量
}
}
例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++;
}
}
例3:
@Aspect
@Component
public class MyAspect {
// 是否安全?不是线程安全的,spring默认都是单实例的,设计到对共享资源的一个数据修改
private long start = 0L;
@Before("execution(* *(..))")
public void before() {
start = System.nanoTime();
}
@After("execution(* *(..))")
public void after() {
long end = System.nanoTime();
System.out.println("cost time:" + (end-start));
}
}
// 解决方案:做成环绕通知,做成局部变量
// 如果配置为多实例?也是不可以的,进入前置通知是一个对象,进入后置通知又是别的对象,那统计的时间就不正确
例4:
public class MyServlet extends HttpServlet {
// 是否安全 线程安全的,因为没有涉及到共享数据的修改
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 是否安全 线程安全的,因为没有涉及到共享数据的修改
private UserDao userDao = new UserDaoImpl();
public void update() {
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
public void update() {
String sql = "update user set password = ? where username = ?";
// 是否安全 线程安全的,Connection属于局部变量,每次都新建,属于每个线程独有的
try (Connection conn = DriverManager.getConnection("","","")){
// ...
} catch (Exception e) {
// ...
}
}
}
例5:
public class MyServlet extends HttpServlet {
// 是否安全 是线程安全的,而且是私有的,没有修改共享数据的地方
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 是否安全 是线程安全的,而且是私有的,没有修改共享数据的地方
private UserDao userDao = new UserDaoImpl();
public void update() {
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
// 是否安全 线程不安全,做成成员变量,被多个线程共享,有可能别的线程刚刚创建好久被close()掉了
private Connection conn = null;
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
conn = DriverManager.getConnection("","","");
// ...
conn.close();
}
}
例6:
public class MyServlet extends HttpServlet {
// 是否安全
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
public void update() {
// 每次线程来的时候都新建一个userDao对象,不存在线程安全问题[不推荐]
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();
}
}
例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 的行为是不确定的,可能导致不安全的发生,被称之为外星方法
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();
}
}
卖票案例:
@Slf4j
public class ExerciseSell {
public static void main(String[] args) {
TicketWindow ticketWindow = new TicketWindow(2000);
List<Thread> list = new ArrayList<>();
// 用来存储买出去多少张票
List<Integer> sellCount = new Vector<>();
for (int i = 0; i < 2000; i++) {
Thread t = new Thread(() -> {
// 分析这里的竞态条件
int count = ticketWindow.sell(randomAmount());
sellCount.add(count);
});
list.add(t);
t.start();
}
list.forEach((t) -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 买出去的票求和
log.debug("selled count:{}",sellCount.stream().mapToInt(c -> c).sum());
// 剩余票数
log.debug("remainder count:{}", ticketWindow.getCount());
}
// Random 为线程安全
static Random random = new Random();
// 随机 1~5
public static int randomAmount() {
return random.nextInt(5) + 1;
}
}
class TicketWindow {
private int count;
public TicketWindow(int count) {
this.count = count;
}
// 多个线程来修改count,会出现安全问题,加上synchronized进行解决
public synchronized int sell(int amount) {
if (this.count >= amount) {
this.count -= amount;
return amount;
} else {
return 0;
}
}
public int getCount() {
return count;
}
}
测试脚本:
for /L %n in (1,1,10) do java -cp ".;C:\Users\14823\.m2\repository\ch\qos\logback\logback-classic\1.2.3\logback-classic-1.2.3.jar;C:\Users\14823\.m2\repository\ch\qos\logback\logback-core\1.2.3\logback-core-1.2.3.jar;C:\Users\14823\.m2\repository\org\slf4j\slf4j-api\1.7.25\slf4j-api-1.7.25.jar" com.compass.test.ExerciseSell
转账练习
测试下面代码是否存在线程安全问题,并尝试改正
@Slf4j
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;
}
}
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;
}
/**
* 不能在方法上直接加synchronized,因为这样锁住的是this对象,不是两个对象共有的对象锁
* 我们这里需要锁住的是两个共享变量this.money和target.money 所以只能锁住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);
}
}
}
}
3.6.Monitor 概念
Java 对象头 以 32 位虚拟机为例:
3.7.monitor原理
Monitor 被翻译为监视器或管程
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针 Monitor 结构如下
- 刚开始 Monitor 中 Owner 为 null
- 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一
个 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 的对象不会关联监视器,不遵从以上规则
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 指针
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
注意 方法级别的 synchronized 不会在字节码指令中有所体现
synchronized使用放锁的时机:
- 当前线程的同步方法、代码块执行结束的时候释放
- 当前线程在同步方法、同步代码块中遇到break 、 return 终于该代码块或者方法的时候释放。
- 出现未处理的error或者exception导致异常结束的时候释放
- 程序执行了 同步对象 wait 方法 ,当前线程暂停,释放锁
如下情况不会释放锁:
- 程序调用 Thread.sleep() Thread.yield() 这些方法暂停线程的执行,不会释放。
- 线程执行同步代码块时,其他线程调用 suspend 方法将该线程挂起,该线程不会释放锁 ,所以我们应该避免使用 suspend 和 resume 来控制线程
3.8.synchronized 原理进阶
3.8.1轻量级锁
- 轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
- 轻量级锁对使用者是透明的,即语法仍然是 synchronized
- 假设有两个方法同步块,利用同一个对象加锁
static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
创建锁记录(Lock Record)对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word
让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存 入锁记录
如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下
-
如果 cas 失败,有两种情况
-
如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
-
如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数
-
当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重 入计数减一
当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头
-
成功,则解锁成功
-
失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
3.8.2锁膨胀
如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
static Object obj = new Object();
public static void method1() {
synchronized (obj) {
// 同步块
}
}
当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
-
即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
-
然后自己进入 Monitor 的 EntryList BLOCKED
- 当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁 流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程
3.8.3自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步 块,释放了锁),这时当前线程就可以避免阻塞。 自旋重试成功的情况
自旋重试成功的情况:
自旋重试失败的情况:
- 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
- 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会
高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。 - Java 7 之后不能控制是否开启自旋功能
3.8.4偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 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 值为 0x05 即最后 3 位为 101,这时它的
thread、epoch、age 都为 0 - 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -
XX:BiasedLockingStartupDelay=0 来禁用延迟 - 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、
age 都为 0,第一次用到 hashcode 时才会赋值
测试偏向锁,和偏向锁延迟性
@Slf4j
public class Demo {
// 添加虚拟机参数 -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.toPrintable());
synchronized (d) {
log.debug("synchronized -------中--------");
System.out.println(classLayout.toPrintable());
}
log.debug("synchronized -------后--------");
System.out.println(classLayout.toPrintable());
}, "t1").start();
}
}
class Dog{
}
输出:
03:00:46 [DEBUG] [t1] c.c.test.Demo - synchronized -------前--------
com.compass.test.Dog object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0)
8 4 (object header: class) 0x20018c63
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
03:00:46 [DEBUG] [t1] c.c.test.Demo - synchronized -------中--------
com.compass.test.Dog object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000001c049805 (biased: 0x0000000000070126; epoch: 0; age: 0)
8 4 (object header: class) 0x20018c63
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
03:00:46 [DEBUG] [t1] c.c.test.Demo - synchronized -------后--------
com.compass.test.Dog object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000001c049805 (biased: 0x0000000000070126; epoch: 0; age: 0)
8 4 (object header: class) 0x20018c63
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
注意 处于偏向锁的对象解锁后,线程 id 仍存储于对象头中
测试禁用偏向锁: 在上面测试代码运行时在添加 VM 参数 -XX:-UseBiasedLocking 禁用偏向锁
03:08:33 [DEBUG] [t1] c.c.test.Demo - synchronized -------前--------
com.compass.test.Dog object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0x20018c63
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
03:08:33 [DEBUG] [t1] c.c.test.Demo - synchronized -------中--------
com.compass.test.Dog object internals:
OFF SZ TYPE DESCRIPTION VALUE // 轻量锁
0 8 (object header: mark) 0x000000001c53f628 (thin lock: 0x000000001c53f628)
8 4 (object header: class) 0x20018c63
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
03:08:33 [DEBUG] [t1] c.c.test.Demo - synchronized -------后--------
com.compass.test.Dog object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0x20018c63
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
// 加锁优先级:偏向锁->轻量锁->重量级锁
测试 hashCode:重写hashCode方法可以有效避免 调用hashCode方法让该对象失去偏向锁
@Slf4j
public class Demo {
// 添加虚拟机参数 -XX:BiasedLockingStartupDelay=0
public static void main(String[] args) throws IOException {
Dog d = new Dog();
ClassLayout classLayout = ClassLayout.parseInstance(d);
// 禁用掉这个对象的偏向锁
d.hashCode();
new Thread(() -> {
log.debug("synchronized -------前--------");
System.out.println(classLayout.toPrintable());
synchronized (d) {
log.debug("synchronized -------中--------");
System.out.println(classLayout.toPrintable());
}
log.debug("synchronized -------后--------");
System.out.println(classLayout.toPrintable());
}, "t1").start();
}
}
class Dog{
}
输出:
03:15:05 [DEBUG] [t1] c.c.test.Demo - synchronized -------前--------
com.compass.test.Dog object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000006276ae3401 (hash: 0x6276ae34; age: 0)
8 4 (object header: class) 0x20018c63
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
03:15:05 [DEBUG] [t1] c.c.test.Demo - synchronized -------中--------
com.compass.test.Dog object internals:
OFF SZ TYPE DESCRIPTION VALUE // 偏向锁失效,使用轻量锁
0 8 (object header: mark) 0x000000001bf0f098 (thin lock: 0x000000001bf0f098)
8 4 (object header: class) 0x20018c63
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
03:15:05 [DEBUG] [t1] c.c.test.Demo - synchronized -------后--------
com.compass.test.Dog object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000006276ae3401 (hash: 0x6276ae34; age: 0)
8 4 (object header: class) 0x20018c63
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
正常状态对象一开始是没有 hashCode 的,第一次调用才生成,当一个对象是可偏向的,但是一旦调用hashCode方法后会导致该对象不能使用偏向1锁,升级为轻量锁。
**撤销 - 其它线程使用对象 :**当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级
private static void test2() throws InterruptedException {
Dog d = new Dog();
Thread t1 = new Thread(() -> {
synchronized (d) {
log.debug(ClassLayout.parseInstance(d).toPrintable());
}
synchronized (Demo.class) {
Demo.class.notify();
}
}, "t1");
t1.start();
Thread t2 = new Thread(() -> {
// 确保t1线程执行完后,t2线程再执行
synchronized (Demo.class) {
try {
Demo.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug(ClassLayout.parseInstance(d).toPrintable());
synchronized (d) {
log.debug(ClassLayout.parseInstance(d).toPrintable());
}
log.debug(ClassLayout.parseInstance(d).toPrintable());
}, "t2");
t2.start();
}
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000001be10805 (biased: 0x000000000006f842; epoch: 0; age: 0)
8 4 (object header: class) 0x20018c63
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
03:38:19 [DEBUG] [t2] c.c.test.Demo - com.compass.test.Dog object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000001be10805 (biased: 0x000000000006f842; epoch: 0; age: 0)
8 4 (object header: class) 0x20018c63
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
03:38:19 [DEBUG] [t2] c.c.test.Demo - com.compass.test.Dog object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000001c47f600 (thin lock: 0x000000001c47f600)
8 4 (object header: class) 0x20018c63
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
03:38:19 [DEBUG] [t2] c.c.test.Demo - com.compass.test.Dog object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0x20018c63
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
撤销 - 调用 wait/notify : 将偏向锁或轻量锁都升级为重量级锁,因为重量级锁只有 wait/notify 才会有
@Slf4j
public class Demo {
public static void main(String[] args) throws InterruptedException {
Dog d = new Dog();
ClassLayout layout = ClassLayout.parseInstance(d);
Thread t1 = new Thread(() -> {
// 1
log.debug(layout.toPrintable());
synchronized (d) {
// 2
log.debug(layout.toPrintable());
try {
d.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
//3
log.debug(layout.toPrintable());
}
}, "t1");
t1.start();
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (d) {
log.debug("notify");
d.notify();
}
}, "t2").start();
}
}
class Dog{
}
03:45:23 [DEBUG] [t1] c.c.test.Demo - com.compass.test.Dog object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0)
8 4 (object header: class) 0x20018c63
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
03:45:23 [DEBUG] [t1] c.c.test.Demo - com.compass.test.Dog object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000001b462005 (biased: 0x000000000006d188; epoch: 0; age: 0)
8 4 (object header: class) 0x20018c63
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
03:45:24 [DEBUG] [t2] c.c.test.Demo - notify
03:45:24 [DEBUG] [t1] c.c.test.Demo - com.compass.test.Dog object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000017b0f4aa (fat lock: 0x0000000017b0f4aa)
8 4 (object header: class) 0x20018c63
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
批量重偏向
-
如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象 的 Thread ID
-
当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至 加锁线程
@Slf4j
public class Demo {
public static void main(String[] args) throws InterruptedException {
test3();
}
private static void test3() throws InterruptedException {
List<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).toPrintable());
}
}
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).toPrintable());
synchronized (d) {
log.debug(i + "\t" +ClassLayout.parseInstance(d).toPrintable());
}
log.debug(i + "\t" +ClassLayout.parseInstance(d).toPrintable());
}
}, "t2");
t2.start();
}
}
class Dog{
}
[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
批量撤销
当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象 都会变为不可偏向的,新建的对象也是不可偏向的
@Slf4j
public class Demo {
public static void main(String[] args) throws InterruptedException {
test4();
}
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).toPrintable());
}
}
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).toPrintable());
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
}
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
}
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).toPrintable());
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
}
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
}
}, "t3");
t3.start();
t3.join();
}
}
class Dog{
}
3.8.5锁消除
@Slf4j
@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
关闭锁消除jvm指令: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
锁粗化 对相同对象多次加锁,导致线程发生多次重入,可以使用锁粗化方式来优化,这不同于之前讲的细分锁的粒度。
3.9.wait notify
9.1wait notify 原理
- Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
- BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
- BLOCKED 线程会在 Owner 线程释放锁时唤醒
- WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入
- EntryList 重新竞争
9.2 wait notify 介绍
- obj.wait() 让进入 object 监视器的线程到 waitSet 等待 [ 对象锁持有者调用 ]
- obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒 [ 对象锁持有者调用 ]
- obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒 [ 对象锁持有者调用 ]
**注意:**它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须配合synchronized使用,而且要获得锁才可以调用。
如果不配合synchronized使用,并且没有获取到锁调用就会出现IllegalMonitorStateException异常
notify和notifyAll的区别
static final Object lock=new Object();
public static void main(String[] args) {
new Thread(()->{
synchronized(lock){
log.debug("执行...");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其他代码");
}
},"t1").start();
new Thread(()->{
synchronized (lock){
log.debug("执行...");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其他代码...");
}
},"t2").start();
sleep(1);
log.debug("唤醒其他线程...");
synchronized (lock){
//lock.notify(); // 随机抽取一个正在等待的线程进行唤醒
lock.notifyAll(); // 唤醒阻塞在该对象上的所有线程
}
}
wait() 方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到 notify 为止 wait(long n) 有时限的等待, 到 n 毫秒后结束等待,或是被 notify ,带naos的第二个参数只有大于0,就让timeOut时间基础上加1,并不是真正的精确到纳秒
sleep(long n) 和 wait(long n)
- sleep 是 Thread 方法,而 wait 是 Object 的方法
- sleep 不需要强制和 synchronized 配合使用,但 wait 需要 和 synchronized 一起用
- sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
- 它们 状态 TIMED_WAITING
9.3 wait和notify正确使用方式
step1
@Slf4j
public class Test1 {
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();
}
new Thread(() -> {
// 这里能不能加 synchronized (room)?
// 不能加,因为加上之后送烟线程也需要等待对象锁,送烟线程就无法得到对象锁,无法进行送烟
hasCigarette = true;
log.debug("烟到了噢!");
}, "送烟").start();
}
}
- 小南睡眠后一直持有对象锁,导致其他的线程无法获取到对象锁,只有等小南线程睡醒后释放锁,其他线程才可以获取对象锁执行代码
- 小南线程必须睡足 2s 后才能醒来,就算烟提前送到,也无法立刻醒来
- 送烟线程加了 synchronized (room) 后,送烟线程也需要等待对象锁,无法进行送烟,main线程 没加
synchronized 就无需获取对象锁,也不需要进行阻塞,可以在小南线程睡眠时,将hasCigarette设置为true - 解决方法,使用 wait - notify 机制
step2
@Slf4j
public class Test1 {
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("没烟,先歇会!");
try {
room.wait();
} 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();
}
new Thread(() -> {
synchronized (room){
// 这里能不能加 synchronized (room)?
// 不能加,因为加上之后送烟线程也需要等待对象锁,送烟线程就无法得到对象锁,无法进行送烟
hasCigarette = true;
room.notify();
log.debug("烟到了噢!");
}
}, "送烟").start();
}
}
使用wait等待的好处就是,小南线程进行阻塞但是会释放对象锁,其余的线程也可以拿到对象锁进行执行,无需等到小南线程醒过来才去执行代码块
step3
@Slf4j
public class Test1 {
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("没烟,先歇会!");
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("外卖到了噢!");
// notify:只能随机唤醒一个线程,并不能精准的唤醒小女线程,解决方案就是使用notifyAll,把他们都唤醒
room.notifyAll();
}
}, "送外卖的").start();
}
}
-
notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线 程,称之为【虚假唤醒】
-
解决方法,改为 notifyAll
step4
@Slf4j
public class Test1 {
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);
// 使用while避免虚假唤醒,下次获得锁的时候继续重新判断,如果用if下次获取到锁的时候就不会重新进行判断
while (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
break;
}
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("外卖到了噢!");
// notify:只能随机唤醒一个线程,并不能精准的唤醒小女线程,解决方案就是使用notifyAll,把他们都唤醒
room.notify();
}
}, "送外卖的").start();
}
}
小技巧
synchronized(lock) {
while(条件不成立) {
lock.wait();
}
// 干活
}
//另一个线程
synchronized(lock) {
lock.notifyAll();
}
3.10 .同步模式-保护性暂停
- 定义:即 Guarded Suspension,用在一个线程等待另一个线程的执行结果
- 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject
- 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者)
- JDK 中,join 的实现、Future 的实现,采用的就是此模式
- 因为要等待另一方的结果,因此归类到同步模式
10.0基础版
@Slf4j
public class PrivatePause {
public static void main(String[] args) {
GuardedObject guarded = new GuardedObject();
new Thread(()->{
// 下载一个网页的html代码,放到list集合中
List<String> list = Util.download();
guarded.complete(list);
},"A").start();
new Thread(()->{
List<String> list = (List<String>)guarded.getResult();
log.debug("获取等待结果...");
log.debug("list="+list.size());
},"B").start();
}
}
class GuardedObject{
private Object response;
private static final Object lock=new Object();
/**
* 执行的任务完后就可以给response设置值,并且唤醒get方法
* @param response
*/
public void complete( Object response){
synchronized (this){
this.response=response;
this.notifyAll();
}
}
/**
* 如果response的结果为null就一直进行等待,直到被complete唤醒
* @return
*/
public Object getResult(){
synchronized (this){
while (response==null){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
return response;
}
}
// 下载方法
public static List<String> download(String targetUrl) {
String defaultUrl="https://www.taobao.com";
List<String> list = new ArrayList<>();
HttpURLConnection connection=null;
try {
if (targetUrl!=null&&targetUrl.length()>0){
connection = (HttpURLConnection) new URL(targetUrl).openConnection();
}else {
connection = (HttpURLConnection) new URL(defaultUrl).openConnection();
}
} catch (IOException e) {
e.printStackTrace();
}
try(BufferedReader reader=new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
String line;
while ((line=reader.readLine())!=null){
list.add(line);
}
} catch (IOException e) {
e.printStackTrace();
}
return list;
}
10.1超时版本
/**
* 带超时时间版本的
* @Parse 单位毫秒
* @return
*/
public Object getResult(long timeOut ){
// 开始时间
long startTime=System.currentTimeMillis();
// 经历时间
long passTime=0;
synchronized (this){
while (response==null){
// 这轮循环应该等待的时间
long waitTime = timeOut - passTime;
// 等待时间>=timeOut直接退出循环
if (waitTime<=0){
break;
}
try {
// timeOut-passTime:避免虚假唤醒
this.wait(waitTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 等待时间=现在时间-开始时间
passTime = System.currentTimeMillis() - startTime;
}
}
return response;
}
10.2join原理
join:让调用join的线程进行等待,直到调用join的线程对象执行完释放锁,调用jion的线程才有机会获得锁,继续执行
// 锁的是this对象,那个线程对象调用,那就以那个线程对象做为锁
public final synchronized void join(long millis)
throws InterruptedException {
//得到开始时间
long base = System.currentTimeMillis();
long now = 0;
// 如果设置的时间小于0,抛出IllegalArgumentException
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
// 判断设置的等待时间如果==0就一直等待直到调用join方法的线程对象执行完,调用jion的线程才会向下运行
if (millis == 0) {
// 如果调用join的线程还存活,就一直进入等待状态
while (isAlive())
wait(0);
}
} else {
// 带超时时间的等待
while (isAlive()) {
// 每轮循环应该等待的时间=应该等待时间-每轮循环耗费的时间
long delay = millis - now;
// 最终等待时间<=0,就结束等待
if (delay <= 0) {
break;
}
wait(delay)
// 执行完一轮循环后,更新每轮循环耗费的时间
now = System.currentTimeMillis() - base;
}
}
}
10.3多任务版
图中 Futures 就好比居民楼一层的信箱(每个信箱有房间编号),左侧的 t0,t2,t4 就好比等待邮件的居民,右 侧的 t1,t3,t5 就好比邮递员 如果需要在多个类之间使用 GuardedObject 对象,作为参数传递不是很方便,因此设计一个用来解耦的中间类, 这样不仅能够解耦【结果等待者】和【结果生产者】,还能够同时支持多个任务的管理【Futures 就是一个容器,用于存放多个线程之间通信的数据】
完整代码如下:
@Slf4j
public class PrivatePause {
public static void main(String[] args) {
// 启动三个收信的居民
for (int i=0;i<3;i++){
new People().start();
}
sleep(1);
// 启动三个邮递员
for (Integer id: Futures.getIds()){
new PostMan(id, "hello world[" + id + "]").start();
}
}
}
@Slf4j
// 居民类
class People extends Thread{
@Override
public void run() {
// 收信
GuardedObject guarded = Futures.createGuarded();
log.debug("收信 id:{}",guarded.getId());
Object email = guarded.getResult(3000);
log.debug("收到信 id:{},内容:{}",guarded.getId(),email.toString());
}
}
@Slf4j
// 邮递员类
class PostMan extends Thread{
private int id;
private String data;
@Override
public void run() {
GuardedObject guarded = Futures.getGuardedById(id);
guarded.complete(data);
log.debug("送信:id:{},内容:{}",id,data);
}
public PostMan(int id,String email){
this.id=id;
this.data=email;
}
}
class Futures{
// 该map需要是线程安全的,key是 GuardedObject的id value就是GuardedObject
private static Map<Integer,GuardedObject> futures= new ConcurrentHashMap<>();
private static int id=1;
/**
* 产生GuardedObject对象,保证id的唯一性
* 将创建好的对象放入到 boxes
* @return 返回GuardedObject
*/
public static GuardedObject createGuarded(){
GuardedObject guardedObject=new GuardedObject();
guardedObject.setId(generateId());
futures.put(guardedObject.getId(),guardedObject);
return guardedObject;
}
/**
* 获取所有的id
* @return
*/
public static Set<Integer> getIds(){
return futures.keySet();
}
/**
* 获取id会被多个线程所访问,所以需要考虑线程安全问题
* @return 产生一个唯一的id
*/
public static synchronized int generateId(){
return id++;
}
/**
* 根据id获取到GuardedObject对象
* @param id
* @return
*/
public static GuardedObject getGuardedById(int id){
GuardedObject object = futures.get(id);
// 获取完结果后,将GuardedObject从map集合中移除
return futures.remove(id);
}
}
class GuardedObject{
private Object response;
private int id;
public int getId() {
return id;
}
public GuardedObject setId(int id) {
this.id = id;
return this;
}
/**
* 执行的任务完后就可以给response设置值,并且唤醒get方法
* @param response
*/
public void complete( Object response){
synchronized (this){
this.response=response;
this.notifyAll();
}
}
/**
* 如果response的结果为null就一直进行等待,直到被complete唤醒
* @return
*/
public Object getResult(){
synchronized (this){
while (response==null){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
return response;
}
/**
* 带超时时间版本的
* @Parse 单位毫秒
* @return
*/
public Object getResult(long timeOut ){
// 开始时间
long startTime=System.currentTimeMillis();
// 经历时间
long passTime=0;
synchronized (this){
while (response==null){
// 这轮循环应该等待的时间
long waitTime = timeOut - passTime;
// 等待时间>=timeOut直接退出循环
if (waitTime<=0){
break;
}
try {
// timeOut-passTime:避免虚假唤醒
this.wait(waitTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 等待时间=现在时间-开始时间
passTime = System.currentTimeMillis() - startTime;
}
}
return response;
}
}
3.11.异步模式-消费者/生产者模式
- 与前面的保护性暂停中的 GuardObject 不同,不需要产生结果和消费结果的线程一一对应
- 消费队列可以用来平衡生产和消费的线程资源
- 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据
- 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
- JDK 中各种阻塞队列,采用的就是这种模式
@Slf4j
// java消息线程之间通信
public class MessageQueue {
// 消息队列
LinkedList<Message> list =new LinkedList();
// 队列消息容量
public int capacity;
public MessageQueue(int capacity){
this.capacity=capacity;
}
/**
* 从队列中获取消息
* @return
*/
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;
}
}
/**
* 往队列中添加消息
* @param message 消息
*/
public void put(Message message){
synchronized (list){
// 如果队列已满,就进行等待,直到消费者有消费
while (list.size()>=capacity){
try {
log.debug("队列为满... 生产者线程等待");
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("生产消息= "+message);
// 将生产出来的数据放入到队列头部
list.addFirst(message);
// 唤醒消费者线程进行消费
list.notifyAll();
}
}
}
class Message{
// 消息id
private int id;
// 数据
private Object data;
public void setId(int id) {
this.id = id;
}
public void setData(Object data) {
this.data = data;
}
public Message(int id,Object data){
this.id = id;
this.data = data;
}
@Override
public String toString() {
return "Message{" +
"id=" + id +
", data=" + data +
'}';
}
}
class TestA{
public static void main(String[] args) {
MessageQueue queue = new MessageQueue(2);
// 启动3个生产者线程
for (int i = 0; i < 3; i++) {
int index=i;
new Thread(()->{
queue.put(new Message(index,"消息["+index+"]"));
},"生产者"+i).start();
}
sleep(1);
// 启动3个消费者线程
for (int i = 0; i < 3; i++) {
new Thread(()->{
queue.take();
},"消费者"+i).start();
}
}
}
输出:
18:52:37 [DEBUG] [生产者:0] c.c.d.MessageQueue - 生产消息= Message{id=0, message=消息[0]}
18:52:37 [DEBUG] [生产者:2] c.c.d.MessageQueue - 生产消息= Message{id=2, message=消息[2]}
18:52:37 [DEBUG] [生产者:1] c.c.d.MessageQueue - 队列为满... 生产者线程等待
18:52:38 [DEBUG] [消费者:0] c.c.d.MessageQueue - 消费消息= Message{id=0, message=消息[0]}
18:52:38 [DEBUG] [消费者:2] c.c.d.MessageQueue - 消费消息= Message{id=2, message=消息[2]}
18:52:38 [DEBUG] [消费者:1] c.c.d.MessageQueue - 队列为空... 消费者线程等待
18:52:38 [DEBUG] [生产者:1] c.c.d.MessageQueue - 生产消息= Message{id=1, message=消息[1]}
18:52:38 [DEBUG] [消费者:1] c.c.d.MessageQueue - 消费消息= Message{id=1, message=消息[1]}
3.12.park&unpark
它们是 LockSupport 类中的方法
// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)
// 先 park 再 unpark
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("start...");
sleep(2);
log.debug("park..");
// 当前线程进入 wait 状态
LockSupport.park();
log.debug("resume...");
}, "t1");
t1.start();
sleep(1);
log.debug("unpark...");
// 唤醒 t1 线程
LockSupport.unpark(t1);
}
3.12.1.与wait & notify 相比
-
wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必
-
park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么【精确】
-
park & unpark 可以先 unpark,而 wait & notify 不能先 notify
3.12.2.unpark和park原理
每个线程都有自己的一个 Parker 对象,由三部分组成 _counter【0或1】 , _cond 和 _mutex
- 调用 park 就是判断当前线程是否需要进入阻塞状态
- 如果_counter=0,进入阻塞状态
- 如果_counter=1,无需进入阻塞状态
- 调用 unpark,唤醒由park阻塞的线程
- 如果这时线程还在阻塞状态【c_counter=0】,就唤醒让他继续向下执行
- 如果这时线程还在运行【_counter=1】,那么下次他调用 park 时,就无需阻塞,就继续向下执行
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N9seVszJ-1651433863045)(C:\Users\14823\Desktop\learn-note\typroa-img\image-20211120204143646.png)]
3.13.线程状态转换
3.13.1 NEW --> RUNNABLE
当调用 t.start() 方法时,由 NEW --> RUNNABLE
// new[新建状态]
Thread t1 = new Thread(() -> {
log.debug("RUNNABLE");
}, "t1");
3.13.2 RUNNABLE <–> WAITING
- t 线程用 synchronized(obj) 获取了对象锁后
- 调用 obj.wait() 方法时,t 线程从 RUNNABLE --> WAITING
- 调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
- 竞争锁成功,t 线程从 WAITING --> RUNNABLE
- 竞争锁失败,t 线程从 WAITING --> BLOCKE
3.13.3RUNNABLE <–> WAITING
- 当前线程调用 t.join() 方法时,当前线程从 RUNNABLE --> WAITING
- 注意是当前线程在t 线程对象的监视器上等待
- t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING --> RUNNABLE
3.13.4RUNNABLE <–> WAITING
- 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE --> WAITING
- 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING --> RUNNABLE
3.13.5 RUNNABLE <–> TIMED_WAITING
- 线程用 synchronized(obj) 获取了对象锁后
- 调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE --> TIMED_WAITING
- t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
- 竞争锁成功,t 线程从 TIMED_WAITING --> RUNNABLE
- 竞争锁失败,t 线程从 TIMED_WAITING --> BLOCKED
3.13.6 RUNNABLE <–> TIMED_WAITING
- 当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE --> TIMED_WAITING
- 注意是当前线程在 t 线程对象的监视器上等待
- 当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从TIMED_WAITING --> RUNNABLE
3.13.7 RUNNABLE <–> TIMED_WAITING
- 当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE --> TIMED_WAITING
- 当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING --> RUNNABLE
3.13.8 RUNNABLE <–> TIMED_WAITING
- 当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线程从 RUNNABLE --> TIMED_WAITING
- 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从TIMED_WAITING–> RUNNABLE
3.13.9 RUNNABLE <–> BLOCKED
- t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED
- 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争
成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然 BLOCKED
3.13.10 RUNNABLE <–> TERMINATED
- 当前线程所有代码运行完毕,进入 TERMINATED
3.14. 多把锁
在两个操作不相关联,无共享数据的时候,可以用多把锁的方式来降低锁的粒度,粒度小并发读就越高,如果两个不相关联的方法公用同一把锁,就会导致锁的粒度太大,并发度低。
- 将锁的粒度细分
- 好处,是可以增强并发度
- 坏处,如果一个线程需要同时获得多把锁,就容易发生死锁
以下代码都共用同一个对象锁 this ,在在调用study方法的时候,study方法就占有 this 对象锁,而别的线程想要调用sleep方法的时候也需要this对象锁,调用sleep的线程就只有等待 调用study的线程释放了锁,才可以继续执行,这样就导致了,两个毫不相干的方法产生了依赖的关系,锁的粒度变大,解决的方法就是:让 study和sleep 持有不同的对象锁
@Slf4j
public class Test1 {
public static void main(String[] args) {
BigRoom room = new BigRoom();
new Thread(()->{
room.sleep();
},"A").start();
new Thread(()->{
room.study();
},"B").start();
}
}
@Slf4j
class BigRoom{
public void sleep(){
synchronized (this){
log.debug("睡觉中...");
// 睡眠2秒
Util.sleep(2);
}
}
public void study(){
synchronized (this){
log.debug("学习中...");
// 睡眠2秒
Util.sleep(3);
}
}
}
改进后的代码:两个方法持有不同的对象锁,无需进行同步等待,可以同时运行
@Slf4j
class BigRoom{
static final Object sleep = new Object();
static final Object study = new Object();
public void sleep(){
synchronized (sleep){
log.debug("睡觉中...");
// 睡眠2秒
Util.sleep(2);
}
}
public void study(){
synchronized (study){
log.debug("学习中...");
// 睡眠2秒
Util.sleep(3);
}
}
}
3.15 线程活跃性
3.15 .1 死锁
有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁
-
t1 线程 获得 A对象 锁,接下来想获取 B对象 的锁
-
t2 线程 获得 B对象 锁,接下来想获取 A对象 的锁
以下代码就是死锁:
@Slf4j
public class Test1 {
static final Object A = new Object();
static final Object B = new Object();
public static void main(String[] args) {
new Thread(()->{
synchronized (A){
// 睡眠0.5秒 先让t2获得B对象锁
sleep(500L);
log.debug("获得A对象锁");
synchronized (B){
log.debug("获得B对象锁");
}
}
},"t1").start();
new Thread(()->{
synchronized (B){
// 睡眠0.5秒 先让t1获得A对象锁
sleep(500L);
log.debug("获得B对象锁");
synchronized (A){
log.debug("获得A对象锁");
}
}
},"t2").start();
}
}
如何定位发生死锁所在代码位置?
检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 进程id 定位死锁:
1.用jps命令查看所有java进程的id
2.利用jstack 进程id 查看java线程的状态信息
详细信息:
2021-11-21 16:50:12
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.251-b08 mixed mode):
"DestroyJavaVM" #14 prio=5 os_prio=0 tid=0x0000000003264000 nid=0x2d230 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"t2" #13 prio=5 os_prio=0 tid=0x000000001bcab000 nid=0x2cbf8 waiting for monitor entry [0x000000001c27f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.compass.test.Test1.lambda$main$1(Test1.java:41)
- waiting to lock <0x00000000d6cfa208> (a java.lang.Object)
- locked <0x00000000d6cfa218> (a java.lang.Object)
at com.compass.test.Test1$$Lambda$2/1792393294.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"t1" #12 prio=5 os_prio=0 tid=0x000000001bca8000 nid=0x2d1c4 waiting for monitor entry [0x000000001c17e000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.compass.test.Test1.lambda$main$0(Test1.java:30)
- waiting to lock <0x00000000d6cfa218> (a java.lang.Object)
- locked <0x00000000d6cfa208> (a java.lang.Object)
at com.compass.test.Test1$$Lambda$1/1914572623.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"Service Thread" #11 daemon prio=9 os_prio=0 tid=0x0000000019ca0000 nid=0x2d1c8 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C1 CompilerThread3" #10 daemon prio=9 os_prio=2 tid=0x0000000019c81000 nid=0x2d1c0 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread2" #9 daemon prio=9 os_prio=2 tid=0x0000000019c66000 nid=0x2d1b0 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread1" #8 daemon prio=9 os_prio=2 tid=0x0000000019c7e000 nid=0x2cbf0 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread0" #7 daemon prio=9 os_prio=2 tid=0x0000000019c79000 nid=0x2cd50 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Monitor Ctrl-Break" #6 daemon prio=5 os_prio=0 tid=0x0000000019c5f000 nid=0x2cd38 runnable [0x000000001b1ce000]
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 <0x00000000d632d5d0> (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 <0x00000000d632d5d0> (a java.io.InputStreamReader)
at java.io.BufferedReader.readLine(BufferedReader.java:389)
at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:61)
"Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x0000000019bb8800 nid=0x2cce4 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x0000000019b64000 nid=0x2d1a8 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x000000000335b000 nid=0x2c724 in Object.wait() [0x000000001aecf000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000000d6108ee0> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)
- locked <0x00000000d6108ee0> (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=0x0000000019b43000 nid=0x2c8e4 in Object.wait() [0x000000001adcf000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000000d6106c00> (a java.lang.ref.Reference$Lock)
at java.lang.Object.wait(Object.java:502)
at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
- locked <0x00000000d6106c00> (a java.lang.ref.Reference$Lock)
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)
"VM Thread" os_prio=2 tid=0x0000000018476000 nid=0x2cec8 runnable
"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x0000000003279800 nid=0x2cf38 runnable
"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x000000000327b000 nid=0x2cf88 runnable
"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x000000000327c800 nid=0x2ce08 runnable
"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x000000000327e000 nid=0x2cdb4 runnable
"GC task thread#4 (ParallelGC)" os_prio=0 tid=0x0000000003281000 nid=0x2cdf4 runnable
"GC task thread#5 (ParallelGC)" os_prio=0 tid=0x0000000003282800 nid=0x2ce9c runnable
"GC task thread#6 (ParallelGC)" os_prio=0 tid=0x0000000003285800 nid=0x2ce80 runnable
"GC task thread#7 (ParallelGC)" os_prio=0 tid=0x0000000003286800 nid=0x2d12c runnable
"GC task thread#8 (ParallelGC)" os_prio=0 tid=0x0000000003288000 nid=0x2d160 runnable
"GC task thread#9 (ParallelGC)" os_prio=0 tid=0x0000000003289000 nid=0x2cef0 runnable
"VM Periodic Task Thread" os_prio=2 tid=0x0000000019c67000 nid=0x2cd64 waiting on condition
JNI global references: 317
// 发现一个死锁
Found one Java-level deadlock:
=============================
"t2":
waiting to lock monitor 0x0000000018485108 (object 0x00000000d6cfa208, a java.lang.Object),
which is held by "t1"
"t1":
waiting to lock monitor 0x0000000018487578 (object 0x00000000d6cfa218, a java.lang.Object),
which is held by "t2"
Java stack information for the threads listed above:
===================================================
"t2":
at com.compass.test.Test1.lambda$main$1(Test1.java:41)
- waiting to lock <0x00000000d6cfa208> (a java.lang.Object)
- locked <0x00000000d6cfa218> (a java.lang.Object)
at com.compass.test.Test1$$Lambda$2/1792393294.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"t1":
at com.compass.test.Test1.lambda$main$0(Test1.java:30)
- waiting to lock <0x00000000d6cfa218> (a java.lang.Object)
- locked <0x00000000d6cfa208> (a java.lang.Object)
at com.compass.test.Test1$$Lambda$1/1914572623.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
// 发现一个死锁
Found 1 deadlock.
使用 jconsole 远程连接工具
哲学家就餐问题
有五位哲学家,围坐在圆桌旁。
- 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
- 吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。
- 如果筷子被身边的人拿着,自己就得等待 【会发生死锁问题】
// 哲学家类
@Slf4j
public class Philosopher extends Thread {
Chopstick left;
Chopstick right;
public Philosopher(String name, Chopstick left, Chopstick right) {
super(name);
this.left = left;
this.right = right;
}
private void eat() {
log.debug("eating...");
}
@Override
public void run() {
while (true) {
// 获得左手筷子
synchronized (left) {
// 获得右手筷子
synchronized (right) {
// 吃饭
eat();
}
// 放下右手筷子
}
// 放下左手筷子
}
}
}
// 筷子类
class Chopstick {
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();
}
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "筷子{" + name + '}';
}
}
3.15 .2 活锁
活锁出现在两个线程互相改变对方的结束条件,导致双方的结束条件都不成立,最后谁也无法结束,例如
@Slf4j
public class TestLiveLock {
static volatile int count=20;
static final Object lock =new Object();
// 解决方案:两者睡眠的时间不一致,采用随机数的方式
public static void main(String[] args) {
// 期望 count<0退出循环
new Thread(()->{
while (count>0){
Util.sleep(200L);
count--;
log.debug("count:{}",count);
}
},"A").start();
// 期望 count>40退出循环
new Thread(()->{
while (count<40){
Util.sleep(200L);
count++;
log.debug("count:{}",count);
}
},"B").start();
}
}
3.15 .3 饥饿
线程饥饿的例子,先来看看使用顺序加锁的方式解决之前的死锁问题
之前的死锁代码:我们改造一下,按照顺序的方式进行加锁,那么就不会出现死锁的问题,那么新的问题又来了,就是会导致线程饥饿问题,有的线程一直得不到执行权,有时候A线程长时间获得锁,有时候B线程长时间获得锁
@Slf4j
public class Test {
static final Object A = new Object();
static final Object B = new Object();
public static void main(String[] args) {
new Thread(()->{
while (true){
synchronized (A){
log.debug("获得A对象锁");
synchronized (B){
log.debug("获得B对象锁");
}
}
}
},"A").start();
new Thread(()->{
while (true){
synchronized (A){
log.debug("获得B对象锁:" );
synchronized (B){
log.debug("获得A对象锁");
}
}
}
},"B").start();
}
}
3.16 ReentrantLock
相对于 synchronized 它具备如下特点
- 可中断 可以设置超时时间
- 可以设置为公平锁
- 支持多个条件变量 [ 多个waitSet ]
- 与 synchronized 一样,都支持可重入 基本语法
基本语法:
// 获取锁
reentrantLock.lock();
try {
// 临界区
} finally {
// 释放锁
reentrantLock.unlock();
}
3.16.1 可重入
- 可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
- 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
@Slf4j
public class ReentrantLockDemo {
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
method1();
}
public static void method1() {
lock.lock();
try {
log.debug("execute method1");
method2();
} finally {
lock.unlock();
}
}
public static void method2() {
lock.lock();
try {
log.debug("execute method2");
method3();
} finally {
lock.unlock();
}
}
public static void method3() {
lock.lock();
try {
log.debug("execute method3");
} finally {
lock.unlock();
}
}
}
// 未加锁就调用lock.unlock 会抛出 IllegalMonitorStateException
3.16.2可打断
注意如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread thread = new Thread(() -> {
try {
log.debug("尝试获得锁");
// 如果此处使用的是 lock方法是不能进行打断的
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("没有获得锁");
return;
} finally {
lock.unlock();
}
}, "lock");
// main线程先获得锁
lock.lock();
// 启动main线程
thread.start();
// 先让main线程睡眠1秒
Util.sleep(1);
// 打断 lock 线程
thread.interrupt();
}
3.16.3 可超时
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread thread = new Thread(() -> {
// 尝试获得锁,获取不到就进入阻塞队列,下次再获取 获取到锁:true,没有获取到锁:false
// lock.tryLock(1,TimeUtil.SECONDS) // 尝试等待 1秒获取锁,如果1秒后获取不了就继续向下执行[支持可打断]
if (!lock.tryLock()){
log.debug("获取不到锁");
return;
}
try {
log.debug("获得到锁");
}finally {
lock.unlock();
}
}, "lock");
// main线程获得锁后,lock线程尝试获得锁,获得不了就不进行阻塞了
// lock.lock();
thread.start();
Util.sleep(1);
}
// lock.tryLock(1,TimeUtil.SECONDS) 可打断 在指定时间内获取不到锁 立刻结束等待
// lock.tryLock() 不可打断 获取不到锁立刻结束等待
3.16.4 解决哲学家就餐问题
@Override
public void run() {
// 原来的版本
/* while (true) {
// 获得左手筷子
synchronized (left) {
// 获得右手筷子
synchronized (right) {
// 吃饭
eat();
}
// 放下右手筷子
}
// 放下左手筷子
}*/
// 使用 ReentrantLock 改进后的版本
while (true){
// 睡眠 300 毫秒方便查看
Util.sleep(300L);
// 尝试获得左手筷子
if (left.tryLock()){
try {
// 尝试获得右手筷子
if (right.tryLock()){
try {
// 两支筷子都有了,可以吃饭了
eat();
}finally {
// 吃完后释放右手筷子
right.unlock();
}
}
}finally {
// 吃完后释放左手筷子
left.unlock();
}
}
}
}
3.16.5 公平性
ReentrantLock 默认是不公平的,可以通过构造方法传递 ture 来设置为公平锁
// 设置为true和false会有两种不同的效果
static ReentrantLock lock = new ReentrantLock(true);
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while (true){
try {
lock.lock();
log.debug("获得锁:A线程");
}finally {
lock.unlock();
}
}
},"A").start();
new Thread(()->{
while (true){
try {
lock.lock();
log.debug("获得锁:B线程");
}finally {
lock.unlock();
}
}
},"B").start();
}
公平锁一般没有必要,会降低并发度,后面分析原理时会讲解
3.16.6条件变量
synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量。
- synchronized 是那些不满足条件的线程都在一间休息室等消息
- 而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒
使用要点:
- await 前需要获得锁
- await 执行后,会释放锁,进入 conditionObject 等待
- await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
- 竞争 lock 锁成功后,从 await 后继续执行
static ReentrantLock lock = new ReentrantLock();
static Condition waitCigaretteQueue = lock.newCondition();
static Condition waitBreakfastQueue = lock.newCondition();
static volatile boolean hasCigarette = false;
static volatile boolean hasBreakfast = false;
public static void main(String[] args) {
new Thread(() -> {
try {
lock.lock();
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
waitCigaretteQueue.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("可以开始干活了[小南]");
}finally {
lock.unlock();
}
}, "小南").start();
new Thread(() -> {
try {
lock.lock();
log.debug("有早餐没?[{}]", hasBreakfast);
if (!hasBreakfast) {
log.debug("没早餐,休息会儿");
try {
waitBreakfastQueue.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("可以开始干活了[小女]");
}finally {
lock.unlock();
}
}, "小女").start();
new Thread(() -> {
try {
lock.lock();
hasCigarette = true;
waitCigaretteQueue.signal();
log.debug("烟到了噢!");
}finally {
lock.unlock();
}
}, "送烟").start();
new Thread(() -> {
try {
lock.lock();
hasBreakfast = true;
waitBreakfastQueue.signal();
log.debug("早餐到了噢!");
}finally {
lock.unlock();
}
}, "送早餐").start();
}
synchronized 和 Condition 使用上都是差不多的,但是最大的一个区别就是 Condition 可以精准的唤醒某个线程,而synchronized只能随机唤醒一个正在等待线程或唤醒所有正在等待的线程
3.17.同步模式-顺序控制
3.17.1 warit和notify (执行先后)
现在有两个线程:需要指定让某个线程先执行
// 目的: 让B线程先输出2 A线程再输出1
@Slf4j
public class FixedOrderWait {
static final Object lock = new Object();
// 用于标记 B线程是否运行过 运行过就是 true
static volatile boolean flag=false;
public static void main(String[] args) {
Thread a = new Thread(() -> {
synchronized (lock){
// 循环判断 B线程是否运行过,如果没有就一直进行等待
while (!flag){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("1");
}
}, "A");
Thread b = new Thread(() -> {
log.debug("2");
synchronized (lock){
// 把运行过的标志设置为true
flag=true;
// 唤醒 A 线程进行打印
lock.notify();
}
}, "B");
// 启动 a b 线程
a.start();
b.start();
}
}
3.17.2 Park Unpark [执行先后]
可以看到,实现上很麻烦:
- 首先,需要保证先 wait 再 notify,否则 wait 线程永远得不到唤醒。因此使用了『运行标记』来判断该不该wait
- 第二,如果有些干扰线程错误地 notify 了 wait 线程,条件不满足时还要重新等待,使用了 while 循环来解决此问题
- 最后,唤醒对象上的 wait 线程需要使用 notifyAll,因为『同步对象』上的等待线程可能不止一个
- 可以使用 LockSupport 类的 park 和 unpark 来简化上面的题目:
public static void main(String[] args) {
Thread a = new Thread(() -> {
// 先进行阻塞 等待 B线程唤醒
LockSupport.park();
log.debug("1");
}, "A");
Thread b = new Thread(() -> {
log.debug("2");
// 唤醒A线程
LockSupport.unpark(a);
}, "B");
// 启动 a b 线程
a.start();
b.start();
}
3.17.3 ReentrantLock [执行先后]
volatile private static int state = 1;
private static ReentrantLock lock = new ReentrantLock();
final private static Condition conditionA = lock.newCondition();
public static void main(String[] args){
Thread threadA = new Thread(()->{
try {
lock.lock();
while(state != 2){
conditionA.await();
}
log.debug("1");
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
lock.unlock();
}
},"A");
Thread threadB = new Thread(()->{
try {
lock.lock();
log.debug("2");
state = 2;
conditionA.signal();
}finally{
lock.unlock();
}
},"B");
// 启动 a b 线程
new Thread(threadA).start();
new Thread(threadB).start();
}
3.17.4 wait notify [交替打印]
@Slf4j
public class WaitNotifyDemo{
public static void main(String[] args) {
WaitNotify syncWaitNotify = new WaitNotify(1, 5);
new Thread(() -> {
syncWaitNotify.print(1, 2, "a");
}).start();
new Thread(() -> {
syncWaitNotify.print(2, 3, "b");
}).start();
new Thread(() -> {
syncWaitNotify.print(3, 1, "c");
}).start();
}
}
class WaitNotify{
/**
* 等待标记:用于表示谁可以运行
*/
private int flag;
private int loopNumber;
public WaitNotify(int flag,int loopNumber){
this.flag=flag;
this.loopNumber=loopNumber;
}
public void SyncWaitNotify(int flag, int loopNumber) {
this.flag = flag;
this.loopNumber = loopNumber;
}
public void print(int waitFlag, int nextFlag, String str) {
for (int i = 0; i < loopNumber; i++) {
synchronized (this) {
// 当前标记不是下一个线程的标记值时一直等待
while (this.flag != waitFlag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.print(str);
// 修改执行下一个线程的标记
flag = nextFlag;
// 唤醒所有的线程
this.notifyAll();
}
}
}
}
3.17.5 Lock 条件变量 [交替打印]
@Slf4j
public class AwaitSignal extends ReentrantLock {
public void start(Condition first) {
this.lock();
try {
log.debug("start");
// 先去唤醒A线程
first.signal();
} finally {
this.unlock();
}
}
public void print(String str, Condition current, Condition next) {
for (int i = 0; i < loopNumber; i++) {
this.lock();
try {
// 一开始先让所有的线程都进入等待
current.await();
log.debug(str.equals("c") ? str+"\n" : str);
// 精准唤醒下一个线程
next.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
this.unlock();
}
}
}
// 循环次数
private int loopNumber;
public AwaitSignal(int loopNumber) {
this.loopNumber = loopNumber;
}
public static void main(String[] args) {
AwaitSignal as = new AwaitSignal(5);
Condition aWaitSet = as.newCondition();
Condition bWaitSet = as.newCondition();
Condition cWaitSet = as.newCondition();
new Thread(() -> { as.print("a", aWaitSet, bWaitSet); }).start();
new Thread(() -> { as.print("b", bWaitSet, cWaitSet); }).start();
new Thread(() -> { as.print("c", cWaitSet, aWaitSet); }).start();
as.start(aWaitSet);
}
}
3.17.6 Park Unpark [交替打印]
public class ParkDemo {
public static void main(String[] args) {
SyncPark syncPark = new SyncPark(5);
Thread t1 = new Thread(() -> {
syncPark.print("a");
});
Thread t2 = new Thread(() -> {
syncPark.print("b");
});
Thread t3 = new Thread(() -> {
syncPark.print("c\n");
});
syncPark.setThreads(t1, t2, t3);
syncPark.start();
}
}
class SyncPark{
private int loopNumber;
private Thread[] threads;
public SyncPark(int loopNumber) {
this.loopNumber = loopNumber;
}
// 把创建好的线程传递过来
public void setThreads(Thread... threads) {
this.threads = threads;
}
//
public void print(String str) {
for (int i = 0; i < loopNumber; i++) {
// 让所有的线程都进入 阻塞状态,唯独让thread[0]先执行
LockSupport.park();
System.out.print(str);
// 取消下一个线程的阻塞状态
LockSupport.unpark(nextThread());
}
}
//
private Thread nextThread() {
// 获取到当前线程
Thread current = Thread.currentThread();
int index = 0;
// 获取到当前正在运行的线程
for (int i = 0; i < threads.length; i++) {
if(threads[i] == current) {
index = i;
break;
}
}
// 返回当前正在运行的线程的下一个该执行的线程
if(index < threads.length - 1) {
return threads[index+1];
} else {
return threads[0];
}
}
// 遍历所有的线程,都将其启动,先给threads[0]执行权
public void start() {
for (Thread thread : threads) {
thread.start();
}
// 如果先执行了unpark,则再执行park加锁的时候就不成功,依旧可以向下运行
LockSupport.unpark(threads[0]);
}
}
3.18本章小结
本章我们需要重点掌握的是
- 分析多线程访问共享资源时,哪些代码片段属于临界区
- 使用 synchronized 互斥解决临界区的线程安全问题
- 掌握 synchronized 锁对象语法
- 掌握 synchronzied 加载成员方法和静态方法语法
- 掌握 wait/notify 同步方法
- 使用 lock 互斥解决临界区的线程安全问题
- 掌握 lock 的使用细节:可打断、锁超时、公平锁、条件变量
- 学会分析变量的线程安全性、掌握常见线程安全类的使用
- 了解线程活跃性问题:死锁、活锁、饥饿
- 应用方面
- 互斥:使用 synchronized 或 Lock 达到共享资源互斥效果
- 同步:使用 wait/notify 或 Lock 的条件变量来达到线程间通信效果
- 原理方面
- monitor、synchronized 、wait/notify 原理
- synchronized 进阶原理
- park & unpark 原理
- 模式方面
- 同步模式之保护性暂停
- 异步模式之生产者消费者
- 同步模式之顺序控制
4.共享模型之内存
4.1 Java 内存模型
JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。
JMM 体现在以下几个方面
- 原子性 - 保证指令不会受到线程上下文切换的影响
- 可见性 - 保证指令不会受 cpu 缓存的影响
- 有序性 - 保证指令不会受 cpu 指令并行优化的影响