04.多线程--04.【多线程卖票出现的数据安全问题】【同步代码块基本用法和原理】

多线程--4

多线程卖票出现的数据安全问题

同步代码块基本用法和原理

----------- android培训、java培训、java学习型技术博客、期待与您交流! ------------

1.    多线程卖票出现的数据安全问题

1). 多线程卖票案例代码回顾

(1). 多线程卖票案例代码回顾

[1]. Runnable接口实现类代码示例

class TicketRunnable implements Runnable{
    private int ticketNum =10;   
    public void run() {
       while(true){
           if(ticketNum >0){
              System.out.println(Thread.currentThread().getName()+":"+ ticketNum--);
           }else
              break;
       }
    }
   
}

[2].测试类代码示例

public class TicketDemoIII {
	public static void main(String[] args) {
		Runnable ticket =new TicketRunnable();
		Thread t1 =new Thread(ticket, "window1_sale");
		Thread t2 =new Thread(ticket, "window2_sale");
		Thread t3 =new Thread(ticket, "window3_sale");
		Thread t4 =new Thread(ticket, "window4_sale");
		
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}

(2). 运行中出现的问题

下面将上面的程序运行了6次的结果统一合并成了如下的结果:

04.多线程--04.【多线程卖票出现的数据安全问题】【同步代码块基本用法和原理】_第1张图片

说明】其中红色圆圈的标出的位置说明本次运行结果出现了问题。紫色的对号标记的运行结果表示本次运行结果是正确的。

2). 数据安全隐患问题

(1). 出现的问题

问题】六次运行中,三次运行正确,还有三次运行不正确。因为Runnable中的run方法的程序如下:

public void run() {
while(true){
        if(ticketNum >0){
           System.out.println(Thread.currentThread().getName()+":"+ ticketNum--);
        }else
           break;
}
}

按照while中的if的判断条件可知,只有 ticketNum >0成立的时候,才会输出打印语句。但是上面的六次运行中,竟然有三次打印出了:window1_sale: 0的情况,这就是数据安全问题。

(2). 多线程中的数据安全问题

[1]. 多线程中的数据安全问题不一定就会发生

举例:上面运行了六次代码,三次运行正确,三次运行错误。

[2]. 把多线程中的数据安全问题不一定就会发生的情况称为数据安全隐患问题

3). 多线程中数据安全问题原因的分析

(1). 多线程中数据安全问题出现的原因

[1]. 满足以下条件的时候,出现多线程数据安全问题

{1}.多条语句共同操作了同一个共享数据

{2}. 这几条操作同一个数据的语句又会被多个线程实例执行

{2}1. 这时候某个线程进入run方法并且操作了线程间的共享数据但是没有执行完run方法中所有关于线程间共享数据的语句交出了CPU的执行权

{2}2. 此时下一个线程进来之后对共享数据进行了修改。等上次没有执行完run方法线程又被CPU执行的时候就会发生这个线程执行同一个run方法的时候,前后两次共享数据内容不一致的错误。这个时候,就非常容易引发多线程的数据安全问题。

注意】如果一个线程能够进入run方法中并且不被中断的一次性所有有关共享数据的语句执行完成那就一定不会发生数据安全隐患问题

(2). 多线程中数据安全问题分析的步骤

[1]. 首先明确哪些代码多线程要运行的代码

[2]. 其次明确多线程之间的共享数据

[3]. 再次明确多线程运行的代码中哪些语句操作共享数据

[4]. 使用中断CPU执行权的方式进行对存在的数据安全问题进行分析

{1}. 在第一次操作到共享数据的语句做一个标记并假设多个线程处于该语句处等待执行

{2}. 假设某一个线程执行了第一条操作了共享数据有关的语句之后CPU立刻切换了其他的线程。

{3}. 假设另一个线程再次进入这个方法并修改了相应的共享数据之后再次被CPU中断了执行

{4}. 最后假设CPU执行权切换到之前没有执行完的共享语句的代码块对应的线程继续执行这样由于这个共享数据被其他线程修改过使得这个线程在操作同一个共享数据的前后读取或者修改这个数据并不是这个线程自己所为,所以此时就能分析出多线程操作共享数据发生的错误了

(3). 分析多线程卖票程序中出现的数据安全问题的原因

[1]. 这里多线程要运行的代码run()中的代码

[2]. 多线程的共享数据要么是Runnable实现子类中的普通成员变量,要么是Thread子类中的静态成员变量。这里面的多线程实例之间的共享数据是Runnable实现子类TicketRunnable类中的普通成员变量privateintticketNum =10;

[3]. run方法操作共享数据的语句如下

{1}. run方法中第一句操作共享数据的语句是if()中的ticketNum和0之间的比较

{2}. 打印语句System.out.println()中的ticketNum--

