多线程教程(六)线程共享带来的问题、synchronized

多线程教程(六)线程共享带来的问题、synchronized

多线程虽然能够带来性能上提升,但是也会带来一些线程共享的问题

两个线程对初始值为 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);
}

上述代码输出的日志可能出现正数、负数或零。主要的原因是

  1. 成员变量存储在主内存中,每个线程拿到的是成员变量的复制,各个线程成员变量的复制互不相关,并在处理完毕之后覆盖主内存上的成员变量

在这里插入图片描述

  1. 操作系统会将jvm转义后的字节码进行重排序,以提高运行效率。在没有开启多线程的时候,操作系统会保证有依赖关系的字节码顺序不进行调换(happen-before关系),以此保证单线程代码的结果一致性。但是开启多线程之后,每个线程只能保证自己线程的依赖关系,对于共享的变量就会产生线程共享问题。
  2. count++、count–都不是原子性的,在字节码层面都是多步操作,存在多线程情况下顺序调换的可能性。

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

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

iconst_1 		// 准备常量1

iadd 			// 自增

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

而对应 i-- 也是类似:

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

iconst_1 		// 准备常量1

isub 			// 自减

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

在这里插入图片描述

上图所示就是结果出现负数的情况

  1. 线程2首先分配cpu时间块,拿到了静态变量i的复制,进行减法操作后开始上下文切换
  2. 线程1在上下文切换后分配到cpu时间块,拿到了静态变量i的复制,进行加法操作,并将i的复制写入主内存的静态变量i
  3. 上下文切换,线程2再次分配到cpu时间块,将减法操作后的静态变量i的复制写入主内存的静态变量i
  4. 最终结果导致,主内存中静态变量i数值为-1(期待结果0)

同理,可能出现结果为正数的情况。

2.临界区与竟态条件

一个程序运行多个线程本身是没有问题的,问题出在多个线程访问共享资源

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

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

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

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

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

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

竞态条件 Race Condition

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

3.临界区的竟态条件解决方案

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

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

该方法实际上是通过加锁,没有锁的线程不能操作共享资源来保证临界区不产生竟态条件

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

该方法将对共享资源的操作改为原子性,这样无论操作系统如何改变字节码的顺序,都不会影响到最终的结果。

4. synchronized解决方案

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

注意

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

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

  • 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点

synchronized

语法

解决

synchronized(对象) // 线程1, 线程2(blocked)

{

 临界区

}
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();

     log.debug("{}",counter);

}

你可以做这样的类比:

  1. synchronized(对象) 中的对象,可以想象为一个房间(room),有唯一入口(门)房间只能一次进入一人进行计算,线程 t1,t2 想象成两个人

  2. 当线程 t1 执行到 synchronized(room) 时就好比 t1 进入了这个房间,并锁住了门拿走了钥匙,在门内执行count++ 代码

  3. 这时候如果 t2 也运行到了 synchronized(room) 时,它发现门被锁住了,只能在门外等待,发生了上下文切换,阻塞住了

  4. 这中间即使 t1 的 cpu 时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去哦),这时门还是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才能开门进入

  5. 当 t1 执行完 synchronized{} 块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥匙给他。t2 线程这时才可以进入 obj 房间,锁住了门拿上钥匙,执行它的 count-- 代码

用图来表示:

在这里插入图片描述

面向对象改进

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

class Room {

     int value = 0;

     public void increment() {

         synchronized (this) {

         value++;

         }

     }

     public void decrement() {

         synchronized (this) {

         value--;

         }

     }

     public int get() {

         synchronized (this) {

         return value;

         }

         }
}


@Slf4j
public class Test1 {
     public static void main(String[] args) throws InterruptedException {

         Room room = new Room();

         Thread t1 = new Thread(() -> {

             for (int j = 0; j < 5000; j++) {

             room.increment();
             }
         }, "t1");
         Thread t2 = new Thread(() -> {
             for (int j = 0; j < 5000; j++) {
             	room.decrement();
             }
         }, "t2");
         t1.start();
         t2.start();
         t1.join();
         t2.join();
         log.debug("count: {}" , room.get());
     }
}

方法上的 synchronized

 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) {

         }
     }
}

不加 synchronized 的方法

不加 synchronzied 的方法就好比不遵守规则的人,不去老实排队(好比翻窗户进去的)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值