东莞做网站贴吧,做网站流量怎么解决,网站页面设计具体步骤,电子商务学出来干什么4 共享模型之管程
本章内容
共享问题synchronized线程安全分析Monitorwait/notify线程状态转换活跃性Lock
4.1 共享带来的问题
4.1.1 小故事
老王#xff08;操作系统#xff09;有一个功能强大的算盘#xff08;CPU#xff09;#xff0c;现在想把它租出去#xff…4 共享模型之管程
本章内容
共享问题synchronized线程安全分析Monitorwait/notify线程状态转换活跃性Lock
4.1 共享带来的问题
4.1.1 小故事
老王操作系统有一个功能强大的算盘CPU现在想把它租出去赚一点外快 小南、小女线程来使用这个算盘来进行一些计算并按照时间给老王支付费用 但小南不能一天24小时使用算盘他经常要小憩一会sleep又或是去吃饭上厕所阻塞 io 操作有时还需要一根烟没烟时思路全无wait这些情况统称为阻塞 在这些时候算盘没利用起来不能收钱了老王觉得有点不划算 另外小女也想用用算盘如果总是小南占着算盘让小女觉得不公平 于是老王灵机一动想了个办法 [ 让他们每人用一会轮流使用算盘 ] 这样当小南阻塞的时候算盘可以分给小女使用不会浪费反之亦然 最近执行的计算比较复杂需要存储一些中间结果而学生们的脑容量工作内存不够所以老王申请了一个笔记本主存把一些中间结果先记在本上 计算流程是这样的 但是由于分时系统有一天还是发生了事故 小南刚读取了初始值 0 做了个 1 运算还没来得及写回结果 老王说 [ 小南你的时间到了该别人了记住结果走吧 ]于是小南念叨着 [ 结果是1结果是1…] 不甘心地到一边待着去了上下文切换 老王说 [ 小女该你了 ]小女看到了笔记本上还写着 0 做了一个 -1 运算将结果 -1 写入笔记本 这时小女的时间也用完了老王又叫醒了小南[小南把你上次的题目算完吧]小南将他脑海中的结果 1 写入了笔记本 小南和小女都觉得自己没做错但笔记本里的结果是 1 而不是 0
4.1.2 Java 的体现
两个线程对初始值为 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);
}4.1.3 问题分析
以上的结果可能是正数、负数、零。为什么呢因为 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 行代码是顺序执行不会交错没有问题 但多线程下这 8 行代码可能交错运行 出现正数的情况 临界区 Critical Section 一个程序运行多个线程本身是没有问题的 问题出在多个线程访问共享资源 多个线程读共享资源其实也没有问题在多个线程对共享资源读写操作时发生指令交错就会出现问题 一段代码块内如果存在对共享资源的多线程读写操作称这段代码块为临界区
例如下面代码中的临界区
static int counter 0;
static void increment()
// 临界区
{counter;
}static void decrement()
// 临界区
{counter--;
}竞态条件 Race Condition
多个线程在临界区内执行由于代码的执行序列不同而导致结果无法预测称之为发生了竞态条件
4.2 synchronized 解决方案
4.2.1 应用之互斥
为了避免临界区的竞态条件发生有多种手段可以达到目的。 阻塞式的解决方案synchronizedLock 非阻塞式的解决方案原子变量
本次课使用阻塞式的解决方案synchronized来解决上述问题即俗称的【对象锁】它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码不用担心线程上下文切换 注意 虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成但它们还是有区别的 互斥是保证临界区的竞态条件发生同一时刻只能有一个线程执行临界区代码同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点 4.2.2 synchronized
语法
synchronized(对象) // 线程1 线程2(blocked)
{临界区
}解决 static int counter 0;
static final Object room new Object();public static void main(String[] args) throws InterruptedException {Thread t1 new Thread(() - {for (int i 0; i 5000; i) {synchronized (room) {counter;}}}, t1);Thread t2 new Thread(() - {for (int i 0; i 5000; i) {synchronized (room) {counter--;}}}, t2);t1.start();t2.start();t1.join();t2.join();log.debug({},counter);
}你可以做这样的类比 synchronized(对象) 中的对象可以想象为一个房间room有唯一入口门房间只能一次进入一人进行计算线程 t1t2 想象成两个人 当线程 t1 执行到 synchronized(room) 时就好比 t1 进入了这个房间并锁住了门拿走了钥匙在门内执行count 代码 这时候如果 t2 也运行到了synchronized(room)时它发现门被锁住了只能在门外等待发生了上下文切换阻塞住了 这中间即使 t1 的 cpu 时间片不幸用完被踢出了门外不要错误理解为锁住了对象就能一直执行下去哦这时门还是锁住的t1 仍拿着钥匙t2 线程还在阻塞状态进不来只有下次轮到 t1 自己再次获得时间片时才能开门进入 当 t1 执行完 synchronized{} 块内的代码这时候才会从 obj 房间出来并解开门上的锁唤醒 t2 线程把钥匙给他。t2 线程这时才可以进入 obj 房间锁住了门拿上钥匙执行它的 count-- 代码.
用图表示 思考
synchronized 实际是用对象锁保证了临界区内代码的原子性临界区内的代码对外是不可分割的不会被线程切换所打断。
为了加深理解请思考下面的问题
如果把 synchronized(obj) 放在 for 循环的外面如何理解-- 原子性如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎样运作-- 锁对象如果 t1 synchronized(obj) 而 t2 没有加会怎么样如何理解-- 锁对象
4.2.3 面向对象改进
把需要保护的共享变量放入一个类 class Room {int value 0;public void increment() {synchronized (this) {value;}}public void decrement() {synchronized (this) {value--;}}public int get() {synchronized (this) {return value;}}
}Slf4j
public class Test1 {public static void main(String[] args) throws InterruptedException {Room room new Room();Thread t1 new Thread(() - {for (int j 0; j 5000; j) {room.increment();}}, t1);Thread t2 new Thread(() - {for (int j 0; j 5000; j) {room.decrement();}}, t2);t1.start();t2.start();t1.join();t2.join();log.debug(count: {} , room.get());}
}4.3 方法上的 synchronized
4.3.1 与使用synchronized(X)等效
class Test{public synchronized void test() {}
}
等价于
class Test{public void test() {synchronized(this) {}}
}class Test{public synchronized static void test() {}
}
等价于
class Test{public static void test() {synchronized(Test.class) {}}
}4.3.2 不加 synchronized 的方法
不加 synchronzied 的方法就好比不遵守规则的人不去老实排队好比翻窗户进去的
4.3.3 所谓的“线程八锁”
其实就是考察 synchronized 锁住的是哪个对象
情况112 或 21
Slf4j(topic c.Number)
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();
}情况21s后12或 2 1s后 1
Slf4j(topic c.Number)
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();
}情况33 1s 12 或 23 1s 1 或 32 1s 1
Slf4j(topic c.Number)
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();
}情况42 1s 后 1
Slf4j(topic c.Number)
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();
}情况52 1s 后 1
Slf4j(topic c.Number)
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();
}情况61s 后12 或 2 1s后 1
Slf4j(topic c.Number)
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();
}情况72 1s 后 1
Slf4j(topic c.Number)
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();
}情况81s 后12 或 2 1s后 1
Slf4j(topic c.Number)
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();
}4.4 变量的线程安全分析
成员变量和静态变量是否线程安全 如果它们没有共享则线程安全 如果它们被共享了根据它们的状态是否能够改变又分两种情况 如果只有读操作则线程安全如果有读写操作则这段代码是临界区需要考虑线程安全
局部变量是否线程安全 局部变量是线程安全的 但局部变量引用的对象则未必 如果该对象没有逃离方法的作用访问它是线程安全的如果该对象逃离方法的作用范围需要考虑线程安全
4.4.1 局部变量线程安全分析
public static void test1() {int i 10;i;
}每个线程调用 test1() 方法时局部变量 i会在每个线程的栈帧内存中被创建多份因此不存在共享
public static void test1();descriptor: ()Vflags: ACC_PUBLIC, ACC_STATICCode:stack1, locals1, args_size00: bipush 102: istore_03: iinc 0, 16: returnLineNumberTable:line 10: 0line 11: 3line 12: 6LocalVariableTable:Start Length Slot Name Signature3 4 0 i I如图 局部变量的引用稍有不同
先看一个成员变量的例子
class ThreadUnsafe {ArrayListString 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 Thread1 java.lang.IndexOutOfBoundsException: Index: 0, Size: 0 at java.util.ArrayList.rangeCheck(ArrayList.java:657) at java.util.ArrayList.remove(ArrayList.java:496) at cn.itcast.n6.ThreadUnsafe.method3(TestThreadSafe.java:35) at cn.itcast.n6.ThreadUnsafe.method1(TestThreadSafe.java:26) at cn.itcast.n6.TestThreadSafe.lambda$main$0(TestThreadSafe.java:14) at java.lang.Thread.run(Thread.java:748)t1 add之后准备将size记为1但还没记的时候被 t2抢走,此时size仍未0
t2 add操作,并成功将size记为1,然后又被t1抢回, t1 继续未完操作,再次将size记为1,这时又被t2抢走
t2 继续操作,remove之后,size记为0,然后又被t1抢走
此时t1再去remove时发现size为0,就报了异常
这个发生的概率还是极低的…
分析 无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量 method3 与 method2 分析相同 将 list 修改为局部变量
class ThreadSafe {public final void method1(int loopNumber) {ArrayListString list new ArrayList();for (int i 0; i loopNumber; i) {method2(list);method3(list);}}private void method2(ArrayListString list) {list.add(1);}private void method3(ArrayListString list) {list.remove(0);}
}那么就不会有上述问题了
分析 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) {ArrayListString list new ArrayList();for (int i 0; i loopNumber; i) {method2(list);method3(list);}}private void method2(ArrayListString list) {list.add(1);}private void method3(ArrayListString list) {list.remove(0);}
}class ThreadSafeSubClass extends ThreadSafe{Overridepublic void method3(ArrayListString list) {new Thread(() - {list.remove(0);}).start();}
}从这个例子可以看出 private 或 final 提供【安全】的意义所在请体会开闭原则中的【闭】
4.4.2 常见线程安全类
StringIntegerStringBufffferRandomVectorHashtablejava.util.concurrent 包下的类
这里说它们是线程安全的是指多个线程调用它们同一个实例的某个方法时是线程安全的。也可以理解为
Hashtable table new Hashtable();new Thread(()-{table.put(key, value1);
}).start();new Thread(()-{table.put(key, value2);
}).start();它们的每个方法是原子的 但注意它们多个方法的组合不是原子的见后面分析
4.4.3 线程安全类方法的组合
分析下面代码是否线程安全
Hashtable table new Hashtable();
// 线程1线程2
if( table.get(key) null) {table.put(key, value);
}4.4.4 不可变类线程安全性
String、Integer 等都是不可变类因为其内部的状态不可以改变因此它们的方法都是线程安全的
有同学或许有疑问String 有 replacesubstring 等方法【可以】改变值啊那么这些方法又是如何保证线程安全的呢
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
public class MyServlet extends HttpServlet {// 是否安全 不安全MapString,Object map new HashMap();// 是否安全 安全String S1 ...;// 是否安全 安全final String S2 ...;// 是否安全 不安全Date D1 new Date();// 是否安全 不安全final Date D2 new Date();public void doGet(HttpServletRequest request, HttpServletResponse response) {// 使用上述变量}
}例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 {// 是否安全 不安全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 ?;// 是否安全 安全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 {// 是否安全 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 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();}
}请比较 JDK 中 String 类的实现
例8
private static Integer i 0;
public static void main(String[] args) throws InterruptedException {ListThread list new ArrayList();for (int j 0; j 2; j) {Thread thread new Thread(() - {for (int k 0; k 5000; k) {synchronized (i) {i;}}}, j);list.add(thread);}list.stream().forEach(t - t.start());list.stream().forEach(t - {try {t.join();} catch (InterruptedException e) {e.printStackTrace();}});log.debug({}, i);
}4.5 习题
4.5.1 卖票练习
测试下面代码是否存在线程安全问题并尝试改正
public class ExerciseSell {public static void main(String[] args) {TicketWindow ticketWindow new TicketWindow(2000);ListThread list new ArrayList();// 用来存储买出去多少张票ListInteger 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~5public static int randomAmount() {return random.nextInt(5) 1;}
}class TicketWindow {private int count;public TicketWindow(int count) {this.count count;}public int getCount() {return count;}public int sell(int amount) {if (this.count amount) {this.count - amount;return amount;} else {return 0;}}
}另外用下面的代码行不行为什么
ListInteger sellCount new ArrayList();测试脚本
for /L %n in (1,1,10) do java -cp .;C:\Users\manyh\.m2\repository\ch\qos\logback\logback-classic\1.2.3\logback-classic-1.2.3.jar;C:\Users\manyh\.m2\repository\ch\qos\logback\logback-core\1.2.3\logback-core-1.2.3.jar;C:\Users\manyh\.m2\repository\org\slf4j\slf4j-api\1.7.25\slf4j-api-1.7.25.jar cn.itcast.n4.exercise.ExerciseSell4.5.2 转账练习
测试下面代码是否存在线程安全问题并尝试改正
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~100public 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;}public void transfer(Account target, int amount) {if (this.money amount) {this.setMoney(this.getMoney() - amount);target.setMoney(target.getMoney() amount);}}
}这样改正行不行为什么 不行,锁要用同一个共用锁,而这里的account是不同对象
public synchronized void transfer(Account target, int amount) {if (this.money amount) {this.setMoney(this.getMoney() - amount);target.setMoney(target.getMoney() amount);}
}4.6 Monitor 概念
4.6.1 Java 对象头
以 32 位虚拟机为例
普通对象 数组对象 其中 Mark Word 结构为 64 位虚拟机 Mark Word 参考资料 https://stackoverflow.com/questions/26357186/what-is-in-java-object-header 4.6.2 原理之 Monitor(锁)
Monitor 被翻译为监视器或管程
每个 Java 对象都可以关联一个 Monitor 对象如果使用 synchronized 给对象上锁重量级之后该对象头的Mark Word 中就被设置指向 Monitor 对象的指针
Monitor 结构 刚开始 Monitor 中 Owner 为 null当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2Monitor中只能有一个 Owner在 Thread-2 上锁的过程中如果 Thread-3Thread-4Thread-5 也来执行 synchronized(obj)就会进入EntryList BLOCKEDThread-2 执行完同步代码块的内容然后唤醒 EntryList 中等待的线程来竞争锁竞争的时是非公平的图中 WaitSet 中的 Thread-0Thread-1 是之前获得过锁但条件不满足进入 WAITING 状态的线程后面讲wait-notify 时会分析 注意 synchronized 必须是进入同一个对象的 monitor 才有上述的效果不加 synchronized 的对象不会关联监视器不遵从以上规则 4.6.3 原理之 synchronized
static final Object lock new Object();
static int counter 0;
public static void main(String[] args) {synchronized (lock) {counter;}
}对应的字节码为 注意 方法级别的 synchronized 不会在字节码指令中有所体现 小故事
故事角色
老王 - JVM小南 - 线程小女 - 线程房间 - 对象房间门上 - 防盗锁 - Monitor房间门上 - 小南书包 - 轻量级锁房间门上 - 刻上小南大名 - 偏向锁批量重刻名 - 一个类的偏向锁撤销到达 20 阈值不能刻名字 - 批量撤销该类对象的偏向锁设置该类不可偏向
小南要使用房间保证计算不被其它人干扰原子性最初他用的是防盗锁当上下文切换时锁住门。这样即使他离开了别人也进不了门他的工作就是安全的。
但是很多情况下没人跟他来竞争房间的使用权。小女是要用房间但使用的时间上是错开的小南白天用小女晚上用。每次上锁太麻烦了有没有更简单的办法呢
小南和小女商量了一下约定不锁门了而是谁用房间谁把自己的书包挂在门口但他们的书包样式都一样因此每次进门前得翻翻书包看课本是谁的如果是自己的那么就可以进门这样省的上锁解锁了。万一书包不是自己的那么就在门外等并通知对方下次用锁门的方式。
后来小女回老家了很长一段时间都不会用这个房间。小南每次还是挂书包翻书包虽然比锁门省事了但仍然觉得麻烦。
于是小南干脆在门上刻上了自己的名字【小南专属房间其它人勿用】下次来用房间时只要名字还在那么说明没人打扰还是可以安全地使用房间。如果这期间有其它人要用这个房间那么由使用者将小南刻的名字擦掉升级为挂书包的方式。
同学们都放假回老家了小南就膨胀了在 20 个房间刻上了自己的名字想进哪个进哪个。后来他自己放假回老家了这时小女回来了她也要用这些房间结果就是得一个个地擦掉小南刻的名字升级为挂书包的方式。老王觉得这成本有点高提出了一种批量重刻名的方法他让小女不用挂书包了可以直接在门上刻上自己的名字
后来刻名的现象越来越频繁老王受不了了算了这些房间都不能刻名了只能挂书包
4.6.4 原理之 synchronized 进阶
轻量级锁
轻量级锁的使用场景如果一个对象虽然有多线程要加锁但加锁的时间是错开的也就是没有竞争那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的即语法仍然是 synchronized
假设有两个方法同步块利用同一个对象加锁
static final Object obj new Object();public static void method1() {synchronized( obj ) {// 同步块 Amethod2();}
}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 的值恢复给对象头 成功则解锁成功失败说明轻量级锁进行了锁膨胀或已经升级为重量级锁进入重量级锁解锁流程
锁膨胀
如果在尝试加轻量级锁的过程中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 线程
自旋优化
重量级锁竞争的时候还可以使用自旋(循环尝试获取重量级锁)来进行优化如果当前线程自旋成功即这时候持锁线程已经退出了同步块释放了锁这时当前线程就可以避免阻塞。 (进入阻塞再恢复,会发生上下文切换,比较耗费性能)
自旋重试成功的情况 自旋重试失败的情况 自旋会占用 CPU 时间单核 CPU 自旋就是浪费多核 CPU 自旋才能发挥优势。在 Java 6 之后自旋锁是自适应的比如对象刚刚的一次自旋操作成功过那么认为这次自旋成功的可能性会高就多自旋几次反之就少自旋甚至不自旋总之比较智能。Java 7 之后不能控制是否开启自旋功能
偏向锁
轻量级锁在没有竞争时就自己这个线程每次重入仍然需要执行 CAS 操作。
Java 6 中引入了偏向锁来做进一步优化只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头之后发现这个线程 ID 是自己的就表示没有竞争不用重新 CAS。以后只要不发生竞争这个对象就归该线程所有 这里的线程id是操作系统赋予的id 和 Thread的id是不同的 例如
static final Object obj new Object();public static void m1() {synchronized( obj ) {// 同步块 Am2();}
}public static void m2() {synchronized( obj ) {// 同步块 Bm3();}
}public static void m3() {synchronized( obj ) {// 同步块 C}
}偏向状态
回忆一下对象头格式 一个对象创建时 如果开启了偏向锁默认开启那么对象创建后markword 值为 0x05 即最后 3 位为 101这时它的 thread、epoch、age 都为 0 偏向锁是默认是延迟的不会在程序启动时立即生效如果想避免延迟可以加 VM 参数 -XX:BiasedLockingStartupDelay0 来禁用延迟 如果没有开启偏向锁那么对象创建后markword 值为 0x01 即最后 3 位为 001这时它的 hashcode、age 都为 0第一次用到 hashcode 时才会赋值
1 测试延迟特性
偏向锁是默认是延迟的不会在程序启动时立即生效如果想避免延迟可以加 VM 参数 -XX:BiasedLockingStartupDelay0 来禁用延迟
2 测试偏向锁
class Dog {}利用 jol 第三方工具来查看对象头信息注意这里up主扩展了 jol 让它输出更为简洁 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注意 处于偏向锁的对象解锁后线程 id 仍存储于对象头中 也就是偏(心)向某个线程了 3测试禁用
在上面测试代码运行时在添加 VM 参数 -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**4测试hashCode **
在Dog d new Dog();后加上一句 d.hashCode(); 正常状态对象一开始是没有 hashCode 的第一次调用才生成 调用了 hashCode() 后会撤销该对象的偏向锁
撤销(偏向) - 调用对象 hashCode
调用了对象的 hashCode但偏向锁的对象 MarkWord 中存储的是线程 id如果调用 hashCode 会导致偏向锁被撤销 轻量级锁会在锁记录中记录 hashCode 重量级锁会在 Monitor 中记录 hashCode
记得去掉 -XX:-UseBiasedLocking
在调用 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(() - {log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));synchronized (d) {log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));}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();
}输出 撤销(偏向) - 调用 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 {VectorDog 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 {VectorDog 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 偏向锁论文: https://www.oracle.com/technetwork/java/biasedlocking-oopsla2006-wp-149958.pdf 锁消除
锁消除 JIT即时编译器会对字节码做进一步优化 Fork(1)
BenchmarkMode(Mode.AverageTime)
Warmup(iterations3)
Measurement(iterations5)
OutputTimeUnit(TimeUnit.NANOSECONDS)
public class MyBenchmark {static int x 0;Benchmarkpublic void a() throws Exception {x;}Benchmarkpublic void b() throws Exception {//这里的o是局部变量,不会被共享,JIT做热点代码优化时会做锁消除Object o new Object();synchronized (o) {x;}}
}java -jar benchmarks.jar
发现两部分的差别并不大,甚至b加了锁比a没加锁还快 java -XX:-EliminateLocks -jar benchmarks.jar
使用 -XX:-EliminateLocks禁用锁消除后就会发现 b性能比a差劲多了 锁粗化 (up没有找到真正能证明锁粗化的例子,所以没讲) 对相同对象多次加锁导致线程发生多次重入可以使用锁粗化方式来优化这不同于之前讲的细分锁的粒度。 4.7 wait / notify
4.7.1 小故事 - 为什么需要 wait 由于条件不满足小南不能继续进行计算 但小南如果一直占用着锁其它人就得一直阻塞效率太低 于是老王单开了一间休息室调用 wait 方法让小南到休息室WaitSet等着去了但这时锁释放开其它人可以由老王随机安排进屋直到小M将烟送来大叫一声 [ 你的烟到了 ] 调用 notify 方法 小南于是可以离开休息室重新进入竞争锁的队列 4.7.2 原理之 wait / notify Owner 线程发现条件不满足调用 wait 方法即可进入 WaitSet 变为 WAITING 状态BLOCKED 和 WAITING 的线程都处于阻塞状态不占用 CPU 时间片BLOCKED 线程会在 Owner 线程释放锁时唤醒WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒但唤醒后并不意味者立刻获得锁仍需进入EntryList 重新竞争
4.7.3 API 介绍 obj.wait() 让进入 object 监视器的线程到 waitSet 等待 obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒 obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒
它们都是线程之间进行协作的手段都属于 Object 对象的方法。必须获得此对象的锁才能调用这几个方法
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(其它代码....);}}).start();new Thread(() - {synchronized (obj) {log.debug(执行....);try {obj.wait(); // 让线程在obj上一直等待下去} catch (InterruptedException e) {e.printStackTrace();}log.debug(其它代码....);}}).start();// 主线程两秒后执行sleep(2);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 - 其它代码....wait() 方法会释放对象的锁进入 WaitSet 等待区从而让其他线程就机会获取对象的锁。无限制等待直到notify 为止
wait(long n) 有时限的等待, 到 n 毫秒后结束等待或是被 notify
4.8 wait notify 的正确姿势
开始之前先看看
sleep(long n) 和 wait(long n) 的区别
sleep 是 Thread 方法而 wait 是 Object 的方法sleep 不需要强制和 synchronized 配合使用但 wait 需要和 synchronized 一起用sleep 在睡眠的同时不会释放对象锁的但 wait 在等待的时候会释放对象锁它们状态 TIMED_WAITING
4.8.1 step/例 1 : sleep会阻碍其它线程执行
static final Object room new Object();
static boolean hasCigarette false;
static boolean hasTakeout false;思考下面的解决方案好不好为什么
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) 不能hasCigarette true;log.debug(烟到了噢);
}, 送烟的).start();输出
20:49:49.883 [小南] c.TestCorrectPosture - 有烟没[false]
20:49:49.887 [小南] c.TestCorrectPosture - 没烟先歇会
20:49:50.882 [送烟的] c.TestCorrectPosture - 烟到了噢
20:49:51.887 [小南] c.TestCorrectPosture - 有烟没[true]
20:49:51.887 [小南] c.TestCorrectPosture - 可以开始干活了
20:49:51.887 [其它人] c.TestCorrectPosture - 可以开始干活了
20:49:51.887 [其它人] c.TestCorrectPosture - 可以开始干活了
20:49:51.888 [其它人] c.TestCorrectPosture - 可以开始干活了
20:49:51.888 [其它人] c.TestCorrectPosture - 可以开始干活了
20:49:51.888 [其它人] c.TestCorrectPosture - 可以开始干活了其它干活的线程都要一直阻塞效率太低 小南线程必须睡足 2s 后才能醒来就算烟提前送到也无法立刻醒来 加了 synchronized (room) 后就好比小南在里面反锁了门睡觉烟根本没法送进门main 没加 synchronized 就好像 main 线程是翻窗户进来的 sleep妨碍其它人干活 解决方法使用 wait - notify
4.8.2 step/例 2 : wait替代sleep
思考下面的实现行吗为什么
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();输出
20:51:42.489 [小南] c.TestCorrectPosture - 有烟没[false]
20:51:42.493 [小南] c.TestCorrectPosture - 没烟先歇会
20:51:42.493 [其它人] c.TestCorrectPosture - 可以开始干活了
20:51:42.493 [其它人] c.TestCorrectPosture - 可以开始干活了
20:51:42.494 [其它人] c.TestCorrectPosture - 可以开始干活了
20:51:42.494 [其它人] c.TestCorrectPosture - 可以开始干活了
20:51:42.494 [其它人] c.TestCorrectPosture - 可以开始干活了
20:51:43.490 [送烟的] c.TestCorrectPosture - 烟到了噢
20:51:43.490 [小南] c.TestCorrectPosture - 有烟没[true]
20:51:43.490 [小南] c.TestCorrectPosture - 可以开始干活了解决了其它干活的线程阻塞的问题 但如果有其它线程也在等待条件呢
4.8.3 step/例 3 : 会发生虚假唤醒*
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.notify();}
}, 送外卖的).start();输出
20:53:12.173 [小南] c.TestCorrectPosture - 有烟没[false]
20:53:12.176 [小南] c.TestCorrectPosture - 没烟先歇会
20:53:12.176 [小女] c.TestCorrectPosture - 外卖送到没[false]
20:53:12.176 [小女] c.TestCorrectPosture - 没外卖先歇会
20:53:13.174 [送外卖的] c.TestCorrectPosture - 外卖到了噢
20:53:13.174 [小南] c.TestCorrectPosture - 有烟没[false]
20:53:13.174 [小南] c.TestCorrectPosture - 没干成活...notify 只能随机唤醒一个 WaitSet 中的线程这时如果有其它线程也在等待那么就可能唤醒不了正确的线程称之为【虚假唤醒】 发生虚假唤醒: 解决方法改为 notifyAll
4.8.4 step/例 4 : ifwait 仅由1次判断机会
new Thread(() - {synchronized (room) {hasTakeout true;log.debug(外卖到了噢);room.notifyAll();}
}, 送外卖的).start();输出
20:55:23.978 [小南] c.TestCorrectPosture - 有烟没[false]
20:55:23.982 [小南] c.TestCorrectPosture - 没烟先歇会
20:55:23.982 [小女] c.TestCorrectPosture - 外卖送到没[false]
20:55:23.982 [小女] c.TestCorrectPosture - 没外卖先歇会
20:55:24.979 [送外卖的] c.TestCorrectPosture - 外卖到了噢
20:55:24.979 [小女] c.TestCorrectPosture - 外卖送到没[true]
20:55:24.980 [小女] c.TestCorrectPosture - 可以开始干活了
20:55:24.980 [小南] c.TestCorrectPosture - 有烟没[false]
20:55:24.980 [小南] c.TestCorrectPosture - 没干成活...用 notifyAll 仅解决某个线程的唤醒问题但使用 if wait 判断仅有一次机会一旦条件不成立就没有重新判断的机会了 notifyAll唤醒了所有,但使用ifwait仅有一次机会,解决方法一旦条件不成立就没有重新判断的机会了.解决办法: 用 while wait当条件不成立再次 wait
4.8.5 step 5 : whilewait
将 if 改为 while
if (!hasCigarette) {log.debug(没烟先歇会);try {room.wait();} catch (InterruptedException e) {e.printStackTrace();}
}改动后
while (!hasCigarette) {log.debug(没烟先歇会);try {room.wait();} catch (InterruptedException e) {e.printStackTrace();}
}输出
20:58:34.322 [小南] c.TestCorrectPosture - 有烟没[false]
20:58:34.326 [小南] c.TestCorrectPosture - 没烟先歇会
20:58:34.326 [小女] c.TestCorrectPosture - 外卖送到没[false]
20:58:34.326 [小女] c.TestCorrectPosture - 没外卖先歇会
20:58:35.323 [送外卖的] c.TestCorrectPosture - 外卖到了噢
20:58:35.324 [小女] c.TestCorrectPosture - 外卖送到没[true]
20:58:35.324 [小女] c.TestCorrectPosture - 可以开始干活了
20:58:35.324 [小南] c.TestCorrectPosture - 没烟先歇会synchronized(lock) {while(条件不成立) {lock.wait();}// 干活
}//另一个线程
synchronized(lock) {lock.notifyAll();
}4.8.6 (同步)模式之保护性暂停
定义
即 Guarded Suspension用在一个线程等待另一个线程的执行结果
要点 有一个结果需要从一个线程传递到另一个线程让他们关联同一个 GuardedObject 如果有结果不断从一个线程到另一个线程那么可以使用消息队列见生产者/消费者 JDK 中join 的实现、Future 的实现采用的就是此模式 因为要等待另一方的结果因此归类到同步模式 实现
class GuardedObject {private Object response;private final Object lock new Object();public Object get() {synchronized (lock) {// 条件不满足则等待while (response null) {try {lock.wait();} catch (InterruptedException e) {e.printStackTrace();} }return response; }}public void complete(Object response) {synchronized (lock) {// 条件满足通知等待线程this.response response;lock.notifyAll();}}}应用
一个线程等待另一个线程的执行结果
public static void main(String[] args) {GuardedObject guardedObject new GuardedObject();new Thread(() - {try {// 子线程执行下载ListString response download();log.debug(download complete...);guardedObject.complete(response);} catch (IOException e) {e.printStackTrace();}}).start();log.debug(waiting...);// 主线程阻塞等待Object response guardedObject.get();log.debug(get response: [{}] lines, ((ListString) response).size());
}执行结果
08:42:18.568 [main] c.TestGuardedObject - waiting...
08:42:23.312 [Thread-0] c.TestGuardedObject - download complete...
08:42:23.312 [main] c.TestGuardedObject - get response: [3] lines带超时版 GuardedObject
如果要控制超时时间呢
class GuardedObjectV2 {private Object response;private final Object lock new Object();public Object get(long millis) {synchronized (lock) {// 1) 记录最初时间long begin System.currentTimeMillis();// 2) 已经经历的时间long timePassed 0;while (response null) {// 4) 假设 millis 是 1000结果在 400 时唤醒了那么还有 600 要等long waitTime millis - timePassed;log.debug(waitTime: {}, waitTime);if (waitTime 0) {log.debug(break...);break; }try {lock.wait(waitTime);} catch (InterruptedException e) {e.printStackTrace();}// 3) 如果提前被唤醒这时已经经历的时间假设为 400timePassed System.currentTimeMillis() - begin;log.debug(timePassed: {}, object is null {}, timePassed, response null);}return response; }}public void complete(Object response) {synchronized (lock) {// 条件满足通知等待线程this.response response;log.debug(notify...);lock.notifyAll();}}
}测试没有超时
public static void main(String[] args) {GuardedObjectV2 v2 new GuardedObjectV2();new Thread(() - {sleep(1);v2.complete(null);sleep(1);v2.complete(Arrays.asList(a, b, c));}).start();Object response v2.get(2500);if (response ! null) {log.debug(get response: [{}] lines, ((ListString) response).size());} else {log.debug(cant get response);}}输出
08:49:39.917 [main] c.GuardedObjectV2 - waitTime: 2500
08:49:40.917 [Thread-0] c.GuardedObjectV2 - notify...
08:49:40.917 [main] c.GuardedObjectV2 - timePassed: 1003, object is null true
08:49:40.917 [main] c.GuardedObjectV2 - waitTime: 1497
08:49:41.918 [Thread-0] c.GuardedObjectV2 - notify...
08:49:41.918 [main] c.GuardedObjectV2 - timePassed: 2004, object is null false
08:49:41.918 [main] c.TestGuardedObjectV2 - get response: [3] lines测试超时
// 等待时间不足
ListString lines v2.get(1500);输出
08:47:54.963 [main] c.GuardedObjectV2 - waitTime: 1500
08:47:55.963 [Thread-0] c.GuardedObjectV2 - notify...
08:47:55.963 [main] c.GuardedObjectV2 - timePassed: 1002, object is null true
08:47:55.963 [main] c.GuardedObjectV2 - waitTime: 498
08:47:56.461 [main] c.GuardedObjectV2 - timePassed: 1500, object is null true
08:47:56.461 [main] c.GuardedObjectV2 - waitTime: 0
08:47:56.461 [main] c.GuardedObjectV2 - break...
08:47:56.461 [main] c.TestGuardedObjectV2 - cant get response
08:47:56.963 [Thread-0] c.GuardedObjectV2 - notify...原理之 join
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;if (delay 0) {break;}wait(delay);now System.currentTimeMillis() - base;}}}多任务版 GuardedObject
图中 Futures 就好比居民楼一层的信箱每个信箱有房间编号左侧的 t0t2t4 就好比等待邮件的居民右
侧的 t1t3t5 就好比邮递员
如果需要在多个类之间使用 GuardedObject 对象作为参数传递不是很方便因此设计一个用来解耦的中间类
这样不仅能够解耦【结果等待者】和【结果生产者】还能够同时支持多个任务的管理 新增 id 用来标识 Guarded Object
class GuardedObject {// 标识 Guarded Objectprivate int id;public GuardedObject(int id) {this.id id;}public int getId() {return id;}// 结果private Object response;// 获取结果// timeout 表示要等待多久 2000public Object get(long timeout) {synchronized (this) {// 开始时间 15:00:00long begin System.currentTimeMillis();// 经历的时间long passedTime 0;while (response null) {// 这一轮循环应该等待的时间long waitTime timeout - passedTime;// 经历的时间超过了最大等待时间时退出循环if (timeout - passedTime 0) {break;}try {this.wait(waitTime); // 虚假唤醒 15:00:01} catch (InterruptedException e) {e.printStackTrace();}// 求得经历时间passedTime System.currentTimeMillis() - begin; // 15:00:02 1s}return response;}}// 产生结果public void complete(Object response) {synchronized (this) {// 给结果成员变量赋值this.response response;this.notifyAll();}}
}中间解耦类
class Mailboxes {private static MapInteger, GuardedObject boxes new Hashtable();private static int id 1;// 产生唯一 idprivate static synchronized int generateId() {return id;}public static GuardedObject getGuardedObject(int id) {return boxes.remove(id);}public static GuardedObject createGuardedObject() {GuardedObject go new GuardedObject(generateId());boxes.put(go.getId(), go);return go;}public static SetInteger getIds() {return boxes.keySet();}
}业务相关类
class People extends Thread{Overridepublic void run() {// 收信GuardedObject guardedObject Mailboxes.createGuardedObject();log.debug(开始收信 id:{}, guardedObject.getId());Object mail guardedObject.get(5000);log.debug(收到信 id:{}, 内容:{}, guardedObject.getId(), mail);}
}class Postman extends Thread {private int id;private String mail;public Postman(int id, String mail) {this.id id;this.mail mail;}Overridepublic void run() {GuardedObject guardedObject Mailboxes.getGuardedObject(id);log.debug(送信 id:{}, 内容:{}, id, mail);guardedObject.complete(mail);}
}测试
public static void main(String[] args) throws InterruptedException {for (int i 0; i 3; i) {new People().start();}Sleeper.sleep(1);for (Integer id : Mailboxes.getIds()) {new Postman(id, 内容 id).start();}
}某次运行结果
10:35:05.689 c.People [Thread-1] - 开始收信 id:3
10:35:05.689 c.People [Thread-2] - 开始收信 id:1
10:35:05.689 c.People [Thread-0] - 开始收信 id:2
10:35:06.688 c.Postman [Thread-4] - 送信 id:2, 内容:内容2
10:35:06.688 c.Postman [Thread-5] - 送信 id:1, 内容:内容1
10:35:06.688 c.People [Thread-0] - 收到信 id:2, 内容:内容2
10:35:06.688 c.People [Thread-2] - 收到信 id:1, 内容:内容1
10:35:06.688 c.Postman [Thread-3] - 送信 id:3, 内容:内容3
10:35:06.689 c.People [Thread-1] - 收到信 id:3, 内容:内容34.8.7 (异步)模式之生产者/消费者
定义
要点 与前面的保护性暂停中的 GuardObject 不同不需要产生结果和消费结果的线程一一对应 消费队列可以用来平衡生产和消费的线程资源 生产者仅负责产生结果数据不关心数据该如何处理而消费者专心处理结果数据 消息队列是有容量限制的满时不会再加入数据空时不会再消耗数据 JDK 中各种阻塞队列采用的就是这种模式 实现
class Message {private int id;private Object message;public Message(int id, Object message) {this.id id;this.message message;}public int getId() {return id;}public Object getMessage() {return message;}
}class MessageQueue {private LinkedListMessage queue;private int capacity;public MessageQueue(int capacity) {this.capacity capacity;queue new LinkedList();}public Message take() {synchronized (queue) {while (queue.isEmpty()) {log.debug(没货了, wait);try {queue.wait();} catch (InterruptedException e) {e.printStackTrace();}}Message message queue.removeFirst();queue.notifyAll();return message;}}public void put(Message message) {synchronized (queue) {while (queue.size() capacity) {log.debug(库存已达上限, wait);try {queue.wait();} catch (InterruptedException e) {e.printStackTrace();}}queue.addLast(message);queue.notifyAll();}}
}应用 :
MessageQueue messageQueue new MessageQueue(2);// 4 个生产者线程, 下载任务
for (int i 0; i 4; i) {int id i;new Thread(() - {try {log.debug(download...);ListString response Downloader.download();log.debug(try put message({}), id);messageQueue.put(new Message(id, response));} catch (IOException e) {e.printStackTrace();}}, 生产者 i).start();
}// 1 个消费者线程, 处理结果
new Thread(() - {while (true) {Message message messageQueue.take();ListString response (ListString) message.getMessage();log.debug(take message({}): [{}] lines, message.getId(), response.size());}
}, 消费者).start();某次运行结果
10:48:38.070 [生产者3] c.TestProducerConsumer - download...
10:48:38.070 [生产者0] c.TestProducerConsumer - download...
10:48:38.070 [消费者] c.MessageQueue - 没货了, wait
10:48:38.070 [生产者1] c.TestProducerConsumer - download...
10:48:38.070 [生产者2] c.TestProducerConsumer - download...
10:48:41.236 [生产者1] c.TestProducerConsumer - try put message(1)
10:48:41.237 [生产者2] c.TestProducerConsumer - try put message(2)
10:48:41.236 [生产者0] c.TestProducerConsumer - try put message(0)
10:48:41.237 [生产者3] c.TestProducerConsumer - try put message(3)
10:48:41.239 [生产者2] c.MessageQueue - 库存已达上限, wait
10:48:41.240 [生产者1] c.MessageQueue - 库存已达上限, wait
10:48:41.240 [消费者] c.TestProducerConsumer - take message(0): [3] lines
10:48:41.240 [生产者2] c.MessageQueue - 库存已达上限, wait
10:48:41.240 [消费者] c.TestProducerConsumer - take message(3): [3] lines
10:48:41.240 [消费者] c.TestProducerConsumer - take message(1): [3] lines
10:48:41.240 [消费者] c.TestProducerConsumer - take message(2): [3] lines
10:48:41.240 [消费者] c.MessageQueue - 没货了, wait4.9 Park Unpark
4.9.1 基本使用
它们是 LockSupport 类中的方法
// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)先 park 再 unpark
Thread t1 new Thread(() - {log.debug(start...);sleep(1);log.debug(park...);LockSupport.park();log.debug(resume...);
},t1);
t1.start();sleep(2);
log.debug(unpark...);
LockSupport.unpark(t1);输出
18:42:52.585 c.TestParkUnpark [t1] - start...
18:42:53.589 c.TestParkUnpark [t1] - park...
18:42:54.583 c.TestParkUnpark [main] - unpark...
18:42:54.583 c.TestParkUnpark [t1] - resume...先 unpark 再 park
Thread t1 new Thread(() - {log.debug(start...);sleep(2);log.debug(park...);LockSupport.park();log.debug(resume...);
}, t1);
t1.start();sleep(1);
log.debug(unpark...);
LockSupport.unpark(t1);输出
18:43:50.765 c.TestParkUnpark [t1] - start...
18:43:51.764 c.TestParkUnpark [main] - unpark...
18:43:52.769 c.TestParkUnpark [t1] - park...
18:43:52.769 c.TestParkUnpark [t1] - resume...4.9.2 特点
与 Object 的 wait notify 相比 waitnotify 和 notifyAll 必须配合 Object Monitor 一起使用而 parkunpark 不必 park unpark 是以线程为单位来【阻塞】和【唤醒】线程而 notify 只能随机唤醒一个等待线程notifyAll是唤醒所有等待线程就不那么【精确】 park unpark 可以先 unpark而 wait notify 不能先 notify
4.9.3 原理之 park unpark
每个线程都有自己的一个(C代码实现的) Parker 对象由三部分组成 _counter _cond 和_mutex
打个比喻 线程就像一个旅人Parker 就像他随身携带的背包条件变量就好比背包中的帐篷。_counter 就好比背包中的备用干粮0 为耗尽1 为充足 调用 park 就是要看需不需要停下来歇息 如果备用干粮耗尽那么钻进帐篷歇息如果备用干粮充足那么不需停留继续前进 调用 unpark就好比令干粮充足 如果这时线程还在帐篷就唤醒让他继续前进如果这时线程还在运行那么下次他调用 park 时仅是消耗掉备用干粮不需停留,继续前进 因为背包空间有限多次调用 unpark 仅会补充一份备用干粮,也就是多次unpark后只会让紧跟着的一次park失效
先调用park 再调用unpark 当前线程调用 Unsafe.park() 方法检查 _counter 本情况为 0这时获得 _mutex 互斥锁线程进入 _cond 条件变量阻塞设置 _counter 0 调用 Unsafe.unpark(Thread_0) 方法设置 _counter 为 1唤醒 _cond 条件变量中的 Thread_0Thread_0 恢复运行设置 _counter 为 0
先调用unpark 再调用park 调用 Unsafe.unpark(Thread_0) 方法设置 _counter 为 1当前线程调用 Unsafe.park() 方法检查 _counter 本情况为 1这时线程无需阻塞继续运行设置 _counter 为 0
4.10 重新理解线程状态转换
概览图 假设有线程 Thread t
情况1 NEW -- RUNNABLE
当调用 t.start() 方法时由 NEW -- RUNNABLE
情况2 RUNNABLE – WAITING
t 线程用 synchronized(obj) 获取了对象锁后 调用 obj.wait() 方法时t 线程从 RUNNABLE -- WAITING 调用 obj.notify() obj.notifyAll() t.interrupt() 时 竞争锁成功t 线程从WAITING -- RUNNABLE竞争锁失败t 线程从WAITING -- BLOCKED
public class TestWaitNotify {final static Object obj new Object();public static void main(String[] args) {new Thread(() - {synchronized (obj) {log.debug(执行....);try {obj.wait();} catch (InterruptedException e) {e.printStackTrace();}log.debug(其它代码....); // 断点}},t1).start();new Thread(() - {synchronized (obj) {log.debug(执行....);try {obj.wait();} catch (InterruptedException e) {e.printStackTrace();}log.debug(其它代码....); // 断点}},t2).start();sleep(0.5);log.debug(唤醒 obj 上其它线程);synchronized (obj) {obj.notifyAll(); // 唤醒obj上所有等待线程 断点}}
}情况3 RUNNABLE – WAITING 当前线程调用 t.join() 方法时当前线程从 RUNNABLE -- WAITING 注意是当前线程在t 线程对象的监视器上等待 t 线程运行结束或调用了当前线程的 interrupt() 时当前线程从 WAITING -- RUNNABLE
情况4 RUNNABLE – WAITING 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE -- WAITING 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() 会让目标线程从 WAITING --RUNNABLE
情况5 RUNNABLE – TIMED_WAITING
t 线程用 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
情况6 RUNNABLE – TIMED_WAITING 当前线程调用 t.join(long n) 方法时当前线程从 RUNNABLE -- TIMED_WAITING 注意是当前线程在t 线程对象的监视器上等待 当前线程等待时间超过了 n 毫秒或t 线程运行结束或调用了当前线程的 interrupt() 时当前线程从 TIMED_WAITING -- RUNNABLE
情况7 RUNNABLE – TIMED_WAITING 当前线程调用 Thread.sleep(long n) 当前线程从 RUNNABLE -- TIMED_WAITING 当前线程等待时间超过了 n 毫秒当前线程从TIMED_WAITING -- RUNNABLE
情况8 RUNNABLE – TIMED_WAITING 当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时当前线 程从 RUNNABLE -- TIMED_WAITING 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() 或是等待超时会让目标线程从 TIMED_WAITING– RUNNABLE
情况9 RUNNABLE – BLOCKED
t 线程用synchronized(obj) 获取对象锁时如果竞争失败从RUNNABLE -- BLOCKED持 obj 锁线程的同步代码块执行完毕会唤醒该对象上所有 BLOCKED的线程重新竞争如果其中 t 线程竞争 成功从 BLOCKED -- RUNNABLE 其它失败的线程仍然BLOCKED
情况10 RUNNABLE -- TERMINATED
当前线程所有代码运行完毕进入 TERMINATED
4.11 多把锁
多把不相干的锁
一间大屋子有两个功能睡觉、学习互不相干。
现在小南要学习小女要睡觉但如果只用一间屋子一个对象锁的话那么并发度很低
解决方法是准备多个房间多个对象锁
例如
class BigRoom {public void sleep() {synchronized (this) {log.debug(sleeping 2 小时);Sleeper.sleep(2);}}public void study() {synchronized (this) {log.debug(study 1 小时);Sleeper.sleep(1);}}}执行
BigRoom bigRoom new BigRoom();new Thread(() - {bigRoom.study();
},小南).start();new Thread(() - {bigRoom.sleep();
},小女).start();某次结果
12:13:54.471 [小南] c.BigRoom - study 1 小时
12:13:55.476 [小女] c.BigRoom - sleeping 2 小时改进
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);}}}某次执行结果
12:15:35.069 [小南] c.BigRoom - study 1 小时
12:15:35.069 [小女] c.BigRoom - sleeping 2 小时将锁的粒度细分 好处是可以增强并发度 坏处如果一个线程需要同时获得多把锁就容易发生死锁
4.12 活跃性
4.12.1 死锁
有这样的情况一个线程需要同时获取多把锁这时就容易发生死锁
示例
t1 线程 获得 A对象 锁接下来想获取 B对 的锁t2 线程获得 B对象 锁接下来想获取 A对象 的锁 例
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();结果
12:22:06.962 [t2] c.TestDeadLock - lock B
12:22:06.962 [t1] c.TestDeadLock - lock A4.12.2 定位死锁
检测死锁可以使用 jconsole工具或者使用 jps 定位进程 id再用 jstack 定位死锁 避免死锁要注意加锁顺序 另外如果由于某个线程进入了死循环导致其它线程一直等待对于这种情况 linux 下可以通过 top 先定位到CPU 占用高的 Java 进程再利用 top -Hp 进程id 来定位是哪个线程最后再用 jstack 排查
4.12.3 哲学家就餐问题 有五位哲学家围坐在圆桌旁。
他们只做两件事思考和吃饭思考一会吃口饭吃完饭后接着思考。吃饭时要用两根筷子吃桌上共有 5 根筷子每位哲学家左右手边各有一根筷子。如果筷子被身边的人拿着自己就得等待
筷子类
class Chopstick {String name;public Chopstick(String name) {this.name name;}Overridepublic String toString() {return 筷子{ name };}
}哲学家类
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...);Sleeper.sleep(1);}Overridepublic void run() {while (true) {// 获得左手筷子synchronized (left) {// 获得右手筷子synchronized (right) {// 吃饭eat();}// 放下右手筷子}// 放下左手筷子}}}就餐
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();执行不多会就执行不下去了
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.Chopstick1540e19d (筷子1) 上的BLOCKED, 拥有者: 苏格拉底
总阻止数: 2, 总等待数: 1堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)- 已锁定 cn.itcast.Chopstick6d6f6e28 (筷子5)
-------------------------------------------------------------------------
名称: 苏格拉底
状态: cn.itcast.Chopstick677327b6 (筷子2) 上的BLOCKED, 拥有者: 柏拉图
总阻止数: 2, 总等待数: 1堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)- 已锁定 cn.itcast.Chopstick1540e19d (筷子1)
-------------------------------------------------------------------------
名称: 柏拉图
状态: cn.itcast.Chopstick14ae5a5 (筷子3) 上的BLOCKED, 拥有者: 亚里士多德
总阻止数: 2, 总等待数: 0堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)- 已锁定 cn.itcast.Chopstick677327b6 (筷子2)
-------------------------------------------------------------------------
名称: 亚里士多德
状态: cn.itcast.Chopstick7f31245a (筷子4) 上的BLOCKED, 拥有者: 赫拉克利特
总阻止数: 1, 总等待数: 1堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)- 已锁定 cn.itcast.Chopstick14ae5a5 (筷子3)
-------------------------------------------------------------------------
名称: 赫拉克利特
状态: cn.itcast.Chopstick6d6f6e28 (筷子5) 上的BLOCKED, 拥有者: 阿基米德
总阻止数: 2, 总等待数: 0堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)- 已锁定 cn.itcast.Chopstick7f31245a (筷子4)这种线程没有按预期结束执行不下去的情况归类为【活跃性】问题除了死锁以外还有活锁和饥饿者两种情
况
4.12.4 活锁
活锁出现在两个线程互相改变对方的结束条件最后谁也无法结束例如
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 调度执行也不能够结束饥饿的情况不
易演示讲读写锁时会涉及饥饿问题
下面我讲一下我遇到的一个线程饥饿的例子
先来看看使用顺序加锁的方式解决之前的死锁问题 顺序加锁的解决方案 但顺序加锁容易产生饥饿问题
例如 哲学家就餐时
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();
new Philosopher(阿基米德, c1, c5).start(); //线程饥饿4.13 ReentrantLock
相对于 synchronized 它具备如下特点
可中断可以设置超时时间可以设置为公平锁支持多个条件变量
与 synchronized 一样都支持可重入
基本语法
// 获取锁
reentrantLock.lock();
try {// 临界区
} finally {// 释放锁reentrantLock.unlock();
}4.13.1 可重入
可重入是指同一个线程如果首次获得了这把锁那么因为它是这把锁的拥有者因此有权利再次获取这把锁
如果是不可重入锁那么第二次获得锁时自己也会被锁挡住
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();}
}输出
17:59:11.862 [main] c.TestReentrant - execute method1
17:59:11.865 [main] c.TestReentrant - execute method2
17:59:11.865 [main] c.TestReentrant - execute method34.13.2 可打断 lock.lockInterruptibly()
示例
ReentrantLock lock new ReentrantLock();Thread t1 new Thread(() - {log.debug(启动...);try {//没有竞争就会获取锁//有竞争就进入阻塞队列等待,但可以被打断lock.lockInterruptibly();//lock.lock(); //不可打断} catch (InterruptedException e) {e.printStackTrace();log.debug(等锁的过程中被打断);return;}try {log.debug(获得了锁);} finally {lock.unlock();}
}, t1);lock.lock();
log.debug(获得了锁);
t1.start();try {sleep(1);log.debug(执行打断);t1.interrupt();
} finally {lock.unlock();
}输出
18:02:40.520 [main] c.TestInterrupt - 获得了锁
18:02:40.524 [t1] c.TestInterrupt - 启动...
18:02:41.530 [main] c.TestInterrupt - 执行打断
java.lang.InterruptedException
at
java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchr
onizer.java:898) at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222) at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335) at cn.itcast.n4.reentrant.TestInterrupt.lambda$main$0(TestInterrupt.java:17) at java.lang.Thread.run(Thread.java:748)
18:02:41.532 [t1] c.TestInterrupt - 等锁的过程中被打断注意如果是不可中断模式那么即使使用了 interrupt 也不会让等待中断
ReentrantLock lock new ReentrantLock();Thread t1 new Thread(() - {log.debug(启动...);lock.lock();try {log.debug(获得了锁);} finally {lock.unlock();}
}, t1);lock.lock();
log.debug(获得了锁);
t1.start();try {sleep(1);log.debug(执行打断);t1.interrupt();sleep(1);
} finally {log.debug(释放了锁);lock.unlock();
}输出
18:06:56.261 [main] c.TestInterrupt - 获得了锁
18:06:56.265 [t1] c.TestInterrupt - 启动...
18:06:57.266 [main] c.TestInterrupt - 执行打断 // 这时 t1 并没有被真正打断, 而是仍继续等待锁
18:06:58.267 [main] c.TestInterrupt - 释放了锁
18:06:58.267 [t1] c.TestInterrupt - 获得了锁4.13.3 锁(可设置)超时
立刻返回结果 lock.tryLock()
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();
}输出
18:15:02.918 [main] c.TestTimeout - 获得了锁
18:15:02.921 [t1] c.TestTimeout - 启动...
18:15:02.921 [t1] c.TestTimeout - 获取立刻失败返回尝试一定时间 lock.tryLock
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();
}输出
18:19:40.537 [main] c.TestTimeout - 获得了锁
18:19:40.544 [t1] c.TestTimeout - 启动...
18:19:41.547 [t1] c.TestTimeout - 获取等待 1s 后失败返回使用 tryLock 解决哲学家就餐问题
class Chopstick extends ReentrantLock {String name;public Chopstick(String name) {this.name name;}Overridepublic String toString() {return 筷子{ name };}
}class Philosopher extends Thread {Chopstick left;Chopstick right;public Philosopher(String name, Chopstick left, Chopstick right) {super(name);this.left left;this.right right;}Overridepublic void run() {while (true) {// 尝试获得左手筷子if (left.tryLock()) {try {// 尝试获得右手筷子if (right.tryLock()) {try {eat();} finally {right.unlock();}}} finally {left.unlock();}}}}private void eat() {log.debug(eating...);Sleeper.sleep(1);}}4.13.4 (可设置是否为)公平锁
公平: 先来就能先执行
不公平: 不保证先来就先执行
ReentrantLock 默认是不公平的
ReentrantLock lock new ReentrantLock(false);lock.lock();
for (int i 0; i 500; i) {new Thread(() - {lock.lock();try {System.out.println(Thread.currentThread().getName() running...);} finally {lock.unlock();}}, t i).start();
}
// 1s 之后去争抢锁
Thread.sleep(1000);new Thread(() - {System.out.println(Thread.currentThread().getName() start...);lock.lock();try {System.out.println(Thread.currentThread().getName() running...);} finally {lock.unlock();}
}, 强行插入).start();lock.unlock();强行插入有机会在中间输出 注意该实验不一定总能复现 t39 running...
t40 running...
t41 running...
t42 running...
t43 running...
强行插入 start...
强行插入 running...
t44 running...
t45 running...
t46 running...
t47 running...
t49 running...改为公平锁后
ReentrantLock lock new ReentrantLock(true);强行插入总是在最后输出
t465 running...
t464 running...
t477 running...
t442 running...
t468 running...
t493 running...
t482 running...
t485 running...
t481 running...
强行插入 running...公平锁一般没有必要会降低并发度后面分析原理时会讲解
4.13.5 (多个)条件变量
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 hasCigrette false;
static volatile boolean hasBreakfast false;public static void main(String[] args) {new Thread(() - {try {lock.lock();while (!hasCigrette) {try {waitCigaretteQueue.await();} catch (InterruptedException e) {e.printStackTrace();}}log.debug(等到了它的烟);} finally {lock.unlock();}}).start();new Thread(() - {try {lock.lock();while (!hasBreakfast) {try {waitbreakfastQueue.await();} catch (InterruptedException e) {e.printStackTrace();}}log.debug(等到了它的早餐);} finally {lock.unlock();}}).start();sleep(1);sendBreakfast();sleep(1);sendCigarette();
}private static void sendCigarette() {lock.lock();try {log.debug(送烟来了);hasCigrette true;waitCigaretteQueue.signal();} finally {lock.unlock();}
}private static void sendBreakfast() {lock.lock();try {log.debug(送早餐来了);hasBreakfast true;waitbreakfastQueue.signal();} finally {lock.unlock();}
}输出
18:52:27.680 [main] c.TestCondition - 送早餐来了
18:52:27.682 [Thread-1] c.TestCondition - 等到了它的早餐
18:52:28.683 [main] c.TestCondition - 送烟来了
18:52:28.683 [Thread-0] c.TestCondition - 等到了它的烟4.13.6 同步模式之顺序控制
固定运行顺序
比如必须先 2 后 1 打印
wait notify 版
// 用来同步的对象
static Object obj new Object();
// t2 运行标记 代表 t2 是否执行过
static boolean t2runed false;public static void main(String[] args) {Thread t1 new Thread(() - {synchronized (obj) {// 如果 t2 没有执行过while (!t2runed) { try {// t1 先等一会obj.wait(); } catch (InterruptedException e) {e.printStackTrace();}}}System.out.println(1);});Thread t2 new Thread(() - {System.out.println(2);synchronized (obj) {// 修改运行标记t2runed true;// 通知 obj 上等待的线程可能有多个因此需要用 notifyAllobj.notifyAll();}});t1.start();t2.start();
}Park Unpark 版
可以看到实现上很麻烦 首先需要保证先 wait 再 notify否则 wait 线程永远得不到唤醒。因此使用了『运行标记』来判断该不该wait 第二如果有些干扰线程错误地 notify 了 wait 线程条件不满足时还要重新等待使用了 while 循环来解决此问题 最后唤醒对象上的 wait 线程需要使用 notifyAll因为『同步对象』上的等待线程可能不止一个
可以使用 LockSupport 类的 park 和 unpark 来简化上面的题目
Thread t1 new Thread(() - {try { Thread.sleep(1000); } catch (InterruptedException e) { }// 当没有『许可』时当前线程暂停运行有『许可』时用掉这个『许可』当前线程恢复运行LockSupport.park();System.out.println(1);
});Thread t2 new Thread(() - {System.out.println(2);// 给线程 t1 发放『许可』多次连续调用 unpark 只会发放一个『许可』LockSupport.unpark(t1);
});t1.start();
t2.start();park 和 unpark 方法比较灵活他俩谁先调用谁后调用无所谓。
并且是以线程为单位进行『暂停』和『恢复』,不需要『同步对象』和『运行标记』
交替输出
线程 1 输出 a 5 次线程 2 输出 b 5 次线程 3 输出 c 5 次。现在要求输出 abcabcabcabcabc 怎么实现
wait notify 版
class SyncWaitNotify {private int flag;private int loopNumber;public 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();}}}
}SyncWaitNotify syncWaitNotify new SyncWaitNotify(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();Lock 条件变量版
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 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();}}Park Unpark 版
package com.tobestronger.n4._4_13.JiaoTiShuChu;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.locks.LockSupport;Slf4j(topic c.JiaoTiShuChuParkUnpark)
public class JiaoTiShuChuParkUnpark {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);}
}class ParkUnpark {private int loopNumber;public ParkUnpark(int loopNumber) {this.loopNumber loopNumber;}public void print(String str, Thread next) {for (int i 0; i loopNumber; i) {LockSupport.park();System.out.print(str);LockSupport.unpark(next);}}}本章小结
本章我们需要重点掌握的是 分析多线程访问共享资源时哪些代码片段属于临界区 使用 synchronized 互斥解决临界区的线程安全问题 掌握 synchronized 锁对象语法掌握 synchronzied 加载成员方法和静态方法语法掌握 wait/notify 同步方法 使用 lock 互斥解决临界区的线程安全问题 掌握 lock 的使用细节可打断、锁超时、公平锁、条件变量 学会分析变量的线程安全性、掌握常见线程安全类的使用 了解线程活跃性问题死锁、活锁、饥饿 应用方面 互斥使用 synchronized 或 Lock 达到共享资源互斥效果同步使用 wait/notify 或 Lock 的条件变量来达到线程间通信效果 原理方面 monitor、synchronized 、wait/notify 原理synchronized 进阶原理park unpark 原理 模式方面 同步模式之保护性暂停 异步模式之生产者消费者 public 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();}
}} } java
SyncWaitNotify syncWaitNotify new SyncWaitNotify(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();Lock 条件变量版
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 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();}}Park Unpark 版
package com.tobestronger.n4._4_13.JiaoTiShuChu;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.locks.LockSupport;Slf4j(topic c.JiaoTiShuChuParkUnpark)
public class JiaoTiShuChuParkUnpark {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);}
}class ParkUnpark {private int loopNumber;public ParkUnpark(int loopNumber) {this.loopNumber loopNumber;}public void print(String str, Thread next) {for (int i 0; i loopNumber; i) {LockSupport.park();System.out.print(str);LockSupport.unpark(next);}}}本章小结
本章我们需要重点掌握的是 分析多线程访问共享资源时哪些代码片段属于临界区 使用 synchronized 互斥解决临界区的线程安全问题 掌握 synchronized 锁对象语法掌握 synchronzied 加载成员方法和静态方法语法掌握 wait/notify 同步方法 使用 lock 互斥解决临界区的线程安全问题 掌握 lock 的使用细节可打断、锁超时、公平锁、条件变量 学会分析变量的线程安全性、掌握常见线程安全类的使用 了解线程活跃性问题死锁、活锁、饥饿 应用方面 互斥使用 synchronized 或 Lock 达到共享资源互斥效果同步使用 wait/notify 或 Lock 的条件变量来达到线程间通信效果 原理方面 monitor、synchronized 、wait/notify 原理synchronized 进阶原理park unpark 原理 模式方面 同步模式之保护性暂停异步模式之生产者消费者同步模式之顺序控制