再次明确多线程运行的代码中哪些语句操作共享数据

[4]. 采用中断CPU执行权方式进行多线程数据安全问题的分析

04.多线程--04.【多线程卖票出现的数据安全问题】【同步代码块基本用法和原理】_第2张图片

假设里面的四个线程对象标记成向下带有数字标记的箭头。这里面分别用线程0~线程3进行代号。假设此时ticketNum的值为1

{1}. 假设线程0获取了CPU的执行权并进行了if判断。发现ticketNum的值为1 >0成立。此时很巧的是,CPU切换到了线程1进行执行。线程0和线程2,线程3一起处于临时阻塞状态。拥有CPU执行资格但是没有CPU执行权。

{2}.此时ticketNum的值为1>0仍然成立,因此线程1可以执行接下来的run方法中的代码。但是此时CPU此时又一次切换了要执行的线程。这样依次类推,假设线程的CPU执行在线程之间的转换如下:


切换到线程3之后,四个线程都具有CPU执行权,并且各自对run的执行都停在了if执行完之后的System.out.println(xxx)语句之前。

{3}. 假设CPU再一次将切换到线程0执行。由于线程0上一次运行的端点就是输出语句,因此这次直接执行打印语句。这样ticketNum--被执行了。此时ticketNum的值变为0。

此时打印出:window1_sale:0

这个时候CPU将执行权切换到线程1,线程1上一次也停在了输出语句前面。因此一执行就进行了ticketNum--操作。这样共享数据ticketNum从0减到了-1。

此时打印出:window1_sale:-1

{4}. 以此类推,假设一执行完打印语句后就切换CPU的执行权到下一个线程。理论分析就会分别打印出:window1_sale: -2和window1_sale:-3

因此ticketNum非正数的情况也全部打印出来,多线程之间的数据安全问题就产生了。

(4). 使用Thread.sleep(xxx)强行切换CPU执行的线程【模拟

[1]. 以上仅仅是理论分析CPU如果在if判断之后进行了CPU执行权的切换导致的可能性。

[2]. 但是实际上,CPU到底在哪里切换线程的执行权是未知的。为了CPUif之后打印语句之前能够真正切换CPU的执行权,就需要当前线程在这两句共享数据的读取修改之间自己放弃CPU执行权因此此时使用Thread.sleep()来让当前线程“睡眠”以间接达到CPU不得不因为当前线程在此放弃了CPU的执行权而切换到其他的线程进行执行

实质】本来是CPU切换了当前执行的线程的时候是将当前执行的线程运行状态转化到临时阻塞状态。但是Thread.sleep()方法却是将线程从运行状态转化到了冻结状态

[3]. 使用了Thread.sleep()方法很有可能把可能在多线程中出现的数据安全问题直接暴露出来!!

[4]. 添加Thread.sleep的run代码如下:

class TicketRunnable implements Runnable{
    private int ticketNum =10;
    public void run() {
       while(true){
           if(ticketNum >0){
              try {
                  Thread.sleep(10);
              }catch (InterruptedException e) {
                  e.printStackTrace();
              }
              System.out.println(Thread.currentThread().getName()+":"+ ticketNum--);
           }else
              break;
       }
    }
}

测试代码不变。测试结果如下:

04.多线程--04.【多线程卖票出现的数据安全问题】【同步代码块基本用法和原理】_第3张图片

打印出来了非正数的票,数据安全问题的确发生。

提醒】如果一个线程进入run方法之后把所有有关共享数据的代码一下子全部都执行完成,那么多线程的数据安全问题是不会发生的

比如:此时线程0对if进行判断之后,发现ticketNum =1>0,进入if执行体之后没有被CPU剥夺执行权,继续运行同时将ticketNum从1减到0之后并打印出window1_sale: 1。此时CPU进行了线程切换之后线程1进入if判断之后,此时ticketNum=0>0不满足,不再打印票。这样后面的线程进来执行的if的时候,都会因为if条件不满足而退出while循环。这样就没有数据安全问题发生。

2.    同步代码块基本用法和原理

1). 同步代码块的基本用法

同步代码块是解决多线程的数据安全问题的专业解决方法之一。

(1). 同步代码块基本格式

synchronized(对象){

//需要被同步的代码

}

注意synchronized后面的()的“对象”是任意类型都可以。但是这个对象必须以多线程之间的共享数据的身份出现,也就是多个线程之间共享这一个对象

(2). 对run方法中代码加同步的判定方法

线程运行的自定义代码全部位于run方法中,为了避免数据安全问题的发生,应该在将哪些代码放置在同步代码块中呢?

[1]. 代码同步判定方法

run方法哪些语句操作了多线程间的共享数据哪些语句放置到同步代码块中。

具体做法】找到第一句最后一句操作共享数据的代码,将这两句及其之间的代码一起放置到同步代码块中

