3.java多线程学习笔记之共享模型之管程

3.1.线程共享变量问题分析

两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?

    static int counter = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                counter++;
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                counter--;
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.debug("{}",counter);
    }

问题分析

以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作,要彻底理 解,必须从字节码来进行分析

例如对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i

而对应 i-- 也是类似:

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i

而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:

image-20211212222422040

如果是单线程以上 8 行代码是顺序执行(不会交错)没有问题:

image-20211116190827088

多线程情况下出现负数的情况:

image-20211116190958729

多线程情况下出现正数的情况:

image-20211116191059317

临界区 Critical Section

  • 一个程序运行多个线程本身是没有问题的

  • 问题出在多个线程访问共享资源

    • 多个线程读共享资源其实也没有问题

    • 在多个线程对共享资源读写操作时发生指令交错,就会出现问题

  • 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

例如,下面代码中的临界区

static int counter = 0;
static void increment() 
// 临界区
{ 
 counter++;
}
static void decrement() 
// 临界区
{ 
 counter--;
}

竞态条件 Race Condition

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

3.2.synchronized 解决方案

应用之互斥

为了避免临界区的竞态条件发生,有多种手段可以达到目的。

  • 阻塞式的解决方案:synchronized,Lock

  • 非阻塞式的解决方案:原子变量 本次课使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一 时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁 的线程可以安全的执行临界区内的代码,不用担心线程上下文切换

注意

虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:

  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码

  • 同步是由于线程执行的先后、顺序不同、需要

一个线程等待其它线程运行到某个点

synchronized 语法

 synchronized(对象) // 线程1, 线程2(blocked)
 {
     临界区 
 }

使用synchronized解决上面代码数据共享安全问题:

@Slf4j
public class Test1 {
    static int counter = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                // 不要错误理解为锁住了对象就能一直执行下去
        		synchronized (Test1.class){
                 counter++;
             }
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                synchronized (Test1.class){
                    counter--;
                }
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.debug("{}",counter);
    }
}

1583571633729

思考

synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切 换所打断。 为了加深理解,请思考下面的问题

  • 如果把 synchronized(obj) 放在 for 循环的外面,如何理解? for循环不执行完,对象锁不会释放,其他线程无法访问
  • 原子性 如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎样运作? 锁的对象不一致,无法保证原子性
  • 锁对象 如果 t1 synchronized(obj) 而 t2 没有加会怎么样?如何理解? t2不会获取对象锁,不会进行阻塞,照常执行

使用面向对象思想进行改进:

   public static void main(String[] args) throws InterruptedException {
        Room room = new Room();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
           room.increment();
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
               room.decrement();
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.debug("{}",room.get());
    }
-------------------------------------------------------------------------------------------------------------------
public class Room {
    int value = 0;
    public void increment() {
        synchronized (this) {
            value++;
        }
    }
    public void decrement() {
        synchronized (this) {
            value--;
        }
    }
    public int get() {
        synchronized (this) {
            return value;
        }
    }

}

3.3.方法上的synchronized

成员方法上锁 this

    class Test{
        public synchronized void test() {

        }
    }
    //等价于
    class Test{
        public void test() {
            synchronized(this) {

            }
        }
    }

静态方法上锁当前类的Class对象

    class Test{
        public synchronized static void test() {
        }
    }
    //等价于
    class Test{
        public static void test() {
            synchronized(Test.class) {

            }
        }
    }

不加synchronized的方法,无需获取对象锁,异步执行,不能保证原子性

3.4.线程八锁

其实就是考察 synchronized 锁住的是哪个对象,是否是锁的同一个对象

以下的情况都要考虑是那个线程先抢到cpu执行权

情况1:输出:12或21

class Number{
 public synchronized void a() {
 	log.debug("1");
 }
 public synchronized void b() {
 	log.debug("2");
 }
}
public static void main(String[] args) {
 Number n1 = new Number();
 new Thread(()->{ n1.a(); }).start();
 new Thread(()->{ n1.b(); }).start();
}

情况2:1秒后 12 或 2 1秒后1

class Number{
 public synchronized void a() {
 	sleep(1);
	 log.debug("1");
 }
 public synchronized void b() {
	 log.debug("2");
 }
}
public static void main(String[] args) {
 Number n1 = new Number();
 new Thread(()->{ n1.a(); }).start();
 new Thread(()->{ n1.b(); }).start();
}
// 锁住的都是this,需要进行同步等待,就看谁先获取到cpu执行权

情况3:输出:3 1秒后12 或 23 1秒后 1 或 32 1秒后 1

class Number{
 public synchronized void a() {
	 sleep(1);
	 log.debug("1");
 }
 public synchronized void b() {
 	log.debug("2");
 }
 public void c() {
 	log.debug("3");
 }
}
public static void main(String[] args) {
 Number n1 = new Number();
 new Thread(()->{ n1.a(); }).start();
 new Thread(()->{ n1.b(); }).start();
 new Thread(()->{ n1.c(); }).start();
}
// n1.a();和n1.b();锁的都是 this对象,n1.c();未加锁无需进行同步等待

情况4:输出:2 1秒后 1

class Number{
 public synchronized void a() {
	 sleep(1);
	 log.debug("1");
 }
 public synchronized void b() {
	 log.debug("2");
 }
}
public static void main(String[] args) {
 Number n1 = new Number();
 Number n2 = new Number();
 new Thread(()->{ n1.a(); }).start();
 new Thread(()->{ n2.b(); }).start();
}
// 锁的不是同一个对象,不会产生互斥

情况5:输出:2 1秒后 1

class Number{
 public static synchronized void a() {
 	sleep(1);
 	log.debug("1");
 }
 public synchronized void b() {
	 log.debug("2");
 }
    }
public static void main(String[] args) {
 Number n1 = new Number();
 new Thread(()->{ n1.a(); }).start();
 new Thread(()->{ n1.b(); }).start();
}
// 锁的不是同一个对象,不会产生互斥,一个锁的this,一个锁的Number.class

情况6:输出:1秒后 12, 或 2 1秒后 1

class Number{
 public static synchronized void a() {
 	sleep(1);
	 log.debug("1");
 }
 public static synchronized void b() {
 	log.debug("2");
 }
}
public static void main(String[] args) {
 Number n1 = new Number();
 new Thread(()->{ n1.a(); }).start();
 new Thread(()->{ n1.b(); }).start();
}
// 锁的都是Number.class对象

情况7::2 1秒后 1

class Number{
 public static synchronized void a() {
 	sleep(1);
 	log.debug("1");
 }
 public synchronized void b() {
 	log.debug("2");
 }
}
public static void main(String[] args) {
 Number n1 = new Number();
 Number n2 = new Number();
 new Thread(()->{ n1.a(); }).start();
 new Thread(()->{ n2.b(); }).start();
}
// 锁的对象不一致,一个锁的是this,一个锁住的是Number.class

情况8::1秒后12, 或 2 1秒后 1

class Number{
 public static synchronized void a() {
 	sleep(1);
 	log.debug("1");
 }
 public static synchronized void b() {
	 log.debug("2");
 }
}
public static void main(String[] args) {
 Number n1 = new Number();
 Number n2 = new Number();
 new Thread(()->{ n1.a(); }).start();
 new Thread(()->{ n2.b(); }).start();
}
// 锁住的都是Number.class,就看谁先拿到cpu执行权

3.5.变量的线程安全分析

成员变量和静态变量是否线程安全?

  • 如果它们没有共享,则线程安全

  • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况

    • 如果只有读操作,则线程安全
    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

局部变量是否线程安全?

局部变量是线程安全的

局部变量引用的对象则未必

  • 如果该对象没有逃离方法的作用访问,它是线程安全的

  • 如果该对象逃离方法的作用范围,需要考虑线程安全

局部变量线程安全分析

public static void test1() {
	 int i = 10;
 	i++;
}	

每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享

虚拟机栈

  • 栈:线程运行需要的内存空间,每个线程是独有的
  • 栈帧:每一个方法运行需要的内存(包括参数,局部变量,返回地址等信息)
  • 每个线程只有一 个活动栈帧(栈顶的栈帧),对应着正在执行的代码

对应的字节码文件:

public static void test1();
 descriptor: ()V
 flags: ACC_PUBLIC, ACC_STATIC
 Code:
 stack=1, locals=1, args_size=0
 0: bipush 10
 2: istore_0
 3: iinc 0, 1
 6: return
 LineNumberTable:
 line 10: 0
 line 11: 3
 line 12: 6
 LocalVariableTable:
 Start Length Slot Name Signature
 3 4 0 i I

image-20211117004848793

成员变量的例子

class ThreadUnsafe {
 ArrayList<String> list = new ArrayList<>();
 public void method1(int loopNumber) {
 	for (int i = 0; i < loopNumber; i++) {
 		// { 临界区, 会产生竞态条件
 		method2();
 		method3();
     // } 临界区
	 }
 }
 private void method2() {
	 list.add("1");
 }
 private void method3() {
	 list.remove(0);
 }
}

执行

static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
	 ThreadUnsafe test = new ThreadUnsafe();
 	for (int i = 0; i < THREAD_NUMBER; i++) {
 		new Thread(() -> {
	 		test.method1(LOOP_NUMBER);
 		}, "Thread" + i).start();
 	}
}

其中一种情况是,如果线程2 还未 add,线程1 remove 就会报错:

Exception in thread "Thread0" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
	at java.util.ArrayList.rangeCheck(ArrayList.java:657)
	at java.util.ArrayList.remove(ArrayList.java:496)
	at com.compass.test.ThreadUnsafe.method3(ThreadUnsafe.java:30)
	at com.compass.test.ThreadUnsafe.method1(ThreadUnsafe.java:22)
	at com.compass.test.ThreadUnsafe.lambda$main$0(ThreadUnsafe.java:41)
	at java.lang.Thread.run(Thread.java:748)

分析:

  • 无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量
  • method3 与 method2 分析相同
  • list是一个不安全的集合,内部对add,remove方法都没有实现线程安全机制,有的线程还没有来得及add,而别的线程又进行remove,就会出现一个IndexOutOfBoundsException异常

image-20211117010006803

将 list 修改为局部变量

    class ThreadSafe {
        public final void method1(int loopNumber) {
            ArrayList<String> list = new ArrayList<>();
            for (int i = 0; i < loopNumber; i++) {
                method2(list);
                method3(list);
            }
        }
        private void method2(ArrayList<String> list) {
            list.add("1");
        }
        private void method3(ArrayList<String> list) {
            list.remove(0);
        }
    }

那么就不会有上述问题了,因为每个线程都有自己的栈,list集合由每个线程自己的栈帧去创建,list集合不再是共享变量,也就不存在多线程安全问题。

