Java并发编程-共享模型之管程(Monitor/Synchronized)(四)

共享问题

java中对全局变量的操作是通过JMM(java内存模型)内存模型实现的,全局变量保存在主存中,但是变量的计算则是在线程的工作内存中。
Java并发编程-共享模型之管程(Monitor/Synchronized)(四)_第1张图片
如果对变量的操作不是原子操作(比如i++是由多条指令的操作集合)那么就会带来线程安全问题。
比如下面的i++和i++操作,结果可能就不是0。

   static int count = 0;

    @Test
    public void testJMM() throws InterruptedException {

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                count++;
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                count--;
            }
        }, "t2");


        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }

原因分析

count++或者count–不是原子操作,可能会在中间的某条指令还没执行完时时间片却用完了,导致写入主内存数据的顺序不一致。

46 getstatic #46 
49 iconst_1
50 iadd
51 putstatic #46 

Java并发编程-共享模型之管程(Monitor/Synchronized)(四)_第2张图片

临界区(Critical Section)

  • 一个程序运行多个线程本身是没有问题的。
  • 问题出在多个线程访问 共享资源。
    • 多个线程 共享资源 其实也没有问题。
    • 在多个线程对 共享资源 读写操作时发生指令交错,就会出现问题。
  • 一段代码块内如果存在对 共享资源 的多线程读写操作,城这点代码块为临界区。
    例如下面代码块中的临界区:
    Java并发编程-共享模型之管程(Monitor/Synchronized)(四)_第3张图片

竞态条件 Race Condition

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

synchronized

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

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

总结

synchronized实际上是用 对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。

变量的线程安全分析

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

  • 如果它们没有共享,则线程安全。
  • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况。
    • 如果只有读操作,则线程安全。
    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全。

局部变量是否线程安全

  • 局部变量是线程安全的。
  • 但局部变量引用的对象则未必
    • 如果该对象没有逃离方法的作用范围,则它是线程安全的。
    • 如果该对象逃离方法的作用范围(比如return 局部变量,也就是局部变量暴露给外部了),需要考虑线程安全。

线程不安全示例

对于成员变量读写,可能会造成线程不安全。

 public static void main(String[] args) {
        int threadCount = 2;
        int loop = 200;
        ThreadUnsafe threadUnsafe = new ThreadUnsafe();
        for (int i = 0; i < threadCount; i++) {
            Thread thread = new Thread(() -> {
                for(int j=0; j < loop; j++) {
                    threadUnsafe.method();
                }
            }, "t" + i);
            thread.start();
        }
    }

    static class ThreadUnsafe {
        private List<String> arr = new ArrayList<>();
        public void method1() {
            addE();
            removeE();
        }

        private void method2() {
            arr.add("1");
        }

        private void method3() {
            arr.remove(0);
        }
    }

执行结果:

com.study.concurrentprogramming.practice.threadCommon
Exception in thread "t0" Exception in thread "t1"
	at java.util.ArrayList.remove(ArrayList.java:505)
	at com.study.concurrentprogramming.practice.threadCommon$ThreadUnsafe.removeE(threadCommon.java:38)
	at com.study.concurrentprogramming.practice.threadCommon$ThreadUnsafe.method(threadCommon.java:30)
	at com.study.concurrentprogramming.practice.threadCommon.lambda$main$0(threadCommon.java:18)
	at java.lang.Thread.run(Thread.java:748)
java.lang.ArrayIndexOutOfBoundsException: -2
	at java.util.ArrayList.remove(ArrayList.java:505)
	at com.study.concurrentprogramming.practice.threadCommon$ThreadUnsafe.removeE(threadCommon.java:38)
	at com.study.concurrentprogramming.practice.threadCommon$ThreadUnsafe.method(threadCommon.java:30)
	at com.study.concurrentprogramming.practice.threadCommon.lambda$main$0(threadCommon.java:18)
	at java.lang.Thread.run(Thread.java:748)

Process finished with exit code 0

导致失败的原因是增加元素不是原子操作,存在指令交错执行,线程0和线程1分别执行了add操作后,可能只对成员列表增加了一个元素,两个线程分别删除时导致数组越界。

变量读取模型

Java并发编程-共享模型之管程(Monitor/Synchronized)(四)_第4张图片

成员变量改成局部变量

