温故Java基础多线程编程—线程安全

Posted Java Android技术干货分享

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了温故Java基础多线程编程—线程安全相关的知识,希望对你有一定的参考价值。

本文是多线程编程系列第二弹,想看温故Java基础(一)多线程编程——多线程入门:

https://blog.csdn.net/qq_26628329/article/details/89209019

二、线程安全

本文将从以下几个方面温故线程安全问题

  1. 什么是线程安全问题

  2. 如何解决线程安全问题

  3. 多线程死锁问题

  4. 多线程的三大特性

  5. Java内存模型

  6. volatile关键字

1.什么是线程安全问题

话不多说,先看一段代码:

 
   
   
 
  1. public class MyRunnable implements Runnable {

  2. private int count = 100;


  3. @Override

  4. public void run() {

  5. while (count > 0) {

  6. try {

  7. Thread.sleep(50);

  8. } catch (Exception e) {

  9. Log.d(TAG,"异常信息打印 e = "+e.toString());

  10. }

  11. System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - count + 1) + "票");

  12. }

  13. }


  14. }


  15. public class ThreadDemo {

  16. public static void main(String[] args) {

  17. MyRunnable runnable = new MyRunnable();

  18. Thread t1 = new Thread(runnable, "线程①");

  19. Thread t2 = new Thread(runnable, "线程②");

  20. t1.start();

  21. t2.start();

  22. }

  23. }

以上是一个熟知的案例两个窗口卖100张票,思考下,如果这种写法会有什么问题呢?看下打印的结果:

 
   
   
 
  1. 线程①,出售第1张票

  2. 线程②,出售第1张票

  3. 线程②,出售第3张票

  4. 线程①,出售第4张票

  5. 线程①,出售第5张票

  6. 线程②,出售第6张票

  7. 线程①,出售第7张票

  8. 线程②,出售第7张票

出现了线程①和线程②同时出售同一张票的情况,这是我们在现实需求中不想见到的。

想一下出现这个问题的原因,CPU在执行多线程时,在执行过程中可能随时切换到其他线程上执行,有可能出现多个线程同事操作同一个变量的情况。

总结一下,多个线程同时共享一个成员变量,对这个变量做更改,也就是写操作的时候可能会出现冲突问题,也就是线程安全问题。

2.如何解决线程安全问题

知道了出现上面线程安全问题的原因,想一下,那如果我们能保证一个线程在执行对变量写操作的时候,不让其他线程执行这部分的操作是不是就行了。Java中为我们提供了(synchronized修饰符)同步代码块技术解决这个问题。

  • 同步代码块

  • 同步函数

  • 静态同步函数

使用同步代码块改造后的售票案例,如下:

 
   
   
 
  1. public class MyRunnable implements Runnable {

  2. private int count = 100;

  3. private Object oj = new Object();


  4. @Override

  5. public void run() {

  6. while (count > 0) {

  7. try {

  8. Thread.sleep(50);

  9. } catch (Exception e) {

  10. Log.d(TAG,"异常信息打印 e = "+e.toString());

  11. }

  12. synchronizedoj){

  13. System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - count + 1) + "票");

  14. }

  15. }

  16. }

  17. }

  18. }

其中锁可以为任意对象,也可以是this。持有锁的线程可以执行同步代码块中的代码,没持有锁的线程,即使获得了CPU的执行权也只能等待锁被释放后抢到锁才能执行。同步代码块中的锁,在代码执行完自动释放。

同步的前提:

  1. 必须是两个及以上线程

  2. 必须是多个线程使用同一个锁

好处:解决多线程的安全问题;弊端:多个线程需要判断锁,抢锁较为消耗资源

同步代码块写法:

 
   
   
 
  1. synchronized(对象锁){

  2. 需要被同步的代码

  3. }

同步函数写法:

 
   
   
 
  1. public synchronized void methodA(){

  2. 需要同步的代码

  3. }

静态同步函数写法:

 
   
   
 
  1. public static synchronized void methodA(){

  2. 需要同步的代码

  3. }

跟同步函数相比方法上多了一个static修饰符,下面说一下静态同步函数和非静态同步函数的区别:

a.同步使用的锁是this对象锁