分析:

  • list 是局部变量,每个线程调用时会创建其不同实例,没有共享
  • 而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象
  • method3 的参数分析与 method2 相同

image-20211117010746683

方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public 会不会代理线程安全问题?

情况1:有其它线程调用 method2 和 method3

情况2:在 情况1 的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法

class ThreadSafe {
    public final void method1(int loopNumber) {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < loopNumber; i++) {
            method2(list);
            method3(list);
        }
    }
    public void method2(ArrayList<String> list) {
        list.add("1");
    }
    public void method3(ArrayList<String> list) {
        list.remove(0);
    }
}

class ThreadSafeSubClass extends ThreadSafe{
    // 新建一个线程去执行remove操作,
    @Override
    public void method3(ArrayList<String> list) {
        new Thread(() -> {
            list.remove(0);
        }).start();
    }
}

可以看出 private 或 final 提供【安全】的意义所在,避免子类对父类的一些方法进行重写,导致可能出现多线程安全问题

常见线程安全类

  • String
  • Integer
  • StringBuffer
  • Random
  • Vector
  • Hashtable
  • java.util.concurrent 包下的类

这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为

Hashtable table = new Hashtable();

new Thread(()->{
 	table.put("key", "value1");
}).start();

new Thread(()->{
 	table.put("key", "value2");
}).start();
  • 它们的每个方法是原子的
  • 但注意它们多个方法的组合不是原子的,见后面分析

线程安全类方法的组合

分析下面代码是否线程安全?

回答:不安全,会受到线程上下文切换的影响,有可能两个线程都同时进入了get方法获取到的都是null,后续线程1设置put了一个值,线程2又put一个值将线程1put的值进行了覆盖。

public void setVale(){
    Hashtable table = new Hashtable();
	// 线程1,线程2
    if( table.get("key") == null) {
        table.put("key", value);
    }		
}

image-20211117012913785

不可变类线程安全性

String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的

有同学或许有疑问,String 有 replace,substring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安 全的呢?

**String类安全的原因:**只能读不能修改,所以是线程安全的,在执行substring()时并没有该有String对象的属性而是新建一个新的字符串来进行操作。

    public String substring(int beginIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        int subLen = value.length - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        // 新建了一个新的字符串
        return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
    }
    public class Immutable{
        private int value = 0;
        public Immutable(int value){
            this.value = value;
        }
        public int getValue(){
            return this.value;
        }
    }

如果想新增一个方法呢?

    public class Immutable{
        private int value = 0;
        public Immutable(int value){
            this.value = value;
        }
        public int getValue(){
            return this.value;
        }

        public Immutable add(int v){
            return new Immutable(this.value + v);
        }
    }

实例分析

例1:Servlet单实例,会被Tomcat的多个线程所共享使用,所以他的成员变量都可能会有线程安全问题。

    public class MyServlet extends HttpServlet {
        // 是否安全? 不是线程安全的,因为HashMap是线程不安全的
        Map<String,Object> map = new HashMap<>();
        // 是否安全? 是的,是不可变类
        String S1 = "...";
        // 是否安全?是的,是不可变类
        final String S2 = "...";
        // 是否安全? 不是线程安全的
        Date D1 = new Date();
        // 是否安全? 不是线程安全的,虽然不能对D2引用值进行修改,但是可以对他的属性修改
        final Date D2 = new Date();

        public void doGet(HttpServletRequest request, HttpServletResponse response) {
            // 使用上述变量
        }
    }

例2:

public class MyServlet extends HttpServlet {
    // 是否安全? 不是线程安全的,属于共享资源,涉及到数据修改
    private UserService userService = new UserServiceImpl();

    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        userService.update(...);
    }
}
public class UserServiceImpl implements UserService {
    // 记录调用次数
    private int count = 0;

    public void update() {
        // ...
        count++;
    }
}

例3:

@Aspect
@Component
public class MyAspect {
    // 是否安全?不是线程安全的,spring默认都是单实例的,设计到对共享资源的一个数据修改
    private long start = 0L;

    @Before("execution(* *(..))")
    public void before() {
        start = System.nanoTime();
    }

    @After("execution(* *(..))")
    public void after() {
        long end = System.nanoTime();
        System.out.println("cost time:" + (end-start));
    }
}
// 解决方案:做成环绕通知,做成局部变量

// 如果配置为多实例?也是不可以的,进入前置通知是一个对象,进入后置通知又是别的对象,那统计的时间就不正确

例4:

public class MyServlet extends HttpServlet {
    // 是否安全 线程安全的,因为没有涉及到共享数据的修改
    private UserService userService = new UserServiceImpl();

    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        userService.update(...);
    }
}
public class UserServiceImpl implements UserService {
    // 是否安全 线程安全的,因为没有涉及到共享数据的修改
    private UserDao userDao = new UserDaoImpl();

    public void update() {
        userDao.update();
    }
}
public class UserDaoImpl implements UserDao {
    public void update() {
        String sql = "update user set password = ? where username = ?";
        // 是否安全 线程安全的,Connection属于局部变量,每次都新建,属于每个线程独有的
        try (Connection conn = DriverManager.getConnection("","","")){
            // ...
        } catch (Exception e) {
            // ...
        }
    }

}

例5:

public class MyServlet extends HttpServlet {
    // 是否安全 是线程安全的,而且是私有的,没有修改共享数据的地方
    private UserService userService = new UserServiceImpl();

    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        userService.update(...);
    }
}
public class UserServiceImpl implements UserService {
    // 是否安全 是线程安全的,而且是私有的,没有修改共享数据的地方
    private UserDao userDao = new UserDaoImpl();

    public void update() {
        userDao.update();
    }
}
public class UserDaoImpl implements UserDao {
    // 是否安全 线程不安全,做成成员变量,被多个线程共享,有可能别的线程刚刚创建好久被close()掉了
    private Connection conn = null;
    public void update() throws SQLException {
        String sql = "update user set password = ? where username = ?";
        conn = DriverManager.getConnection("","","");
        // ...
        conn.close();
    }
}

例6:

public class MyServlet extends HttpServlet {
    // 是否安全
    private UserService userService = new UserServiceImpl();

    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        userService.update(...);
    }
}
public class UserServiceImpl implements UserService {
    public void update() {
        // 每次线程来的时候都新建一个userDao对象,不存在线程安全问题[不推荐]
        UserDao userDao = new UserDaoImpl();
        userDao.update();
    }
}
public class UserDaoImpl implements UserDao {
    // 是否安全 没有线程安全问题 
    private Connection = null;
    public void update() throws SQLException {
        String sql = "update user set password = ? where username = ?";
        conn = DriverManager.getConnection("","","");
        // ...
        conn.close();
    }
}

例7:

public abstract class Test {

    public void bar() {
        // 是否安全 线程不安全 变量的引用泄漏
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        foo(sdf);
    }

    public abstract foo(SimpleDateFormat sdf);


    public static void main(String[] args) {
        new Test().bar();
    }
}

