多线程同时执行的时候,如果没有设置同步。那么由于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 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; } finally { ticket--; } }
|
执行结果如下 :

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