6、共享模型之管程(悲观锁)
约 24933 字大约 83 分钟
2026-01-17
共享模型 之管程(悲观锁)
可见性,原子性,有序性
- 关键字
synchronized可以修饰方法或者以同步代码块的形式来使用,他主要确保多个线程在同一个时刻。只有一个线程处于方法或者是代码块中,他保证线程对变量的访问的可见性和排他性(互斥访问)- 关键字
volatile可以用来修饰字段(成员变量),就是告知程序在任何对该变量的访问均需要从共享内存中获取,而对他的改变必须同步刷新回共享内存,他能够保证所有线程对变量的访问的可见性。
java中多线程操作共享变量的问题
两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?
代码说明
public class Test14 {
public static int i=0;
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
for(int j=0;j<5000;j++){
i++;
}
});
Thread t2=new Thread(()->{
for(int j=0;j<5000;j++){
i--;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
//输出:正常输出应该是0.但是此程序输出的结果每一次都不是0,并且还不一样,这就是java并发操作共享变量带来的问题问题分析
以上的结果可能是正数、负数、零。为什么呢?因为 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
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
synchronized解决方案
应用之互斥
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
- 阻塞式的解决方案:
synchronized,Lock - 非阻塞式的解决方案:原子变量
本次使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
- 注意
- 虽然
java中互斥和同步都可以采用synchronized关键字来完成,但它们还是有区别的:
- 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
- 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
synchronized
语法
synchronized(对象) // 线程1, 线程2(blocked)阻塞状态
{
//临界区,也就是需要受保护的代码
}
//对对象加锁,但是需要保证同一时刻只有一个线程对此对象进行操作加锁改进代码
public class Test14 {
public static int i=0;
// 在这里需要创建一个对象,应为使用锁需要一个共享的对象来让各个线程加锁访问
static Object lock=new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
for(int j=0;j<5000;j++){
// 现在可以对临界区的代码进行加锁
synchronized (lock){
i++;
}
}
});
Thread t2=new Thread(()->{
for(int j=0;j<5000;j++){
// 对临界区进行加锁
synchronized (lock){//同样锁住的是lock对象
i--;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
//现在无论运行多少次,结果都是0synchronized锁原理理解
synchronized(对象)中的对象,可以想象为一个房间(room),有唯一入口(门)房间只能一次进入一人 进行计算,线程t1,t2想象成两个人- 当线程
t1执行到synchronized(room)时就好比t1进入了这个房间,并锁住了门拿走了钥匙,在门内执行count++代码 - 这时候如果
t2也运行到了synchronized(room)时,它发现门被锁住了,只能在门外等待,发生了上下文切换,阻塞住了 - 这中间即使
t1的cpu时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去哦),这时门还是锁住的,t1仍拿着钥匙,t2线程还在阻塞状态进不来,只有下次轮到t1自己再次获得时间片时才能开门进入 - 当
t1执行完synchronized{}块内的代码,这时候才会从obj房间出来并解开门上的锁,唤醒t2线程把钥匙给他。t2线程这时才可以进入obj房间,锁住了门拿上钥匙,执行它的count--代码
图解

- 当线程
图解上述代码执行过程

思考
synchronized 实际是用对象锁保证了临界区内代码的原子性(也就是说同一时间只能有一个线程去执行临界区的代码),临界区内的代码对外是不可分割的,不会被线程切换所打断。
如果把
synchronized(obj)放在for循环的外面,如何理解?-- 原子性i++对应的字节码指令有4条指令,如果把锁放在循环里面,那么也就是保证i++对应的四条字节码指令具有原子性,不可分割型,必须一次执行完成,但是如果把锁放在循环的外面,一共有4*5000条指令,说明这些指令的执行不可分割。两种加锁的方式最终结果是一样的。如果
t1.synchronized(obj1)而t2.synchronized(obj2)会怎样运作?-- 锁对象如果想要保护临界资源,就要使多个线程锁住的是同一个对象。如果不同的线程锁的是不同的对象,那么就相当于给不同的房间加锁,失去了锁的意义。
如果
t1.synchronized(obj)而t2没有加会怎么样?如何理解?-- 锁对象不可以,如果
t2没有对临界区进行加锁,那么t2线程在进行上下文切换的时候,也就不会去进行获取对象锁,自然还是可以执行临界区的代码,不能保证临界区资源的原子性。
面向对象改进
public class Test14 {
public static void main(String[] args) throws InterruptedException {
Room room=new Room();
Thread t1=new Thread(()->{
room.increase();
});
Thread t2=new Thread(()->{
room.deincrease();
});
t1.start();
t2.start();
// t1.join();
// t2.join();
System.out.println(room.getCount());
}
}
//使用面向对象思想对上面的代码进行改进
class Room{
private int count=0;
public void increase(){
// this表示锁主的是当前的对象
synchronized (this){
count++;
}
}
public void deincrease(){
synchronized (this){
count--;
}
}
// 获取count值的方法
public int getCount(){
// 要保证对象出去临界区资源后才可以获取值,所以也需要加锁
synchronized (this){
return count;
}
}
}方法上的 synchronized
synchronized锁只可以锁对象
class Test{
//把关键字添加在成员方法上,也相当于给当前的对象添加锁,也就是this对象加锁
public synchronized void test() {
}
}
//等价于
class Test{
public void test() {
synchronized(this) {
}
}
}静态方法加锁
class Test{
public synchronized static void test() {
}
}
//等价于
class Test{
public static void test() {
//静态方法加锁相当于锁主的是class对象,即类对象
synchronized(Test.class) {
}
}
}
//静态方法加锁,因为静态方法是属于类的,所以相当于给类的class对象加锁不加 synchronized 的方法
不加 synchronzied 的方法就好比不遵守规则的人,不去老实排队(好比翻窗户进去的)
线程八锁
如果线程锁主的是同一个对象,那么会有互斥的一种效果,但是如果锁住的是不同的对象,那么此时线程之间可能会并行或者并发执行。线程八锁实际上就是判断锁住的是否是同一个对象。
题目一
public class Test15 {
public static void main(String[] args) {
Number number=new Number();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"begin");
number.test01();
},"t1").start();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"begin.......");
number.test02();
},"t2").start();
}
}
class Number{
//说明锁住的是this对象,因为是普通的方法
public synchronized void test01(){
System.out.println("1.........");
}
public synchronized void test02(){
System.out.println("2..........");
}
}
//输出结果,也有可能是线程t2先执行,然后t1执行,因为锁住的是同一个对象
t1begin
t2begin.......
1.........
2..........题目二
public class Test15 {
public static void main(String[] args) {
Number n1=new Number();//同一把锁对象
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+"begin");
n1.test01();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1").start();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"begin.......");
n1.test02();
},"t2").start();
}
}
//锁都是添加在普通的方法上,相当于锁住的是this对象,哪一个线程抢到锁,那个线程先执行
class Number{
public synchronized void test01() throws InterruptedException {
Thread.sleep(2);//在这里先休眠2秒
System.out.println("1.........");
}
public synchronized void test02(){
System.out.println("2..........");
}
}
//输出情况:
//可能是t1线程先休眠2秒,然后t2线程在打印结果
//也可能是t2线程先打印结果,然后t1线程休眠两秒题目三
//添加一个没有加锁的方法
public class Test15 {
public static void main(String[] args) {
Number n1=new Number();//同一把锁
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+"begin");
n1.test01();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1").start();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"begin.......");
n1.test02();
},"t2").start();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"begin.......");
n1.test03();
},"t3").start();
}
}
//没有添加锁的方法可以和加锁的两个方法并发执行
class Number{
public synchronized void test01() throws InterruptedException {
Thread.sleep(2);
System.out.println("1.........");
}
public synchronized void test02(){
System.out.println("2..........");
}
public void test03(){
System.out.println("3..........");
}
}
//此时打印结果可能有3中
3,2s后,1,2
3,2,2s后,1
2,3,2s后,1
//应为t3线程没有加锁,所以和t1,t2线程完全可以并发进行,不需要保证互斥的访问题目四
public class Test15 {
public static void main(String[] args) {
//锁住的是不同的对象,所以线程之间不会互斥进行访问,线程之间可以并行执行
Number n1=new Number();
Number n2=new Number();
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+"begin");
n1.test01();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1").start();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"begin.......");
n2.test02();
},"t2").start();
}
}
class Number{
//锁住的是this对象
public synchronized void test01() throws InterruptedException {
Thread.sleep(2);
System.out.println("1.........");
}
//锁住的是this对象
public synchronized void test02(){
System.out.println("2..........");
}
}
//输出结果,一定是先输出2,在输出1,
t1begin
t2begin.......
2..........
1.........
//上面的两个线程锁住的是不同的对象,没有互斥的效果,两个线程是并行执行题目五
public class Test15 {
public static void main(String[] args) {
//两把不同的锁对象
Number n1=new Number();
Number n2=new Number();
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+"begin");
n1.test01();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1").start();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"begin.......");
n2.test02();
},"t2").start();
}
}
class Number{
//锁住的是类对象,在这里方法是静态的,所以锁住的是class类对象
public static synchronized void test01() throws InterruptedException {
Thread.sleep(2);
System.out.println("1.........");
}
public synchronized void test02(){
System.out.println("2..........");
}
}
//输出结果
t1begin
t2begin.......
2..........
1.........
//因为锁住的是不同的对象,所以没有互斥的关系,先输出2,后输出1题目六
public class Test15 {
public static void main(String[] args) {
//注意这里只有一个对象,和第八题分开
Number n1=new Number();
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+"begin");
n1.test01();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1").start();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"begin.......");
n1.test02();
},"t2").start();
}
}
class Number{
//给类对象加锁
public static synchronized void test01() throws InterruptedException {
Thread.sleep(2);
System.out.println("1.........");
}
//类对象加锁
public static synchronized void test02(){
System.out.println("2..........");
}
}
//两个锁都是锁类对象,因为内存中只有一份类对象,所以两个线程之间有互斥的关系
//打印结果
2,2s后,1
2s后,1,2
//主要看调度器先调度哪一个线程题目七
public class Test15 {
public static void main(String[] args) {
//两把对象锁
Number n1=new Number();
Number n2=new Number();
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+"begin");
n1.test01();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1").start();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"begin.......");
n2.test02();
},"t2").start();
}
}
class Number{
//锁住的是类对象
public static synchronized void test01() throws InterruptedException {
Thread.sleep(2);
System.out.println("1.........");
}
//锁住的是this对象
public synchronized void test02(){
System.out.println("2..........");
}
}
//锁住的是不同的对象,t1锁住的是类对象,t2锁住的是n2对象,所以输出结果是:2,1题目八
public class Test15 {
public static void main(String[] args) {
//注意这里是2个对象,要和第六题分开
Number n1=new Number();
Number n2=new Number();
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+"begin");
n1.test01();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1").start();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"begin.......");
n2.test02();
},"t2").start();
}
}
class Number{
public static synchronized void test01() throws InterruptedException {
Thread.sleep(2);
System.out.println("1.........");
}
public static synchronized void test02(){
System.out.println("2..........");
}
}
//两个方法都是类方法,所以两个线程锁住的都是一个类对象,所以有互斥的关系
2,2s后,1
2s后,1,2
//主要看调度器先调度哪一个线程变量的线程安全分析
成员变量和静态变量是否线程安全?
- 如果它们没有共享,则线程安全
- 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
- 如果只有读操作,则线程安全
- 如果有读写操作,则这段代码是临界区,需要考虑线程安全
局部变量是否线程安全
- 局部变量是线程安全的
- 但局部变量引用的对象则未必(涉及jvm中的逃逸分析)
- 如果该对象没有逃离方法的作用访问,它是线程安全的
- 如果该对象逃离方法的作用范围,需要考虑线程安全
局部变量线程安全分析
代码说明
public static void test1() {
int i = 10;//i是定义在方法内部的局部变量
i++;
}每个线程调用test1()方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享
反编译结果分析
//下面是test1()方法反编译后的结果,也就是方法的描述信息
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=0
0: bipush 10 //10做入操作数栈操作,操作数栈是栈帧中对应的操作数栈
2: istore_0 //吧10存储到局部变量表中,局部变量表也在栈帧中
3: iinc 0, 1 //取出局部变量表0位置处的值做自增操作
6: return
LineNumberTable:
line 10: 0
line 11: 3
line 12: 6
LocalVariableTable:
Start Length Slot Name Signature
3 4 0 i I图解
为什么对于非引用的局部变量没有线程安全问题呢?
对于每一个
jvm实例,里面的java虚拟机栈,程序计数器,本地方法栈,这三个内存结构都是线程私有的,也就是每一个线程对应一份,线程在执行代码的时候,会拷贝指令代码到自己的私有内存区域进行计算操作,各个线程相互独立,所以不会存在线程安全的问题。
局部变量的引用
代码说明
public class Test16 {
public static final int THREAD_NUMBER=2;
public static final int LOOP_NUM=200;
public static void main(String[] args) {
ThreadUnSafe threadUnSafe=new ThreadUnSafe();
for(int i=0;i<THREAD_NUMBER;i++){
new Thread(()->{
threadUnSafe.method01(LOOP_NUM);
},"thread"+(i+1)).start();
}
}
}
class ThreadUnSafe{
// 共享资源
ArrayList <String>list=new ArrayList();
public void method01(int loopNum){
for(int i=0;i<loopNum;i++){
method02();
method03();
}
}
private void method02(){
list.add("1");
}
private void method03(){
list.remove(0);
}
}
//输出
Exception in thread "thread1" java.lang.IndexOutOfBoundsException: Index: 1, Size: 1
at java.util.ArrayList.rangeCheck(ArrayList.java:659)
at java.util.ArrayList.remove(ArrayList.java:498)
at rzf.qq.com.MyThread.ThreadUnSafe.method03(Test16.java:32)
at rzf.qq.com.MyThread.ThreadUnSafe.method01(Test16.java:25)
at rzf.qq.com.MyThread.Test16.lambda$main$0(Test16.java:12)
at java.lang.Thread.run(Thread.java:748)- 分析
- 无论哪个线程中的
method2引用的都是同一个对象中的list成员变量 method3与method2分析相同
- 无论哪个线程中的
图解