其中 foo 的行为是不确定的,可能导致不安全的发生,被称之为外星方法

    public void foo(SimpleDateFormat sdf) {
        String dateStr = "1999-10-11 00:00:00";
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                try {
                    sdf.parse(dateStr);
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }

卖票案例:

@Slf4j
public class ExerciseSell {
    public static void main(String[] args) {
        TicketWindow ticketWindow = new TicketWindow(2000);

        List<Thread> list = new ArrayList<>();
        // 用来存储买出去多少张票
        List<Integer> sellCount = new Vector<>();
        for (int i = 0; i < 2000; i++) {
            Thread t = new Thread(() -> {
                // 分析这里的竞态条件
                int count = ticketWindow.sell(randomAmount());
                sellCount.add(count);
            });
            list.add(t);
            t.start();
        }

        list.forEach((t) -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        // 买出去的票求和
        log.debug("selled count:{}",sellCount.stream().mapToInt(c -> c).sum());
        // 剩余票数
        log.debug("remainder count:{}", ticketWindow.getCount());
    }
    // Random 为线程安全
    static Random random = new Random();
    // 随机 1~5
    public static int randomAmount() {
        return random.nextInt(5) + 1;
    }
}
class TicketWindow {
    private int count;

    public TicketWindow(int count) {
        this.count = count;
    }

    // 多个线程来修改count,会出现安全问题,加上synchronized进行解决
    public synchronized int sell(int amount) {
        if (this.count >= amount) {
            this.count -= amount;
            return amount;
        } else {
            return 0;
        }
    }

    public int getCount() {
        return count;
    }
}

测试脚本:


for /L %n in (1,1,10) do java -cp ".;C:\Users\14823\.m2\repository\ch\qos\logback\logback-classic\1.2.3\logback-classic-1.2.3.jar;C:\Users\14823\.m2\repository\ch\qos\logback\logback-core\1.2.3\logback-core-1.2.3.jar;C:\Users\14823\.m2\repository\org\slf4j\slf4j-api\1.7.25\slf4j-api-1.7.25.jar"  com.compass.test.ExerciseSell

转账练习

测试下面代码是否存在线程安全问题,并尝试改正

@Slf4j
public class ExerciseTransfer {
    public static void main(String[] args) throws InterruptedException {
        Account a = new Account(1000);
        Account b = new Account(1000);
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                a.transfer(b, randomAmount());
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                b.transfer(a, randomAmount());
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();// 查看转账2000次后的总金额
        log.debug("total:{}",(a.getMoney() + b.getMoney()));
    }
    // Random 为线程安全
    static Random random = new Random();
    // 随机 1~100
    public static int randomAmount() {
        return random.nextInt(100) +1;
    }
}
class Account {
    private int money;
    public Account(int money) {
        this.money = money;
    }
    public int getMoney() {
        return money;
    }
    public void setMoney(int money) {
        this.money = money;
    }

    /**
     * 不能在方法上直接加synchronized,因为这样锁住的是this对象,不是两个对象共有的对象锁
     * 我们这里需要锁住的是两个共享变量this.money和target.money 所以只能锁住Account.class
     *
     */
    public  void transfer(Account target, int amount) {
      synchronized (Account.class){
          if (this.money > amount) {
              this.setMoney(this.getMoney() - amount);
              target.setMoney(target.getMoney() + amount);
          }
      }
    }
}

3.6.Monitor 概念

Java 对象头 以 32 位虚拟机为例:

image-20211117145411150

image-20211117145436893

3.7.monitor原理

Monitor 被翻译为监视器管程

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

image-20211117151251346

  • 刚开始 Monitor 中 Owner 为 null
  • 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一
    个 Owner
  • 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入
    EntryList BLOCKED
  • Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的
  • 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲
    wait-notify 时会分析

注意: synchronized 必须是进入同一个对象的 monitor 才有上述的效果 不加 synchronized 的对象不会关联监视器,不遵从以上规则

synchronized 原理

static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
	 synchronized (lock) {
 	counter++;
 }
}

对应的字节码为:

public static void main(java.lang.String[]);
 descriptor: ([Ljava/lang/String;)V
 flags: ACC_PUBLIC, ACC_STATIC
Code:
 stack=2, locals=3, args_size=1
 0: getstatic #2 // <- lock引用 (synchronized开始)
 3: dup
 4: astore_1 // lock引用 -> slot 1
 5: monitorenter // 将 lock对象 MarkWord 置为 Monitor 指针
 6: getstatic #3 // <- i
 9: iconst_1 // 准备常数 1
 10: iadd // +1
 11: putstatic #3 // -> i
 14: aload_1 // <- lock引用
 15: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
 16: goto 24
 19: astore_2 // e -> slot 2 
 20: aload_1 // <- lock引用
 21: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
 22: aload_2 // <- slot 2 (e)
 23: athrow // throw e
 24: return
 Exception table:
 from to target type
 6 16 19 any
 19 22 19 any
 LineNumberTable:
 line 8: 0
 line 9: 6
 line 10: 14
 line 11: 24
 LocalVariableTable:
 Start Length Slot Name Signature
 0 25 0 args [Ljava/lang/String;
 StackMapTable: number_of_entries = 2
 frame_type = 255 /* full_frame */
 offset_delta = 19
 locals = [ class "[Ljava/lang/String;", class java/lang/Object ]
 stack = [ class java/lang/Throwable ]
 frame_type = 250 /* chop */
 offset_delta = 4

注意 方法级别的 synchronized 不会在字节码指令中有所体现

synchronized使用放锁的时机:

  • 当前线程的同步方法、代码块执行结束的时候释放
  • 当前线程在同步方法、同步代码块中遇到break 、 return 终于该代码块或者方法的时候释放。
  • 出现未处理的error或者exception导致异常结束的时候释放
  • 程序执行了 同步对象 wait 方法 ,当前线程暂停,释放锁

如下情况不会释放锁:

  • 程序调用 Thread.sleep() Thread.yield() 这些方法暂停线程的执行,不会释放。
  • 线程执行同步代码块时,其他线程调用 suspend 方法将该线程挂起,该线程不会释放锁 ,所以我们应该避免使用 suspend resume 来控制线程

3.8.synchronized 原理进阶

3.8.1轻量级锁

  • 轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
  • 轻量级锁对使用者是透明的,即语法仍然是 synchronized
  • 假设有两个方法同步块,利用同一个对象加锁
  static final Object obj = new Object();
    public static void method1() {
        synchronized( obj ) {
            // 同步块 A
            method2();
        }
    }
    public static void method2() {
        synchronized( obj ) {
            // 同步块 B
        }
    }

创建锁记录(Lock Record)对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word

image-20211117154310005

让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存 入锁记录

image-20211117154610407

如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下

image-20211117154655468

  • 如果 cas 失败,有两种情况

    • 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程

    • 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数

image-20211117155021598

当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重 入计数减一

image-20211117155307898

当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头

  • 成功,则解锁成功

  • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

3.8.2锁膨胀

如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

  static Object obj = new Object();

    public static void method1() {
        synchronized (obj) {
            // 同步块
        }
    }

当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁

image-20211117155605674

这时 Thread-1 加轻量级锁失败,进入锁膨胀流程

  • 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址

  • 然后自己进入 Monitor 的 EntryList BLOCKED

image-20211117160221543

  • 当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁 流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList BLOCKED 线程

3.8.3自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步 块,释放了锁),这时当前线程就可以避免阻塞。 自旋重试成功的情况

自旋重试成功的情况:

image-20211117160630645

自旋重试失败的情况:

image-20211117160656153

  • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
  • 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会
    高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
  • Java 7 之后不能控制是否开启自旋功能

3.8.4偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。

Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现 这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有

    static final Object obj = new Object();
    public static void m1() {
        synchronized( obj ) {
            // 同步块 A
            m2();
        }
    }
    public static void m2() {
        synchronized( obj ) {
            // 同步块 B
            m3();
        }
    }
    public static void m3() {
        synchronized( obj ) {
            // 同步块 C

        }
    }

image-20211117161110648

image-20211117161137915

回忆一下对象头格式

image-20211118022109172

一个对象创建时:

  • 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的
    thread、epoch、age 都为 0
  • 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -
    XX:BiasedLockingStartupDelay=0 来禁用延迟
  • 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、
    age 都为 0,第一次用到 hashcode 时才会赋值

测试偏向锁,和偏向锁延迟性

@Slf4j
public class Demo {
    // 添加虚拟机参数 -XX:BiasedLockingStartupDelay=0
    public static void main(String[] args) throws IOException {
        Dog d = new Dog();
        ClassLayout classLayout = ClassLayout.parseInstance(d);

        new Thread(() -> {
            log.debug("synchronized -------前--------");
            System.out.println(classLayout.toPrintable());
            synchronized (d) {
                log.debug("synchronized -------中--------");
                System.out.println(classLayout.toPrintable());
            }
            log.debug("synchronized -------后--------");
            System.out.println(classLayout.toPrintable());
        }, "t1").start();
}
}
class Dog{


}

输出:

03:00:46 [DEBUG] [t1] c.c.test.Demo - synchronized ---------------
com.compass.test.Dog object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000005 (biasable; age: 0)
  8   4        (object header: class)    0x20018c63
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

03:00:46 [DEBUG] [t1] c.c.test.Demo - synchronized ---------------
com.compass.test.Dog object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000001c049805 (biased: 0x0000000000070126; epoch: 0; age: 0)
  8   4        (object header: class)    0x20018c63
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

03:00:46 [DEBUG] [t1] c.c.test.Demo - synchronized ---------------
com.compass.test.Dog object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000001c049805 (biased: 0x0000000000070126; epoch: 0; age: 0)
  8   4        (object header: class)    0x20018c63
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

注意 处于偏向锁的对象解锁后,线程 id 仍存储于对象头中

测试禁用偏向锁: 在上面测试代码运行时在添加 VM 参数 -XX:-UseBiasedLocking 禁用偏向锁

03:08:33 [DEBUG] [t1] c.c.test.Demo - synchronized ---------------
com.compass.test.Dog object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0x20018c63
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

03:08:33 [DEBUG] [t1] c.c.test.Demo - synchronized ---------------
com.compass.test.Dog object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE              // 轻量锁
  0   8        (object header: mark)     0x000000001c53f628 (thin lock: 0x000000001c53f628)
  8   4        (object header: class)    0x20018c63
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

03:08:33 [DEBUG] [t1] c.c.test.Demo - synchronized ---------------
com.compass.test.Dog object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0x20018c63
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    // 加锁优先级:偏向锁->轻量锁->重量级锁

测试 hashCode:重写hashCode方法可以有效避免 调用hashCode方法让该对象失去偏向锁

@Slf4j
public class Demo {
    // 添加虚拟机参数 -XX:BiasedLockingStartupDelay=0
    public static void main(String[] args) throws IOException {
        Dog d = new Dog();
        ClassLayout classLayout = ClassLayout.parseInstance(d);
        // 禁用掉这个对象的偏向锁
        d.hashCode();
        new Thread(() -> {
            log.debug("synchronized -------前--------");
            System.out.println(classLayout.toPrintable());
            synchronized (d) {
                log.debug("synchronized -------中--------");
                System.out.println(classLayout.toPrintable());
            }
            log.debug("synchronized -------后--------");
            System.out.println(classLayout.toPrintable());
        }, "t1").start();
}
}
class Dog{


}

输出:


03:15:05 [DEBUG] [t1] c.c.test.Demo - synchronized ---------------
com.compass.test.Dog object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000006276ae3401 (hash: 0x6276ae34; age: 0)
  8   4        (object header: class)    0x20018c63
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

03:15:05 [DEBUG] [t1] c.c.test.Demo - synchronized ---------------
com.compass.test.Dog object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE    // 偏向锁失效,使用轻量锁
  0   8        (object header: mark)     0x000000001bf0f098 (thin lock: 0x000000001bf0f098)
  8   4        (object header: class)    0x20018c63
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

03:15:05 [DEBUG] [t1] c.c.test.Demo - synchronized ---------------
com.compass.test.Dog object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000006276ae3401 (hash: 0x6276ae34; age: 0)
  8   4        (object header: class)    0x20018c63
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

正常状态对象一开始是没有 hashCode 的,第一次调用才生成,当一个对象是可偏向的,但是一旦调用hashCode方法后会导致该对象不能使用偏向1锁,升级为轻量锁。

**撤销 - 其它线程使用对象 :**当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级

   private static void test2() throws InterruptedException {
        Dog d = new Dog();
        Thread t1 = new Thread(() -> {
            synchronized (d) {
                log.debug(ClassLayout.parseInstance(d).toPrintable());
            }
            synchronized (Demo.class) {
                Demo.class.notify();
            }

        }, "t1");

        t1.start();
        Thread t2 = new Thread(() -> {
            // 确保t1线程执行完后,t2线程再执行
            synchronized (Demo.class) {
                try {
                    Demo.class.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.debug(ClassLayout.parseInstance(d).toPrintable());
            synchronized (d) {
                log.debug(ClassLayout.parseInstance(d).toPrintable());
            }
            log.debug(ClassLayout.parseInstance(d).toPrintable());
        }, "t2");
        t2.start();
    }
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000001be10805 (biased: 0x000000000006f842; epoch: 0; age: 0)
  8   4        (object header: class)    0x20018c63
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

03:38:19 [DEBUG] [t2] c.c.test.Demo - com.compass.test.Dog object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000001be10805 (biased: 0x000000000006f842; epoch: 0; age: 0)
  8   4        (object header: class)    0x20018c63
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

03:38:19 [DEBUG] [t2] c.c.test.Demo - com.compass.test.Dog object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000001c47f600 (thin lock: 0x000000001c47f600)
  8   4        (object header: class)    0x20018c63
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

03:38:19 [DEBUG] [t2] c.c.test.Demo - com.compass.test.Dog object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0x20018c63
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

撤销 - 调用 wait/notify : 将偏向锁或轻量锁都升级为重量级锁,因为重量级锁只有 wait/notify 才会有

@Slf4j
public class Demo {
    public static void main(String[] args) throws InterruptedException {
        Dog d = new Dog();

        ClassLayout layout = ClassLayout.parseInstance(d);

        Thread t1 = new Thread(() -> {
            // 1
            log.debug(layout.toPrintable());
            synchronized (d) {
                // 2
                log.debug(layout.toPrintable());
                try {
                    d.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //3
                log.debug(layout.toPrintable());
            }
        }, "t1");
        t1.start();

        new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (d) {
                log.debug("notify");
                d.notify();
            }
        }, "t2").start();
    }
}
class Dog{

}
03:45:23 [DEBUG] [t1] c.c.test.Demo - com.compass.test.Dog object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000005 (biasable; age: 0)
  8   4        (object header: class)    0x20018c63
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

03:45:23 [DEBUG] [t1] c.c.test.Demo - com.compass.test.Dog object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000001b462005 (biased: 0x000000000006d188; epoch: 0; age: 0)
  8   4        (object header: class)    0x20018c63
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

03:45:24 [DEBUG] [t2] c.c.test.Demo - notify
03:45:24 [DEBUG] [t1] c.c.test.Demo - com.compass.test.Dog object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000017b0f4aa (fat lock: 0x0000000017b0f4aa)
  8   4        (object header: class)    0x20018c63
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

批量重偏向

  • 如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象 的 Thread ID

  • 当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至 加锁线程

@Slf4j
public class Demo {
    public static void main(String[] args) throws InterruptedException {
        test3();
    }
    private static void test3() throws InterruptedException {
        List<Dog> list = new Vector<>();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 30; i++) {
                Dog d = new Dog();
                list.add(d);
                synchronized (d) {
                    log.debug(i + "\t" +ClassLayout.parseInstance(d).toPrintable());
                }
            }
            synchronized (list) {
                list.notify();
            }
        }, "t1");
        t1.start();

        Thread t2 = new Thread(() -> {
            synchronized (list) {
                try {
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            log.debug("==========================================================================================");
            for (int i = 0; i < 30; i++) {
                Dog d = list.get(i);
                log.debug(i + "\t" +ClassLayout.parseInstance(d).toPrintable());
                synchronized (d) {
                    log.debug(i + "\t" +ClassLayout.parseInstance(d).toPrintable());
                }
                log.debug(i + "\t" +ClassLayout.parseInstance(d).toPrintable());
            }
        }, "t2");
        t2.start();
    }
}
class Dog{

}
[t1] - 0 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 1 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 2 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 3 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 4 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 5 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 6 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 7 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 8 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 9 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 10 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 11 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 12 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 13 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 14 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 15 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 16 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 17 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 18 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 23 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 24 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - ===============> 
[t2] - 0 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 0 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 0 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 1 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 1 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 1 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 2 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 2 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 2 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 3 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 3 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 3 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 4 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 4 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 4 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 5 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 5 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 5 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 6 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 6 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 6 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 7 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 

批量撤销

当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象 都会变为不可偏向的,新建的对象也是不可偏向的

@Slf4j
public class Demo {
    public static void main(String[] args) throws InterruptedException {
        test4();
    }
    static Thread t1,t2,t3;
    private static void test4() throws InterruptedException {
        Vector<Dog> list = new Vector<>();
        int loopNumber = 39;
        t1 = new Thread(() -> {
            for (int i = 0; i < loopNumber; i++) {
                Dog d = new Dog();
                list.add(d);
                synchronized (d) {
                    log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
                }
            }
            LockSupport.unpark(t2);
        }, "t1");
        t1.start();
        t2 = new Thread(() -> {
            LockSupport.park();
            log.debug("==========================================================================================");
            for (int i = 0; i < loopNumber; i++) {
                Dog d = list.get(i);
                log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
                synchronized (d) {
                    log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
                }
                log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
            }
            LockSupport.unpark(t3);
        }, "t2");
        t2.start();
        t3 = new Thread(() -> {
            LockSupport.park();
            log.debug("==========================================================================================");

            for (int i = 0; i < loopNumber; i++) {
                Dog d = list.get(i);
                log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
                synchronized (d) {
                    log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
                }
                log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
            }
        }, "t3");
        t3.start();
        t3.join();
      
    }

}
class Dog{

}

3.8.5锁消除

@Slf4j
@Fork(1)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations=3)
@Measurement(iterations=5)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class MyBenchmark {
    static int x = 0;
    @Benchmark
    public void a() throws Exception {
        x++;
    }
    @Benchmark
    public void b() throws Exception {
        // 此对象不是共享变量,加锁也就无意义,即时编译器在编译的时候进行优化,将锁进行消除
        Object o = new Object();
        synchronized (o) {
            x++; }
    }
}

java -jar benchmarks.jar

Benchmark Mode Samples Score Score error Units 
c.i.MyBenchmark.a avgt 5 1.542 0.056 ns/op 
c.i.MyBenchmark.b avgt 5 1.518 0.091 ns/op 

关闭锁消除jvm指令:java -XX:-EliminateLocks -jar benchmarks.jar

Benchmark Mode Samples Score Score error Units 
c.i.MyBenchmark.a avgt 5 1.507 0.108 ns/op 
c.i.MyBenchmark.b avgt 5 16.976 1.572 ns/op 

锁粗化 对相同对象多次加锁,导致线程发生多次重入,可以使用锁粗化方式来优化,这不同于之前讲的细分锁的粒度。

3.9.wait notify

9.1wait notify 原理

image-20211118143050239

  • Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
  • BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
  • BLOCKED 线程会在 Owner 线程释放锁时唤醒
  • WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入
  • EntryList 重新竞争

9.2 wait notify 介绍

  • obj.wait() 让进入 object 监视器的线程到 waitSet 等待 [ 对象锁持有者调用 ]
  • obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒 [ 对象锁持有者调用 ]
  • obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒 [ 对象锁持有者调用 ]

**注意:**它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须配合synchronized使用,而且要获得锁才可以调用。

如果不配合synchronized使用,并且没有获取到锁调用就会出现IllegalMonitorStateException异常

notify和notifyAll的区别

 static final Object lock=new Object();
    public static void main(String[] args) {

        new Thread(()->{
           synchronized(lock){
               log.debug("执行...");
               try {
                   lock.wait();
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               log.debug("其他代码");
           }

        },"t1").start();

        new Thread(()->{
          synchronized (lock){
              log.debug("执行...");
              try {
                  lock.wait();
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              log.debug("其他代码...");
          }
        },"t2").start();

        sleep(1);
        log.debug("唤醒其他线程...");
        synchronized (lock){

            //lock.notify(); // 随机抽取一个正在等待的线程进行唤醒
            lock.notifyAll(); // 唤醒阻塞在该对象上的所有线程
        }

    }

wait() 方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到 notify 为止 wait(long n) 有时限的等待, 到 n 毫秒后结束等待,或是被 notify ,带naos的第二个参数只有大于0,就让timeOut时间基础上加1,并不是真正的精确到纳秒

sleep(long n) 和 wait(long n)

  • sleep 是 Thread 方法,而 wait 是 Object 的方法
  • sleep 不需要强制和 synchronized 配合使用,但 wait 需要 和 synchronized 一起用
  • sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
  • 它们 状态 TIMED_WAITING

9.3 wait和notify正确使用方式

step1
@Slf4j
public class Test1 {
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (room) {
                log.debug("有烟没?[{}]", hasCigarette);
                if (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    sleep(2);
                }
                log.debug("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
                    log.debug("可以开始干活了[小南]");
                }
            }
        }, "小南").start();

        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                synchronized (room) {
                    log.debug("可以开始干活了");
                }
            }, "其它人").start();
    }

      
           new Thread(() -> {
               // 这里能不能加 synchronized (room)? 
               // 不能加,因为加上之后送烟线程也需要等待对象锁,送烟线程就无法得到对象锁,无法进行送烟
               hasCigarette = true;
               log.debug("烟到了噢!");
           }, "送烟").start();
       }
  
}
  • 小南睡眠后一直持有对象锁,导致其他的线程无法获取到对象锁,只有等小南线程睡醒后释放锁,其他线程才可以获取对象锁执行代码
  • 小南线程必须睡足 2s 后才能醒来,就算烟提前送到,也无法立刻醒来
  • 送烟线程加了 synchronized (room) 后,送烟线程也需要等待对象锁,无法进行送烟,main线程 没加
    synchronized 就无需获取对象锁,也不需要进行阻塞,可以在小南线程睡眠时,将hasCigarette设置为true
  • 解决方法,使用 wait - notify 机制
