java并发编程(3) 共享模型之管程 1

文章目录

  • 前言
  • 1. java的内存模型
    • 1. 并发编程的两个问题
    • 2. Java内存模型的抽象结构
  • 2. 问题分析
  • 3. 临界区和竞态条件
    • 1 临界区 Critical Section
    • 2 竞态条件
  • 4. 解决方法
    • 1. synchronized
    • 2. 使用方法
    • 3. 理解
    • 4. 使用面向对象改造代码
    • 5. synchronized加在方法上
    • 6. 线程8锁
      • 1. 先一后二或者先二后一
      • 2. 1秒后12,或者2然后1秒后再1
      • 3. 加多一个普通方法
      • 4. 锁的是不同对象
      • 5. 静态方法
      • 6. 锁当前类对象
      • 7. 两个对象,一个线程锁对象,一个线程锁类
      • 8. 两个对象,锁住统一对像,都是类
      • 9. 小结
    • 7. synchronized知识补充
      • 7.1 作用机制


前言

这一系列资料基于黑马的视频:java并发编程,目前还没有看完,整体下来这是我看过的最好的并发编程的视频。下面是根据视频做的笔记。


1. java的内存模型

这一部分基于《java并发编程的艺术》这本书

1. 并发编程的两个问题

在并发编程中,需要注意两个问题:

  • 线程之间如何通信(交换信息)
  • 线程之间如何同步(这里的线程是指并发执行的活动实体)

通信机制:

  • 共享内存:在共享内存之中,线程之间通过读 - 写内存中的公共状态进行隐式通信。也就说所有的线程共同享有一个内存区域,该区域的数据随着线程内存的数据更新而更新,其他线程就从其中读取来获得最新的消息。Java中采用的就是这种模型
  • 消息传递:线程之间没有公共状态,线程之间必须通过发送消息来显示进行通信。

同步:

  • 同步是指程序中用于控制不同线程间操作发生相对顺序的机制。在共享内存并发模型里面,同步是显示进行的
  • 程序员要显示指定某个方法或者某段代码需要在线程之间互斥执行。为了能达到消息的读取统一。

2. Java内存模型的抽象结构

Java中,所有的实例区静态域 和数组元素都存储在堆内存中。堆内存在线程中是共享的。局部变量方法定义参数,和异常处理器参数不会在线程中共享,这三种不会存在内存可见性的问题,也不受内存模型的影响。


JMM : Java内存模型,这种模型决定了一个线程对共享变量的写入对另一个线程可间。在这种模型中,线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了共享变量的副本。简单来说就是有一个公共空间,以及每个线程的私有空间,线程对于共享变量的改变首先发生在私有空间,最后同步到公有空间,而线程是不可以直接获取到其他线程的私有空间的变量的,只能从公有空间去读取,抽象图示:
java并发编程(3) 共享模型之管程 1_第1张图片
在图中, 可以看出,如果A要和B进行通信,那么至少需要以下步骤:

  • 线程A首先把数据写入本次内存A
  • 线程A把本地内存A中的共享变量刷新到主存中去
  • 线程B到主存中去读取线程A之前已经更新过的共享变量

下面演示下这个过程,我们假设一开始有一个变量x=0,之后线程1把x改成了1,然后B再读取主存中的x=1:
java并发编程(3) 共享模型之管程 1_第2张图片

结论:整体来看,这两个步骤实际上是线程A在向线程B发送消息,而且这个通信过程必须要经过主存。JMM通过控制主存与每个线程的本地内存之间的交互,来为Java程序员提供内存可见性保证。



2. 问题分析

既然知道了JMM内存模型,了解了线程之间的通信,那么这种模型下面难免会出现一些错误的情况,比如:

  • 线程没有来得及把本地内存的共享变量刷新到主存导致其他线程接受不到消息

下面用代码来体现这个错误,两个线程,一个+5000次,一个-5000次,但是结果不是0:

@Slf4j
public class Test1 {
    static int count = 0;

    public static void main(String[] args) 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();
        //t2先运行完就等t1
        t1.join();
        //t1先运行完就等t2
        t2.join();
        log.debug("{}", count);
        //DEBUG [main] (12:40:47,384) (Test1.java:33) - -206
    }
}

问题分析:
以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作,要彻底理解,必须从字节码来进行分析
例如对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:

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

而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:
java并发编程(3) 共享模型之管程 1_第3张图片
上面的操作在单线程下面执行是没有问题的,因为不会产生共享内存的问题,但是在多线程下就不行了,就会尝试共享内存引起的一些问题。
单线程下:
java并发编程(3) 共享模型之管程 1_第4张图片