- 因为两个线程同时访问共享变量,如果对
list列表的删除和添加元素不是互斥的话,可能线程1去添加删除一个元素,此时线程2又去删除一个元素,会产生数组越界。
修改为局部变量
代码说明
public class Test16 {
public static final int THREAD_NUMBER=2;
public static final int LOOP_NUM=200;
public static void main(String[] args) {
ThreadSafe threadSafe=new ThreadSafe();
for(int i=0;i<THREAD_NUMBER;i++){
new Thread(()->{
threadSafe.method01(LOOP_NUM);
},"thread"+(i+1)).start();
}
}
}
class ThreadSafe{
//注意这里的final修饰
public final void method01(int loopNum){
// 修改为局部变量
ArrayList <String>list=new ArrayList<String>();
for(int i=0;i<loopNum;i++){
method02(list);
method03(list);
}
}
//注意下面两个方法是私有的
//因为方法是私有的,这就可以保证其他地方不能调用下面的两个方法,所以传进来的参数不会暴漏给其他的方法
private void method02(ArrayList <String>list){
list.add("1");
}
private void method03(ArrayList <String>list){
list.remove(0);
}
}
//此时程序正常运行- 分析
list是局部变量,每个线程调用时会创建其不同实例,没有共享- 而
method2的参数是从method1中传递过来的,与method1中引用同一个对象,并且在这里method02方法也是私有的方法,其他的方法不能调用method02方法,自然参数也就不会暴漏给其他的方法. method3的参数分析与method2相同
图示分析

- 每一个线程内部都有自己的私有
list对象,因此不会相互影响,所以不会造成线程安全问题.
访问修饰符问题
public class Test16 {
public static final int THREAD_NUMBER=2;
public static final int LOOP_NUM=200;
public static void main(String[] args) {
ThreadSafe threadSafe=new ThreadSafe();
for(int i=0;i<THREAD_NUMBER;i++){
new Thread(()->{
threadSafe.method01(LOOP_NUM);
},"thread"+(i+1)).start();
}
}
}
class ThreadSafe{
//注意这里的final修饰
public final void method01(int loopNum){
// 修改为局部变量
ArrayList <String>list=new ArrayList<String>();
for(int i=0;i<loopNum;i++){
method02(list);
method03(list);
}
}
//注意方法的访问修饰是public
public void method02(ArrayList <String>list){
list.add("1");
}
public void method03(ArrayList <String>list){
list.remove(0);
}
}
//此时程序正常运行- 也就是访问修饰符不会影响程序的并发安全问题.因为我们传入参数的时候,实际上传入的是对象的地址,访问的是同一个对象.
子类继承父类线程是否安全
代码说明
public class Test16 {
public static final int THREAD_NUMBER=2;
public static final int LOOP_NUM=200;
public static void main(String[] args) {
SubClass threadSafe=new SubClass();
for(int i=0;i<THREAD_NUMBER;i++){
new Thread(()->{
threadSafe.method01(LOOP_NUM);
},"thread"+(i+1)).start();
}
}
}
class ThreadSafe{
public final void method01(int loopNum){
// 修改为局部变量
ArrayList <String>list=new ArrayList<String>();
for(int i=0;i<loopNum;i++){
method02(list);
method03(list);
}
}
//注意下面两个方法的访问类型都是public ,也就是可以被子类重写
public void method02(ArrayList <String>list){
list.add("1");
}
public void method03(ArrayList <String>list){
list.remove(0);
}
}
class SubClass extends ThreadSafe{
// 对父类添加的方法进行重写,对共享资源进行操作
public void method03(ArrayList <String>list){
new Thread(()->{
list.remove(0);
}).start();
}
}- 此时不是线程安全的,因为在子线程中也对
list进行访问操作,相当于访问了共享的资源, - 但是如果吧
method02和method03方法访问修饰修改为私有的,那么是线程安全的,也就是在一定程度上,如果吧方法的访问修饰改为私有的,那么子类就不可以重写私有方法,所以子类的覆盖的方法是另外一个方法. - 另外在一些公共方法前面最好添加
final关键字,也是防止子类对方法进行修改操作.
小结
- 把方法变为私有,或者添加
final关键字,一定程度上可以保证线程安全问题.
从这个例子可以看出 private 或 final 提供【安全】的意义所在,请体会开闭原则中的【闭】
常见的线程安全类
String字符串是线程安全的.内部状态不可以改变保证线程安全Integer:包装类都是线程安全的,内部状态不可以改变保证线程安全StringBuffer:字符串拼接的线程安全类,使用synchronized关键字保证线程安全Random产生随机数的线程安全类Vector线程安全的list实现Hashtable线程安全的map实现java.util.concurrent包下的类
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为:因为每一个实例的方法前面都有synchronized关键字
Hashtable table = new Hashtable();
new Thread(()->{
table.put("key", "value1");
}).start();
new Thread(()->{
table.put("key", "value2");
}).start();- 它们的每个方法是原子的,也就是对于每一个方法,已经添加
synchronized关键字,是原子操作.线程的上下文切换不会导致并发安全问题。 - 但注意它们多个方法的组合不是原子的,见后面分析
线程安全类方法的组合
分析下面代码是否线程安全?
Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
table.put("key", value);
}
//不是线程安全的图解

不可变类线程安全性
String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的 有同学或许有疑问,String 有 replace,substring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安全的呢?
代码说明
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}- 我们以
substring源码来说明,其实最后使用new String(value, beginIndex, subLen);返回的是一个新的字符串,并不会影响字符串的本身.所以是线程安全的. - 在源码中把
Sring设置为final类,目的就是遵循了设计模式中的开闭原则,防止继承String进而修改类源码.比如有些子类继承与String类,然后对其中的一些方法进行重写,覆盖原来的方法,这样就不能保证线程安全。 - 另外,如果变量使用final声明的话,仅仅能保证变量的引用不在改变,而不能保证对象本身内部的属性不能改变,所以使用final声明的对象仍然不能保证线程安全。
练习
卖票问题.
测试下面代码是否存在线程安全问题,并尝试改正
import java.util.List;
import java.util.Random;
import java.util.Vector;
public class Exercise01 {
public static void main(String[] args) throws InterruptedException {
TicketWindow t=new TicketWindow(1000);
List<Integer>list=new Vector<>();
// 所有的线程集合
List<Thread>threadList=new Vector<>();
// 模拟同时有多少个人来买票
for(int i=0;i<3000;i++){
Thread thread=new Thread(()->{
// 随机数表示买票的多少
int count=t.sell(randomAccount());
try {
Thread.sleep(randomAccount());
} catch (InterruptedException e) {
e.printStackTrace();
}
// add方法也是线程安全的
list.add(count);
});
// 把所有线程添加到集合中,本来就是线程安全的方法
threadList.add(thread);
thread.start();
}
// 在主线程中要等待所有子线程运行完毕
for (Thread thread : threadList) {
thread.join();
}
// 下面是主线程的代码,但是主线程要等待上面的所有子线程运行结束才可以统计
// 统计余票的数量
System.out.println("剩余的票数:"+t.getCount());
// 输出卖出的票数
System.out.println("卖出的票数:"+list.stream().mapToInt(i->i).sum());
}
// random为线程安全的类
static Random random=new Random();
// 随机1-5
public static int randomAccount(){
return random.nextInt(5)+1;
}
}
//售票窗口
class TicketWindow {
//共享变量
private int count;
public TicketWindow(int count) {
this.count = count;
}
// 获取与,余票的数量
public int getCount() {
return count;
}
// 对count进行读写,属于临界区,所以对此方法加锁,也就是对this对象加锁
// 下面对共享变量的访问是原子操作
public synchronized int sell(int amount) {
if (this.count >= amount) {
this.count -= amount;
return amount;
} else {
return 0;
}
}
}转账
多个需要保护的对象
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;
}
public void transfer(Account target, int amount) {
if (this.money > amount) {
this.setMoney(this.getMoney() - amount);
target.setMoney(target.getMoney() + amount);
}
}
}
//如果把锁加在this对象上面,那么锁住的只能是某一个对象的money变量,这里要锁住的是两个对象共享的变量,所以必须把锁添加在类上,如果没理解,请看线程8锁那一块
//也就是说如果加所加载方法上面,只能锁住this对象,不能锁住target对象
public synchronized void transfer(Account target, int amount) {
if (this.money > amount) {
this.setMoney(this.getMoney() - amount);
target.setMoney(target.getMoney() + amount);
}
}
//要锁主类对象,也就是说this对象的money和target对象的money属性都需要被保护
public void transfer(Account target, int amount) {
//在这里锁住的是类而第一种加锁的方式只能锁住this对象
synchronized(Account.class){
if (this.money > amount) {
this.setMoney(this.getMoney() - amount);
target.setMoney(target.getMoney() + amount);
}
}
}如果有多个需要保护的变量,需要锁住的是类对象
Monitor的概念
Java 对象头 以 32 位虚拟机为例
integer:12字节=8+4
int:4字节
普通对象

