Java并发编程之线程安全

1、线程安全问题

1.1、 线程出现问题的根本原因分析

线程出现问题的根本原因是因为线程上下文切换,导致线程里的指令没有执行完就切换执行其它线程了

示例

public class Test {
    static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 1; i < 5000; i++){
                count++;
            }
        });
        Thread t2 =new Thread(()->{
            for (int i = 1; i < 5000; i++){
                count--;
            }
        });
        t1.start();
        t2.start();
        t1.join(); // 主线程等待t1线程执行完
        t2.join(); // 主线程等待t2线程执行完

        // main线程只有等待t1, t2线程都执行完之后, 才能打印count, 否则main线程不会等待t1,t2
        // 直接就打印count的值为0
        System.out.println("count的值是: "+count);
    }
}

结果:

count的值是: 261

Process finished with exit code 0

为什么不是0? 字节码的层面进行分析

  • 因为在Java中对静态变量的 自增/自减 并不是原子操作
  • count++ 和 count-- 操作实际都是需要这个4个指令完成的
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i
    
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i

单线程执行: count的值不会计算错

在这里插入图片描述

出现负数的情况: 一个线程没有完成一次完整的自增/自减(多个指令) 的操作, 就被别的线程进行操作, 此时就会出现线程安全问题

在这里插入图片描述

  • 首先线程2去静态变量中读取到值0, 准备常数1, 完成isub减法,变-1操作, 正常还剩下一个putstatic i写入-1的过程; 最后的指令没有执行, 就被线程1抢去了cpu的执行权
  • 此时线程1进行操作, 读取静态变量0, 准备常数1, iadd加法, i=1, 此时将putstatic i写入 1; 当线程2重新获取到cpu的执行权时, 它通过自身的程序计数器知道自己该执行putstatic 写入-1了; 此时它就直接将结果写为-1
  • 出现正数原因于此一致

1.2、 问题描述

临界区

  • 一个程序运行多线程本身是没有问题的
  • 问题出现在多个线程共享资源(临界资源)的时候
      1)多个线程同时对共享资源进行读操作本身也没有问题 - 对读操作没问题
      2)问题出现在对对共享资源同时进行读写操作时就有问题了 - 同时读写操作有问题
  • 先定义一个叫做临界区的概念:一段代码内如果存在对共享资源的多线程读写操作,那么称这段代码为临界区; 共享资源也成为临界资源
static int counter = 0;
static void increment() 
// 临界区 
{   
   counter++; 
}

static void decrement() 
// 临界区 
{ 
   counter--; 
}

竞态条件

多个线程在临界区执行,那么由于代码指令的执行不确定而导致的结果问题,称为竞态条件

1.3、 synchronized 解决方案

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

  • 阻塞式解决方案: synchronized , Lock (ReentrantLock)
  • 非阻塞式解决方案: 原子变量 (CAS)

现在讨论使用synchronized来进行解决,即俗称的对象锁,它采用互斥的方式让同一时刻至多只有一个线程持有对象锁,其他线程如果想获取这个锁就会阻塞住,这样就能保证拥有锁的线程可以安全的执行临界区内的代码

synchronized语法

synchronized(对象) { // 线程1获得锁, 那么线程2的状态是(blocked)
 	临界区
}

上面的实例程序使用synchronized后如下,计算出的结果是正确!

public class Test {
    static int counter = 0;
    static final Object room = new Object();
    public static void main(String[] args) throws InterruptedException {
        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();
        t1.join();
        t2.join();
        System.out.println("count的值是: "+counter);
    }
}

结果:

count的值是: 0

Process finished with exit code 0

synchronized 加在方法上

加在实例方法上, 锁对象就是对象实例