[2]. 举例说明。下面的代码是Runnable实现子类的run方法中的代码

04.多线程--04.【多线程卖票出现的数据安全问题】【同步代码块基本用法和原理】_第4张图片

这样对这个Runnable实现子类中的代码进行同步之后的代码如下:

class TicketRunnable implements Runnable{
    private int ticketNum =10;
    //Lock for Multi-Threads
    private Object objLock = new Object();
   
    public void run() {
       while(true){
           synchronized (objLock) {
              if(ticketNum <=0)
                  break;
             
              System.out.println(Thread.currentThread().getName()+":"+ ticketNum--);
           }
       }
    }
}

注意】测试代码不变。

[3]. 打印结果

04.多线程--04.【多线程卖票出现的数据安全问题】【同步代码块基本用法和原理】_第5张图片

问题】为什么Thread类对象t3和t4没有打印出来结果呢?

{1}. 由于在测试代码中使用了tX.start();语句(X =1, 2, 3, 4),因此t3和t4一定处于临时阻塞状态,即具有CPU执行资格但是没有CPU执行权。具体哪一个线程被执行是由CPU决定的。

{2}. 这里面t3和t4的窗口没有打印出来的原因就是start()开启之后,两个线程就再也没有抢到CPU的执行权,因此自始至终都处于临时阻塞状态,因此没有打印出来。

2). 同步代码块原理----锁机制

(1). 同步代码块原理 ----锁机制

synchronized(obj){

//需要被同步的代码

}

[1]. 对象的

每个对象一个标志位取值为0的时候表示“上锁”;取值为1的时候表示“开锁”。这个对象的标志位称为对象的锁

[2]. 持有锁的线程

一个线程进入同步代码块的时候,如果此时同步代码块上的obj的锁1(也就是开锁),就把这个线程称为持有锁的线程

[3]. 同步代码块的原理

{1}. 持有锁的线程可以在同步代码块中运行。

{2}. 不持有锁的线程即使获取了CPU的执行权,也不能进入同步代码块执行。

持有对象的锁的线程可以在同步代码块中运行。

{3}. 具体执行过程

{3}1. 对同步代码块上锁

一个线程进入同步代码块的时候,如果发现同步代码块的obj的锁的值是1在执行同步代码块的内容之前先把obj锁的值变成0(也就是给同步代码块上锁)同时这个线程也被称为持有锁的线程

{3}2. 对同步代码块开锁

持有锁的线程执行同步代码块的时候,会同步代码块锁的值变为0表示对同步代码块开锁,这样别的线程就有机会运行这个同步代码块中的代码。同时原来持有锁的线程也变成了非持有锁的线程

(2). 同步代码块的执行原理解决多线程中共享数据的安全问题

同步代码块的执行原理保证了对同步代码块中的共享数据操作时安全的,原因如下:

[1]. 假设这个时候这个持有锁的线程失去CPU的执行权,别的线程想运行这个同步代码块中的时候,发现同步代码块已经被上锁,所以没有办法进入到同步代码块中执行同步代码。

[2]. 这一点保证了如果某个持有锁的线程即使没有一下子操作完所有和共享数据有关的代码,别的非持有锁的线程没有机会同步代码块被持有锁的线程上锁之后执行同步代码块中和共享数据有关的代码

[3]. 因此失去CPU执行权持有锁的线程再次获得CPU执行权的时候,线程间的共享数据的值仍然是这个持有锁的线程CPU中断执行时候的值,并没有发生改变

因此持有锁的线程同步代码块的上锁开锁执行原理保证了对同步代码块中线程间的共享数据操作的正确性

(2). 对多线程卖票案例中同步代码块运行过程进行分析

[1]. 标记的代码如图

04.多线程--04.【多线程卖票出现的数据安全问题】【同步代码块基本用法和原理】_第6张图片

[2]. 执行过程分析

{1}. 假设线程0获得了CPU的执行权,并执行到同步代码块。 此时同步代码块对象objLock锁的值是1。线程0首先对同步代码块上锁自己也变成了持有锁的线程。运行完Thread.sleep()之后,线程0交出了CPU的执行权。

{2}. 假设此时CPU将执行权交给了线程3。线程3运行到同步代码块的时候,发现同步代码快已经被锁上,无法运行。

{3}. 假设此时CPU再次将执行权从线程3转交给持有锁的线程--线程0。此时线程0继续在中断之前的断点后(这里为打印语句)执行另一条代码ticketNum--操作。由于在持有锁的线程0被中断执行之后,没有其他线程进入到同步代码块中修改ticketNum的值,因此这个共享数据的值对持有锁的线程来说仍然保持不变。因此这个时候线程0操作共享数据是正确的。多线程对共享数据操作的安全隐患被消除

