首页 > 代码库 > 线程同步
线程同步
首先,假设现在有一个容器类,它能够简单地增加元素,删除元素,并在每次增删元素之后显示自身持有了多少个元素、是否为空。为了能够共用,它是单例的。
/** * source1 */ class Container { // 象征元素个数的索引对象 private int index =50; private final static Container c = new Container(); private Container() { super(); } public static Container getInstance() { return c; } public void add() throws InterruptedException { index ++; System.out.print(c + "\t"); } public void sub() throws InterruptedException { if (index != 0) { index --; } System.out.print(c + "\t"); Thread.sleep(500); } public String toString() { if (index != 0) { return "Container points " + index; } else { return "Container is empty"; } } }
现在需求是尽快消耗掉Container中预存的50个元素,
对于这个需求,可以通过反复调用Container.add()来实现,但是这样做太慢了,每次调用都要等待0.5s,
这个时间可以认为是实际开发中,方法内部其他业务逻辑所需要消耗的时间。
合适的做法可能是启用若干个专门的线程,通过并发来删除元素。
/** * source2 */ class SimpleThread implements Runnable { // 持有共有容器 Container c = Container.getInstance(); @Override public void run() { try { move(); } catch (Exception e) { e.printStackTrace(); } } private void move() throws Exception { while (true) { c.sub(); } } }
试验一下
/** * source3 */ public static void main(String[] args) { new Thread(new SimpleThread()).start(); new Thread(new SimpleThread()).start(); new Thread(new SimpleThread()).start(); new Thread(new SimpleThread()).start(); }
Container points 48 Container points 45 Container points 47 Container points 46 Container points 49 Container points 42 Container points 41 Container points 40 Container points 42 Container points 42 Container points 38 Container points 35 Container points 36 Container points 38 Container points 37 Container points 32 Container points 32 Container points 31 Container points 32 Container points 30 Container points 29 Container points 27 Container points 26 Container points 28 Container points 25 Container points 23 Container points 20 ……
试验运行了一会就被停止了,因为早在一开始,打印值就完全混乱了...
引发混乱的原因是,所有的线程能通过Container.sub(),来访问并改变Container.index的值。
而Container对象只有一个,并被4个线程同时持有,这4个线程又不停地在就绪状态和运行状态间切换,这种切换并不受Java代码控制,很容易发生诸如下面的现象:
某个线程(Thread0)刚执行到index --(假设此时index为50),时间片就消耗完毕了,那么暂时就无法打印此时的index值(49)。
接着时间片被另一个线程(Thread1)获得,而这个线程比较幸运,执行完了index --,还有机会打印index值,而此时打印值已经成了48。
index(49)会在下次Thread0获得时间片时被打印出来,那么打印结果就完全错误了。
不光如此,由于demo中用Thread.sleep(500)来代表实际项目中业务逻辑运行的时间,index--的时间相对0.5来说可以忽略不计,
那么虽然打印值不正确,但是index真正的值基本上是正确地一点点减少。
但是实际项目中,过程不会那么简单,“index--”象征的删除操作需要的时间会被放大,如果在“index--”时线程进入就绪状态,另一个线程就会在现在index值的基础上进行删除操作
这样一来,同一个元素就被两次(或者更多)执行了删除操作,容器内部的发生了元素不同步。
Deolin是这样理解线程不同步的,
每个线程都致力于把“对象”从“过去”状态变成“将来”状态。
变换的过程中,“对象”容易被另一个线程抢先变成了“将来”状态,
而原线程依然认为“对象”停留在“过去”,形成信息不同步。
解决方法是,想办法将“index --”和“println(c)”绑定起来,让“index--”执行完了以后,必须等“println(c)”也执行完,才允许其他线程执行“index--”。
Java中提供了这样的关键字——synchronized
public void add() throws InterruptedException { synchronized (this) { index ++; Thread.sleep(500); System.out.print(c + "\t"); } } public void sub() throws InterruptedException { synchronized (this) { if (index != 0) { index --; } Thread.sleep(500); System.out.print(c + "\t"); } }
再测试一下
Container points 49 Container points 48 Container points 47 Container points 46 Container points 45 Container points 44 Container points 43 Container points 42 Container points 41 Container points 40 Container points 39 Container points 38 Container points 37 Container points 36 Container points 35 Container points 34 Container points 33 Container points 32 Container points 31 Container points 30 Container points 29 Container points 28 Container points 27 Container points 26 Container points 25 Container points 24 Container points 23 Container points 22 Container points 21 Container points 20 Container points 19 Container points 18 Container points 17 Container points 16 Container points 15 Container points 14 Container points 13 Container points 12 Container points 11 Container points 10 Container points 9 Container points 8 Container points 7 Container points 6 Container points 5 Container points 4 Container points 3 Container points 2 Container points 1 Container is empty Container is empty Container is empty Container is empty ……
结果正确。
线程一旦执行到synchronized{}内部,synchronized{}代码块会被“上锁”,线程会“获得锁”,其他线程只能被拦截在synchronized{}以外,
(所有被拦截的线程,会进入一种叫做同步阻塞的状态,被JVM放入锁池(lock pool))
直到获得锁的线程将synchronized{}剩余的代码执行完毕,释放“锁”。下个获得时间片的线程才有机会“获得锁”,进入synchronized{}
为了向安全性(线程同步)让步,Deolin觉得synchronized是一种牺牲效率的“反并发”做法。
锁住越多的代码意味着越安全,代价就是更低效,最极端的情况,锁住了一切,那么就根本没有并发了。
锁住越少的代码意味着越高效,代价就是要coding中要考虑更多,test过程中需要更多的case来检证source的安全性。
synchronized的另一个组成部分是()中对对象的声明。synchronized(this)的对象实际上算是个象征,如果对象中某一个synchronized代码块锁被获得了,
那么访问其他的synchronized代码块也会进入阻塞状态。
改变source2,让线程对象有两种运行策略。
/** * source2 */ class SimpleThread implements Runnable { // 持有共有容器 Container c = Container.getInstance(); // true:调用add();false:调用sub(); boolean strategy; public simpleThread(boolean strategy) { this.strategy = strategy; } @Override public void run() { try { move(); } catch (Exception e) { e.printStackTrace(); } } private void move() throws Exception { while (true) { if (strategy) { c.add(); } else { c.sub(); } } } }
改变sub()和add()方法,让sub()方法内sleep时间足够长,是suber线程能长时间获得锁。
public void add() throws InterruptedException { synchronized (this) { index ++; System.out.print(c + "\t"); Thread.sleep(500); } } public void sub() throws InterruptedException { synchronized (this) { if (index != 0) { index --; } System.out.print(c + "\t"); System.out.println("subing"); Thread.sleep(10000); } }
测试一下
Container points 49 subing Container points 48 subing Container points 47 subing Container points 46 subing Container points 45 subing Container points 46 Container points 47 Container points 48 Container points 49 Container points 50 ……
所有有“subing”的行,其下一行都等待了足足10s才打印出来。
当希望用synchronized关键字锁住整个方法时,语法发生了改变,不再需要()部分,默认为this对象。
public synchronized void add() throws InterruptedException { index ++; System.out.print(c + "\t"); Thread.sleep(500); } public synchronized void sub() throws InterruptedException { if (index != 0) { index --; } System.out.print(c + "\t"); System.out.println("subing"); Thread.sleep(10000); }
重入特性
synchronized方法与synchronized(this)是等价的,都是对象锁
JVM会通过一个计数器统计某个对象的上锁次数。
当一个线程进入了synchronized声明的代码之后,计数器变成了1。
此时该线程可以通过代码流程的引导,进入该对象中别的被synchronized声明的代码(它能够这样做,因为他持有了该对象的锁)
一旦进入了新的synchronized代码,计数器会+1,变成2,
直到退出从新的synchronized代码退出或是返回,计数器变回1。
而只有计数器变成0,锁池中的其他线程才有机会进入synchronized代码。
基于这点,获得了对象锁的线程可以自由调用对象的任何方法。
而一个线程必须等对象的每个对象锁全部释放,才有机会逃离lock pool,获得对象锁
那么,关于线程同步,在开发过程中,本质上就是考虑4件事情
哪个线程获得了锁?
线程了获得了哪个对象的锁?
还有哪些线程在等锁?
对象锁释放完全了吗?
static synchronized方法
特性与synchronized方法基本一致,唯一不同的是锁的对象
synchronized方法对象是类的某一个实例(this),而static synchronized方法对象是类的所有实例,某种程度上可以认为对象是类的字节吗。
前者获得锁的条件是没有线程获得this所有的锁,后者获得锁的条件是没有线程获得该类型所有对象所有的锁。
线程同步