Java 多线程【线程安全】

线程安全问题在理想状态下,不容易出现问题,但一旦出现对软件的影响是非常大的。

可能导致线程安全出问题的原因:
1:是否是多线程环境
2:是否有共享数据
3:是否有多条语句操作共享数据

下面是我遇到的问题:

假设有100张票需要卖出,同时我有两个窗口。那么同一张票肯定不能重复卖出,两个窗口也是同时开始卖票的,这几需要多线程来解决。下面是代码:

package threads;

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

        PrimeThread p = new PrimeThread();

        Thread my = new Thread(p, "窗口1");
        Thread m = new Thread(p, "窗口2");
        my.start();
        m.start();
    }
}
package threads;

public class PrimeThread implements Runnable{
    private int ans = 100;//共享数据
    @Override
    public void run() {

        while(ans > 0) {
            try {
                Thread.sleep(100);//这里延迟100毫秒,突出以下问题
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+":"+(ans--));//多条语句操作共享数据
        }

    }

}

按以上代码的输出结果应该是100到1,但是其结果却是:

窗口2:100
窗口1:100
窗口1:99
窗口2:98
窗口1:97
窗口2:96
窗口1:95
窗口2:94
窗口1:93
窗口2:92
窗口2:91
窗口1:90
窗口1:89
窗口2:88
窗口1:87
窗口2:86
窗口1:85
窗口2:84
窗口1:83
窗口2:82
窗口1:81
窗口2:81
窗口1:80
窗口2:79
窗口2:78
窗口1:78
窗口1:77
窗口2:76
窗口2:75
窗口1:74
窗口1:73
窗口2:72
窗口2:71
窗口1:71
窗口2:70
窗口1:70
窗口1:69
窗口2:68
窗口2:66
窗口1:67
窗口1:65
窗口2:64
窗口1:63
窗口2:62
窗口1:61
窗口2:60
窗口1:59
窗口2:58
窗口1:57
窗口2:56
窗口2:55
窗口1:55
窗口1:54
窗口2:53
窗口1:52
窗口2:51
窗口1:50
窗口2:49
窗口1:48
窗口2:48
窗口1:46
窗口2:47
窗口1:45
窗口2:44
窗口2:42
窗口1:43
窗口2:40
窗口1:41
窗口2:39
窗口1:38
窗口2:37
窗口1:36
窗口2:35
窗口1:34
窗口2:33
窗口1:32
窗口2:31
窗口1:30
窗口2:29
窗口1:28
窗口2:27
窗口1:26
窗口2:25
窗口1:24
窗口2:23
窗口1:22
窗口2:21
窗口1:20
窗口2:19
窗口1:18
窗口2:17
窗口1:16
窗口2:15
窗口1:14
窗口2:13
窗口1:12
窗口2:11
窗口1:10
窗口2:9
窗口1:8
窗口2:7
窗口1:6
窗口2:5
窗口1:4
窗口2:3
窗口1:2
窗口2:1
窗口1:0

同一张票被卖了多次,这与我们预期中每张票只输出一次完全不一样。而且虽然我们限定了ans值大于0,但是还有可能出现0,当线程再多的话还可能出现负值。

这就是线程安全出了问题,以为它符合了我们前边说的导致线程安全出问题的全部三种原因,而其中一种就有可能导致线程安全出现问题。

出现重复值的问题:
导致上述问题中同一张票出现多次与CPU的一次操作必须是原子性的特点有关。
在我们刚才的代码中有这样一个语句。

System.out.println(Thread.currentThread().getName()+”:”+(ans–));

这个语句其实是先把ans当前的值输出,然后再执行ans = ans-1。
先假设窗口1这个线程先到达这一句并且操作系统正在执行输出操作,但还没有执行减一操作,这时窗口2这个线程也到达该语句,因为线程具有随机性,所以CPU先执行了两次输出操作,所以就出现了重复值的问题。

出现0和负值的问题:
假设此时ans = 1,窗口1线程进入循环,因为CPU的原子性,后边的语句还没有被执行,但就在这时,窗口2的线程因为ans的值还没有变为0,所以也紧随其后进入了循环,而执行后边的语句是正好是两个线程分先后顺序地执行完了

System.out.println(Thread.currentThread().getName()+”:”+(ans–));

语句,所以就出现了0和负值的情况。

那么既然出现了问题就要解决问题,而之前我们也知道了导致线程不安全的三种原因
1:是否是多线程环境
2:是否有共享数据
3:是否有多条语句操作共享数据

但是第一条和第二条又是必须的,那么就只能从第三条上解决。而解决第三条就需要当一个线程在调用操作共享数据的语句时,其他线程就不能调用。这就需要线程同步。

可以利用Java中的synchronize关键字建一个同步代码块将操作语句锁起来

package threads;

public class PrimeThread implements Runnable{
    private int ans = 100;
    private Object obj = new Object();
    @Override
    public void run() {

        while(ans > 0) {
            synchronized(obj) {//将操作共享数据的代码锁起来
                try {
                    Thread.sleep(100);//这里延迟100毫秒,突出一下问题
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+":"+(ans--));
            }

        }

    }

}

这样就解决了线程不同步的问题。

同步的特点:

  • 前提:是需要多个线程,在解决问题时需要注意,多个线程使用的是同一个锁对象。
  • 好处:同步的出现解决了多线程的安全问题
  • 弊端:当线程相当多时,因为每个线程都会去判断同步上的锁,只是很耗费资源的,无形中会降低程序的运行效率。

上边说了同步代码块,格式为:synchronize(对象) {}
同步带么快的所对象是任意对象,所以这里的对象就可以是任意的;

同步方法的锁对象是 this
静态方法的锁对象是 类的字节码文件对象

下面进行一个对比:
同步方法:

package threads;

import java.lang.reflect.Method;

public class PrimeThread implements Runnable{
    private int ans = 100;
    int x = 0;
    private Object obj = new Object();
    @Override
    public void run() {

        while(ans > 0) {

            if(x%2 == 0) {
                synchronized (this) {//对象
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+":"+(ans--));
                }
            }else {
                md();
            }


        }
    }

    private synchronized void md() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+":"+(ans--));
    }

}

静态同步方法:

package threads;

import java.lang.reflect.Method;

public class PrimeThread implements Runnable{
    private static int ans = 100;
    int x = 0;
    private Object obj = new Object();
    @Override
    public void run() {

        while(ans > 0) {

            if(x%2 == 0) {
                synchronized (PrimeThread.class) {//对象
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+":"+(ans--));
                }
            }else {
                md();
            }


        }
    }

    private static synchronized void md() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+":"+(ans--));
    }

}

因为静态方法是随着类的建立而建立的,所以对象要是类才行。

刚才的三种同步方法最后都会输出一个0,这是因为ans的判断没有放在同步中,如果你细心的话就会发现
这里写图片描述
输出0和1的窗口永远是不一样的,这种现象在解释为什么会出现0和负数时已经解释过了,只需再同步代码中加一个判断就可以解决。

猜你喜欢

转载自blog.csdn.net/k_young1997/article/details/81536262