{4}. 当持有锁的线程0执行完同步代码块之后,就会对同步代码块开锁。同时自身也变成了非持有锁的线程。这样其他的线程就有机会来执行这个同步代码块。

(3). 同步代码块的使用前提 (2个必须)

[1]. 必须要有2个以上的线程才需要同步

[2]. 同步代码块锁的宿主对象必须多线程间的共享数据出现,也就是多个线程共用同一个同步代码块的锁

错误举例代码

class TicketRunnableII implements Runnable{
    private int ticketNum =10;   
    public void run() {
       ObjectobjLock =new Object();
       while(true){
           synchronized (objLock ) {
              if(ticketNum <=0)
                  break;
             
              try {
                  Thread.sleep(10);
              }catch (InterruptedException e) {
                  // TODO Auto-generated catch block
                  e.printStackTrace();
              }
             
              System.out.println(Thread.currentThread().getName()+":"+ ticketNum--);
           }
       }
    }
}

运行结果】----同步失效

04.多线程--04.【多线程卖票出现的数据安全问题】【同步代码块基本用法和原理】_第7张图片

同步失效的原因:

同步有效的前提是两个必须:必须多个线程+ 必须多线程共用同一个同步锁对象。这里面线程间没有公用同一个锁对象,因此失效。失效的具体原因如下:

{1}. 这里面的锁对象局部变量,这样每个线程运行run方法的时候,都有自己一份同步锁对象的副本。这样当每个线程运行同步代码块的时候,这个同步锁对象都为这一个线程所拥有

{2}. 假设线程0进入run方法之后,在自己的栈空间中新建了一个临时的objLock。此时objLock中的锁的值是1。运行到同步代码块之后,线程0马上把属于自己的临时的objLock的锁标志的值改成了0(上锁)。继续运行代码直Thread.sleep()方法,线程0失去了CPU的执行权。假设此时CPU将执行权交给了线程3。

{3}. 线程3进入到run方法之后,同样也在自己的栈空间中新建了一个属于自己的临时objLock锁对象。objLock的锁的初始值是1。这样线程3运行到同步代码块的时候,使用自己的临时对象objLock的锁标志位(值为1)进行是否可以运行同步代码的依据。经判断可以运行,这样假设线程3一口气运行完成了自己的run方法,那么在线程0继续运行剩余的代码之前,对线程0而言共享数据ticketNum的值被别的线程进行了修改。因此线程0再次运行的时候,数据是错误的。安全隐患发生。-----没锁住!!!

(4). run方法中需要被同步部分

[1].问题:多线程中run方法的同步代码块可以加在整个while循环上么?

[2]. 假设此时将同步代码块加载了run方法的整个while上 ----代码如下

class TicketRunnableIII implements Runnable{
    private int ticketNum =10;
    private Object objLock =new Object();
 
    public void run() {
       synchronized (objLock) {
           while(true){
              if(ticketNum <=0)
                  break;
             
              try {
                  Thread.sleep(10);
              }catch (InterruptedException e) {
                  e.printStackTrace();
              }
             
              System.out.println(Thread.currentThread().getName()+":"+ ticketNum--);
           }
       }
    }
}

[3]. 运行结果

04.多线程--04.【多线程卖票出现的数据安全问题】【同步代码块基本用法和原理】_第8张图片

无论怎么运行,都只有一个窗口卖票

【原因分析】某个线程进入run方法之后,就马上进入了同步代码快并对这个同步代码块进行了上锁,这样就开始“卖票”。此时这个线程运行到sleep方法之后就交出了CPU的执行权。但是其他获得了CPU执行权的线程由于while整个方法被同步代码块锁住而无法运行。直到持有锁的线程再次获得了CPU执行权之后,这个while循环才能继续运行。这样while循环只能被同一个线程执行完成。执行完成之后,ticketNum的值变成了0,并且释放了锁。但是其他线程再次进入这个run中执行的时候while中的if已经不满足条件,所以没有“票”可以被卖出。因此其余的线程陆续结束。

本来多线程进行多个窗口卖票的效果由于while被放在同步代码块中使得一个线程完成了所有的任务而其他线程没有事情做。变成了单线程的效果。

结论

假设run方法中最开始有一个(死)循环为了让所有的线程都能有效地执行run方法中和这个循环有关的有效代码,一定run中的最外层的循环放置到同步代码块之外

(5). 同步代码块的优缺点

[1]. 同步代码块的优点:解决了多线程之间共享数据的安全问题

[2]. 同步代码块的缺点:每个线程运行同步代码块之前都要对锁对象进行判断,消耗了系统资源,使得多线程代码的运行速度变慢。

----------- android培训、java培训、java学习型技术博客、期待与您交流! ------------

 

你可能感兴趣的:(Java基础)