public class Demo {
	//在方法上加上synchronized关键字
	public synchronized void test() {

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

加在静态方法上, 锁对象就是当前类的Class实例

public class Demo {
	//在静态方法上加上synchronized关键字
	public synchronized static void test() {
	
	}
	//等价于
	public void test() {
		synchronized(Demo.class) {
		
		}
	}
}

2、线程八锁案例分析

其实就是考察synchronized 锁住的是哪个对象, 如果锁住的是同一对象, 就不会出现线程安全问题

1. 锁住同一个对象都是this(e1对象),结果为:1,2或者2,1

public class EightLockTest {
    // 锁对象就是this, 也就是e1
    public synchronized void a() {
        System.out.println("1");
    }
    // 锁对象也是this, e1
    public synchronized void b() {
        System.out.println("2");
    }
    public static void main(String[] args) {
        EightLockTest e1 = new EightLockTest();
        new Thread(() -> e1.a()).start();
        new Thread(() -> e1.b()).start();
    }
}

2. 锁住同一个对象都是this(e1对象),结果为:1s后1,2 或者 2,1s后1

public class EightLockTest {
    // 锁对象就是this, 也就是e1
    public synchronized void a(){
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("1");
    }

    // 锁对象也是this, e1
    public synchronized void b() {
        System.out.println("2");
    }

    public static void main(String[] args) {
        EightLockTest e1 = new EightLockTest();
        new Thread(() -> e1.a()).start();
        new Thread(() -> e1.b()).start();
    }
}

3. a,b锁住同一个对象都是this(e1对象),c没有上锁。结果为:3,1s后1,2 || 2,3,1s后1 || 3,2,1s后1

/**
 * Description: 会出现安全问题, 因为前两个线程, 执行run方法时, 都对相同的对象加锁;
 *              而第三个线程,调用的方法c, 并没有加锁, 所以它可以同前两个线程并行执行;
 *  打印结果顺序为: 分析: 因为线程3和线程1,2肯定是并行执行的, 所以有以下情况
 *               3,1s后1,2 || 2,3,1s后1 || 3,2,1s后1
 *               至于 1,3,2的情况是不会发生的, 可以先调用到1,但需要sleep一秒.3肯定先执行了
 */
public class EightLockTest {
    // 锁对象就是this, 也就是e1
    public synchronized void a() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("1");
    }

    // 锁对象也是this, e1
    public synchronized void b() {
        System.out.println("2");
    }

    public void c() {
        System.out.println("3");
    }

    public static void main(String[] args) {
        EightLockTest e1 = new EightLockTest();
        new Thread(() -> e1.a()).start();
        new Thread(() -> e1.b()).start();
        new Thread(() -> e1.c()).start();
    }
}

4.a锁住对象this(n1对象),b锁住对象this(n2对象),不互斥。结果为:2,1s后1

public class EightLockTest {
    // 锁对象是e1
    public synchronized void a() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("1");
    }

    // 锁对象是e2
    public synchronized void b() {
        System.out.println("2");
    }

    public static void main(String[] args) {
        EightLockTest e1 = new EightLockTest();
        EightLockTest e2 = new EightLockTest();
        new Thread(() -> e1.a()).start();
        new Thread(() -> e2.b()).start();
    }
}

5.a锁住的是EightLockTest.class对象, b锁住的是this(e1),不会互斥; 结果: 2,1s后1

public class EightLockTest {
    // 锁对象是EightLockTest.class类对象
    public static synchronized void a() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("1");
    }

    // 锁对象是e2
    public synchronized void b() {
        System.out.println("2");
    }

    public static void main(String[] args) {
        EightLockTest e1 = new EightLockTest();
        new Thread(() -> e1.a()).start();
        new Thread(() -> e1.b()).start();
    }
}

6.a,b锁住的是EightLockTest.class对象, 会发生互斥; 结果为:2,1s后1 || 1s后1,2

public class EightLockTest {
    // 锁对象是EightLockTest.class类对象
    public static synchronized void a() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("1");
    }

    // 锁对象是EightLockTest.class类对象
    public static synchronized void b() {
        System.out.println("2");
    }

    public static void main(String[] args) {
        EightLockTest e1 = new EightLockTest();
        new Thread(() -> e1.a()).start();
        new Thread(() -> e1.b()).start();
    }
}

7.a锁住的是EightLockTest.class对象, b锁住的是this(e1),不会互斥; 结果: 2,1s后1

public class EightLockTest {
    // 锁对象是EightLockTest.class类对象
    public static synchronized void a() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("1");
    }

    // 锁对象是this,e2对象
    public synchronized void b() {
        System.out.println("2");
    }

    public static void main(String[] args) {
        EightLockTest e1 = new EightLockTest();
        EightLockTest e2 = new EightLockTest();
        new Thread(() -> e1.a()).start();
        new Thread(() -> e2.b()).start();
    }
}

8.a,b锁住的是EightLockTest.class对象, 会发生互斥; 结果为:2,1s后1 || 1s后1,2

public class EightLockTest {
    // 锁对象是EightLockTest.class类对象
    public static synchronized void a() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("1");
    }

    // 锁对象是EightLockTest.class类对象
    public static synchronized void b() {
        System.out.println("2");
    }

    public static void main(String[] args) {
        EightLockTest e1 = new EightLockTest();
        EightLockTest e2 = new EightLockTest();
        new Thread(() -> e1.a()).start();
        new Thread(() -> e2.b()).start();
    }
}

3、变量的线程安全分析

成员变量和静态变量的线程安全分析

  • 如果变量没有在线程间共享,那么变量是安全的
  • 如果变量在线程间共享
      1)如果只有读操作,则线程安全
      2)如果有读写操作,则这段代码是临界区,需要考虑线程安全

局部变量线程安全分析

  • 局部变量【局部变量被初始化为基本数据类型】是安全的
  • 但局部变量引用的对象则未必 (要看该对象是否被共享且被执行了读写操作)
      1)如果该对象没有逃离方法的作用范围,它是线程安全的
      2)如果该对象逃离方法的作用范围,需要考虑线程安全

线程安全的情况

  • 局部变量表是存在于栈帧中, 而虚拟机栈中又包括很多栈帧, 虚拟机栈是线程私有的;
  • 局部变量【局部变量被初始化为基本数据类型】是安全的,示例如下
public static void test1() {
     int i = 10;
     i++;
}
  • 每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冬天vs不冷

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值