吃苹果比赛的同步安全问题

在本人学习Java的过程中,遇到了很多形形色色的问题。当时琢磨了好久才琢磨出这样的总结,贴出来供大家参考参考。以下观点仅代表本人在学习过程中的观点,望大家能够共同讨论,查漏补缺。


通过吃苹果比赛的实例,经过实际操作之后,我们不难看出会产生线程的同步安全问题。而具体的,我们使用继承的方式以及使用实现接口的方式都会造成不同的线程同步安全问题:

1、对于通过继承的方法来创建多线程,会造成不能操作同一个共享数据的问题
2、对于通过实现接口的方式,则会出现线程安全问题,例如出现共享的数据出现负数的问题

那么此时我们应该怎么解决呢?


针对出现的线程同步安全问题,通过查阅资料,我们不难发现的是我们可以通过同步锁来对此类问题进行解决。而在Java中对于同步锁的操作,我们常用的大致又分为了三种操作:

1、synchronized关键字
2、synchronized方法
3、Lock锁机制

首先我们需要来分析一下为什么会出现线程同步安全的问题(针对实现Runnable接口出现的出现数据负数的问题):

先将原代码贴出:
package learning.test.demo1;

//定义一个类实现Runnable接口
class AppleImple implements Runnable {
    // 定义一个苹果总数
    private int num = 50;

   // 复写run方法
    @Override
    public void run() {
    
            for (int i = 0; i < 50; i++) {
                if (num > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                 }
                 
/*
    由于是实现关系,而接口中是没有getName方法的,因此我们需要使用到静态方法来获取
    线程名
**/        
               System.out.println(Thread.currentThread().getName() + 
               "吃了第" + num-- + "个苹果");
                    
                }
            }
       }
}

public class AppleThreadByImplements {
        public static void main(String[] args) {
            // 创建三个线程
            // 创建实现类
            AppleImple ai = new AppleImple();
                new Thread(ai, "小黄").start();
                new Thread(ai, "小李").start();
                new Thread(ai, "小红").start();
    }
}
1、当代码执行到还剩下一个苹果的时候,我们的for循环此时判断num > 0,为true,因此进入了if语句,第一个线程A拿到了这个苹果,然后他进入了休眠

2、此时线程B抢到了执行权,而这时候,num还是为1的,因此通过if语句判断,num>0,因此又进入了if语句,这时候又陷入了休眠

3、此时线程C抢到了执行权,而这时候,num还是为1的,因此通过if语句判断,num>0,因此又进入了if语句,这时候又陷入了休眠

4、而这个时候恰好线程A苏醒了,他打印出了第1个苹果,这时候num--。

5、然后线程B接着苏醒,这时候num为0,因此线程B打印出第0个苹果,这时候num--,num为-1
   
6、然后这时候线程C又苏醒了,打印出了第-1个苹果。

因为线程是一直在相互切换的,我们需要一种有效的方法防止其自由切换,就好像你在家里你不锁门,然后就会有其他人可能会推门进来


那么此时我们就需要引入同步锁

同步锁概念:

为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制.
 
同步监听对象/同步锁/同步监听器/互斥锁:对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.
  
Java程序运行使用任何对象作为同步监听对象,但是一般的,我们把当前并发访问的共同资源作为
同步监听对象.

注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能
在外等着.

对于同步锁,我们还要注意的就是其实他还是需要一个锁对象的,而这个锁对象就是一开始在类中创建的对象。

如果我们在局部方法中创建这个对象,那么则意味着当创建线程的时候,每创建一次就会创建一个锁,这样很明显是违背了只有一个锁的原则,这样还是会造成线程不安全的问题,因此,我们需要在全局字段中去创建这个锁对象


synchronized关键字
class Apple1 implements Runnable {

    // 定义一个私有字段
    private int num = 50;

    // 覆盖run方法
    public void run(){

        for (int i = 0; i < 50; i++) {
            synchronized(this){
                if(num > 0){
                /*
                    此时,我们是看不出结果的,因为线程切换的太快,
                    现在我们就使用sleep方法模拟网络延迟
                **/
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                System.out.println(Thread.currentThread().getName() 
                + "吃了第" + num-- + "个苹果");
                 }
             }
           
         }
    }
}

public class AppleThreadBySynchronized {
    public static void main(String[] args) {

        // 创建一个苹果对象
        Apple1 a = new Apple1();

        // 创建三个线程
        new Thread(a, "小黄").start();
        new Thread(a, "小丽").start();
        new Thread(a, "小雪").start();

    }
}
 当引入了同步锁之后,就不会出现类似的线程安全问题。我们可以这样理解:
 
 当线程A进来的时候,他是携带了一把锁,然后他就会一直占着这个程序的运行权限,就等同于
 你把门锁了,没有人能够进来。哪怕是你在里面睡着了,别人也只能等你出来他才能进去,因此
 只有当线程A完成了他的操作,线程B或者线程C才有机会获得执行权。

这样就保证了线程的安全性


同理的,使用synchronized方法以及使用Lock锁机制也是通过这样上锁的方式来保证我们的线程同步安全的问题,不同的只是在于操作的方式不同

下面我就贴出关于synchronized方法以及Lock锁机制的操作方式

synchronized方法
//同步方法示例
//定义一个类实现Runnable接口
class Apple implements Runnable{

    //定义一个私有化字段
    private int num = 50;

    @Override
    public void run() {

        for (int i = 0; i < 50; i++) {
            //定义一个同步方法
            synchronizedDemo();
        }
    }

    synchronized private void synchronizedDemo() {

        if(num > 0){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName() +
             "吃了第" + num-- + "个苹果");

        }
    }
}

public class SynchronizedByMethodDemo {
    public static void main(String[] args) {
        //创建一个实现类对象
        Apple a = new Apple();

        //创建三个线程
        new Thread(a,"小黄").start();
        new Thread(a,"小海").start();
        new Thread(a,"小清").start();
    }
}

通过以上实例,我们不难看出同步方法只是将我们的代码块封装进一个方法中,然后使用synchronized修饰这个方法,本质跟我们的synchronized关键字是相同的。

不过必须注意的是!!synchronized不能用来修饰run方法!!!!只能用来修饰普通的方法,然后在run方法中调用!!


Lock锁机制
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;



//定义一个类实现Runnable方法
class LockDemo implements Runnable{

    //创建锁
    //添加同步锁
    Lock lock = new ReentrantLock();

    //定义私有化字段
    private int num = 50;


    @Override
    public void run() {

        for (int i = 0; i < 50; i++) {
            lockDemo();
        }
    }


    private void lockDemo() {


        lock.lock(); 
        try{

            if(num > 0){

                //添加延时
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + 
                "吃了第" + num-- + "个苹果");

            }
        }finally{

            lock.unlock();
        }

    }
}


public class SynchronizedByLockDemo {
    public static void main(String[] args) {
        //创建对象
        LockDemo l = new LockDemo();

        //创建线程
        new Thread(l,"黄").start();
        new Thread(l,"海").start();
        new Thread(l,"权").start();
    }
}


对于Lock锁机制而言,不同之处在于就是我们不再是使用synchronized关键字,而是使用lock和unlock方法进行加锁还有释放锁的操作。

而Lock锁机制和synchronized关键字又有什么区别呢?我将在另外一篇帖子中阐述。

你可能感兴趣的:(吃苹果比赛的同步安全问题)