多线程安全(一)

首先感谢授课XXX老师。

1.什么是线程安全问题

  当多个线程共享同一个全局变量,做写的操作时候,可能会受到其他线程的干扰,导致数据出现问题,这种现象就叫做线程安全问题。做读的时候不会产生线程安全问题。

什么安全:多个线程同时共享一个全局变量,做写操作的时候会发生线程安全。

多个线程共享同一个局部变量做写的操作时候,不会发生线程安全问题。

2.如何解决简单的线程安全问题(锁)

  所谓简单就是不涉及分布式,集群等行列。

因为用以下几种解决方法完全得不到解决(实际上也就是两种)!!!

① 使用 synchronized 代码块

② 使用 synchronized 函数

③ 使用 静态同步代码块 

抢夺cpu资源高的线程会首先拿到锁,然后进入逻辑中进行操作,其他线程只能进行等待,然后这个线程执行完毕后释放锁,这个时候其他线程会争夺这个锁,也就会产生资源争夺的问题,所以synchronized 的效率是很低的,然后抢夺cpu资源高的线程再次进入逻辑进行操作,以此类推。

案例

以抢车票为例,代码会尽量简洁。

100张火车票,两个售票窗口。同时出售火车票,在不解决安全问题的情况下看一看会出现什么问题。

 1 package com.mydream.cn;
 2 
 3 class Train extends Thread {
 4     int count = 1;    // 售票次数
 5     @Override
 6     public  void run() {
 7         while (count<=100) {
 8             try {
 9                 Thread.currentThread().sleep(30);//为了让程序满足并发条件,让他进入睡眠状态。好让后面同时进行
10             } catch (InterruptedException e) {
11                 e.printStackTrace();
12             }
13             sale();
14         }
15     }
16     // 售票方法逻辑
17     public void sale() { 
18         System.out.println(count);
19         count++;
20     }
21 }
22 public class TrainDemo {
23     public static void main(String[] args) {
24             Train t = new Train();
25             Thread th2 = new Thread(t);
26             Thread th1 = new Thread(t);
27             th1.start();
28             th2.start();
29     }
30 }

 运行结果是出现了101张票!并且在开始时候还出现了重复的票数,例如1,1,3,这种情况,并不是我们想象的1~100。

那么是什么原因呢???

思路分析

  首先定义了一个全局变量count然后开启两个线程但是是一个对象,也就是同时操作一个全局变量。在主函数main中运行他们,th1线程和th2线程因为有一个sleep函数会等待30毫秒,这样就有机会创造出代码并发的可能性。

分析一

  1,1,3,这种不按顺序走的逻辑思维。首先两个线程都得到释放,那么就同时进入system.out打印方法中,然后就会打印出1 , 1 ,3这种不安顺序的代码。这种想起来应该很简单的。也就是说同时进入的时候因为都是>=100,第一次一起进入你打印  1  我也打印  1  

分析二 

  第101张票出现的情况,按照上面思路,当运行到第100张的时候,第一个线程打印出了100,然后进行了+1操作,而在判断程序中第二个线程已经进入到sale()逻辑方法中了,此时他是100,因为地换一个线程的+1操作,导致他变成了101,然后因为判断是>=100所以后面的进不来了。才导致会打印出101的情况。

解决方法一

 1 // 售票方法逻辑
 2     public void sale() { 
 3         synchronized(this){
 4             // 再次判断逻辑,因为进入到这里可能都是100,再次进行判断即可
 5             if (count<=100) {
 6                 System.out.println(count);
 7                 count++;
 8             }
 9         }
10     }

synchronized 代码块,在售票方法逻辑中加入代码块,并且写入判断逻辑,可能有人会说为什么还要在写入判断逻辑?我直接写入判断逻辑不加synchronized 不是一样么,请记住synchronized 是用来进行线程安全同步的,既然是同步那么就会安全,可是synchronized 已经进入了sale()方法中了,那么我要是同时都是100进入那也没毛病啊~~~~。所以在此加入逻辑就会防止101的出现。

synchronized 代码块中括号中的变量该放什么呢?答案是放什么都可以,只要线程中使用的是同一把锁就可以了。

例如:

 1 private Object obj =  new Object();
 2     
 3     // 售票方法逻辑
 4     public void sale() { 
 5         synchronized(obj){
 6             // 再次判断逻辑,因为进入到这里可能都是100,再次进行判断即可
 7             if (count<=100) {
 8                 System.out.println(count);
 9                 count++;
10             }
11         }
12     }

解决方法二

1     // 售票方法逻辑
2     public synchronized void sale() { 
3         if (count<=100) {
4             System.out.println(count);
5             count++;
6         }
7     }

synchronized 函数,synchronized 原理就是使用this当锁,我们可以假设一种情况,也是两个线程,分别用不同的锁,这里不同的锁就是 synchronized 函数 和 synchronized 代码块用this当锁,如果发现是同步的,那么就证明了 synchronized 函数 是用this当锁的,相反就不是,在这里答案是是的。

解决方式三

static synchronized 函数  就是在synchronized 函数 前面 加上 static。如果一旦加入了static 那么 synchronized 代码块this锁 不会 和 它同步了。

static 关键字是比所有代码都先编译的,所以也就不会有this的说法,难道你能在static修饰的方法中调用this么?可能是不可以的了。