step2
@Slf4j
public class Test1 {
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (room) {
                log.debug("有烟没?[{}]", hasCigarette);
                if (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
                    log.debug("可以开始干活了[小南]");
                }
            }
        }, "小南").start();

        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                synchronized (room) {
                    log.debug("可以开始干活了");
                }
            }, "其它人").start();
    }


           new Thread(() -> {
             synchronized (room){
                 // 这里能不能加 synchronized (room)?
                 // 不能加,因为加上之后送烟线程也需要等待对象锁,送烟线程就无法得到对象锁,无法进行送烟
                 hasCigarette = true;
                 room.notify();
                 log.debug("烟到了噢!");
             }
           }, "送烟").start();

   }
}

使用wait等待的好处就是,小南线程进行阻塞但是会释放对象锁,其余的线程也可以拿到对象锁进行执行,无需等到小南线程醒过来才去执行代码块

step3
@Slf4j
public class Test1 {
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (room) {
                log.debug("有烟没?[{}]", hasCigarette);
                if (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
                    log.debug("可以开始干活了");
                } else {log.debug("没干成活...");
                }
            }
        }, "小南").start();
        new Thread(() -> {
            synchronized (room) {
                Thread thread = Thread.currentThread();
                log.debug("外卖送到没?[{}]", hasTakeout);
                if (!hasTakeout) {
                    log.debug("没外卖,先歇会!");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("外卖送到没?[{}]", hasTakeout);
                if (hasTakeout) {
                    log.debug("可以开始干活了");
                } else {
                    log.debug("没干成活...");
                }
            }
        }, "小女").start();
        sleep(1);
        new Thread(() -> {
            synchronized (room) {
                hasTakeout = true;
                log.debug("外卖到了噢!");
                // notify:只能随机唤醒一个线程,并不能精准的唤醒小女线程,解决方案就是使用notifyAll,把他们都唤醒
                room.notifyAll();
            }
        }, "送外卖的").start();
   }
}
  • notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线 程,称之为【虚假唤醒】

  • 解决方法,改为 notifyAll