证明方式:一个线程使用同步代码块(this锁)一个线程使用同步函数,如果两个线程售票出现数据错误,证明不是this锁。看代码:

 
   
   
 
  1. class MyRunnable2 implements Runnable {

  2. private int count = 100;

  3. public boolean flag = true;

  4. @Override

  5. public void run() {

  6. if (flag) {


  7. while (count > 0) {


  8. synchronized (this) {

  9. if (count > 0) {

  10. try {

  11. Thread.sleep(50);

  12. } catch (Exception e) {

  13. // TODO: handle exception

  14. }

  15. System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - count + 1) + "票");

  16. count--;

  17. }

  18. }


  19. }


  20. } else {

  21. while (count > 0) {

  22. sale();

  23. }

  24. }


  25. }


  26. public synchronized void sale() {

  27. if (count > 0) {

  28. try {

  29. Thread.sleep(50);

  30. } catch (Exception e) {

  31. // TODO: handle exception

  32. }

  33. System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - count + 1) + "票");

  34. count--;

  35. }

  36. }

  37. }


  38. public class ThreadDemo2 {

  39. public static void main(String[] args) throws InterruptedException {

  40. MyRunnable2 myRunnable2 = new MyRunnable2();

  41. Thread t1 = new Thread(myRunnable2, "①号窗口");

  42. Thread t2 = new Thread(myRunnable2, "②号窗口");

  43. t1.start();

  44. Thread.sleep(40);

  45. threadTrain1.flag = false;

  46. t2.start();

  47. }

  48. }

b.静态同步函数使用的锁是类的字节码文件 ×××.class不是同一个对象

证明方式与1类似不再重复。

3. 多线程死锁问题

两个问题:什么是死锁?为什么会发生死锁现象?

a.什么是多线程死锁?

同步中嵌套同步,导致锁无法释放

看下如下代码:

 
   
   
 
  1. class MyRunnable3 implements Runnable {


  2. private int trainCount = 100;

  3. public boolean flag = true;

  4. private Object ob = new Object();


  5. @Override

  6. public void run() {

  7. if (flag) {

  8. while (true) {

  9. synchronized (ob) {

  10. sale();

  11. }

  12. }

  13. } else {

  14. while (true) {

  15. sale();

  16. }

  17. }

  18. }



  19. public synchronized void sale() {

  20. synchronized (ob) {

  21. if (trainCount > 0) {

  22. try {

  23. Thread.sleep(40);

  24. } catch (Exception e) {


  25. }

  26. System.out.println(Thread.currentThread().getName() + ",出售 第" + (100 - trainCount + 1) + "张票.");

  27. trainCount--;

  28. }

  29. }

  30. }

  31. }


  32. public class DeadlockThread {


  33. public static void main(String[] args) throws InterruptedException {


  34. MyRunnable3 myRunnable3 = new MyRunnable3();

  35. Thread thread1 = new Thread(myRunnable3, "①窗口");

  36. Thread thread2 = new Thread(myRunnable3, "②窗口");

  37. thread1.start();

  38. Thread.sleep(40);

  39. threadTrain.flag = false;

  40. thread2.start();

  41. }


  42. }

b.为什么会发生死锁现象?上述代码中,

线程1先拿到同步代码块中的ob锁,然后再拿到同步函数中的this锁;

线程2先拿到同步函数中的this锁,然后再拿到同步代码块中的ob锁。

同步中嵌套同步,互相不释放锁。

c.如何避免死锁?

不要在同步中嵌套同步

4.多线程的三大特性

a.原子性

一个或多个操作,要么全部执行,并且不会被打断,要么都不执行。例如银行转账,一个账户做减法,一个账户做加法,这两个操作必须具备原子性才不会出现差错。原子性保证了数据的一致性。

b.可见性

多个线程共享一个变量,做写操作时,一个线程修改这个变量的值,其他线程能够立即看到修改的值

c.有序性

程序执行的顺序按照代码的先后顺序执行。一般来说处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。如下:

int a = 10; //语句1

int r = 2; //语句2

a = a + 3; //语句3

r = a*a; //语句4

则因为重排序,他还可能执行顺序为 2-1-3-4,1-3-2-4 但绝不可能 2-1-4-3,因为这打破了依赖关系。显然重排序对单线程运行是不会有任何问题,而多线程就不一定了,所以我们在多线程编程时就得考虑这个问题了。