多线程下出现负数的情况:线程2的结果把线程1的覆盖掉了
java并发编程(3) 共享模型之管程 1_第5张图片


多线程下出现正数的情况:线程1的结果把线程2的覆盖掉了
java并发编程(3) 共享模型之管程 1_第6张图片
小结一下:上面写的情况发生的原因都是因为线程没来的及把自身的数据给同步到主存中,导致把下一个线程同步的数据覆盖了。



3. 临界区和竞态条件

1 临界区 Critical Section

  • 一个程序运行多个线程本身是没有问题的
  • 问题出在多个线程访问共享资源
    1、多个线程只是访问其实也没什么问题
    2、出现问题主要是在对共享资源进行写操作
  • 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

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

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

2 竞态条件

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



4. 解决方法

1. synchronized

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

  • 阻塞式的解决方案:synchronized,Lock
  • 非阻塞的解决方法:原子类

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

类比过来就是有一个房间,线程就是一个一个的人,一个人进去了这个房间就给这个房间上锁,其他人进不去,等到这个房间里面的人干完了所有的事,开门,下一个人进去。

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

  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
  • 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点

2. 使用方法

synchronized(对象){
	//代码
}

使用synchronized来解决这个问题的代码:

@Slf4j
public class Test1 {
    static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for(int i = 0; i < 5000; i++){
                synchronized (Test1.class){
                    count ++;
                }
            }
        }, "t1");

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

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.debug("{}", count);
        //DEBUG [main] (12:40:47,384) (Test1.java:33) - -206
    }
}

java并发编程(3) 共享模型之管程 1_第7张图片

解释:

  • 用上面这张图来解释,一开始进入了这个room,就相当于拿到了这个锁,synchronized(room),其他人被锁在门外
  • 第二个人要进入的时候发现锁没有拿到,只能等待第一个人开门
  • 尽管此时CPU的时间片用完了,也没有关系,因为锁还是在第一个人手上,第二个人想进来也进不来,只能等第一个人又分配到时间片执行完成释放锁,后面才可以继续

java并发编程(3) 共享模型之管程 1_第8张图片
可以看到此时就算线程1有CPU时间片也没有用,因为锁还在线程2手上,线程1无法执行,必须等线程2再次分配到时间片执行完成释放锁,线程1的才可以开始执行,这样就可以保证顺序不被打乱了。


3. 理解

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

  1. 如果把synchronized(obj)放在for循环的外面,如何理解?
    a. 放在外面就是5000*4=20000条指令不会被其他线程打断
    b. 也就是说放在外面就会导致先执行5000次++,再执行5000次–

  2. 如果t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎样运作?
    a. 不同的锁就代表了两个线程可以同时操作同一个共享数据,因为是不同锁
    b. 这就导致了还是有可能出现不是0的情况
    c. 必须加同一对象锁才可以

  3. 如果t1 synchronized(obj) 而t2没加会怎么样?如何理解?
    a. t2没加,也就不会尝试去获取锁,那么同样t2会直接修改,不安全


4. 使用面向对象改造代码

把增加的方法和减少的方法都封装成一个到对象中

//改成面向对象封装
@Slf4j
public class Test1 {
    static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Room room = new Room();
        Thread t1 = new Thread(()->{
            room.increment();
        }, "t1");

        Thread t2 = new Thread(()->{
            room.decrement();
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.debug("{}", room.getCount());
        //DEBUG [main] (12:40:47,384) (Test1.java:33) - -206
    }

}

class Room{
    private int count = 0;

    public void increment(){
        synchronized (this){
            count ++;
        }

    }

    public void decrement(){
        synchronized (this){
            count --;
        }

    }

    public int getCount(){
        synchronized (this){
            return count;
        }
    }

}


5. synchronized加在方法上

