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和负数时已经解释过了,只需再同步代码中加一个判断就可以解决。
还没有评论,来说两句吧...