其中klass word1存储的是对象的类型信息,是一个指针,指向的是方法区的类型信息。
数组对象

组的对象头还要额外添加数组的长度信息
其中 Mark Word 结构为

hashcode:哈希码,age:垃圾回收看的分代的年龄,biased_lock:偏向锁,01:加锁的状态,前面的30位代表的是锁对象的地址。ptr_to_lock_record表示锁的地址,后两位表示是那种锁。
64为虚拟机的Mark Word

Monitor锁
如下面的图所示,当开始执行synchronized代码块的时候,会将obj对象和操作系统的monitor对象进行关联,然后obj对象中的前30位记录monitor对象的地址,后两位修改为10,表示重量级锁,

- Monitor 被翻译为监视器或管程
- 每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针
- Monitor 结构如下

- 刚开始Monitor中的Owner为null.
- 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一个 Owner,其中monitor是操作系统层面的对象,而obj是java层面的对象。
- 在 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才会有效,如果进入不同对象的monitor,那么无效。因为一个对象会关联一个monitor管程。不添加
synchronized锁的对象不会关联monitor对象,也不遵循以上的规则。
synchronized原理
字节码角度理解锁
源码说明
public class Test17 {
static final Object lock = new Object();
static int count = 0;
public Test17() {
}
public static void main(String[] args) {
synchronized(lock) {
++count;
}
}
}字节码文件
public static void main(java.lang.String[]);
descriptor: ([吗 Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: getstatic #2 // Field lock:Ljava/lang/Object;也就是拿到lock锁的引用对象,我们可以看到实际是object对象,synchronized的开始,拿到的是引用的地址
3: dup //复制锁的指令
4: astore_1//把复制的锁存储到局部变量表为1的slot槽位置,是为了以后解锁使用
5: monitorenter //synchronized对应的指令,其实就是将锁对象的markw word设置为monitor指针,将lock与操作系统的monitor进行关联
6: getstatic #3 // Field count:I //代表取出count
9: iconst_1//准备常数1
10: iadd//做累加操作
11: putstatic #3 // Field count:I.重新写回count操作
14: aload_1//加载局部变量表中1位置的临时锁对象的地址
15: monitorexit//将对象头的mark word进行重置,加锁前mark word存储的是hash code等信息,但是加锁后存储的是monotor指针,所以要重置为加锁之前的信息,然后唤醒entry list中的一个阻塞线程
只是把monitor和obj对象头中的信息交换了一下,并没有丢失。
16: goto 24//执行第24条指令,也就是返回
//如果锁中发生异常,就执行下面的代码,也可以正常释放锁
19: astore_2//异常对象存储到局部变量表2的位置
20: aload_1//重新加载锁的引用
21: monitorexit//将对象头的mark word进行重置,加锁前mark word存储的是hash code等信息,但是加锁后存储的是monotor指针,所以要重置为加锁之前的信息,然后唤醒entry list中的一个阻塞线程
22: aload_2//加载异常对象
23: athrow//抛出异常
24: return
Exception table://异常表检测范围是6-16如果出现异常,就到19行去执行
//检测范围从19-22,如果出现异常,就到19行去处理
from to target type
6 16 19 any
19 22 19 any
LineNumberTable:
line 8: 0
line 9: 6
line 10: 14
line 12: 24
LocalVariableTable:
Start Length Slot Name Signature
0 25 0 args [Ljava/lang/String;- 所以从字节码角度,不管我们加锁的代码块是否可以正常运行,底层都可以正常加锁和释放锁,不会出现加锁然后无法释放的情况。
- 总结来说,就是synchronized锁在底层真正使用的是monitor进行加锁,一个锁对象对应一个monitor对象。monitor锁是由操作系统进行提供的,成本很高,开销大。属于重量级锁。
synchronized原理进阶
重量级锁--monitor
轻量级锁--锁记录
故事角色
- 老王 - JVM
- 小南 - 线程
- 小女 - 线程
- 房间 - 对象
- 房间门上 - 防盗锁 - Monitor
- 房间门上 - 小南书包 - 轻量级锁
- 房间门上 - 刻上小南大名 - 偏向锁,锁偏向于某一个线程使用
- 批量重刻名 - 一个类的偏向锁撤销到达 20 阈值
- 不能刻名字 - 批量撤销该类对象的偏向锁,设置该类不可偏向
小南要使用房间保证计算不被其它人干扰(原子性),最初,他用的是防盗锁,当上下文切换时,锁住门。这样,即使他离开了,别人也进不了门,他的工作就是安全的。
但是,很多情况下没人跟他来竞争房间的使用权。小女是要用房间,但使用的时间上是错开的,小南白天用,小女晚上用。每次上锁太麻烦了,有没有更简单的办法呢?
小南和小女商量了一下,约定不锁门了,而是谁用房间,谁把自己的书包挂在门口,但他们的书包样式都一样,因此每次进门前得翻翻书包,看课本是谁的,如果是自己的,那么就可以进门,这样省的上锁解锁了。万一书包不是自己的,那么就在门外等,并通知对方下次用锁门的方式。
后来,小女回老家了,很长一段时间都不会用这个房间。小南每次还是挂书包,翻书包,虽然比锁门省事了,但仍然觉得麻烦。于是,小南干脆在门上刻上了自己的名字:【小南专属房间,其它人勿用】,下次来用房间时,只要名字还在,那么说明没人打扰,还是可以安全地使用房间。如果这期间有其它人要用这个房间,那么由使用者将小南刻的名字擦掉,升级为挂书包的方式。
同学们都放假回老家了,小南就膨胀了,在 20 个房间刻上了自己的名字,想进哪个进哪个。后来他自己放假回老家了,这时小女回来了(她也要用这些房间),结果就是得一个个地擦掉小南刻的名字,升级为挂书包的方式。老王觉得这成本有点高,提出了一种批量重刻名的方法,他让小女不用挂书包了,可以直接在门上刻上自己的名字
后来,刻名的现象越来越频繁,老王受不了了:算了,这些房间都不能刻名了,只能挂书包
轻量级锁
- 轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
- 轻量级锁对使用者是透明的,即语法仍然是
synchronized,假设有两个方法同步块,利用同一个对象加锁 - 使用轻量级锁是jvm自动进行的,如果轻量级锁加锁失败了,jvm才会换为重量级锁。
代码说明
public class Test17 {
static final Object lock=new Object();
public static void main(String[] args) {
}
public static void method01(){
synchronized (lock){
method02();
}
}
public static void method02(){
synchronized (lock){
// 同步块B
}
}
}- 创建锁记录(
lock record)对象,其实这个锁记录就可以认为是monitor对象,每一个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word(也就是对象头中的mark word结构)。锁记录包含两部分:1,对象引用存储的是锁对象的地址,2,lock record记录的是对象头的mark word信息。,现在可以看到对象头中的锁标记是01,表示没有加锁的状态。

- 让锁记录中的
object reference指向刚才的锁对象, 并且尝试用cas替换掉Object中的mark word,并且将mark work的值存入锁记录。对象头中的01表示为没有加锁,添加到锁记录中就变为00,表示轻量级锁。在这里是将对象头中的hashcode等信息和lock recode信息做一个交换,因为解锁的时候,还需要进行恢复对象的状态。obj中01表示没有加锁状态,而锁记录中00表示轻量级锁状态。

- 如果
cas替换成功(也就是对象的状态是01的时候,就是没加锁时候,如果其他线程已经把01修改为00,那么这个时候加锁就会失败),对象头中的锁记录地址和状态00,表示由该线程对对象加锁

- 如果
cas失败,有两种情况- 如果是其他线程已经持有该对象的轻量级锁,这个时候表示有竞争,进入锁膨胀的过程。
- 如果是自己执行了
synchronized重入(也就是自己的线程又给同一个对象添加锁),那么需要添加一条lock record作为重入的计数,就是重新创建一个栈帧,把栈帧的锁记录重新做上面的一系列操作,比如cas操作,交换对象头和锁记录中的信息。重入实际就是判断同一个线程对锁对象添加了几次锁,直接可以根据lock record的数量就可以计算。但是这样加锁会失败,因为第一次已经把obj对象头中的10修改为00,表示已经加锁了,但是会产生一个新的锁记录,锁记录的对象头中记录的是null值,最后在解锁的时候,解一次锁,就会去掉一个lock record记录。锁记录个数代表加锁的次数。

- 当退出
synchronized的代码块(解锁)时候,如果有lock record值为null的情况,表示有锁重入,这个时候重置锁记录,表示重入次数减一。

- 当退出
synchronized代码块锁记录的值不是null这种情况时候,这个时候使用cas将mark word的值恢复为对象头原来的初始值。cas是一个原子操作,不可以打断。- 回恢复成功,那么就解锁成功。
- 失败,那么说明轻量级锁进行了锁膨胀,或者是升级为重量级锁,进入重量级锁的解锁流程。
锁膨胀
如果在尝试加轻量级锁的过程中,CAS操作无法成功,这个时候有一种情况就是有其他线程为锁对象已经添加上轻量级锁(也就是说明有线程在竞争),这个时候需要进行锁的膨胀,将轻量级锁升级为重量级锁。
代码说明
public class Test17 {
static final Object lock=new Object();
public static void main(String[] args) {
}
public static void method01(){
synchronized (lock){
//同步代码块
}
}
}- 当Thread-1进行添加轻量级锁的时候,Thread-0已经对该对象添加了轻量级锁,可以看到obj对象中锁状态已经变为00,

- 这个时候Thread-1添加轻量级锁失败,需要进入锁膨胀的过程,因为线程1没有申请锁成功,必须申请重量级锁进入阻塞队列等待,所以要升级。这个时候是对obj对象升级为重量级锁,而不是申请新的锁对象。
- 首先为object对象申请一个重量级的monitor锁,让object指向重量级锁的地址。因为Thread-1没有拿到锁,就需要进入阻塞状态,但是轻量级锁没有阻塞这种状态,所以要升级为重量级锁。
- 然后自己进入monitor的
EntryList BLOCK阻塞队列中。其中锁的类型也修改为重量级锁标志10.

- 当Thread-0退出同步代码块进行解锁的时候,使用cas将mark word恢复给对象头,如果失败,那么会进入重量级锁的解锁流程,也就是按照obj对象中monitor对象的地址找到monitor对象,设置owner为null,同时唤醒entryList 中的block线程。
- 切记:obj中记录的是monitor对象地址,而monitor对象中记录的是obj对象中对象头的信息。
自旋优化
在重量级锁进行竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁的线程已经退出同步代码块,释放了锁),什么叫自旋,也就是让将要进入阻塞队列的线程先不要进入阻塞队列,让他在原地进行几次循环等待,等待娶她线程让出锁,然后在获取锁,这样做可以减小上下文切换带来的压力,但是如果有很多线程都进入自旋的状态,那么很吃cpu的性能。这个时候当前线程就可以避免被阻塞(阻塞会发生上下文切换)。
自旋重试成功的情况
线程1(cpu 1上) | 对象的Mark | 线程2(cpu2上) |
|---|---|---|
| - | 10(表示重量级锁) | - |
| 访问同步块(获取monitor) | 10(重置锁)重量锁指针 | - |
| 成功(加锁) | 10(重置锁)重量锁指针 | - |
| 执行同步块 | 10(重置锁)重量指针锁 | - |
| 执行同步块 | 10(重置锁)重量指针锁 | 访问同步块,获取monitor |
| 执行同步块 | 10(重置锁)重量指针锁 | 自旋重试,没有获取锁,在这里循环重试 |
| 执行完毕 | 10(重置锁)重量锁指针 | 自旋重试 |
| 成功(解锁) | 01(无锁) | 自旋重试 |
| - | 10(重置锁)重量锁指针 | 成功(加锁) |
| - | 10(重置锁)重量锁指针 | 执行同步块 |
| …… |
- 自旋优化,适合是多核
cpu的。
自旋重试失败的情况
线程1(cpu 1上) | 对象的Mark | 线程2(cpu2上) |
|---|---|---|
| - | 10(表示重量级锁) | - |
| 访问同步块(获取monitor) | 10(重置锁)重量锁指针 | - |
| 成功(加锁) | 10(重置锁)重量锁指针 | - |
| 执行同步块 | 10(重置锁)重量指针锁 | - |
| 执行同步块 | 10(重置锁)重量指针锁 | 访问同步块,获取monitor |
| 执行同步块 | 10(重置锁)重量指针锁 | 自旋重试,没有获取锁,在这里循环重试 |
| 执行完毕 | 10(重置锁)重量锁指针 | 自旋重试 |
| 成功(解锁) | 01(无锁) | 自旋重试 |
| - | 10(重置锁)重量锁指针 | 自旋成功 |
| - | 10(重置锁)重量锁指针 | 阻塞 |
| …… |
- 在jdk6之后,自旋锁是自适应的,比如对象刚刚一次自旋操作成功过,那么会认为这次自旋成功的可能性会高,就多旋转几次,反之,就少旋转几次甚至不旋转。
- 自旋会占用cpu的时间,单核的cpu会浪费性能,但是多核的cpu会发挥其优势。
- java 7之后不能控制是否开启自旋功能。
偏向锁
轻量级锁在没有竞争的时候(也就是说只有当前一个线程),每一次仍然需要进行cas操作,开销依然很大。
jdk6中引入偏向锁进行优化,只有第一次使用cas将线程id(可以理解为线程的名字)设置到对象头的mark word头,之后发现这个线程的id是自己的就表示没有竞争,不用重新进行cas操作,以后只要不发生竞争,这个对象就归该线程所有。线程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
}
}轻量级锁锁重入
可以看到,在m1()方法中获取到锁之后,在m2()中,还会去重新尝试获取锁,使用的是cas操作,同理,在m3()方法中还会使用cas重新获取锁。这样每次重新获取锁,然后添加锁记录会影响开销。每次尝试获取锁,都会添加锁记录,然后尝试使用cas去修改锁对象信息,