  • 成员方法:相当于锁住当前对象
  • 静态方法:相当于锁住当前类
  • 不加 synchronzied 的方法就好比不遵守规则的人,不去老实排队(好比翻窗户进去的)
class Test{
     	//加载成员方法上
        public synchronized void test() {

        }
    }
    //等价于
    class Test{
        public void test() {
            //锁住当前对象
            synchronized(this) {

            }
        }
    }
//------------------------------------------------------------------------------------------------
    class Test{
        //静态方法
        public synchronized static void test() {
        }
    }
   // 等价于
    class Test{
        public static void test() {
            //锁住当前类
            synchronized(Test.class) {

            }
        }
    }

6. 线程8锁

1. 先一后二或者先二后一

原因:都是成员方法。锁住的是同一个对象n

@Slf4j
public class EightLock_1 {
    public static void main(String[] args) {
        Number n = new Number();
        new Thread(()->{
            log.debug("begin");
            n.a();
        }, "t1").start();
        new Thread(()->{
            log.debug("begin");
            n.b();
        }, "t2").start();
    }
    //DEBUG [t1] (23:59:21,334) (EightLock_1.java:16) - begin
    //DEBUG [t2] (23:59:21,334) (EightLock_1.java:20) - begin
    //DEBUG [t1] (23:59:21,338) (EightLock_1.java:30) - 1
    //DEBUG [t2] (23:59:21,338) (EightLock_1.java:34) - 2
}

@Slf4j
class Number{
    //成员方法,锁的是this对象
    public synchronized void a(){
        log.debug("1");
    }
    //成员方法,锁的是this对象,和a锁的是一个对象
    public synchronized void b(){
        log.debug("2");
    }
}

2. 1秒后12,或者2然后1秒后再1

原因:如果是t1,先执行,就睡一秒,再打印1,释放锁,这时线程二瞬间拿到锁打印2。如果是t2先执行,就是先打印2,线程1睡一秒后再打印1

/**
 * @author 
 * @Description
 * @verdion
 * @date 2021/9/7 23:56
 */
@Slf4j
public class EightLock_1 {
    public static void main(String[] args) {
        Number n = new Number();
        new Thread(()->{
            log.debug("begin");
            n.a();
        }, "t1").start();
        new Thread(()->{
            log.debug("begin");
            n.b();
        }, "t2").start();
    }
    //DEBUG [t1] (00:02:04,551) (EightLock_1.java:17) - begin
    //DEBUG [t2] (00:02:04,551) (EightLock_1.java:21) - begin
    //DEBUG [t1] (00:02:05,570) (EightLock_1.java:37) - 1
    //DEBUG [t2] (00:02:05,570) (EightLock_1.java:41) - 2
}

@Slf4j
class Number{
    //成员方法,锁的是this对象
    public synchronized void a(){
        //睡眠一秒,sleep不会释放锁
        sleep.mySleep(1);
        log.debug("1");
    }
    //成员方法,锁的是this对象,和a锁的是一个对象
    public synchronized void b(){
        log.debug("2");
    }
}


3. 加多一个普通方法

普通方法不用拿锁,同步执行

  1. 3 1s 12
  2. 23 1s 1
  3. 32 1s 1
@Slf4j
public class EightLock_1 {
    public static void main(String[] args) {
        Number n = new Number();
        new Thread(()->{
            log.debug("begin");
            n.a();
        }, "t1").start();
        new Thread(()->{
            log.debug("begin");
            n.b();
        }, "t2").start();

        new Thread(()->{
            log.debug("begin");
            n.c();
        }, "t3").start();
    }
    //DEBUG [t1] (00:08:28,981) (EightLock_1.java:17) - begin
    //DEBUG [t3] (00:08:28,981) (EightLock_1.java:26) - begin
    //DEBUG [t2] (00:08:28,981) (EightLock_1.java:21) - begin
    //DEBUG [t3] (00:08:28,985) (EightLock_1.java:46) - 3
    //DEBUG [t1] (00:08:29,997) (EightLock_1.java:38) - 1
    //DEBUG [t2] (00:08:29,999) (EightLock_1.java:42) - 2
    //
    //Process finished with exit code 0
}

@Slf4j
class Number{
    //成员方法,锁的是this对象
    public synchronized void a(){
        //睡眠一秒,sleep不会释放锁
        sleep.mySleep(1);
        log.debug("1");
    }
    //成员方法,锁的是this对象,和a锁的是一个对象
    public synchronized void b(){
        log.debug("2");
    }

    public void c(){
        log.debug("3");
    }
}


4. 锁的是不同对象

先2后1,因为n1和n2同时执行方法,1睡眠了1秒,这时候syn锁就没啥用了

@Slf4j
public class EightLock_1 {
    public static void main(String[] args) {
        Number n1 = new Number();
        Number n2 = new Number();
        new Thread(()->{
            log.debug("begin");
            n1.a();
        }, "t1").start();
        new Thread(()->{
            log.debug("begin");
            n2.b();
        }, "t2").start();

    }

}

@Slf4j
class Number{
    //成员方法,锁的是this对象
    public synchronized void a(){
        //睡眠一秒,sleep不会释放锁
        sleep.mySleep(1);
        log.debug("1");
    }