5.Java内存模型

Java内存模型即共享内存模型,简称JMM,它决定一个线程对共享变量写入时,能对另一个线程可见。线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了改线程读写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。

来看一张图简单梳理下JMM:

6.volatile关键字

a.volatile有什么用

让多线程的共享变量在诸线程间可见

 
   
   
 
  1. class ThreadVolatileDemo extends Thread {

  2. public boolean flag = true;

  3. @Override

  4. public void run() {

  5. System.out.println("开始执行子线程....");

  6. while (flag) {

  7. }

  8. System.out.println("线程停止");

  9. }

  10. public void setRuning(boolean flag) {

  11. this.flag = flag;

  12. }


  13. }


  14. public class ThreadVolatile {

  15. public static void main(String[] args) throws InterruptedException {

  16. ThreadVolatileDemo threadVolatileDemo = new ThreadVolatileDemo();

  17. threadVolatileDemo.start();

  18. Thread.sleep(3000);

  19. threadVolatileDemo.setRuning(false);

  20. System.out.println("flag 已经设置成false");

  21. Thread.sleep(1000);

  22. System.out.println(threadVolatileDemo.flag);


  23. }

  24. }

运行:

 
   
   
 
  1. 开始执行子线程....

代码中我们已经设置setRuning(false)了,为什么感觉不起作用呢?

根据上面的Java内存模型知道,线程间不可见,本地内存读取的是主内存的副本,主内存改变值了,本地内存并没有取主内存拿改变后的值。

解决:使用volatile 修饰flag

 
   
   
 
  1. public volatile boolean flag = true;

保证共享变量可见性。

b.volatile的非原子性

可以使用原子类:

 
   
   
 
  1. public class VolatileNoAtomic extends Thread {

  2. static int count = 0;

  3. private static AtomicInteger atomicInteger = new AtomicInteger(0);


  4. @Override

  5. public void run() {

  6. for (int i = 0; i < 1000; i++) {

  7. //等同于i++

  8. atomicInteger.incrementAndGet();

  9. }

  10. System.out.println(atomicInteger);

  11. }


  12. public static void main(String[] args) {

  13. // 初始化10个线程

  14. VolatileNoAtomic[] volatileNoAtomic = new VolatileNoAtomic[10];

  15. for (int i = 0; i < 10; i++) {

  16. // 创建

  17. volatileNoAtomic[i] = new VolatileNoAtomic();

  18. }

  19. for (int i = 0; i < volatileNoAtomic.length; i++) {

  20. volatileNoAtomic[i].start();

  21. }

  22. }


  23. }

c.volatile和synchronized区别

仅靠volatile不能保证线程的安全性。(原子性)

①volatile轻量级,只能修饰变量。synchronized重量级,还可修饰方法

②volatile只能保证数据的可见性,不能用来同步,因为多个线程并发访问volatile修饰的变量不会阻塞。

synchronized不仅保证可见性,而且还保证原子性,因为,只有获得了锁的线程才能进入临界区,从而保证临界区中的所有语句都全部执行。多个线程争抢synchronized锁对象时,会出现阻塞。

线程安全性

线程安全性包括两个方面,①可见性。②原子性。

从上面自增的例子中可以看出:仅仅使用volatile并不能保证线程安全性。而synchronized则可实现线程的安全性。欢迎关注干货分享: 参考阅读:

https://www.cnblogs.com/cb0327/p/4986286.html#_label6


以上是关于温故Java基础多线程编程—线程安全的主要内容,如果未能解决你的问题,请参考以下文章

3.5万字 JavaSE温故而知新!(结合jvm 基础+高级+多线程+面试题)

Java多线程,线程安全与不安全的理解,程序的多线程并发编程的基础概念,进程与线程的区别是什么

Java多线程,线程安全与不安全的理解,程序的多线程并发编程的基础概念,进程与线程的区别是什么

Java多线程,线程安全与不安全的理解,程序的多线程并发编程的基础概念,进程与线程的区别是什么

看完就知道在Github点赞近90KJava多线程笔记这么吃香,原因如下

Java 并发编程 -- 并发编程线程基础(线程安全问题可见性问题synchronized / volatile 关键字CASUnsafe指令重排序伪共享Java锁的概述)