public static void main(String[] args) {
        int threadCount = 2;
        int loop = 200;
        ThreadSafe threadUnsafe = new ThreadSafe();
        for (int i = 0; i < threadCount; i++) {
            Thread thread = new Thread(() -> {
                for(int j=0; j < loop; j++) {
                    threadUnsafe.method();
                }
            }, "t" + i);
            thread.start();
        }
    }

    static class ThreadSafe {

        public void method1() {
            List<String> arr = new ArrayList<>();
            addE(arr);
            removeE(arr);
        }

        private void method2(List<String> arr) {
            arr.add("1");
        }

        private void method3(List<String> arr) {
            arr.remove(0);
        }
    }
变量读取模型

Java并发编程-共享模型之管程(Monitor/Synchronized)(四)_第5张图片

常见线程安全类

  • String
  • StringBuffer
  • Integer
  • Random
  • Vector
  • HashTable
  • java.util.concurrent(juc)包下的类
    这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为:
    • 它们的每个方法是原子的。
    • 但注意 它们多个方法的 组合不是原子的。

不可变类线程安全

String、Integer等都是 不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的。
每个不可变类的状态都是固定的,因为是不变的,所以一个线程一旦创建了不可变类,那么不同的线程只是读取它们的状态而不会改变它们的状态,所以是线程安全的。

总结:

  • 方法是否线程安全,首先要看对象是否是单个实例,如果单个实例,那么方法中的变量是否多个线程共享的,如果共享就是线程不安全的。
  • 变量能写成局部变量,尽量使用方法内局部变量。
  • 如果设计一个不可变类,一定要设计成final类,不然子类可能覆盖不可变类的方法,造成线程不安全,这就是开闭原则的思想,借鉴String类的实现。

Monitor

java对象头

以32为虚拟机为例:
Integer对象保护:8字节对象头 + 4字节数据;int 变量只有4字节数据。
普通 对象:

|===============================================================================================================|
|                                         Object Header(64bits)                                                 |
|==================================================|============================================================|
|                    Mark Word(32bits)            |              Klass Word(32bits) 指向方法区的Class对象     |
|===================================================|===========================================================|
其中Mark Word结构为:
|===================================================================================|===========================|
|                 Mark Word(32bits)                                               |State(不同状态下的markwork值)|
=====================================================================================|===========================|
| hashcode:25                               |  age:4            | biased_lock:0 | 01 |           Normal          |
|====================================================================================|===========================|
| thread:23 | epoch:2                       | age:4             | biased_lock:1 | 01 |           Biased          |
|====================================================================================|===========================|
|                              ptr_to_lock_record:30                            | 00 |  Lightweight Locked       |
|====================================================================================|===========================|
|                              ptr_to_heavyweight_monitor:30                    | 10 |  Heavyweight Locked       |
|====================================================================================|===========================|
|                                                                               | 11 |           Marked for GC   |
|====================================================================================|===========================|   

Monitor(锁)原理

Monitor被翻译成监视器或管程。
每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word中就被设置指向Monitor对象的指针,Monitor是 操作系统的对象非java中管理的对象,synchronized给对象上锁的过程就是把对象头的Mark Word指向操作系统的一个Monitor对象。
Monitor结构如下:
Java并发编程-共享模型之管程(Monitor/Synchronized)(四)_第6张图片

  • 刚开始Monitor中Owner为null。
  • 当Thread2执行Synchronize(obj)就会将Monitor的所有者Owner置位Thread2,Monitor中只能有一个Owner。
  • 在Thead2上锁的过程中,如果Thread3、Thread4、Thread5也来执行synchronized(obj),就会进入EntryList Blocked队列(等待或者阻塞队列)。
  • Thread2执行完同步代码块的内容,然后唤醒EntryList中等待的线程来竞争 锁,竞争是非公平的(非FIFO的)。
  • 图中WaitSet中的Thread0,Thread1是之前或得过锁,但是条件不满足执行wait方法进入了Wating状态的线程。

注意:

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

synchronized进阶

在java6之前synchronized获取的对象是直接关联操作系统的Monitor锁的,但是Monitor是操作系统提供的,成本比较高,某些场景会降低程序的性能。为了解决这个问题,java6开始对synchronized获取锁的逻辑做了优化。
首先通过一个小故事来形象的感知一下synchronized优化后获取锁的原理。