    public synchronized void b(){
        log.debug("2");
    }

}

5. 静态方法

先2后1,一个线程锁类本身,一个线程锁对象,两个线程同时执行了

@Slf4j
public class EightLock_1 {
    public static void main(String[] args) {
        Number n1 = new Number();
      
        new Thread(()->{
            log.debug("begin");
            n1.a();
        }, "t1").start();
        new Thread(()->{
            log.debug("begin");
            n1.b();
        }, "t2").start();

    }

}

@Slf4j
class Number{
    //静态方法,锁当前类
    public static synchronized void a(){
        //睡眠一秒,sleep不会释放锁
        sleep.mySleep(1);
        log.debug("1");
    }

    public synchronized void b(){
        log.debug("2");
    }

}


6. 锁当前类对象

和1一样,都是锁当前类对象,两个静态方法,此时又是回到了1的情况了,syn锁生效。

@Slf4j
public class EightLock_1 {
    public static void main(String[] args) {
        Number n1 = new Number();

        new Thread(()->{
            log.debug("begin");
            n1.a();
        }, "t1").start();
        new Thread(()->{
            log.debug("begin");
            n1.b();
        }, "t2").start();

    }

}

@Slf4j
class Number{
    //成员方法,锁的是this对象
    public static synchronized void a(){
        //睡眠一秒,sleep不会释放锁
        sleep.mySleep(1);
        log.debug("1");
    }
    //成员方法,锁的是this对象
    public static synchronized void b(){
        log.debug("2");
    }

}


7. 两个对象,一个线程锁对象,一个线程锁类

先2 1s后 1,因为此时锁的不同同一个东西

@Slf4j
public class EightLock_1 {
    public static void main(String[] args) {
        Number n1 = new Number();
        Number n2 = new Number();
        new Thread(()->{
            log.debug("begin");
            //类
            n1.a();
        }, "t1").start();
        new Thread(()->{
            log.debug("begin");
            //当前对象n2
            n2.b();
        }, "t2").start();

    }

}

@Slf4j
class Number{
    //静态方法,锁住类
    public static synchronized void a(){
        //睡眠一秒,sleep不会释放锁
        sleep.mySleep(1);
        log.debug("1");
    }
    //成员方法,锁的是this对象
    public  synchronized void b(){
        log.debug("2");
    }

}

8. 两个对象,锁住统一对像,都是类

虽然有两个对象,但是锁的是静态方法,也就是类本身,只有一个,效果又回到了1了

@Slf4j
public class EightLock_1 {
    public static void main(String[] args) {
        Number n1 = new Number();
        Number n2 = new Number();
        new Thread(()->{
            log.debug("begin");
            n1.a();
        }, "t1").start();
        new Thread(()->{
            log.debug("begin");
            n2.b();
        }, "t2").start();

    }

}

@Slf4j
class Number{
    //成员方法,锁的是this对象
    public static synchronized void a(){
        //睡眠一秒,sleep不会释放锁
        sleep.mySleep(1);
        log.debug("1");
    }
    //成员方法,锁的是this对象
    public static  synchronized void b(){
        log.debug("2");
    }

}


9. 小结

总之,不管怎么变,记住两个概念

  • 成员方法锁对象
  • 静态方法锁类

方法:有睡眠的看睡眠时间,没有睡眠就随机



7. synchronized知识补充

这部分也是从《java并发编程的艺术》一书里面摘下来的,这里详细说明了synchronized的作用机制究竟是什么


7.1 作用机制

主要保证多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,保证了线程对变量访问的可见性和排他性。

在书上写道,对同步块的实现使用了monitorenter和monitorexit指令,分别表示对同步块进行监视和退出监视同步块,同步方法是依靠方法修饰符上面的ACC_SYNCHRONIZED来完成的。


无论使用哪种方法,本质都是对一个对象的监视器(monitor)进行获取,这个过程是排他的,也就是说同一时刻只有一个线程能获取到synchronized所保护的这个对象的锁。

任何一个对象都有自己的监视器,当这个对象由同步块或者同步方法调用的时候,syn锁的是当前对象,此时线程需要获取到这个对象的监视器才可以进入同步块和同步方法,其他线程没有获取到的只能在外面等待阻塞。进入BLOCKED状态。

对象、监视器、同步队列和执行线程之间的关系如图:
java并发编程(3) 共享模型之管程 1_第9张图片
可以看到任意线程对由Syncheronized保护的Object对象的访问,首先尝试获取Object的监视器,获取成功就可以进入同步块或者同步方法中操作。获取失败的时候会进入同步队列中,线程状态变成BLOCKED。当访问Object的前驱(获取了锁的线程)释放了锁,这时候会唤醒阻塞在同步队列中的线程,让它重新尝试去获取锁的监视器。





如有错误,欢迎指出

你可能感兴趣的:(多线程,java,开发语言,后端)