那么 这种锁 用的是什么呢?答案是 .class 文件 。如何测试呢?相同的方法,把 synchronized 代码块 锁换成 类名.class 测试是否同步。答案是同步的。

模拟 this 和 同步函数是否相同

 1 class ThreadTrain2 implements Runnable {
 2     // 总共有一百张火车 当一变量被static修饰的话存放在永久区,当class文件被加载的时候就会被初始化。
 3     private static int train1Count = 100;
 4     private Object oj = new Object();
 5     public boolean flag = true;
 6     @Override
 7     public void run() {
 8         // 为了能够模拟程序一直在抢票的话。 where
 9         if (flag) {
10                   //执行同步代码块this锁
11                   while (train1Count > 0) {
12             
13                       synchronized (this) {
14                           if(train1Count>0){
15                               try {
16                                   Thread.sleep(50);
17                               } catch (Exception e) {
18                                   // TODO: handle exception
19                               }
20                               System.out.println(Thread.currentThread().getName()+ ",出售第" + (100 - train1Count + 1) + "票");
21                               train1Count--;
22                           }
23                       }
24                     
25                 }
26         }
27         else{
28              // 同步函数
29             while (train1Count > 0) {
30             
31                 // 出售火車票
32                 sale();
33             }
34         }
35         
36     }
37 
38     public   synchronized void sale() {
39 
40         // 同步代码块 synchronized 包裹需要线程安全的问题。
41         // synchronized (oj) {
42         if (train1Count > 0) {
43             try {
44                 Thread.sleep(50);
45             } catch (Exception e) {
46                 // TODO: handle exception
47             }
48             System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - train1Count + 1) + "票");
49             train1Count--;
50         }
51         // }
52 
53     }
54 }
55 
56 public class ThreadDemo2 {
57 
58     public static void main(String[] args) throws InterruptedException {
59         ThreadTrain2 threadTrain2 = new ThreadTrain2();
60         Thread t1 = new Thread(threadTrain2, "窗口①");
61         Thread t2 = new Thread(threadTrain2, "窗口②");
62         t1.start();
63         Thread.sleep(40);
64         threadTrain2.flag=false;
65         t2.start();
66     }
67 
68 }

有多线程安全的解决那么就会出现一些问题,出现死锁状态,也就是说两个锁的状态是你等我解锁,我等你解锁。直接上代码

package com.itmayiedu;

class ThreadTrain3 implements Runnable {
    // 总共有一百张火车 当一变量被static修饰的话存放在永久区,当class文件被加载的时候就会被初始化。
    private static int train1Count = 100;
    private Object oj = new Object();
    public boolean flag = true;

    @Override
    public void run() {
        // 为了能够模拟程序一直在抢票的话。 where
        if (flag) {
            // 执行同步代码块this锁
            while (true) {
                synchronized (oj) {
                    sale();
                }
            }
        } else {
            // 同步函数
            while (true) {

                // 出售火車票
                sale();
            }
        }

    }

    public synchronized void sale() {

        // 同步代码块 synchronized 包裹需要线程安全的问题。
        synchronized (oj) {
            if (train1Count > 0) {
                try {
                    Thread.sleep(50);
                } catch (Exception e) {
                    // TODO: handle exception
                }
                System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - train1Count + 1) + "票");
                train1Count--;
            }
        }

    }
}

public class ThreadDemo3 {

    public static void main(String[] args) throws InterruptedException {
        ThreadTrain3 threadTrain3 = new ThreadTrain3();
        Thread t1 = new Thread(threadTrain3, "窗口①");
        Thread t2 = new Thread(threadTrain3, "窗口②");
        t1.start();
        Thread.sleep(40);
        threadTrain3.flag = false;
        t2.start();
    }

}

什么是死锁?同步中嵌套同步,导致锁无法释放

上面锁中用到了两个锁,一个是this一个是obj,分析运行程序,现进入true然后马上进入false

true   得到obj锁 进入sale() 得到 this 锁

false   得到this0锁 进入sale() 得到 obj 锁

然后就可能会出现   交叉想等待,这种想法要好好理解,并且重要! 抽象一点就是this被ojb锁掉了 另一个 ojb被this锁掉了 然后互相等待解锁,可是完全等不到,就会出现多线程死锁情况。

多线程有三大特性

原子性、可见性、有序性

什么是原子性

即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

一个很经典的例子就是银行账户转账问题: 

比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。这2个操作必须要具备原子性才能保证不出现一些意外的问题。

我们操作数据也是如此,比如i = i+1;其中就包括,读取i的值,计算i,写入i。这行代码在Java中是不具备原子性的,则多线程运行肯定会出问题,所以也需要我们使用同步和lock这些东西来确保这个特性了。 

原子性其实就是保证数据一致、线程安全一部分,

什么是可见性

当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

若两个线程在不同的cpu,那么线程1改变了i的值还没刷新到主存,线程2又使用了i,那么这个i值肯定还是之前的,线程1对变量的修改线程没看到这就是可见性问题。 

什么是有序性

程序执行的顺序按照代码的先后顺序执行。

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

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,因为这打破了依赖关系。

显然重排序对单线程运行是不会有任何问题,而多线程就不一定了,所以我们在多线程编程时就得考虑这个问题了。

猜你喜欢

转载自www.cnblogs.com/liclBlog/p/9375275.html