synchronized解决并发线程3个问题(原子性,可见性,有序性)以及synchronized可重入特性

08
五月
2021

并发编程中的三个问题

1.可见性

/**
 * 一个线程对共享变量的修改,另一个线程不能立即得到最新值
 */
public class Test01 {

  private static Boolean flag = true;

  public static void main (String[] args) {
    new Thread(() -> {
      while (flag) {

      }
    }).start();

    try {
      TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }

    new Thread(() -> {
      flag = false;
      System.out.println("flag更新为false");
    }).start();
  }
}

总结:
并发编程时,会出现可见性问题,当一个线程对共享变量进行了修改,另外的线程并没有立即看到修改后的最新值。

2.原子性

/**
 * 定义5个线程,每个线程对变量num加到1000.  线程安全的情况下num最终等于5000
 */
public class Test02 {

  private static int num = 0;

  public static void main (String[] args) {
    Runnable run = () -> {
      for (int i = 1; i <= 1000; i++) {
        num++;
      }
    };
    List<Thread> threads = new ArrayList<>();
    for (int i = 0; i < 5; i++) {
      Thread thread = new Thread(run);
      threads.add(thread);
      thread.start();
    }

    for (Thread t : threads) {
      try {
        t.join();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }

    System.out.println("num:" + num);
  }
}

在这里插入图片描述

在这里插入图片描述
总结:
num++并非原子操作。
并发编程时,会出现原子性问题,当一个线程对共享变量操作到一半时,另外的线程也有可能来操作共享变量,干扰了前一个线程的操作。

3.有序性

jcstress是java并发压测工具。https://wiki.openjdk.java.net/display/CodeTools/jcstress
修改pom文件,添加依赖:

<dependency>
	<groupId>org.openjdk.jcstress</groupId>
	<artifactId>jcstress-core</artifactId>
	<version>${jcstress.version}</version>
</dependency>

在这里插入图片描述
I_Result 是一个对象,有一个属性 r1 用来保存结果,在多线程情况下可能出现几种结果?

  1. 线程1先执行actor1,这时ready = false,所以进入else分支结果为1。
  2. 线程2执行到actor2,执行了num = 2;和ready = true,线程1执行,这回进入 if 分支,结果为
    4。
  3. 线程2先执行actor2,只执行num = 2;但没来得及执行 ready = true,线程1执行,还是进入
    else分支,结果为1。
  4. 还有一种结果0。
    线程2先执行actor2发生了重排序,先执行 ready = true,还没执行num = 2;线程1执行,num 这时候事默认值0,结果为0

并发编程中的三个问题用synchronized解决

1.可见性

public class Test01 {

  private static Boolean flag = true;

  public static void main (String[] args) {
    new Thread(() -> {
      while (flag) {
        //System.out.println(flag);  用这个也可以实现,因为底层用了synchronized
        synchronized (Test01.class){  //lock: flag要从主内存中拿最新的值

        }
      }
    }).start();

    try {
      TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }

    new Thread(() -> {
      flag = false;
      System.out.println("flag更新为false");
    }).start();
  }
}

在这里插入图片描述

总结:
synchronized保证可见性的原理,执行synchronized时,会对应lock原子操作会刷新工作内存中共享变
量的值

2.原子性

public class Test02 {

  private static int num = 0;
  private static Object obj=new Object();

  public static void main (String[] args) {
    Runnable run = () -> {
      for (int i = 1; i <= 1000; i++) {
        synchronized (obj) {
          num++;
        }
      }
    };
    List<Thread> threads = new ArrayList<>();
    for (int i = 0; i < 5; i++) {
      Thread thread = new Thread(run);
      threads.add(thread);
      thread.start();
    }

    for (Thread t : threads) {
      try {
        t.join();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }

    System.out.println("num:" + num);
  }
}

在这里插入图片描述
synchronized保证原子性的原理
对number++;增加同步代码块后,保证同一时间只有一个线程操作number++;。就不会出现安全问题。
小结
synchronized保证原子性的原理,synchronized保证只有一个线程拿到锁,能够进入同步代码块。

3.有序性

在这里插入图片描述
之前出现问题的原因
线程2先执行actor2发生了重排序,先执行 ready = true,还没执行num = 2;线程1执行,num 这时候事默认值0,结果为0.
怎么处理的
线程2先执行actor2发生了重排序,先执行 ready = true,还没执行num = 2;线程1执行,这时候线程1拿不到锁,所以只能等线程2执行完了num=2,释放了锁,线程1才能执行,因为num不会再出现=0的情况了

synchronized保证有序性的原理
synchronized后,虽然进行了重排序,保证只有一个线程会进入同步代码块,也能保证有序性。
小结
synchronized保证有序性的原理,我们加synchronized后,依然会发生重排序,只不过,我们有同步代码块,可以保证只有一个线程执行同步代码中的代码。保证有序性.

synchronized的特性—可重入特性

public class Test02 {

  public static void main (String[] args) {
    Runnable sellTicket = new Runnable() {
      @Override
      public void run () {
        synchronized (Test02.class) {
          System.out.println("我是run");
          test01();
        }
      }

      public void test01 () {
        synchronized (Test02.class) {
          System.out.println("我是test01");
        }
      }
    };
    new Thread(sellTicket).start();
    new Thread(sellTicket).start();
  }
}

可重入原理
synchronized的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁.
可重入的好处

  1. 可以避免死锁
  2. 可以让我们更好的来封装代码
    小结
    synchronized是可重入锁,内部锁对象中会有一个计数器记录线程获取几次锁啦,在执行完同步代码块时,计数器的数量会-1,知道计数器的数量为0,就释放这个锁。

来源:https://www.bilibili.com/video/BV1aJ411V763?p=19&spm_id_from=pageDriver

TAG

网友评论

共有访客发表了评论
请登录后再发布评论,和谐社会,请文明发言,谢谢合作! 立即登录 注册会员