偏向锁
使用线程的id号。也就是把线程的id添加到monitor中。

偏向状态
64位虚拟机对象头
- 10:表示是重量级锁。
- 00:表示是轻量级锁。
- 01:正常状态,没有添加锁。
- biased_lock:表示是否启用偏向锁。

一个对象创建时
- 如果开启偏向锁(默认开启),那么对象创建后,markword值为0x05,,也就是最后三位是101,这时他的thread,epoch,age都是0.
- 偏向锁开启默认是延迟的,不会再程序启动时立即生效,如果想避免延迟,可以添加vm参数:XX:BiasedLockingStartupDelay=0来禁用延迟。
- 如果没有开启偏向锁,那么对象创建后,markword值为0x01,,也就是最后三位是001,这时他的hashcode,age,都是0,第一次使用hashcode的时候才会赋值。
- 当给锁对象的对象头markword添加线程id后,当锁退出后,markword中存储的还是第一次解锁线程的线程id,除非是其他线程重新使用此对象锁,markdown中的线程id才会改变。
- **偏向锁的使用场景是当只有一个线程的时候,也就是没有竞争的时候。**当冲突很少的时候适合于偏向锁。
加锁的顺序:偏向锁--->轻量级锁--->重量级锁
撤销对象锁的偏向状态- 调用对象的hashCode()方法
当一个可偏向的对象获取对象的哈希码之后,会禁用偏向锁,因为哈希码占用了存储线程id的位置,也就是如果调用某个锁的hashcode()方法,那么此对象的偏向锁会被撤销。而轻量级锁的哈希码存储在所记录的栈帧中,而重量级锁的哈希码存储在monitor中。而偏向锁没有额外的存储位置。
撤销-其他线程使用对象
轻量级锁和偏向锁使用的前提是两个线程在访问对象锁的时间都是错开的。而重量级锁多个线程可以并发执行。
当某个线程给锁对象添加偏向锁后,然后解锁,当再次有其他线程给对象添加锁的时候,那么这次对象的偏向锁状态会失效,升级为轻量级锁,也就是对象从可偏向状态转变为不可偏向状态。
private static void test2() throws InterruptedException {
Dog d = new Dog();
Thread t1 = new Thread(() -> {
synchronized (d) {
log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
}
synchronized (TestBiased.class) {
TestBiased.class.notify();
}
// 如果不用 wait/notify 使用 join 必须打开下面的注释
// 因为:t1 线程不能结束,否则底层线程可能被 jvm 重用作为 t2 线程,底层线程 id 是一样的
/*try {
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}*/
}, "t1");
t1.start();
Thread t2 = new Thread(() -> {
synchronized (TestBiased.class) {
try {
TestBiased.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
synchronized (d) {
log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
}
log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
}, "t2");
t2.start();
}
//输出
[t1] - 00000000 00000000 00000000 00000000 00011111 01000001 00010000 00000101
[t2] - 00000000 00000000 00000000 00000000 00011111 01000001 00010000 00000101
[t2] - 00000000 00000000 00000000 00000000 00011111 10110101 11110000 01000000
[t2] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001撤销-调用wait/notify
wait/notify只有重量级锁才有,当调用这两个方法时候,会把偏向锁和轻量级锁升级为重量级锁。
批量重偏向
如果对象被多个线程访问,但是没有竞争,这个时候偏向线程t1的对象仍然有机会偏向t2,重偏向会重新设置对象的id。
当撤销偏向锁阈值超过20次后,jvm会这样觉得,我是不是偏向错了呢。于是在给对象加锁时候重新偏向至加锁线程。
private static void test3() throws InterruptedException {
Vector<Dog> list = new Vector<>();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 30; i++) {
Dog d = new Dog();
list.add(d);
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
}
synchronized (list) {
list.notify();
}
}, "t1");
t1.start();
Thread t2 = new Thread(() -> {
synchronized (list) {
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("===============> ");
for (int i = 0; i < 30; i++) {
Dog d = list.get(i);
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
}, "t2");
t2.start();
}批量撤销
当撤销偏向锁的阈值达到40次之后,jvm也会这样觉得,自己确实偏向错了,根本就不该偏向,于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的。
static Thread t1,t2,t3;
private static void test4() throws InterruptedException {
Vector<Dog> list = new Vector<>();
int loopNumber = 39;
t1 = new Thread(() -> {
for (int i = 0; i < loopNumber; i++) {
Dog d = new Dog();
list.add(d);
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
}
LockSupport.unpark(t2);
}, "t1");
t1.start();
t2 = new Thread(() -> {
LockSupport.park();
log.debug("===============> ");
for (int i = 0; i < loopNumber; i++) {
Dog d = list.get(i);
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
LockSupport.unpark(t3);
}, "t2");
t2.start();
t3 = new Thread(() -> {
LockSupport.park();
log.debug("===============> ");
for (int i = 0; i < loopNumber; i++) {
Dog d = list.get(i);
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
}, "t3");
t3.start();
t3.join();
log.debug(ClassLayout.parseInstance(new Dog()).toPrintableSimple(true));
}锁消除
@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
//-XX:-EliminateLocks表示关闭锁消除
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锁小结
创建一个对象最初是无锁状态的,一个对象获取锁后升级为偏向锁,当出现锁竞争的时候,就升级为轻量级锁,轻量级锁然后在升级为重量级锁,但是在轻量级锁升级为重量级锁的过程中,有一个自旋优化的过程,这样可以减小开销。
轻量级锁,在当前线程A创建一个锁记录,然后尝试通过CAS把markword更新为指向线程A的锁记录的指针,如果成功了,那么markword最后两位就变成00(轻量级锁),如果此时又来了一个B线程,那么会在B线程中创建一个锁记录,尝试CAS把markword更新为指向线程A的该锁记录的指针,如果失败的话,会查看markword的指针指向的是不是B线程中的某个栈帧(锁记录),如果是,即A和B是同一个线程,也就是当前操作是重入锁操作,即在当前线程对某个对象重复加锁,这是允许的,也就是可以获取到锁了。如果markword记录的不是B线程中的某个栈帧(锁记录),那么线程B就会尝试自旋,如果自选超过一定次数,就会升级成重量级锁(轻量级锁升级成重量级锁的第一种时机:自选次数超过一定次数),如果B线程在自选的过程中,又来了一个线程C来竞争该锁,那么此时直接轻量级锁膨胀成重量级锁(轻量级锁升级成重量级锁的第二种时机:有两个以上的线程在竞争同一个锁。注:A,B,C3线程>2个线程)
如果一开始是无锁状态,那么第一个线程获取索取锁的时候,判断是不是无锁状态,如果是无锁(001),就通过CAS将mark word里的部分地址记录为当前线程的ID,同时最后倒数第三的标志位置为1,即倒数三位的结果是(101),表示当前为轻量级锁。下一个如果该线程再次获取该锁的时候,就直接判断mark word里记录的线程ID是不是我当前的线程ID,如果是的话,就成功获取到锁了,即不需再进行CAS操作,这就是相对轻量级锁来说,偏向锁的优势(只需进行第一次的CAS,而无需每次都进行CAS,当然这个理想过程是没有其他线程来竞争该锁)。如果中途有其他线程来竞争该锁,发现已经是101状态,那么就会查看偏向锁记录的线程是否还存活,如果未存活,即偏向锁的撤消,将markword记录的锁状态从101(偏向锁)置未001(无锁),然后重新偏向当前竞争成功的线程,如果当前线程还是存活状态,那么就升级成轻量级锁。
Wait/Notify
wait/notify原理

Owner线程发现条件不满足,调用wait方法,即可进入WaitSet变为WAITING状态(这个状态是)BLOCKED(处于block的线程是正在等待锁的线程)和WAITING(已经获取到锁,但是又放弃锁,进入waitset队列,原因是线程所需的条件没有得到满足) 的线程都处于阻塞状态,不占用CPU时间片BLOCKED线程会在Owner线程释放锁时唤醒(处于block的线程是没有获取锁的线程),正在等待锁的线程。WAITING线程会在Owner线程调用notify或notifyAll时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList重新竞争,而block在owner释放锁后,会唤醒block队列中的一个线程获取锁。waitSet和EntryList中的线程都处于阻塞状态,不会占用cpu的时间。
waitSet的线程是必须是处于owner中的线程才有资格进入waitSet队列,进入这个队列可以调用wait()方法,处于owner中的线程才可以调用notify()方法唤醒waitSet中的线程。
转换关系

当多个线程同时请求某个对象监视器时,对象监视器会设置几种状态用来区分请求的线程:
- Contention List:所有请求锁的线程将被首先放置到该竞争队列;
- Entry List:Contention List中那些有资格成为候选人的线程被移到Entry List;
- Wait Set:那些调用wait方法被阻塞的线程被放置到Wait Set;
- OnDeck:任何时刻最多只能有一个线程正在竞争锁,该线程称为OnDeck;
- Owner:获得锁的线程称为Owner;
- !Owner:释放锁的线程。
新请求锁的线程将首先被加入到Conetention List中,当某个拥有锁的线程(Owner状态)调用unlock之后,如果发现 EntryList为空则从Contention List中移动线程到EntryList
wait和notify对应的是monitor里面处于阻塞的线程。
API介绍
obj.wait()让进入object监视器的线程到waitSet等待obj.notify()在object上正在waitSet等待的线程中挑一个唤醒obj.notifyAll()让object上正在waitSet等待的线程全部唤醒
它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,成为owner之后,才能调用这几个方法
代码测试
public class Test18 {
public static Object object=new Object();
public static void main(String[] args) throws InterruptedException {
synchronized (object){
// object先获取锁之后,才可以调用wait进入waitSet队列等待
object.wait();
}
}
}API使用
public class Test18 {
public final static Object object=new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
synchronized (object){
System.out.println(Thread.currentThread().getName()+" 开始执行代码......");
try {
// t1线程被阻塞,进入阻塞队列中,也就是waitSet队列
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 执行其他的代码........");
}
},"t1");
t1.start();
Thread t2=new Thread(()->{
synchronized (object){
System.out.println(Thread.currentThread().getName()+" 开始执行代码......");
try {
// t2线程被阻塞,进入阻塞队列中,也就是waitSet队列
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 执行其他的代码........");
}
},"t2");
t2.start();
// 主线程在2秒后执行
Thread.sleep(2);
System.out.println(Thread.currentThread().getName()+" 唤醒object对象上其他的锁.......");
// 此时主线程获得锁
synchronized (object){
// object.notify();//唤醒阻塞队列中的一个线程
object.notifyAll();
}
}
}
//
t1 开始执行代码......
t2 开始执行代码......
main 唤醒object对象上其他的锁.......
t2 执行其他的代码........
t1 执行其他的代码........带时限的wait()
public class Test18 {
public final static Object object=new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
synchronized (object){
System.out.println(Thread.currentThread().getName()+" 开始执行代码......");
try {
// t1线程被阻塞,进入阻塞队列中,也就是waitSet队列
object.wait(2000);//等待两秒钟自动唤醒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 执行其他的代码........");
}
},"t1");
t1.start();
}
}wait()方法会释放对象的锁,进入WaitSet等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到notify为止wait(long n)有时限的等待, 到n毫秒后结束等待,或是被notify
正确使用wait、notify方法
sleep(long n) 和 wait(long n) 的区别
sleep是Thread静态方法,而wait是Object的方法 ,所有的对象都有wait()方法。sleep不需要强制和synchronized配合使用,但wait需要和synchronized一起用 ,因为wait首先需要获取对象的锁,草可以使用。而sleep()什么时候都可以使用。sleep在睡眠的同时,不会释放对象锁的,其他线程不能获取锁,但是会释放cpu的使用权,但wait在等待的时候会释放对象锁,也就是调用wait()方法会释放对象锁。所以使用wait()效率会更高。- 它们状态 TIMED_WAITING是一样的.也就是说都是有时限的等待。
锁的对象尽量添加
final关键字,保证对象的引用尽量不会改变
代码说明
public class Test19 {
private final static Object lock=new Object();
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
synchronized (lock){
try {
System.out.println("t1 线程获得锁");
// Thread.sleep(20000);不会释放锁
lock.wait();//会释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"t1").start();
// 现在主线程也尝试获取锁
Thread.sleep(2);
synchronized (lock){
System.out.println("主线程获取到了锁");
}
}
}
//输出
t1 线程获得锁
主线程获取到了锁step01
public class Test20 {
static final Object room=new Object();
static boolean hasCigarette=false;//代表是否有烟
static boolean hasTakeout=false;
public static void main(String[] args) throws InterruptedException {
//小南的线程是最先执行的
new Thread(()->{
synchronized (room){
System.out.println(Thread.currentThread().getName()+" 有没有烟:"+hasCigarette);
if(!hasCigarette){
System.out.println(Thread.currentThread().getName()+" 没有烟,先休息会");
try {
Thread.sleep(2);//不会释放锁,效率比较低
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+" 有没有烟:"+hasCigarette);
if(hasCigarette){
System.out.println(Thread.currentThread().getName()+" 开始去干活");
}
}
},"小南").start();
for(int i=0;i<5;i++){
new Thread(()->{
synchronized (room){
System.out.println(Thread.currentThread().getName()+" 开始干活去");
}
},"其他人").start();
}
// 1秒后开始送烟
Thread.sleep(1);
new Thread(()->{//送烟并不需要添加锁
synchronized(object){//在这里加锁也不能解决问题
System.out.println(Thread.currentThread().getName()+" 烟到了");
hasCigarette=true;
}
},"送烟").start();
}
}
小南 有没有烟:false
小南 没有烟,先休息会
送烟 烟到了
小南 有没有烟:true
小南 开始去干活
其他人 开始干活去
其他人 开始干活去
其他人 开始干活去
其他人 开始干活去
其他人 开始干活去- 不足之处
- 小南的线程必须睡足2秒,就算是烟提前送到,小南也不能干活。
- 在小南没有烟期间,因为使用的是
sleep,所以小南线程并没有释放锁,所以其他人也只能等待,小南线程结束之后其他人才可以干活,效率很低。 - 加了
synchronized (room)后,就好比小南在里面反锁了门睡觉,烟根本没法送进门,main没加synchronized就好像main线程是翻窗户进来的 - 解决方法,使用
wait - notify(主要是在等待的时候回释放锁) 机制
step02
public class Test20 {
static final Object room = new Object();
static boolean hasCigarette = false;//代表是否有烟
static boolean hasTakeout = false;
public static void main(String[] args) throws InterruptedException {
//小南的线程是最先执行的
new Thread(() -> {
synchronized (room) {
System.out.println(Thread.currentThread().getName() + " 有没有烟:" + hasCigarette);
if (!hasCigarette) {
System.out.println(Thread.currentThread().getName() + " 没有烟,先休息会");
try {
// Thread.sleep(2);
// wait方法抛出的异常在其他的方法调用interrupt打断时候抛出
room.wait();//会释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " 有没有烟:" + hasCigarette);
if (hasCigarette) {
System.out.println(Thread.currentThread().getName() + " 开始去干活");
}
}
}, "小南").start();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (room) {
System.out.println(Thread.currentThread().getName() + " 开始干活去");
}
}, "其他人").start();
}
// 1秒后开始送烟
Thread.sleep(1);
new Thread(() -> {
synchronized (room){
System.out.println(Thread.currentThread().getName() + " 烟到了");
hasCigarette = true;
// 烟送到后,然后叫醒小南线程
room.notify();
}
}, "送烟").start();
}
小南 有没有烟:false
小南 没有烟,先休息会
其他人 开始干活去
其他人 开始干活去
其他人 开始干活去
其他人 开始干活去
其他人 开始干活去
送烟 烟到了
小南 有没有烟:true
小南 开始去干活- 使用
wait最大的好处就是可以释放锁,小南等待的同事其他线程可以获取锁。 - 解决了其它干活的线程阻塞的问题
- 但如果有其它线程也在等待条件呢?
step03
public class Test20 {
static final Object room = new Object();
static boolean hasCigarette = false;//代表是否有烟
static boolean hasTakeout = false;
public static void main(String[] args) throws InterruptedException {
//小南的线程是最先执行的
new Thread(() -> {
synchronized (room) {
System.out.println(Thread.currentThread().getName() + " 有没有烟:" + hasCigarette);
if (!hasCigarette) {
System.out.println(Thread.currentThread().getName() + " 没有烟,先休息会");
try {
// Thread.sleep(2);
// wait方法抛出的异常在其他的方法调用interrupt打断时候抛出
room.wait();//会释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " 有没有烟:" + hasCigarette);
if (hasCigarette) {
System.out.println(Thread.currentThread().getName() + " 开始去干活");
}
}
}, "小南").start();
new Thread(() -> {
synchronized (room) {
System.out.println(Thread.currentThread().getName() + " 有没有外卖:" + hasTakeout);
if (!hasTakeout) {
System.out.println(Thread.currentThread().getName() + " 没有外卖,先休息会");
try {
// Thread.sleep(2);
// wait方法抛出的异常在其他的方法调用interrupt打断时候抛出
room.wait();//会释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " 有没有外卖:" + hasTakeout);
if (hasTakeout) {
System.out.println(Thread.currentThread().getName() + " 开始去干活");
}
}
}, "小女").start();
// 1秒后开始外卖
Thread.sleep(1);
new Thread(() -> {
synchronized (room){
System.out.println(Thread.currentThread().getName() + " 外卖到了");
hasTakeout = true;
room.notify();//随机叫醒的
//room.notifyAll();可以解决问题,但是又把全部线程都唤醒
}
}, "送外卖").start();
}
}
小南 有没有烟:false
小南 没有烟,先休息会
小女 有没有外卖:false
小女 没有外卖,先休息会
送外卖 外卖到了
小南 有没有烟:false
//虽然外卖送到了,但是唤醒的却是小南线程,唤醒线程错误notify只能随机唤醒一个entryList(blocking阻塞队列)中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程,称之为【虚假唤醒】- 解决方法,改为
notifyAll
step04
public class Test20 {
static final Object room = new Object();
static boolean hasCigarette = false;//代表是否有烟
static boolean hasTakeout = false;
public static void main(String[] args) throws InterruptedException {
//小南的线程是最先执行的
new Thread(() -> {
synchronized (room) {
System.out.println(Thread.currentThread().getName() + " 有没有烟:" + hasCigarette);
while (!hasCigarette) {//这里使用循环
System.out.println(Thread.currentThread().getName() + " 没有烟,先休息会");
try {
// Thread.sleep(2);
// wait方法抛出的异常在其他的方法调用interrupt打断时候抛出
room.wait();//会释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " 有没有烟:" + hasCigarette);
if (hasCigarette) {
System.out.println(Thread.currentThread().getName() + " 开始去干活");
}
}
}, "小南").start();
new Thread(() -> {
synchronized (room) {
System.out.println(Thread.currentThread().getName() + " 有没有外卖:" + hasTakeout);
if (!hasTakeout) {
System.out.println(Thread.currentThread().getName() + " 没有外卖,先休息会");
try {
// Thread.sleep(2);
// wait方法抛出的异常在其他的方法调用interrupt打断时候抛出
room.wait();//会释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " 有没有外卖:" + hasTakeout);
if (hasTakeout) {
System.out.println(Thread.currentThread().getName() + " 开始去干活");
}
}
}, "小女").start();
// 1秒后开始外卖
Thread.sleep(1);
new Thread(() -> {
synchronized (room){
System.out.println(Thread.currentThread().getName() + " 外卖到了");
hasTakeout = true;
// 烟送到后,然后叫醒小南线程
// room.notify();
//这里唤醒了所有的线程,但是烟没有送到,所以小南哪里西药使用一个循环去等待
room.notifyAll();
}
}, "送外卖").start();
}
}
小南 有没有烟:false
小南 没有烟,先休息会
小女 有没有外卖:false
小女 没有外卖,先休息会
送外卖 外卖到了
小女 有没有外卖:true
小女 开始去干活
小南 没有烟,先休息会
Process finished with exit code -1- 用
notifyAll仅解决某个线程的唤醒问题,但使用if + wait判断仅有一次机会,一旦条件不成立,就没有重新判断的机会了 - 解决方法,用
while + wait,当条件不成立,再次wait
小结
这不是操作系统中学习的信号量机制吗。
使用wait()和notify()的正确方法
synchronized(lock) {
while(条件不成立) {
lock.wait();
}
// 干活
}
//另一个线程
synchronized(lock) {
//为什么需要使用while()循环,因为这里直接唤醒所有的线程,可能存在虚假唤醒问题,也就是某些线程条件不满足,但是被唤醒,所以使用循环让其等待
lock.notifyAll();
}模式
同步模式之保护性暂停
定义
即 Guarded Suspension,用在一个线程等待另一个线程的执行结果,生产者消费者模式。 要点
- 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个
GuardedObject - 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者)
JDK中,join的实现、Future的实现,采用的就是此模式- 因为要等待另一方的结果,因此归类到同步模式

代码说明
public class Test21 {
public static void main(String[] args) {
//这个对象就是一把锁,两个线程共用锁
GuardedObject g=new GuardedObject();
// 线程t1 t2是同时执行的
new Thread(()->{
try {
// 线程1在等待获取结果
System.out.println(Thread.currentThread().getName()+" 正在开始获取结果");
Object o=g.get();
System.out.println(Thread.currentThread().getName()+" 获取的结果是: "+o.toString());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1").start();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" 产生数据结果");
g.complete("hrefgvdcxsvdcxs");
System.out.println(Thread.currentThread().getName()+" 产生结果完毕");
},"t2").start();
}
}
class GuardedObject{
// 代表将来的结果
private Object redponse;
/**
* 获取结果
* @return 返回获取的结果
*/
public Object get() throws InterruptedException {
// 第一步:先要拿到锁对象
synchronized (this){
while (redponse == null){//防止进行虚假唤醒
this.wait();//会释放锁
// 当有其他线程唤醒时,就退出循环
}
}
return redponse;
}
public void complete(Object o){
synchronized (this){
// 给结果变量进行赋值
this.redponse=o;
// 赋值完成后唤醒等待的线程,this就是一把锁对象
this.notifyAll();
}
}
}
t1 正在开始获取结果
t2 产生数据结果
t2 产生结果完毕
t1 获取的结果是: hrefgvdcxsvdcxs- 使用
join的局限性在于必须等待另一个线程执行结束,比如t2线程执行完毕后必须等待t1线程也执行完毕,但是使用保护性暂停模式的话,t2线程执行完成后可以做其他的事情,不必等待t1线程也执行结束 - 使用
join的话,等待结果的变量必须设置为全局的,但是使用保护性暂停模式的话,可以设置为局部的。
改进
上面不带参数的get方法的话,如果没有产生结果,那么就会陷入死等状态,可以设置一个最大的时间。一旦超过这个时间的话,没有等到结果,就退出。

join原理
源码解读
public final void join() throws InterruptedException {
join(0);//无参数的join实际上调用参数为0的有参数方法
}
//有参数的方法
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();//记录开始时间
long now = 0;//记录经历的时间
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {//如果时间等于0
while (isAlive()) {//判断线程是否存活
wait(0);
}
} else {
while (isAlive()) {//保护性暂停就是指的这里,如果条件不满足,就一直循环等待
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;//求一次经历时间
}
}
}
//wait()调用的是object的本地方法
public final native void wait(long timeout) throws InterruptedException;join源码就是用的保护性暂停模式- 保护性暂停模式是一个线程等待另一个线程的结果,join是一个线程等待另一个线程的结束。
扩展-多任务版 GuardedObject
图中 Futures 就好比居民楼一层的信箱(每个信箱有房间编号),左侧的 t0,t2,t4 就好比等待邮件的居民,右侧的 t1,t3,t5 就好比邮递员,如果需要在多个类之间使用 GuardedObject 对象,作为参数传递不是很方便,因此设计一个用来解耦的中间类,这样不仅能够解耦【结果等待者】和【结果生产者】,还能够同时支持多个任务的管理

代码
不好理解
public class Test21 {
public static void main(String[] args) throws InterruptedException {
// 产生三个居民进行收信
for(int i=0;i<3;i++){
new People().start();
}
// 一秒钟后开始送信
Thread.sleep(1);
for (Integer id : Mailboxes.getIds()) {
new Postman(id,"id+abcde").start();
}
}
}
class People extends Thread{
@Override
public void run() {
GuardedObject g=new GuardedObject();
try {
System.out.println("开始收信:"+g.getId());
Object o=g.get(5000);
System.out.println("信的内容:"+o.toString());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Postman extends Thread{
private int id;
private String mail;
public Postman(int id, String mail) {
this.id = id;
this.mail = mail;
}
@Override
public void run() {
// 送信的逻辑,根据id号码获取信件的内容
GuardedObject g=Mailboxes.getGuardedObject(id);
System.out.println("开始送信:"+id+" "+mail);
g.complete(mail);
}
}
class Mailboxes{
private static Map<Integer,GuardedObject> box=new Hashtable<>();
private static int id;
// 产生唯一的id
//多个线程会访问此方法,,所以需要保证唯一性
// 锁关键字添加在static方法上,相当于给类添加了锁
public static synchronized int generateId(){
return id++;
}
// 产生GuardedObject对象
public static GuardedObject createGuardedObject(){
GuardedObject go=new GuardedObject();
box.put(go.getId(),go);//box类是线程安全的
return go;
}
// 获取键值得集合
public static Set<Integer> getIds(){
return box.keySet();//box类是线程安全的
}
// 根据id获取信件的内容
public static GuardedObject getGuardedObject(Integer id){
// 在这里使用remove()意思是获取到一封信件后,应该把信件删除
return box.remove(id);
}
}
class GuardedObject{
private int id;
// 代表将来的结果
private Object redponse;
public void setId(int id) {
this.id = id;
}
public int getId() {
return id;
}
/**
* 带时限的等待
* @param timeOut 等待的时间
* @return 返回获取的结果
* @throws InterruptedException
*/
public Object get(long timeOut) throws InterruptedException {
// 第一步:先要拿到锁对象
synchronized (this){
// 记录一下开始的时间
long begin=System.currentTimeMillis();
// 经历的时间
long passTime=0;
while (redponse == null){//防止进行虚假唤醒
// 判断是否超时
if(passTime > timeOut){
System.out.println("产生超时");
break;
}
// timeOut-passTime防止线程被虚假唤醒
this.wait((timeOut-passTime));//会释放锁
// 当有其他线程唤醒时,就退出循环
// 获取经历时间
passTime=System.currentTimeMillis()-begin;
}
}
return redponse;
}
/**
* 获取结果
* @return 返回获取的结果
*/
public Object get() throws InterruptedException {
// 第一步:先要拿到锁对象
synchronized (this){
while (redponse == null){//防止进行虚假唤醒
this.wait();//会释放锁
// 当有其他线程唤醒时,就退出循环
}
}
return redponse;
}
public void complete(Object o){
synchronized (this){
// 给结果变量进行赋值
this.redponse=o;
// 赋值完成后唤醒等待的线程
this.notifyAll();
}
}
}上面这种模式,邮递员和居民是一对一的关系,而生产者消费者模式可以一对多,也就是一个生产者可以对应多个消费者。
生产者消费者模式-异步模式
定义
- 要点
- 与前面的保护性暂停中的 GuardObject 不同,不需要产生结果和消费结果的线程一一对应
- 消费队列可以用来平衡生产和消费的线程资源
- 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据
- 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
- JDK 中各种阻塞队列,采用的就是这种模式,当存放消息的队列满了之后,生产消息的进程就会被阻塞。

之所以叫做异步模式就是生产者生产的消息不需要被立即的消费掉,但是前面的保护性暂停模式,生产者生产的消息会被立即的消费掉,是同步的关系。保护性暂停是一一对应的关系。共同点都是在多个线程之间传递消息。
生产者消费者模式
public class Test {
public static void main(String[] args) throws InterruptedException {
MessageQueue messageQueue = new MessageQueue(2);
// 创建生产者线程
for (int i = 0; i < 3; i++) {
final int id=i;
new Thread(()->{
try {
messageQueue.put(new Message(id," message "+id));
} catch (InterruptedException e) {
e.printStackTrace();
}
},"producer"+id).start();
}
// 创建一个消费者线程
new Thread(()->{
// 消费者消费消息
try {
while (true){
Thread.sleep(2000);
Message message = messageQueue.take();
System.out.println(Thread.currentThread().getName()+" "+message);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"reducer").start();
}
}
/**
* 是java线程之间通信的类
*/
class MessageQueue{
// 创建集合存放消息
private LinkedList<Message>queue=new LinkedList();
// 队列的容量
private int capcity;
public MessageQueue(int capcity) {
this.capcity = capcity;
}
// 获取消息的方法
public Message take() throws InterruptedException {
// 首先检查队列是否空
// 这里的队列相当于一把锁
synchronized (queue){
while (queue.isEmpty()){
// 如果队列是空,那么就进入entryList后等待,直到队列不空为止
queue.wait();
}
Message message=queue.removeFirst();
// 通知生产者线程
queue.notifyAll();
// 返回队列头部的元素
return message;
}
}
// 存放消息的方法
public void put(Message message) throws InterruptedException {
synchronized (queue){
// 检查队列是否是满的
while (queue.size() == capcity){
// 如果满,就进入阻塞队列进行等待
queue.wait();
}
// 将新的消息添加到队列的尾部
queue.add(message);
// 唤醒等待消息的线程
queue.notifyAll();
}
}
}
//只有get()方法,说明对象的内部不可改变,只有构造的时候可以初始化
final class Message{
private int id;
private Object value;
@Override
public String toString() {
return "Message{" +
"id=" + id +
", value=" + value +
'}';
}
public Message(int id, Object value) {
this.id = id;
this.value = value;
}
public int getId() {
return id;
}
public Object getValue() {
return value;
}
}Park()和UnPark()
基本使用
他们是LockSupport()中的方法
//暂停当前的线程,那个线程调用park,就暂停哪一个方法
LockSupport.park()//也就是把此语句写在某个线程里面
//恢复某一个线程的运行
LockSupport.unpark(需要恢复的线程对象)park线程对应的状态是wait状态,也就是无时限的等待。
unpark既可以在park之前调用,也可以在park之后调用,unpark()就是用来恢复被暂停线程的运行。
案例
public class LockSupportTest {
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
int i=0;
while (true){
try {
System.out.println(Thread.currentThread().getName()+" "+i++);
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(i == 10){
LockSupport.park();
}
}
});
t1.start();
int j=0;
while (true){
try {
j++;
System.out.println(Thread.currentThread().getName()+" "+j);
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(j == 20){
//在这里唤醒被暂停的线程
LockSupport.unpark(t1);
}
}
}
}特点
与 Object 的 wait & notify 相比
wait,notify和notifyAll必须配合Object Monitor一起使用(也就是必须先获取对象的monitor锁),而park,unpark不必park & unpark是以线程为单位来【阻塞】和【唤醒】线程,而notify只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么【精确】- unpark()可以精确的唤醒某一个线程。
park & unpark可以先unpark,而wait & notify不能先notify
park和unpark底层原理
每个线程都有自己的一个 Parker 对象(底层是用c代码实现的),由三部分组成 _counter , _cond 和 _mutex 打个比喻
- 线程就像一个旅人,
Parker就像他随身携带的背包,条件变量(_cond)就好比背包中的帐篷。_counter就好比背包中的备用干粮(0 为耗尽,1 为充足) - 调用
park就是要看需不需要停下来歇息- 如果备用干粮耗尽,那么钻进帐篷歇息
- 如果备用干粮充足,那么不需停留,继续前进
- 调用
unpark,就好比令干粮充足- 如果这时线程还在帐篷,就唤醒让他继续前进
- 调用unpark()就是先让_counter变为1,也就是干粮变为充足,然后在叫醒线程接着执行。
- 如果这时线程还在运行,此时调用unpark()操作,那么这个时候就是相当于补充干粮,也就是把_counter()变为1。那么下次他调用
park时,仅是消耗掉备用干粮,不需停留继续前进- 因为背包空间有限,多次调用
unpark仅会补充一份备用干粮(也就是多次调用unpark,只会有一次起作用)
- 因为背包空间有限,多次调用
先调用park然后调用unpark的情况
调用park

- 当前线程调用
Unsafe.park()方法 - 检查
_counter,本情况为 0,这时,获得_mutex互斥锁 - 线程进入
_cond条件变量阻塞 - 设置
_counter = 0
调用unpark

- 调用
Unsafe.unpark(Thread_0)方法,首先设置_counter为 1 - 唤醒
_cond条件变量(可以认为是阻塞队列)中的 Thread_0,这种情况是线程在阻塞队列中时的情况。 Thread_0恢复运行- 设置
_counter为 0
先调用unpark后调用park

- 调用
Unsafe.unpark(Thread_0)方法,先试设置_counter为 1,也就是先补充能量。 - 当前线程调用
Unsafe.park()方法 - 检查
_counter,本情况为 1,这时线程无需阻塞,继续运行 - 设置
_counter为 0
重新理解线程之间的转换
- new:初始状态,仅仅创建java线程对象,并没有和操作系统的线程对象关联起来。当调用start()就可以和操作系统线程关联,由底层调度执行。
- running:java层面包含三个,运行状态的线程,等待调度的就绪现场,被操作系统阻塞起来的线程。
- watting:当线程获取到锁之后,调用wait()方法后就会从运行状态转换为watting状态。无时限的一直等待。

假设有线程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()时,因为线程被这三个方法调用的时候,但是唤醒之后不一定就是runnable,首先会进入entryList队列,等待某个线程释放锁之后去和之前竞争锁的线程一起竞争锁,所以有两种情况。- 竞争锁成功,t 线程从
WAITING --> RUNNABLE - 竞争锁失败,t 线程从
WAITING --> BLOCKED2,block状态是当线程无法获取锁的时候,会进入阻塞状态。block状态也就是线程没有竞争到sunchronized时候就会进入block状态。
- 竞争锁成功,t 线程从
代码说明
public class Test24 {
public static Object object=new Object();
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
synchronized (object){
System.out.println(Thread.currentThread().getName()+" 开始执行");
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 执行其他代码");
}
},"t1").start();
new Thread(()->{
synchronized (object){
System.out.println(Thread.currentThread().getName()+" 开始执行");
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 执行其他代码");
}
},"t2").start();
// 主线程先休息2秒
Thread.sleep(2);
synchronized (object){
// 主线程唤醒全部子线程
System.out.println(Thread.currentThread().getName()+" 唤醒全部线程");
object.notifyAll();
}
}
}情况 3 RUNNABLE <--> WAITING
当前线程调用
t.join()(当前线程等待线程t执行完成)方法时,当前线程从RUNNABLE --> WAITING注意是当前线程在
t线程对象的监视器上等待,比如想让主线程等待t线程执行完在执行,那么就在主线程中调用t线程的join方法。想让某一个线程等待,就在某个线程中调用另一个线程的join方法。此时主线程就会从running进入waiting等待。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
- 竞争锁成功,t 线程从
情况 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 (block线程在entryList队列中 )的线程重新竞争(也就是处于EntryList队列上面的线程),如果其中 t 线程竞争成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然 BLOCKED
情况 10 RUNNABLE <--> TERMINATED
- 当前线程所有代码运行完毕,进入 TERMINATED
多把锁
先看一个场景
多把不相干的锁 一间大屋子有两个功能:睡觉、学习,互不相干。 现在小南要学习,小女要睡觉,但如果只用一间屋子(一个对象锁)的话,那么并发度很低 解决方法是准备多个房间(多个对象锁)
代码说明
public class Test25 {
public static void main(String[] args) {
BigRoom b=new BigRoom();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" 开始休息");
try {
b.Sleep();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 休息好了");
},"t1").start();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" 开始学习");
try {
b.learning();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 学习好了");
},"t2").start();
}
}
//锁住整个对象的话线程之间的并发度很低,某一时刻t1需要睡觉,但是锁住的是整个对象,而t2不能获取该锁,所以不可以学习
//但是这两个线程不是互斥关系
class BigRoom{
public void Sleep() throws InterruptedException {
synchronized (this){
// 锁住的是当前的对象
System.out.println("休息两秒钟.....");
Thread.sleep(2);
}
}
public void learning() throws InterruptedException {
synchronized (this){
System.out.println("学习2秒钟.....");
Thread.sleep(2);
}
}
}解决方法
可以把大房间分成两个小房间,一个用来学习,一个用来休息,也就是申请多把锁对象。
代码说明
public class Test25 {
public static void main(String[] args) {
BigRoom b=new BigRoom();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" 开始休息");
try {
b.Sleep();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 休息好了");
},"t1").start();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" 开始学习");
try {
b.learning();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 学习好了");
},"t2").start();
}
}
//锁住整个对象的话线程之间的并发度很低,某一时刻t1需要睡觉,但是锁住的是整个对象,而t2不能获取该锁,所以不可以学习
//但是这两个线程不是互斥关系
class BigRoom{
//增加多把锁来提高程序的并发度,但是要保证多个线程之间的业务是没有关联的,也就是不是互斥关系
//相当于把一个大房子分成两个小房子
private final Object studyRoom=new Object();
private final Object sleepRoom=new Object();
public void Sleep() throws InterruptedException {
synchronized (sleepRoom){
// 锁住的是当前的对象
System.out.println("休息两秒钟.....");
Thread.sleep(2);
}
}
public void learning() throws InterruptedException {
synchronized (studyRoom){
System.out.println("学习2秒钟.....");
Thread.sleep(2);
}
}
}将锁的粒度细分
- 好处,是可以增强并发度
- 坏处,如果一个线程需要同时获得多把锁,就容易发生死锁
活跃性
活跃性保函三种情况:死锁,活锁,饥饿
死锁
- 有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁
t1线程 获得A对象 锁,接下来想获取B对象 的锁t2线程 获得B对象 锁,接下来想获取A对象 的锁 例:
代码说明
public class Test26 {
public static void main(String[] args) {
test01();
}
public static void test01(){
// 申请两把锁
Object lock01=new Object();
Object lock02=new Object();
Thread t1=new Thread(()->{
synchronized (lock01){
System.out.println(Thread.currentThread().getName()+" 获取了lock01");
try {
Thread.sleep(2);
System.out.println(Thread.currentThread().getName()+" 想获取lock02");
synchronized (lock02){
System.out.println(Thread.currentThread().getName()+" 获取lock02成功");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.setName("t1");
Thread t2=new Thread(()->{
synchronized (lock02){
System.out.println(Thread.currentThread().getName()+" 获取了lock02");
try {
Thread.sleep(1);
System.out.println(Thread.currentThread().getName()+" 想获取lock01");
synchronized (lock01){
System.out.println(Thread.currentThread().getName()+" 获取lock01成功");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t2.setName("t2");
t1.start();
t2.start();
}
}
//输出结果
t1 获取了lock01
t2 获取了lock02
t2 想获取lock01
t1 想获取lock02
//两个线程第二次获取锁都没有成功定位死锁
检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁:
cmd > jps
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
12320 Jps
22816 KotlinCompileDaemon
33200 TestDeadLock // JVM 进程
11508 Main
28468 Launcher
cmd > jstack 33200
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
2018-12-29 05:51:40
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.91-b14 mixed mode):
"DestroyJavaVM" #13 prio=5 os_prio=0 tid=0x0000000003525000 nid=0x2f60 waiting on condition
[0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Thread-1" #12 prio=5 os_prio=0 tid=0x000000001eb69000 nid=0xd40 waiting for monitor entry
[0x000000001f54f000]
java.lang.Thread.State: BLOCKED (on object monitor)//没有获取到锁,处于block状态
at thread.TestDeadLock.lambda$main$1(TestDeadLock.java:28)
- waiting to lock <0x000000076b5bf1c0> (a java.lang.Object)//等待的锁对象
- locked <0x000000076b5bf1d0> (a java.lang.Object)//自身获取的锁对象
at thread.TestDeadLock$$Lambda$2/883049899.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
"Thread-0" #11 prio=5 os_prio=0 tid=0x000000001eb68800 nid=0x1b28 waiting for monitor entry
[0x000000001f44f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at thread.TestDeadLock.lambda$main$0(TestDeadLock.java:15)
- waiting to lock <0x000000076b5bf1d0> (a java.lang.Object)
- locked <0x000000076b5bf1c0> (a java.lang.Object)
at thread.TestDeadLock$$Lambda$1/495053715.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
// 略去部分输出,jvm会列出死锁的线程
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x000000000361d378 (object 0x000000076b5bf1c0, a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x000000000361e768 (object 0x000000076b5bf1d0, a java.lang.Object),
which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1":
at thread.TestDeadLock.lambda$main$1(TestDeadLock.java:28)
- waiting to lock <0x000000076b5bf1c0> (a java.lang.Object)
- locked <0x000000076b5bf1d0> (a java.lang.Object)
at thread.TestDeadLock$$Lambda$2/883049899.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
"Thread-0":
at thread.TestDeadLock.lambda$main$0(TestDeadLock.java:15)
- waiting to lock <0x000000076b5bf1d0> (a java.lang.Object)
- locked <0x000000076b5bf1c0> (a java.lang.Object)
at thread.TestDeadLock$$Lambda$1/495053715.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
Found 1 deadlock.- 避免死锁要注意加锁顺序
- 另外如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后再用 jstack 排查
哲学家就餐问题
图示

有五位哲学家,围坐在圆桌旁。
- 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
- 吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。
- 如果筷子被身边的人拿着,自己就得等待
代码说明
public class Test27 {
public static void main(String[] args) {
// 创建5个筷子
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();
}
}
class Philosopher extends Thread{
Chopstick left;
Chopstick right;
public Philosopher(String name,Chopstick left,Chopstick right){
super(name);
this.left=left;
this.right=right;
}
public void eat() throws InterruptedException {
System.out.println(Thread.currentThread().getName()+" 开始吃饭");
Thread.sleep(2);
}
@Override
public void run() {
while (true){
// 尝试获取左边的筷子
synchronized (left){
// 尝试获取右边的筷子
synchronized (right){
try {
eat();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
class Chopstick{
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "Chopstick{" +
"name='" + name + '\'' +
'}';
}
}- 可以使用 jconsole 检测死锁
活锁
活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束,例如
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) {
Thread.sleep(0.2);
count--;
log.debug("count: {}", count);
}
}, "t1").start();
new Thread(() -> {
// 期望超过 20 退出循环
while (count < 20) {
Thread.sleep(0.2);
count++;
log.debug("count: {}", count);
}
}, "t2").start();
}
}- 活锁如何解决,让线程睡眠的时间是一个随机数即可。
饥饿
很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束,饥饿的情况不易演示,讲读写锁时会涉及饥饿问题
下面我讲一下我遇到的一个线程饥饿的例子,先来看看使用顺序加锁的方式解决之前的死锁问题
死锁

顺序加锁
- 也就是让某一个线程按顺序获取他需要的锁,等他把锁释放后,其他的线程在获取锁。
顺序加锁解决哲学家问题
public class Test27 {
public static void main(String[] args) {
// 创建5个筷子
Chopstick c1=new Chopstick("1");
Chopstick c2=new Chopstick("2");
Chopstick c3=new Chopstick("3");
Chopstick c4=new Chopstick("4");
Chopstick c5=new Chopstick("5");
//哲学家拿筷子是顺序拿取
new Philosopher("苏格拉底",c1,c2).start();
new Philosopher("柏拉图",c2,c3).start();
new Philosopher("亚里士多德",c3,c4).start();
new Philosopher("牛顿",c4,c5).start();
new Philosopher("阿基米德",c1,c5).start();
}
}
class Philosopher extends Thread{
Chopstick left;
Chopstick right;
public Philosopher(String name,Chopstick left,Chopstick right){
super(name);
this.left=left;
this.right=right;
}
public void eat() throws InterruptedException {
System.out.println(Thread.currentThread().getName()+" 开始吃饭");
Thread.sleep(2);
}
@Override
public void run() {
while (true){
// 尝试获取左边的筷子
synchronized (left){
// 尝试获取右边的筷子
synchronized (right){
try {
eat();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
class Chopstick{
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "Chopstick{" +
"name='" + name + '\'' +
'}';
}
}ReentrantLock
可重入锁。
相对于 synchronized 它具备如下特点
- 可中断,但是synchronized锁就不能被中断。
- 可以设置超时时间(设置一个等待时间,如果时间内没有获取到锁,就放弃争抢锁,执行其他的逻辑)
- 可以设置为公平锁(防止发生饥饿,比如先到先得)
- 支持多个条件变量(也就是说有多个等待队列,因为不同条件发生的等待被放入不同的等待队列),而synchronized是所条件引起的等待都去wait队列中等待。
- 与synchronized相同之处是都支持可重入,也就是同一个线程对同一个锁对象多次添加锁。
- synchronized是关键字级别保护临界区资源,而ReentrantLock是在对象级别进行保护。
继承关系

语法说明
//创建对象
// 调用lock()方法获取锁
reentrantLock.lock();//加锁在try里面或者外面都可以
try {
// 临界区
} finally {
// 释放锁
reentrantLock.unlock();
}
//加锁和解锁是成对出现可重入特性
可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住,synchronize和ReentrantLock都是可重入的锁
代码说明
public class Test28 {
// 获取对象
private static ReentrantLock reentrantLock=new ReentrantLock();
public static void main(String[] args) {
// 获取锁
reentrantLock.lock();
try {
System.out.println("main get lock");
m1();
} finally {
//解锁
reentrantLock.unlock();
}
}
public static void m1(){
//加锁
//锁重入
try {
//再次获取锁,相当于锁重入
reentrantLock.lock();
System.out.println("m1 get lock");
m2();
}finally {
reentrantLock.unlock();
}
}
public static void m2(){
//加锁
//锁重入
try {
reentrantLock.lock();
System.out.println("m2 get lock");
}finally {
reentrantLock.unlock();
}
}
}
//上面的锁重入都是同一个线程在获取锁操作可打断性特性
代码说明
public class Test29 {
private static ReentrantLock r=new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
System.out.println("尝试获取锁");
try {
// 如果没有竞争,此方法就会获取对象锁,如果有竞争,就会进入阻塞队列
// 可以被其他线程使用interrupt方法进行打断,不要在等下去
r.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("没有获取到锁");
return;
}
try {
System.out.println("获取到了锁");
}finally {
r.unlock();
}
},"t1");
// 先让主线程获取锁
r.lock();
thread.start();
Thread.sleep(2);
// 主线程打断t1线程
thread.interrupt();
}
}如果使用的是lock()模式,即使去打断,也不会真正的打断。
超时特性
代码说明
public class Test30 {
public static ReentrantLock lock=new ReentrantLock();
public static void main(String[] args) {
Thread thread = new Thread(() -> {
// 尝试去获取锁,会返回一个boolean值表示是否获取成功
try {
System.out.println(Thread.currentThread().getName()+" 尝试获取锁");
if(!lock.tryLock(1, TimeUnit.SECONDS)){
// 没有带参数时间的tryLock(),获取不到锁,会立刻返回
// 带参数的锁表示等待一段时间,如果换没有获取到锁,就返回
System.out.println(Thread.currentThread().getName()+" 没有获取到锁");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println(Thread.currentThread().getName()+" 获取不到锁");
return;
}
// 如果获取成功,就执行临界区的代码
try {
System.out.println(Thread.currentThread().getName()+" 获取锁成功,执行临界区代码");
}finally {
lock.unlock();
}
},"t1");
thread.start();
// 有竞争,先让主线程获取锁
lock.tryLock();
System.out.println(Thread.currentThread().getName()+" 获取到了锁");
}
}哲学家就餐问题
public class Test27 {
public static void main(String[] args) {
// 创建5个筷子
Chopstick c1=new Chopstick("1");
Chopstick c2=new Chopstick("2");
Chopstick c3=new Chopstick("3");
Chopstick c4=new Chopstick("4");
Chopstick c5=new Chopstick("5");
new Philosopher("苏格拉底",c1,c2).start();
new Philosopher("柏拉图",c2,c3).start();
new Philosopher("亚里士多德",c3,c4).start();
new Philosopher("牛顿",c4,c5).start();
new Philosopher("阿基米德",c1,c5).start();
}
}
class Philosopher extends Thread{
Chopstick left;
Chopstick right;
public Philosopher(String name,Chopstick left,Chopstick right){
super(name);
this.left=left;
this.right=right;
}
public void eat() throws InterruptedException {
System.out.println(Thread.currentThread().getName()+" 开始吃饭");
Thread.sleep(2);
}
@Override
public void run() {
while (true){
// 尝试获取左边的筷子,使用此方法表示获取不到左边的筷子时候,就放弃等待
if(left.tryLock()){
try{
// 尝试获取右手的筷子
if(right.tryLock()){
try{
//如果可以走到这里,说明两把筷子都拿到了,所以就可以去吃饭
eat();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
right.unlock();
}
}
}finally {
// 如果某一个哲学家获取锁没有成功,就会释放自己收中的锁,可以避免死锁
left.unlock();
}
}
}
}
}
class Chopstick extends ReentrantLock {
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "Chopstick{" +
"name='" + name + '\'' +
'}';
}
}公平锁特性
- synchronized就是不公平的锁,当一个线程持有锁的时候,其他的线程就会进入阻塞队列进行等待,但是当线程把锁释放之后,其他线程是抢占式的获取锁,没有遵循先来先得的原则,可能会发生饥饿现象,所以是不公平的。
- ReentrantLock也是不公平的锁,但是可以通过设置成为公平的锁。公平锁一般没有必要,会降低并发度,公平锁是用来解决饥饿为题的。
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
}
//默认是非公平的锁条件变量特性
Locj接口
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
//条件变量
Condition newCondition();
}条件变量接口
public interface Condition {
//调用此方法,线程就会进入条件变量等待
void await() throws InterruptedException;
void awaitUninterruptibly();
//有时限的等待
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
//唤醒条件变量中的某一个线程
void signal();
//唤醒条件变量中所有的等待线程
void signalAll();
}synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比
- synchronized 是那些不满足条件的线程都在一间休息室等消息
- 而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒
使用要点
- await 前需要获得锁
- await 执行后,会释放锁,进入 conditionObject 等待
- await 的线程被唤醒(或打断、或超时)后重新竞争 lock 锁
- 竞争 lock 锁成功后,从 await 后继续执行
代码演示
public class Test31 {
public static ReentrantLock lock=new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
// 返回一个条件变量,可以看做休息室,通一把锁可以有多个条件变量
Condition condition = lock.newCondition();
Condition condition1 = lock.newCondition();
// 必须先加锁,在进入休息室,想进入哪一个休息室,就调用哪一个条件变量
lock.lock();
condition.wait();
// 其他线程想叫醒你,可以调用下面方法
condition.signal();
// 也可以把某一个条件变量中的所有线程全部唤醒
condition.signalAll();
}
}
//获取条件变量的方法
public Condition newCondition() {
return sync.newCondition();
}条件变量案例
public class Test32 {
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) throws InterruptedException {
new Thread(() -> {
try {
// 添加锁
lock.lock();
// 判断是否有烟
System.out.println(Thread.currentThread().getName()+" 有没有烟 "+hasCigrette);
while (!hasCigrette) {
try {
// 如果没烟,就添加到等烟的休息室
System.out.println(Thread.currentThread().getName()+" 进入休息室等待");
waitCigaretteQueue.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+" 等到了它的烟");
} finally {
// 解锁操作
lock.unlock();
}
}).start();
new Thread(() -> {
try {
lock.lock();
System.out.println(Thread.currentThread().getName()+" 有没有早餐 "+hasBreakfast);
while (!hasBreakfast) {
try {
System.out.println(Thread.currentThread().getName()+" 进入休息室等待");
waitbreakfastQueue.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+" 等到了它的早餐");
} finally {
lock.unlock();
}
}).start();
Thread.sleep(1);
sendBreakfast();
Thread.sleep(1);
sendCigarette();
}
private static void sendCigarette() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+" 送烟来了");
hasCigrette = true;
waitCigaretteQueue.signal();
} finally {
lock.unlock();
}
}
private static void sendBreakfast() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+" 送早餐来了");
hasBreakfast = true;
waitbreakfastQueue.signal();
} finally {
lock.unlock();
}
}
}模式
同步模式之顺序控制
固定运行顺序
案例
先2后1打印
notify/wait版本实现
public class Test33 {
static final Object lock=new Object();
// 表示t2线程是否运行过
static boolean t2Runed=false;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lock){
// 先获取锁对象
while (!t2Runed){
try {
// t1线程进入wait后就会释放锁,此时t2线程可以获取锁
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("1");
}
}, "t1");
Thread t2 = new Thread(() -> {
// t2线程先获取锁
synchronized (lock){
System.out.println("2");
// 表示t2线程已经运行过
t2Runed=true;
// 唤醒t1线程
lock.notify();
}
}, "t2");
// 如果不加控制,那么系统调用t1 t2线程之间的顺序是不确定的
t1.start();
t2.start();
}
}park和unpark实现
public class Test34 {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
// 先把t1线程挂起
LockSupport.park();
System.out.println("1");
}, "t1");
Thread t2 = new Thread(() -> {
System.out.println("2");
// 唤醒正在阻塞队列的t1线程
LockSupport.unpark(t1);
}, "t2");
t1.start();
t2.start();
}
}ReentrantLock方法
交替输出
三个线程交替输出a,b,c各5次。
小结
本章我们需要重点掌握的是
- 分析多线程访问共享资源时,哪些代码片段属于临界区(多个线程对某一段代码既有读又有写得是临界区),有两种方法实现对临界区的保护,sychronized和ReentrantLock。
- 使用 synchronized 互斥解决临界区的线程安全问题
- 掌握 synchronized 锁对象语法
- 掌握 synchronzied 加载成员方法(锁住this对象)和静态方法(锁住class对象)语法
- 掌握 wait/notify 同步方法
- 互斥是保护临界区资源由于线程的上下文切换而产生指令交错,保证临界区代码的原子性,而同步时保证线程由于条件不满足而产生等待,等条件恢复后就继续运行。
- 使用 lock(指的是ReentrantLock锁) 互斥解决临界区的线程安全问题,比synchronized功能强大。
- 掌握 lock 的使用细节:可打断、锁超时(保证不会产生死等)、公平锁(sychronized和ReentrantLock默认非公平)、条件变量
- 学会分析变量的线程安全性、掌握常见线程安全类的使用
- 了解线程活跃性问题:死锁、活锁、饥饿
- 应用方面
- 互斥:使用 synchronized 或 Lock 达到共享资源互斥效果
- 同步:使用 wait/notify 或 Lock 的条件变量来达到线程间通信效果
- 原理方面
- monitor、synchronized 、wait/notify 原理
- synchronized 进阶原理
- park & unpark 原理
- 模式方面
- 同步模式之保护性暂停(一一对应关系)
- 异步模式之生产者消费者(非一一对应关系)
- 同步模式之顺序控制
- 使用 synchronized 互斥解决临界区的线程安全问题
贡献者
版权所有
版权归属:codingLab
许可证:bugcode