step4
@Slf4j
public class Test1 {
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (room) {
                log.debug("有烟没?[{}]", hasCigarette);
                // 使用while避免虚假唤醒,下次获得锁的时候继续重新判断,如果用if下次获取到锁的时候就不会重新进行判断
                while (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    break;
                }
                log.debug("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
                    log.debug("可以开始干活了");
                } else {log.debug("没干成活...");
                }
            }
        }, "小南").start();
        new Thread(() -> {
            synchronized (room) {
                Thread thread = Thread.currentThread();
                log.debug("外卖送到没?[{}]", hasTakeout);
                if (!hasTakeout) {
                    log.debug("没外卖,先歇会!");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("外卖送到没?[{}]", hasTakeout);
                if (hasTakeout) {
                    log.debug("可以开始干活了");
                } else {
                    log.debug("没干成活...");
                }
            }
        }, "小女").start();
        sleep(1);
        new Thread(() -> {
            synchronized (room) {
                hasTakeout = true;
                log.debug("外卖到了噢!");
                // notify:只能随机唤醒一个线程,并不能精准的唤醒小女线程,解决方案就是使用notifyAll,把他们都唤醒
                room.notify();
            }
        }, "送外卖的").start();
   }
}

小技巧

synchronized(lock) {
 while(条件不成立) {
 lock.wait();
 }
 // 干活
}
//另一个线程
synchronized(lock) {
 lock.notifyAll();
}

3.10 .同步模式-保护性暂停

  1. 定义:即 Guarded Suspension,用在一个线程等待另一个线程的执行结果
  • 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject
  • 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者)
  • JDK 中,join 的实现、Future 的实现,采用的就是此模式
  • 因为要等待另一方的结果,因此归类到同步模式

image-20211119034242498

10.0基础版

@Slf4j
public class PrivatePause {

    public static void main(String[] args) {
        GuardedObject guarded = new GuardedObject();

        new Thread(()->{
            // 下载一个网页的html代码,放到list集合中
            List<String> list = Util.download();
            guarded.complete(list);
        },"A").start();

        new Thread(()->{
            List<String> list = (List<String>)guarded.getResult();
            log.debug("获取等待结果...");
            log.debug("list="+list.size());
        },"B").start();
    }
}

class GuardedObject{
    private Object response;
    private static  final Object lock=new Object();


    /**
     * 执行的任务完后就可以给response设置值,并且唤醒get方法
     * @param response
     */
    public void complete( Object response){
        synchronized (this){
            this.response=response;
            this.notifyAll();
        }
    }

