多线程同时执行的时候,如果没有设置同步。那么由于CPU操作的原子性
假设某个共享成员被访问一次就自增一次或自减一次
在作数据操作的时候,可能某几个线程拿到的是同一个值,这显然不是我们想要的

为什么要同步

Runnable
首先回到模拟电影院售票的案例上,可参考Runnable接口,链接在上方
执行相关操作会发现,打印的数据中会出现同票甚至可能出现负票
首先就同票的问题作分析 : 
CPU的一次操作必须是原子的。也就是说这个操作不能被拆分
        t = 1         thread1与thread2同时打印t++
        由于t++ 的原理是 temp = t; t = t + 1; 返回temp
情况一:    thread1                thread2                情况二:        thread1             thread2
1:        temp = t;                                            temp = t;
2:                            temp = t;                         t = t + 1;
3:        t = t + 1;                                                                temp = t;
4:                            t = t + 1;                        return temp;
5:                            return temp;                                        t = t + 1;
6:        return temp;                                                            return temp;    
        故而打印的都是1                                        故而thread1打印1,thread2打印2
其次是负票的情况,也是由于CPU操作原子性,最后有可能多个线程同时通过了判断
但是打印的时候拿到的值却错开了,也就是靠近边界时都通过判断但是打印却发生了情况二这种情景

产生问题的场景

那么哪些情况又会产生线程安全问题呢?
可以从这些角度去考虑,首先必须是多线程环境,其次有共享数据(变量)
然后是操作共享数据的语句(发生变化),语句越多出现安全问题的可能性越高
总结    A.是否有多线程环境
    B.是否有共享数据
    C.是否有多条语句操作共享数据

解决方案

同步代码块

格式

同步代码块能解决安全问题的主要原因在代码块参数对象上,这个对象也被称为同步锁
但是多个存在安全问题的线程只能使用同一把锁,并且代码块的代码尽量少,且锁对象不能为空
格式如下 : 
1
2
3
synchronized(对象){
//需要同步的代码;
}
简单的案例 : 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Runnable runnable = new Runnable() {
private int number = 100;
private Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {
if (number > 0)
System.out.println(Thread.currentThread().getName() + " : " + number--);
else
break;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
Thread thread1 = new Thread(runnable, "业务员1");
Thread thread2 = new Thread(runnable, "业务员2");
Thread thread3 = new Thread(runnable, "业务员3");
thread1.start();
thread2.start();
thread3.start();
执行代码,可以发现即使是数字的顺序都是有序的了,这是因为同步代码块里面的代码只能有一个线程在执行
具体结果如下 :

同步的特点

多个线程;多个线程使用的是同一个锁对象
    好处 : 同步解决了多线程的安全问题
    弊端 : 当线程相对多时,因为每个线程都会判断同步上的锁,这是很耗费资源的,降低程序的运行效率

同步方法

有些时候,一个方法的整个代码都需要加锁.这时候java提供了同步方法
就是在方法上添加一个synchronized关键字(修饰符的位置任意),一般把它写到权限修饰符后面
实例同步方法的锁对象是this,也就是当前对象本身
静态(类)同步方法的锁对象是该类的字节码文件对象(Class文件对象)
实例同步方法简单的案例 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
Runnable runnable = new Runnable() {
private int number = 100;
private Object obj = new Object();
int x = 0;
@Override
public void run() {
while (true) {
x++;
if (x % 2 == 0) {
synchronized (this) {
if (number > 0)
System.out.println(Thread.currentThread().getName() + " : " + number--);
else
break;
}
}else {
count();
if (number <= 0) break;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private synchronized void count() {
if (number > 0)
System.out.println(Thread.currentThread().getName() + " : " + number--);
}
};
Thread thread1 = new Thread(runnable, "业务员1");
Thread thread2 = new Thread(runnable, "业务员2");
Thread thread3 = new Thread(runnable, "业务员3");
thread1.start();
thread2.start();
thread3.start();
执行结果如下 :

校验实例同步方法锁对象为this,将同步代码块锁对象修改为obj;可以发现出现同数字以及乱序
执行结果如下:

由于匿名内部类不能使用静态属性,简单案例只需要在实例同步方法案例的基础上创建一个Runnable接口的实现类
将匿名内部类的同步代码块的锁对象改为this.getClass()或者(类名.class)并将同步方法以及共享成员number改为静态
执行结果如下 :

校验实例同步方法锁对象为字节码文件,将同步代码块锁对象修改其他对象;可以发现同数字或乱序

异常的情况

如果在同步代码块中出现异常,那么会发生什么情况呢?是否会释放锁对象呢?
下面是一个简单的测试案例 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Runnable ticket = new Runnable() {
private int ticket = 100;
private Object lock = new Object();
@Override
public void run() {
while (true) {
synchronized (lock) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "正在出售第" + ticket + "张票!");
if (Thread.currentThread().getName().equals("窗口1") && ticket < 80)
System.out.println(10 / 0);
ticket--;
} else
break;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
Thread thread1 = new Thread(ticket,"窗口1");
Thread thread2 = new Thread(ticket,"窗口2");
Thread thread3 = new Thread(ticket,"窗口3");
thread1.start();
thread2.start();
thread3.start();
从执行结果来看,是会释放锁对象的。
但是由于在票减少之前出现了异常,故而票打印了但是却没有减少.出现了一次同票
执行结果如下 : 

修改代码,添加try-catch-finally结构,将同步代码中必须的操作放到finally中
1
2
3
4
5
6
7
8
9
10
11
12
if (ticket > 0) {
try {
System.out.println(Thread.currentThread().getName() + "正在出售第" + ticket + "张票!");
if (Thread.currentThread().getName().equals("窗口1") && ticket < 80)
System.out.println(10 / 0);
} catch (ArithmeticException e) {
e.printStackTrace();
break;//Thread.currentThread().interrupt();
} finally {
ticket--;
}
}
执行结果如下 :

如果使用中断,会发生什么?
在上述代码中,会一直抛出ArithmeticException与InterruptException并只有"窗口一"在执行
由于中断是一种抛出异常的方式,而sleep方法预定这个中断异常对象是不会执行的
而这两个异常对象一直存在于该线程的run方法内
也就导致了"窗口一"没有等待,把同步后面的睡眠时间修改得尽量小或者为0,
多次测试就会发现其他的线程也能执行,但是只要遇到"窗口一"就会抛出两个异常

相关习题

线程练习