故事主角:

  • 老王:JVM。
  • 小明:线程
  • 小芳:线程
  • 房间:对象,synchronized操作的对象。
  • 房间门上防盗锁:Monitor,重量级锁。
  • 房间门上挂书包:轻量级锁。
  • 房间门上写名字:偏向锁。
  • 批量写名字:一个类的偏向锁撤销次数 达到20 这个阈值。
  • 不能写名字:批量撤销一个类的对象的偏向锁,设置该类不可偏向。
    故事内容:
    小明要使用房间保证计算不被其他人干扰(原子性),最初,他用的是防盗锁,当上下文切换时,门被锁住了,所以即使他离开了,别人也进不去,他的工作就是安全的。
    但是,很多情况下没人跟他来竞争房间的使用权。小芳用房间的时间和小明是错开的,小明白天用,小芳晚上用。每次上锁太麻烦,于是就有了优化。
    小明和小芳约定,不锁门了,而是谁用房间,谁就把自己的书包挂在门口,但是他们的书包样式是一样的,因此每次进门前要先翻翻书包,看看课本是谁的,如果是自己的,那么就可以进门,这样省的上锁解锁了。如果书包不是自己的,就在门外等,并通知对方下次用上防盗锁的方式,因为产生了竞争关系,需要加强锁机制,所以使用了重量级锁。
    后来,小芳回老家了,很长一段时间都不会用这个房间。小明每次还是挂书包,翻书包,虽然被上锁省事但是还是麻烦。
    于是,小明干脆在门上写上了自己的名字,下次来用房间时,只要自己的名字还在,那么就可以安全的使用房间。如果这期间有其他人用这个房间,那么使用者将小明的名字擦掉,升级为挂书包的方式。
    同学们都放假回老家了,小明开始膨胀了,在20个房间都写上了自己的名字,想进哪个就进哪个。后来他自己回老家了,这是小芳回来了,她要用这些房间,结果就是一个一个的擦掉小明的名字,升级为挂书包的方式。老王觉得这成本有点高,提出了一种批量写名字的方法,他让小芳不用挂书包了,可以直接在门上写上自己的名字。
    后来,写名字的现象越来越多,老王受不了了,规定这些房间不能不能写名字,只能挂书包。

轻量级锁

轻量级锁的使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者透明,语法还是synchronized。
假如有两个方法同步块,利用同一个对象加锁。

public class sync {
    static final Object obj = new Object();

    public static void method1() {
        synchronized (obj) {
            method2();
        }
    }

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

锁膨胀

自旋优化

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

偏向锁

锁消除

锁的优缺点比较

Java并发编程-共享模型之管程(Monitor/Synchronized)(四)_第7张图片

wait/notify

为什么需要wait

  • 由于条件不满足,小南不能继续进行计算
  • 但小南如果一直占用锁,其他人就得一直阻塞,效率太低
    Java并发编程-共享模型之管程(Monitor/Synchronized)(四)_第8张图片
  • 于是老王单开了一间休息室(调用wait方法),让小南到休息室(waitSet)等着去了,但这时锁释放开,其他人可以由老王随机安排进屋。
  • 直到小M将烟送来,大叫一声【你的烟到了】(调用notify方法)。
    Java并发编程-共享模型之管程(Monitor/Synchronized)(四)_第9张图片
  • 小南于是可以离开休息室,重新进入竞争锁的队列(entryList),会在entryList队列中重新排队公平竞争,不存在优先级问题。
    Java并发编程-共享模型之管程(Monitor/Synchronized)(四)_第10张图片

wait/notify原理

Java并发编程-共享模型之管程(Monitor/Synchronized)(四)_第11张图片

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

API介绍

  • obj.wait()让进入object监视器的线程到waitSet等待。
  • obj.notify()在object上正在waitSet等待的线程中挑一个唤醒到entryList排队竞争。
  • obj.notifyAll()让object上正在waitSet等待的线程全部唤醒到entryList排队竞争。
    他们都是线程之间协作的手段,都属于Object对象的方法。都是释放锁,因此必须获得对象的锁,才能调用这几个方法。

sleep(long n)和wait(long n)的区别

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

线程状态转换

活跃性

Lock

你可能感兴趣的:(并发编程,java)