    /**
     *  如果response的结果为null就一直进行等待,直到被complete唤醒
     * @return
     */
    public Object getResult(){
        synchronized (this){
            while (response==null){
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        return response;
    }
}
// 下载方法
public static List<String> download(String targetUrl)   {
        String defaultUrl="https://www.taobao.com";
        List<String> list = new ArrayList<>();
        HttpURLConnection connection=null;
        try {
           if (targetUrl!=null&&targetUrl.length()>0){
               connection = (HttpURLConnection) new URL(targetUrl).openConnection();
           }else {
               connection = (HttpURLConnection) new URL(defaultUrl).openConnection();
           }
        } catch (IOException e) {
            e.printStackTrace();
        }

        try(BufferedReader reader=new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
           String line;
            while ((line=reader.readLine())!=null){
                list.add(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }


        return list;
    }

10.1超时版本

    /**
     *  带超时时间版本的
     * @Parse 单位毫秒
     * @return
     */
    public Object getResult(long timeOut ){
        // 开始时间
        long startTime=System.currentTimeMillis();
        // 经历时间
        long passTime=0;

        synchronized (this){
            while (response==null){
                // 这轮循环应该等待的时间
                long waitTime = timeOut - passTime;
                // 等待时间>=timeOut直接退出循环
                if (waitTime<=0){
                    break;
                }
                try {
                    // timeOut-passTime:避免虚假唤醒
                    this.wait(waitTime);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 等待时间=现在时间-开始时间
                passTime = System.currentTimeMillis() - startTime;
            }

        }
        return response;
    }

10.2join原理

join:让调用join的线程进行等待,直到调用join的线程对象执行完释放锁,调用jion的线程才有机会获得锁,继续执行

// 锁的是this对象,那个线程对象调用,那就以那个线程对象做为锁
public final synchronized void join(long millis)
    throws InterruptedException {
   		 //得到开始时间
        long base = System.currentTimeMillis();
        long now = 0;
		// 如果设置的时间小于0,抛出IllegalArgumentException
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
		// 判断设置的等待时间如果==0就一直等待直到调用join方法的线程对象执行完,调用jion的线程才会向下运行
        if (millis == 0) {
            // 如果调用join的线程还存活,就一直进入等待状态
            while (isAlive()) 
                wait(0);
            }
        } else {
   		  // 带超时时间的等待
            while (isAlive()) {
                // 每轮循环应该等待的时间=应该等待时间-每轮循环耗费的时间
                long delay = millis - now;
                // 最终等待时间<=0,就结束等待
                if (delay <= 0) {
                    break;
                }
                wait(delay)
                 // 执行完一轮循环后,更新每轮循环耗费的时间
                now = System.currentTimeMillis() - base;
            }
        }
    }

10.3多任务版

图中 Futures 就好比居民楼一层的信箱(每个信箱有房间编号),左侧的 t0,t2,t4 就好比等待邮件的居民,右 侧的 t1,t3,t5 就好比邮递员 如果需要在多个类之间使用 GuardedObject 对象,作为参数传递不是很方便,因此设计一个用来解耦的中间类, 这样不仅能够解耦【结果等待者】和【结果生产者】,还能够同时支持多个任务的管理【Futures 就是一个容器,用于存放多个线程之间通信的数据】

image-20211120180046238

完整代码如下:

@Slf4j
public class PrivatePause {

    public static void main(String[] args) {
        // 启动三个收信的居民
       for (int i=0;i<3;i++){
           new People().start();
       }

       sleep(1);
       // 启动三个邮递员

      for (Integer id: Futures.getIds()){
        new PostMan(id, "hello world[" + id + "]").start();
      }


    }
}

@Slf4j
// 居民类
class People extends Thread{
    @Override
    public void run() {
        // 收信
        GuardedObject guarded = Futures.createGuarded();
        log.debug("收信 id:{}",guarded.getId());
        Object email = guarded.getResult(3000);
        log.debug("收到信 id:{},内容:{}",guarded.getId(),email.toString());
    }
}

@Slf4j
// 邮递员类
class PostMan extends Thread{
    private int id;
    private String data;
    @Override
    public void run() {
        GuardedObject guarded = Futures.getGuardedById(id);

        guarded.complete(data);
        log.debug("送信:id:{},内容:{}",id,data);
    }

    public PostMan(int id,String email){
        this.id=id;
        this.data=email;
    }

}

class Futures{
    // 该map需要是线程安全的,key是 GuardedObject的id value就是GuardedObject
    private static Map<Integer,GuardedObject> futures= new ConcurrentHashMap<>();
    private static int id=1;

    /**
     * 产生GuardedObject对象,保证id的唯一性
     * 将创建好的对象放入到 boxes
     * @return 返回GuardedObject
     */
    public static GuardedObject createGuarded(){
        GuardedObject guardedObject=new GuardedObject();
        guardedObject.setId(generateId());
        futures.put(guardedObject.getId(),guardedObject);
        return guardedObject;
    }

    /**
     *  获取所有的id
     * @return
     */
    public static Set<Integer> getIds(){
        return futures.keySet();
    }

    /**
     *  获取id会被多个线程所访问,所以需要考虑线程安全问题
     * @return 产生一个唯一的id
     */
    public static synchronized int generateId(){
        return id++;
    }

    /**
     *  根据id获取到GuardedObject对象
     * @param id
     * @return
     */
    public static GuardedObject getGuardedById(int id){
        GuardedObject object = futures.get(id);
        // 获取完结果后,将GuardedObject从map集合中移除
        return futures.remove(id);
    }
}

class GuardedObject{
    private Object response;
    private int id;

    public int getId() {
        return id;
    }

    public GuardedObject setId(int id) {
        this.id = id;
        return this;
    }

    /**
     * 执行的任务完后就可以给response设置值,并且唤醒get方法
     * @param response
     */
    public void complete( Object response){
        synchronized (this){
            this.response=response;
            this.notifyAll();
        }
    }

    /**
     *  如果response的结果为null就一直进行等待,直到被complete唤醒
     * @return
     */
    public Object getResult(){
        synchronized (this){
            while (response==null){
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        return response;
    }

    /**
     *  带超时时间版本的
     * @Parse 单位毫秒
     * @return
     */
    public Object getResult(long timeOut ){
        // 开始时间
        long startTime=System.currentTimeMillis();
        // 经历时间
        long passTime=0;

        synchronized (this){
            while (response==null){
                // 这轮循环应该等待的时间
                long waitTime = timeOut - passTime;
                // 等待时间>=timeOut直接退出循环
                if (waitTime<=0){
                    break;
                }
                try {
                    // timeOut-passTime:避免虚假唤醒
                    this.wait(waitTime);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 等待时间=现在时间-开始时间
                passTime = System.currentTimeMillis() - startTime;
            }

        }
        return response;
    }
}

3.11.异步模式-消费者/生产者模式

  • 与前面的保护性暂停中的 GuardObject 不同,不需要产生结果和消费结果的线程一一对应
  • 消费队列可以用来平衡生产和消费的线程资源
  • 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据
  • 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
  • JDK 中各种阻塞队列,采用的就是这种模式

image-20211120190630987

@Slf4j
// java消息线程之间通信
public class MessageQueue {
    // 消息队列
    LinkedList<Message> list =new LinkedList();
    // 队列消息容量
    public int capacity;

    public MessageQueue(int capacity){
        this.capacity=capacity;
    }

    /**
     * 从队列中获取消息
     * @return
     */
    public Message take(){
        synchronized (list){
            while (list.isEmpty()){
                try {
                    log.debug("队列为空... 消费者线程等待");
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 从队列中获取消息 并从队列中移除掉该消息,让生产者去生产消息
            Message message = list.removeFirst();
            log.debug("消费消息= "+message);
            // 唤醒生产者去生产数据
            list.notifyAll();
            return message;
        }

    }

    /**
     * 往队列中添加消息
     * @param message 消息
     */
    public void put(Message message){
        synchronized (list){
            // 如果队列已满,就进行等待,直到消费者有消费
            while (list.size()>=capacity){
                try {
                    log.debug("队列为满... 生产者线程等待");
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            log.debug("生产消息= "+message);
            // 将生产出来的数据放入到队列头部
            list.addFirst(message);
            // 唤醒消费者线程进行消费
            list.notifyAll();
        }
    }
}

class Message{
    // 消息id
    private int id;
    // 数据
    private Object data;

    public void setId(int id) {
        this.id = id;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public Message(int id,Object data){
        this.id = id;
        this.data = data;
    }

    @Override
    public String toString() {
        return "Message{" +
                "id=" + id +
                ", data=" + data +
                '}';
    }
}
class TestA{
    public static void main(String[] args) {
        MessageQueue queue = new MessageQueue(2);
        // 启动3个生产者线程
        for (int i = 0; i < 3; i++) {
           int index=i;
            new Thread(()->{
                queue.put(new Message(index,"消息["+index+"]"));
            },"生产者"+i).start();
        }
        sleep(1);
        // 启动3个消费者线程
        for (int i = 0; i < 3; i++) {
 
            new Thread(()->{
                queue.take();
            },"消费者"+i).start();
        }

    }

}

输出:

18:52:37 [DEBUG] [生产者:0] c.c.d.MessageQueue - 生产消息= Message{id=0, message=消息[0]}
18:52:37 [DEBUG] [生产者:2] c.c.d.MessageQueue - 生产消息= Message{id=2, message=消息[2]}
18:52:37 [DEBUG] [生产者:1] c.c.d.MessageQueue - 队列为满... 生产者线程等待
18:52:38 [DEBUG] [消费者:0] c.c.d.MessageQueue - 消费消息= Message{id=0, message=消息[0]}
18:52:38 [DEBUG] [消费者:2] c.c.d.MessageQueue - 消费消息= Message{id=2, message=消息[2]}
18:52:38 [DEBUG] [消费者:1] c.c.d.MessageQueue - 队列为空... 消费者线程等待
18:52:38 [DEBUG] [生产者:1] c.c.d.MessageQueue - 生产消息= Message{id=1, message=消息[1]}
18:52:38 [DEBUG] [消费者:1] c.c.d.MessageQueue - 消费消息= Message{id=1, message=消息[1]}

3.12.park&unpark

它们是 LockSupport 类中的方法

// 暂停当前线程
LockSupport.park(); 
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)
// 先 park 再 unpark
   public static void main(String[] args)   {
        Thread t1 = new Thread(() -> {
            log.debug("start...");
            sleep(2);
            log.debug("park..");
            // 当前线程进入 wait 状态
            LockSupport.park();
            log.debug("resume...");
        }, "t1");
        t1.start();

        sleep(1);
        log.debug("unpark...");
        // 唤醒 t1 线程
        LockSupport.unpark(t1);
    }

3.12.1.与wait & notify 相比

  • wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必

  • park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么【精确】

  • park & unpark 可以先 unpark,而 wait & notify 不能先 notify

3.12.2.unpark和park原理

每个线程都有自己的一个 Parker 对象,由三部分组成 _counter【0或1】 , _cond 和 _mutex

  • 调用 park 就是判断当前线程是否需要进入阻塞状态
    • 如果_counter=0,进入阻塞状态
    • 如果_counter=1,无需进入阻塞状态
  • 调用 unpark,唤醒由park阻塞的线程
    • 如果这时线程还在阻塞状态【c_counter=0】,就唤醒让他继续向下执行
    • 如果这时线程还在运行【_counter=1】,那么下次他调用 park 时,就无需阻塞,就继续向下执行

image-20211120204118053

image-20211120204207295

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N9seVszJ-1651433863045)(C:\Users\14823\Desktop\learn-note\typroa-img\image-20211120204143646.png)]

3.13.线程状态转换

image-20211120205008127

3.13.1 NEW --> RUNNABLE

当调用 t.start() 方法时,由 NEW --> RUNNABLE

       // new[新建状态]
        Thread t1 = new Thread(() -> {
            log.debug("RUNNABLE");
        }, "t1");

3.13.2 RUNNABLE <–> WAITING

  • t 线程用 synchronized(obj) 获取了对象锁后
    • 调用 obj.wait() 方法时,t 线程从 RUNNABLE --> WAITING
    • 调用 obj.notify() , obj.notifyAll() , t.interrupt()
      • 竞争锁成功,t 线程从 WAITING --> RUNNABLE
      • 竞争锁失败,t 线程从 WAITING --> BLOCKE

3.13.3RUNNABLE <–> WAITING

  • 当前线程调用 t.join() 方法时,当前线程从 RUNNABLE --> WAITING
    • 注意是当前线程在t 线程对象的监视器上等待
  • t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING --> RUNNABLE

3.13.4RUNNABLE <–> WAITING

  • 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE --> WAITING
  • 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING --> RUNNABLE

3.13.5 RUNNABLE <–> TIMED_WAITING

  • 线程用 synchronized(obj) 获取了对象锁后
  • 调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE --> TIMED_WAITING
    • t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt()
    • 竞争锁成功,t 线程从 TIMED_WAITING --> RUNNABLE
    • 竞争锁失败,t 线程从 TIMED_WAITING --> BLOCKED

3.13.6 RUNNABLE <–> TIMED_WAITING

  • 当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE --> TIMED_WAITING
    • 注意是当前线程在 t 线程对象的监视器上等待
  • 当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从TIMED_WAITING --> RUNNABLE

3.13.7 RUNNABLE <–> TIMED_WAITING

  • 当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE --> TIMED_WAITING
  • 当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING --> RUNNABLE

3.13.8 RUNNABLE <–> TIMED_WAITING

  • 当前线程调用 LockSupport.parkNanos(long nanos)LockSupport.parkUntil(long millis) 时,当前线程从 RUNNABLE --> TIMED_WAITING
  • 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从TIMED_WAITING–> RUNNABLE

3.13.9 RUNNABLE <–> BLOCKED

  • t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED
  • 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争
    成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然 BLOCKED

3.13.10 RUNNABLE <–> TERMINATED

  • 当前线程所有代码运行完毕,进入 TERMINATED

3.14. 多把锁

在两个操作不相关联,无共享数据的时候,可以用多把锁的方式来降低锁的粒度,粒度小并发读就越高,如果两个不相关联的方法公用同一把锁,就会导致锁的粒度太大,并发度低。

  • 将锁的粒度细分
    • 好处,是可以增强并发度
    • 坏处,如果一个线程需要同时获得多把锁,就容易发生死锁

以下代码都共用同一个对象锁 this ,在在调用study方法的时候,study方法就占有 this 对象锁,而别的线程想要调用sleep方法的时候也需要this对象锁,调用sleep的线程就只有等待 调用study的线程释放了锁,才可以继续执行,这样就导致了,两个毫不相干的方法产生了依赖的关系,锁的粒度变大,解决的方法就是:让 study和sleep 持有不同的对象锁

@Slf4j
public class Test1 {

    public static void main(String[] args) {

        BigRoom room = new BigRoom();
        new Thread(()->{
            room.sleep();
        },"A").start();

        new Thread(()->{
            room.study();
        },"B").start();
    }
}
@Slf4j
class BigRoom{

    public void  sleep(){
        synchronized (this){
            log.debug("睡觉中...");
            // 睡眠2秒
            Util.sleep(2);
        }
    }

    public void study(){
        synchronized (this){
            log.debug("学习中...");
            // 睡眠2秒
            Util.sleep(3);
        }
    }
}

改进后的代码:两个方法持有不同的对象锁,无需进行同步等待,可以同时运行

@Slf4j
class BigRoom{
   static final Object sleep = new Object();
   static final Object study = new Object();
    public void  sleep(){
        synchronized (sleep){
            log.debug("睡觉中...");
            // 睡眠2秒
            Util.sleep(2);
        }
    }

    public void study(){
        synchronized (study){
            log.debug("学习中...");
            // 睡眠2秒
            Util.sleep(3);
        }
    }
}

3.15 线程活跃性

3.15 .1 死锁

有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁

  • t1 线程 获得 A对象 锁,接下来想获取 B对象 的锁

  • t2 线程 获得 B对象 锁,接下来想获取 A对象 的锁

以下代码就是死锁:

@Slf4j
public class Test1 {
    static final Object A = new Object();
    static final Object B = new Object();
    public static void main(String[] args) {


        new Thread(()->{
           synchronized (A){
               // 睡眠0.5秒 先让t2获得B对象锁
               sleep(500L);
               log.debug("获得A对象锁");
               synchronized (B){
                   log.debug("获得B对象锁");
               }
           }
        },"t1").start();

        new Thread(()->{
           synchronized (B){
               // 睡眠0.5秒 先让t1获得A对象锁
               sleep(500L);
               log.debug("获得B对象锁");
               synchronized (A){
                   log.debug("获得A对象锁");
               }
           }
        },"t2").start();
    }
}

如何定位发生死锁所在代码位置?

检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 进程id 定位死锁:

1.用jps命令查看所有java进程的id

image-20211121164901657

2.利用jstack 进程id 查看java线程的状态信息

image-20211121165042170

详细信息:

2021-11-21 16:50:12
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.251-b08 mixed mode):

"DestroyJavaVM" #14 prio=5 os_prio=0 tid=0x0000000003264000 nid=0x2d230 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"t2" #13 prio=5 os_prio=0 tid=0x000000001bcab000 nid=0x2cbf8 waiting for monitor entry [0x000000001c27f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at com.compass.test.Test1.lambda$main$1(Test1.java:41)
        - waiting to lock <0x00000000d6cfa208> (a java.lang.Object)
        - locked <0x00000000d6cfa218> (a java.lang.Object)
        at com.compass.test.Test1$$Lambda$2/1792393294.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

"t1" #12 prio=5 os_prio=0 tid=0x000000001bca8000 nid=0x2d1c4 waiting for monitor entry [0x000000001c17e000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at com.compass.test.Test1.lambda$main$0(Test1.java:30)
        - waiting to lock <0x00000000d6cfa218> (a java.lang.Object)
        - locked <0x00000000d6cfa208> (a java.lang.Object)
        at com.compass.test.Test1$$Lambda$1/1914572623.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

"Service Thread" #11 daemon prio=9 os_prio=0 tid=0x0000000019ca0000 nid=0x2d1c8 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C1 CompilerThread3" #10 daemon prio=9 os_prio=2 tid=0x0000000019c81000 nid=0x2d1c0 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread2" #9 daemon prio=9 os_prio=2 tid=0x0000000019c66000 nid=0x2d1b0 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread1" #8 daemon prio=9 os_prio=2 tid=0x0000000019c7e000 nid=0x2cbf0 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" #7 daemon prio=9 os_prio=2 tid=0x0000000019c79000 nid=0x2cd50 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Monitor Ctrl-Break" #6 daemon prio=5 os_prio=0 tid=0x0000000019c5f000 nid=0x2cd38 runnable [0x000000001b1ce000]
   java.lang.Thread.State: RUNNABLE
        at java.net.SocketInputStream.socketRead0(Native Method)
        at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
        at java.net.SocketInputStream.read(SocketInputStream.java:171)
        at java.net.SocketInputStream.read(SocketInputStream.java:141)
        at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
        at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
        at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
        - locked <0x00000000d632d5d0> (a java.io.InputStreamReader)
        at java.io.InputStreamReader.read(InputStreamReader.java:184)
        at java.io.BufferedReader.fill(BufferedReader.java:161)
        at java.io.BufferedReader.readLine(BufferedReader.java:324)
        - locked <0x00000000d632d5d0> (a java.io.InputStreamReader)
        at java.io.BufferedReader.readLine(BufferedReader.java:389)
        at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:61)

"Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x0000000019bb8800 nid=0x2cce4 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x0000000019b64000 nid=0x2d1a8 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x000000000335b000 nid=0x2c724 in Object.wait() [0x000000001aecf000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x00000000d6108ee0> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)
        - locked <0x00000000d6108ee0> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:165)
        at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:216)

"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x0000000019b43000 nid=0x2c8e4 in Object.wait() [0x000000001adcf000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x00000000d6106c00> (a java.lang.ref.Reference$Lock)
        at java.lang.Object.wait(Object.java:502)
        at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
        - locked <0x00000000d6106c00> (a java.lang.ref.Reference$Lock)
        at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)

"VM Thread" os_prio=2 tid=0x0000000018476000 nid=0x2cec8 runnable

"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x0000000003279800 nid=0x2cf38 runnable

"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x000000000327b000 nid=0x2cf88 runnable

"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x000000000327c800 nid=0x2ce08 runnable

"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x000000000327e000 nid=0x2cdb4 runnable

"GC task thread#4 (ParallelGC)" os_prio=0 tid=0x0000000003281000 nid=0x2cdf4 runnable

"GC task thread#5 (ParallelGC)" os_prio=0 tid=0x0000000003282800 nid=0x2ce9c runnable

"GC task thread#6 (ParallelGC)" os_prio=0 tid=0x0000000003285800 nid=0x2ce80 runnable

"GC task thread#7 (ParallelGC)" os_prio=0 tid=0x0000000003286800 nid=0x2d12c runnable

"GC task thread#8 (ParallelGC)" os_prio=0 tid=0x0000000003288000 nid=0x2d160 runnable

"GC task thread#9 (ParallelGC)" os_prio=0 tid=0x0000000003289000 nid=0x2cef0 runnable

"VM Periodic Task Thread" os_prio=2 tid=0x0000000019c67000 nid=0x2cd64 waiting on condition

JNI global references: 317

// 发现一个死锁 
Found one Java-level deadlock:
=============================
"t2":
  waiting to lock monitor 0x0000000018485108 (object 0x00000000d6cfa208, a java.lang.Object),
  which is held by "t1"
"t1":
  waiting to lock monitor 0x0000000018487578 (object 0x00000000d6cfa218, a java.lang.Object),
  which is held by "t2"

Java stack information for the threads listed above:
===================================================
"t2":
        at com.compass.test.Test1.lambda$main$1(Test1.java:41)
        - waiting to lock <0x00000000d6cfa208> (a java.lang.Object)
        - locked <0x00000000d6cfa218> (a java.lang.Object)
        at com.compass.test.Test1$$Lambda$2/1792393294.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)
"t1":
        at com.compass.test.Test1.lambda$main$0(Test1.java:30)
        - waiting to lock <0x00000000d6cfa218> (a java.lang.Object)
        - locked <0x00000000d6cfa208> (a java.lang.Object)
        at com.compass.test.Test1$$Lambda$1/1914572623.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)
// 发现一个死锁
Found 1 deadlock.

使用 jconsole 远程连接工具

image-20211121165449276

image-20211121165615720

哲学家就餐问题

image-20211121170532083

有五位哲学家,围坐在圆桌旁。

  • 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
  • 吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。
  • 如果筷子被身边的人拿着,自己就得等待 【会发生死锁问题】
// 哲学家类
@Slf4j
public class Philosopher extends Thread {
    Chopstick left;
    Chopstick right;

    public Philosopher(String name, Chopstick left, Chopstick right) {
        super(name);
        this.left = left;
        this.right = right;
    }
    private void eat() {
        log.debug("eating...");

    }

    @Override
    public void run() {
        while (true) {
            // 获得左手筷子
            synchronized (left) {
                // 获得右手筷子
                synchronized (right) {
                    // 吃饭
                    eat();
                }
                // 放下右手筷子
            }
            // 放下左手筷子
        }
    }
}
// 筷子类
class Chopstick {
    public static void main(String[] args) {
        Chopstick c1 = new Chopstick("1");
        Chopstick c2 = new Chopstick("2");
        Chopstick c3 = new Chopstick("3");
        Chopstick c4 = new Chopstick("4");
        Chopstick c5 = new Chopstick("5");
        new Philosopher("苏格拉底", c1, c2).start();
        new Philosopher("柏拉图", c2, c3).start();
        new Philosopher("亚里士多德", c3, c4).start();
        new Philosopher("赫拉克利特", c4, c5).start();
        new Philosopher("阿基米德", c5, c1).start();

    }
    String name;
    public Chopstick(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "筷子{" + name + '}';
    }
}

3.15 .2 活锁

活锁出现在两个线程互相改变对方的结束条件,导致双方的结束条件都不成立,最后谁也无法结束,例如

@Slf4j
public class TestLiveLock {
    static volatile int count=20;
    static final  Object lock =new Object();
    // 解决方案:两者睡眠的时间不一致,采用随机数的方式
    public static void main(String[] args) {
        // 期望 count<0退出循环
        new Thread(()->{
            while (count>0){
                Util.sleep(200L);
                count--;
                log.debug("count:{}",count);
            }
        },"A").start();
        // 期望 count>40退出循环
        new Thread(()->{
            while (count<40){
                Util.sleep(200L);
                count++;
                log.debug("count:{}",count);
            }
        },"B").start();
    }
}

3.15 .3 饥饿

线程饥饿的例子,先来看看使用顺序加锁的方式解决之前的死锁问题

image-20211121172851972

image-20211121175602488

之前的死锁代码:我们改造一下,按照顺序的方式进行加锁,那么就不会出现死锁的问题,那么新的问题又来了,就是会导致线程饥饿问题,有的线程一直得不到执行权,有时候A线程长时间获得锁,有时候B线程长时间获得锁

@Slf4j
public class Test {
    static final Object A = new Object();
    static final Object B = new Object();
    public static void main(String[] args) {


        new Thread(()->{
           while (true){
               synchronized (A){

                   log.debug("获得A对象锁");
                   synchronized (B){
                       log.debug("获得B对象锁");
                   }
               }
           }
        },"A").start();

        new Thread(()->{
            while (true){
                synchronized (A){

                    log.debug("获得B对象锁:" );
                    synchronized (B){
                        log.debug("获得A对象锁");
                    }
                }
            }
        },"B").start();
    }
}

3.16 ReentrantLock

相对于 synchronized 它具备如下特点

  • 可中断 可以设置超时时间
  • 可以设置为公平锁
  • 支持多个条件变量 [ 多个waitSet ]
  • 与 synchronized 一样,都支持可重入 基本语法

基本语法:

// 获取锁
reentrantLock.lock();
try {
 // 临界区
} finally {
 // 释放锁
 reentrantLock.unlock();
}

3.16.1 可重入

  • 可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
  • 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
@Slf4j
public class ReentrantLockDemo {

    static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        method1();
    }
    public static void method1() {
        lock.lock();
        try {
            log.debug("execute method1");
            method2();
        } finally {
            lock.unlock();
        }
    }
    public static void method2() {
        lock.lock();
        try {
            log.debug("execute method2");
            method3();
        } finally {
            lock.unlock();
        }
    }
    public static void method3() {
        lock.lock();
        try {
            log.debug("execute method3");
        } finally {
            lock.unlock();
        }
    }

}
// 未加锁就调用lock.unlock 会抛出 IllegalMonitorStateException

3.16.2可打断

注意如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断


