java基础(多线程)-共享模型之管程

一、共享资源带来的问题

class ThreadProblem{
    static int counter = 0;
    public static void testThread(){
        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();
        try {
            t1.join();
            t2.join();
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println(counter);
    }

}

1.1 问题分析

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

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

getstatic   i   //获取静态变量i的值

iconst_1       //准备变量1

1add            //自增

putstatic      //把修改后的值存入静态变量i

 java基础(多线程)-共享模型之管程_第1张图片

 在多线程下这8行代码(8行代码指i++,i--的JVM字节码)可能交错运行:

java基础(多线程)-共享模型之管程_第2张图片

1.2 临界区 Critical Section

  • 一个程序运行多个线程本身没有问题的
  • 问题出在铎哥 线程访问共享资源
  1.  多个线程读共享资源其实也没有问题
  2.  在多个线程对共享资源读写操作时发生指令交错,就会出现问题
  • 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
static int counter = 0;
static void increment(){
    //临界区
    counter++;
}

1.3 竞态条件 Race Condition

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

二、synchronized解决方案

2.1 应用之互斥

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

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

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

注意:

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

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

2.2 synchronized语法

synchronized(对象){//对象可以使任意的,但要保证操作同一个【共享资源】使用同一个对象

        临界区

}

class ThreadSynchronized{
    static int counter = 0;
    static Object room = new Object();
    public static void testSynchronized(){
        Thread t1 = new Thread(()-> {
            for (int i = 0; i < 5000; i++) {
                synchronized(room){
                    counter++;
                }
            }
        },"t1");

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

        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println(counter);
    }
}

2.3  synchronized的理解

做一个类比:

  • synchronized(对象) 中的对象,可以想象成一个房间(room),有唯一的入口(门),房间只能一次进入一人进行计算,线程t1,t2想象成两个人。
  • 当线程t1执行到synchronized(room)时就好比t1进入了这个房间,并锁住了门拿走了钥匙,在门内执行counter++代码。
  • 这时候如果t2也运行到了synchronized(room)时,它发现门被锁住了,只能在门外等待,发生了上下文切换,阻塞住了。
  • 这中间即使t1的cpu时间片不幸用完,被踢出了门外(不要错误的理解为锁住了对象就能一直执行下去哦),这时门还是被锁住的,t1仍然拿着钥匙,t2线程还在阻塞状态进不来,只有下次CPU时间片分配给t1,t1再次获得时间片才能开门进入。
  • 当t1执行完synchronized(room)块内的代码,这时候才会从obj房间出来,并揭开门上的锁,唤醒t2等其他线程,其他某个线程拿到钥匙才可以进入obj房间,锁住门拿着钥匙,执行它的counter--代码。

java基础(多线程)-共享模型之管程_第3张图片

java基础(多线程)-共享模型之管程_第4张图片 2.4 synchronized总结

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

2.5 锁对象面向对象的改进

把需要保护的共享变量放入一个类

class Room{
    private int value=0;
    public void increment(){
        synchronized (this){
            value++;
        }
    }
    public void decrement(){
        synchronized (this){
            value--;
        }
    }
    public int get(){
        synchronized (this){
            return value;
        }
    }
}

 三、方法上的synchronized

public synchronized void decrement(){
     value--;
}
相当于,锁对象是this对象
public void decrement(){
    synchronized (this){
        value--;
    }
}

public synchronized static void decrement(){
     value--;
}
相当于,锁对象是类对象
public void decrement(){
    synchronized (Room.class){
        value--;
    }
}

 

你可能感兴趣的:(java)