    static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            try {
                log.debug("尝试获得锁");
                // 如果此处使用的是 lock方法是不能进行打断的
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.debug("没有获得锁");
                return;
            } finally {
                lock.unlock();
            }
        }, "lock");

        // main线程先获得锁
        lock.lock();
        // 启动main线程
        thread.start();
        // 先让main线程睡眠1秒
        Util.sleep(1);
        // 打断 lock 线程
        thread.interrupt();

    }

3.16.3 可超时

    static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            // 尝试获得锁,获取不到就进入阻塞队列,下次再获取 获取到锁:true,没有获取到锁:false
            // lock.tryLock(1,TimeUtil.SECONDS) // 尝试等待 1秒获取锁,如果1秒后获取不了就继续向下执行[支持可打断]
              if (!lock.tryLock()){
                  log.debug("获取不到锁");
                  return;
              }
              try {
                  log.debug("获得到锁");
              }finally {
                  lock.unlock();
              }
        }, "lock");
        // main线程获得锁后,lock线程尝试获得锁,获得不了就不进行阻塞了
        // lock.lock();
        thread.start();
        Util.sleep(1);
        
    }
//  lock.tryLock(1,TimeUtil.SECONDS) 可打断 在指定时间内获取不到锁 立刻结束等待
// lock.tryLock() 不可打断 获取不到锁立刻结束等待

3.16.4 解决哲学家就餐问题


    @Override
    public void run() {
     // 原来的版本   
    /*    while (true) {
            // 获得左手筷子
            synchronized (left) {
                // 获得右手筷子
                synchronized (right) {
                    // 吃饭
                    eat();
                }
                // 放下右手筷子
            }
            // 放下左手筷子
        }*/

        // 使用 ReentrantLock 改进后的版本
        while (true){
            // 睡眠 300 毫秒方便查看
            Util.sleep(300L);
            // 尝试获得左手筷子
            if (left.tryLock()){
                try {
                    // 尝试获得右手筷子
                    if (right.tryLock()){
                        try {
                            // 两支筷子都有了,可以吃饭了
                            eat();
                        }finally {
                            // 吃完后释放右手筷子
                            right.unlock();
                        }
                    }
                }finally {
                    // 吃完后释放左手筷子
                    left.unlock();
                }
            }


        }
    }

3.16.5 公平性

ReentrantLock 默认是不公平的,可以通过构造方法传递 ture 来设置为公平锁

// 设置为true和false会有两种不同的效果 
static ReentrantLock lock = new ReentrantLock(true);
    public static void main(String[] args) throws InterruptedException {

        new Thread(()->{
            while (true){
                try {
                    lock.lock();
                    log.debug("获得锁:A线程");
                }finally {
                    lock.unlock();
                }
            }
        },"A").start();

        new Thread(()->{
            while (true){
                try {
                    lock.lock();
                    log.debug("获得锁:B线程");
                }finally {
                    lock.unlock();
                }
            }
        },"B").start();
    }

公平锁一般没有必要,会降低并发度,后面分析原理时会讲解

3.16.6条件变量

synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量

  • synchronized 是那些不满足条件的线程都在一间休息室等消息
  • 而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒

使用要点:

  • await 前需要获得锁
  • await 执行后,会释放锁,进入 conditionObject 等待
  • await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
  • 竞争 lock 锁成功后,从 await 后继续执行
 static ReentrantLock lock = new ReentrantLock();
    static Condition waitCigaretteQueue = lock.newCondition();
    static Condition waitBreakfastQueue = lock.newCondition();
    static volatile boolean hasCigarette = false;
    static volatile boolean hasBreakfast = false;
    public static void main(String[] args) {
        new Thread(() -> {
           try {
               lock.lock();
               log.debug("有烟没?[{}]", hasCigarette);
               if (!hasCigarette) {
                   log.debug("没烟,先歇会!");
                   try {
                       waitCigaretteQueue.await();
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }

                   log.debug("可以开始干活了[小南]");

           }finally {
               lock.unlock();
           }
        }, "小南").start();

        new Thread(() -> {
            try {
                lock.lock();
                log.debug("有早餐没?[{}]", hasBreakfast);
                if (!hasBreakfast) {
                    log.debug("没早餐,休息会儿");
                    try {
                        waitBreakfastQueue.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                log.debug("可以开始干活了[小女]");

            }finally {
                lock.unlock();
            }
        }, "小女").start();

        new Thread(() -> {
            try {
                 lock.lock();
                 hasCigarette = true;
                 waitCigaretteQueue.signal();
                 log.debug("烟到了噢!");
             }finally {
                 lock.unlock();
             }

        }, "送烟").start();

        new Thread(() -> {
            try {
                lock.lock();
                hasBreakfast = true;
                waitBreakfastQueue.signal();
                log.debug("早餐到了噢!");
            }finally {
                lock.unlock();
            }
        }, "送早餐").start();

    }

synchronized 和 Condition 使用上都是差不多的,但是最大的一个区别就是 Condition 可以精准的唤醒某个线程,而synchronized只能随机唤醒一个正在等待线程或唤醒所有正在等待的线程

3.17.同步模式-顺序控制

3.17.1 warit和notify (执行先后)

现在有两个线程:需要指定让某个线程先执行

// 目的: 让B线程先输出2 A线程再输出1
@Slf4j
public class FixedOrderWait {
    static  final  Object lock = new Object();
    // 用于标记 B线程是否运行过 运行过就是 true
    static  volatile  boolean flag=false;
    public static void main(String[] args) {

        Thread a = new Thread(() -> {
            synchronized (lock){
                // 循环判断 B线程是否运行过,如果没有就一直进行等待
               while (!flag){
                   try {
                       lock.wait();
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }

                log.debug("1");
            }
        }, "A");

        Thread b = new Thread(() -> {
            log.debug("2");
          synchronized (lock){
              // 把运行过的标志设置为true
              flag=true;
              // 唤醒 A 线程进行打印
               lock.notify();
          }
        }, "B");

        // 启动 a b 线程
        a.start();
        b.start();
    }
}

3.17.2 Park Unpark [执行先后]

可以看到,实现上很麻烦:

  • 首先,需要保证先 wait 再 notify,否则 wait 线程永远得不到唤醒。因此使用了『运行标记』来判断该不该wait
  • 第二,如果有些干扰线程错误地 notify 了 wait 线程,条件不满足时还要重新等待,使用了 while 循环来解决此问题
  • 最后,唤醒对象上的 wait 线程需要使用 notifyAll,因为『同步对象』上的等待线程可能不止一个
  • 可以使用 LockSupport 类的 park 和 unpark 来简化上面的题目:
 public static void main(String[] args) {

        Thread a = new Thread(() -> {
            // 先进行阻塞 等待 B线程唤醒
            LockSupport.park();
            log.debug("1");
        }, "A");

        Thread b = new Thread(() -> {
           log.debug("2");
           // 唤醒A线程
           LockSupport.unpark(a);
        }, "B");

        // 启动 a b 线程
        a.start();
        b.start();
    }

3.17.3 ReentrantLock [执行先后]

    volatile private static int state = 1;
    private static  ReentrantLock lock = new ReentrantLock();
    final private static Condition conditionA = lock.newCondition();

    public static void main(String[] args){

        Thread threadA = new Thread(()->{
            try {
                lock.lock();
                while(state != 2){
                    conditionA.await();
                }
                log.debug("1");
               
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally{
                lock.unlock();
            }
        },"A");


        Thread threadB = new Thread(()->{
            try {
                lock.lock();
                log.debug("2");
                state = 2;
                conditionA.signal();
            }finally{
                lock.unlock();
            }
        },"B");

        // 启动 a b 线程
           new Thread(threadA).start();
           new Thread(threadB).start();
    }

3.17.4 wait notify [交替打印]

@Slf4j
public class WaitNotifyDemo{

    public static void main(String[] args) {

        WaitNotify syncWaitNotify = new WaitNotify(1, 5);
        new Thread(() -> {
            syncWaitNotify.print(1, 2, "a");
        }).start();
        new Thread(() -> {
            syncWaitNotify.print(2, 3, "b");
        }).start();
        new Thread(() -> {
            syncWaitNotify.print(3, 1, "c");
        }).start();

    }

}

class WaitNotify{

    /**
     * 等待标记:用于表示谁可以运行
     */
    private int flag;
    private int loopNumber;
    public WaitNotify(int flag,int loopNumber){
        this.flag=flag;
        this.loopNumber=loopNumber;
    }
    public void SyncWaitNotify(int flag, int loopNumber) {
        this.flag = flag;
        this.loopNumber = loopNumber;
    }
    public void print(int waitFlag, int nextFlag, String str) {
        for (int i = 0; i < loopNumber; i++) {
            synchronized (this) {
                // 当前标记不是下一个线程的标记值时一直等待
                while (this.flag != waitFlag) {
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.print(str);
                // 修改执行下一个线程的标记
                flag = nextFlag;
                // 唤醒所有的线程
                this.notifyAll();
            }

        }
    }
}

3.17.5 Lock 条件变量 [交替打印]

@Slf4j
public class AwaitSignal extends ReentrantLock {

    public void start(Condition first) {
        this.lock();
        try {
            log.debug("start");
            // 先去唤醒A线程
            first.signal();
        } finally {
            this.unlock();
        }
    }

    public void print(String str, Condition current, Condition next) {
        for (int i = 0; i < loopNumber; i++) {
            this.lock();
            try {
                // 一开始先让所有的线程都进入等待
                current.await();
                log.debug(str.equals("c") ? str+"\n" : str);
                // 精准唤醒下一个线程
                next.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                this.unlock();
            }
        }
    }
    // 循环次数
    private int loopNumber;
    public AwaitSignal(int loopNumber) {
        this.loopNumber = loopNumber;
    }

    public static void main(String[] args) {

        AwaitSignal as = new AwaitSignal(5);

        Condition aWaitSet = as.newCondition();
        Condition bWaitSet = as.newCondition();
        Condition cWaitSet = as.newCondition();

        new Thread(() -> { as.print("a", aWaitSet, bWaitSet); }).start();
        new Thread(() -> { as.print("b", bWaitSet, cWaitSet); }).start();
        new Thread(() -> { as.print("c", cWaitSet, aWaitSet); }).start();

        as.start(aWaitSet);

    }
}

3.17.6 Park Unpark [交替打印]

public class ParkDemo {
    public static void main(String[] args) {
        SyncPark syncPark = new SyncPark(5);
        Thread t1 = new Thread(() -> {
            syncPark.print("a");
        });
        Thread t2 = new Thread(() -> {
            syncPark.print("b");
        });
        Thread t3 = new Thread(() -> {
            syncPark.print("c\n");
        });
        syncPark.setThreads(t1, t2, t3);
        syncPark.start();


    }
}
class SyncPark{
    private int loopNumber;
    private Thread[] threads;
    public SyncPark(int loopNumber) {
        this.loopNumber = loopNumber;
    }
    // 把创建好的线程传递过来
    public void setThreads(Thread... threads) {
        this.threads = threads;
    }
    // 
    public void print(String str) {
        for (int i = 0; i < loopNumber; i++) {
            // 让所有的线程都进入 阻塞状态,唯独让thread[0]先执行
            LockSupport.park();
            System.out.print(str);
            // 取消下一个线程的阻塞状态
            LockSupport.unpark(nextThread());
        }
    }
    //
    private Thread nextThread() {
        // 获取到当前线程
        Thread current = Thread.currentThread();
        int index = 0;
        // 获取到当前正在运行的线程
        for (int i = 0; i < threads.length; i++) {
            if(threads[i] == current) {
                index = i;
                break;
            }
        }
        // 返回当前正在运行的线程的下一个该执行的线程
        if(index < threads.length - 1) {
            return threads[index+1];
        } else {
            return threads[0];
        }
    }
    // 遍历所有的线程,都将其启动,先给threads[0]执行权
    public void start() {
        
        for (Thread thread : threads) {
            thread.start();
        }
        // 如果先执行了unpark,则再执行park加锁的时候就不成功,依旧可以向下运行
        LockSupport.unpark(threads[0]);
    }

}

3.18本章小结

本章我们需要重点掌握的是

  • 分析多线程访问共享资源时,哪些代码片段属于临界区
  • 使用 synchronized 互斥解决临界区的线程安全问题
    • 掌握 synchronized 锁对象语法
    • 掌握 synchronzied 加载成员方法和静态方法语法
    • 掌握 wait/notify 同步方法
  • 使用 lock 互斥解决临界区的线程安全问题
    • 掌握 lock 的使用细节:可打断、锁超时、公平锁、条件变量
  • 学会分析变量的线程安全性、掌握常见线程安全类的使用
  • 了解线程活跃性问题:死锁、活锁、饥饿
  • 应用方面
    • 互斥:使用 synchronized 或 Lock 达到共享资源互斥效果
    • 同步:使用 wait/notify 或 Lock 的条件变量来达到线程间通信效果
  • 原理方面
    • monitor、synchronized 、wait/notify 原理
    • synchronized 进阶原理
    • park & unpark 原理
  • 模式方面
    • 同步模式之保护性暂停
    • 异步模式之生产者消费者
    • 同步模式之顺序控制

4.共享模型之内存

4.1 Java 内存模型

JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。
JMM 体现在以下几个方面

  • 原子性 - 保证指令不会受到线程上下文切换的影响
  • 可见性 - 保证指令不会受 cpu 缓存的影响
  • 有序性 - 保证指令不会受 cpu 指令并行优化的影响
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值