在上一篇博客里讲解了JAVA的线程的内存模型,见:JAVA并发编程2_线程安全&内存模型,接着上一篇提到的问题解决多线程共享资源的情况下的线程安全问题。
不安全线程分析
public class Test implements Runnable { private int i = 0; private int getNext() { return i++; } @Override public void run() { // synchronized while (true) { synchronized(this){ if(i<10){ System.out.println(getNext()); }else break; } } } public static void main(String[] args) { Test t = new Test(); Thread t1 = new Thread(t); Thread t2 = new Thread(t); t1.start(); t2.start(); Thread.yield(); } }
与之前的代码的区别在于run方法被synchronized关键字修饰。
根据上一篇博客的分析:多线程在访问共享资源的时候由于CPU轮流给每个任务分配其占用的时间,而CPU的调度是随机的,因此就会发生某个线程正在访问该变量的时候CPU却将时间片分发给了其他的线程,这样就会发生这样的现象:一个线程从主内存读取到某个变量的值还没来得及修改(或者修改后刷新主内存),另一个线程就获得了CPU的执行权,也从主内存读取改变量的值。当CPU执行权再次回到第一个线程的时候会接着之前的中断处执行(修改变量等),执行权回到第二个线程时却不能看到第一个线程中改变了的值。归结起来就是说违背了线程内存的可见性。避免上看起来产生第一种输出的可能顺序如下图所示(实际上可能的情况非常多,因为i++不是单个的原子操作):
i++对应下面的JVM指令,因此在期间另一个线程都可能会修改这个变量。
4: aload_0
5: iconst_0
6: putfield #2 // Field i:I
为了体现内存的可见性,synchronized关键字能使它保护的代码以串行的方式来访问(同一时刻只能由一个线程访问)。保证某个线程以一种可预测的方式来查看另一个线程的执行结果。
线程同步
JAVA提供的锁机制包括同步代码块和同步方法。
每个Java对象都可以用做一个实现同步的锁,这些所成为内置锁(Intrinsic Lock)或监视器锁(Monitor Lock),一个线程进入同步带吗快之前会自动获得锁,并且推出同步带吗快时自动释放锁。获得内置锁的位移途径就是进入由这个锁保护的同步代码块或方法并且该锁还未被其他线程获得。
Java内置锁相当于互斥体(互斥锁),意味着最多有一个线程持有这种锁。当线程A尝试获取一个由线程B持有的锁时,线程A必须等待或者阻塞,知道线程B释放这个锁,如果线程B永远不释放锁,那么线程A将永远等待下去。
每次只能有一个线程执行内置锁保护的代码块,因此这个锁保护的同步代码块会以原子方式执行,多个线程在执行该代码块时也不会相互干扰。
原子性的含义:一组语句作为一个不可分割的单元被执行。任何一个执行同步代码块的线程,都不可能看到有其他线程正在执行由同一个锁保护的同步代码块。
千万注意:并不是说synchronized代码块或者synchronized方法是不可分割的整体,是原子的,因为,显然使用不同锁的话之间不存在互斥关系。
买票例子的引入
下面是模拟火车站卖票的程序,理论上是要将编号为1-10的票卖按照由大到小顺序卖出去,结果用两个窗口(线程)卖就出现了这样的结果,有些编号的票卖了两次,有些没卖出去,并且还有编号为0的票卖了出去。显然结果错误的。
public class Test implements Runnable { private int i = 10; private void sale(){ while (true) { if (i >0){ try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread() + "正在卖第" + i + "张票"); i--; } else break; } } @Override public void run() { sale(); } public static void main(String[] args) { Test t = new Test(); Thread t1 = new Thread(t); Thread t2 = new Thread(t); t1.start(); t2.start(); Thread.yield(); } }
出现这种结果的原因就是没有对多个线程共同访问的资源进行同步加锁。下面我们对其进行线程同步,达到想要的效果:
synchronized代码块:
synchronized (lock){ //同步的代码 }
lock必须是一个引用类型的变量。
使用synchronized同步代码块:
public class Test implements Runnable { private int i = 10; private void sale(){ Object o = new Object(); while (true) { synchronized(o){ if (i >0){ System.out.println(Thread.currentThread() + "正在卖第" + i + "张票"); i--; }else break; } } } @Override public void run() { sale(); } public static void main(String[] args) { Test t = new Test(); Thread t1 = new Thread(t); Thread t2 = new Thread(t); t1.start(); t2.start(); Thread.yield(); } }
咦?使用了同步代码块了怎么结果还是不对呢??我们先看正确的同步:
public class Test implements Runnable { private int i = 10; Object o = new Object();// 通常使用:/*static*/ byte[] lock = new byte[0]; private void sale(){ while (true) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } synchronized(o){ if (i >0){ System.out.println(Thread.currentThread() + "正在卖第" + i + "张票"); i--; }else break; } } } @Override public void run() { sale(); } public static void main(String[] args) { Test t = new Test(); Thread t1 = new Thread(t); Thread t2 = new Thread(t); t1.start(); t2.start(); Thread.yield(); }
这里线程同步的原理是怎样的呢?因为任何一个Java对象都可以作为一个同步锁,上面代码的对象o就是一个同步锁。
一个线程执行到synchronized代码块,线程尝试给同步锁上锁,如果同步锁已经被锁,则线程不能获取到锁,线程就被阻塞;如果同步锁没被锁,则线程将同步锁上锁,并且持有该锁,然后执行代码块;代码块正常执行结束或者非正常结束,同步锁都将解锁。
所以线程执行同步代码块时,持有该同步锁。其他线程不能获取锁,就不能进入同步代码块(前提是使用同一把锁),只能等待锁被释放。
这时候回头看上上段代码中的同步代码块,由于两个线程使用的锁是不一样的(创建了两个对象),因此,就算线程A在执行同步代码块,当线程2获得CPU执行权时,检查到这个锁并未被其他线程锁定,因此不具有互斥性,不能达到线程同步的效果。
同步方法
将synchronized作为关键字修饰类的某个方法,这样该方法就变成了同步方法。
直接将sale函数改为synchronized方法的结果是虽然卖票不会乱序,但是只有一个线程在卖票。所以稍微做些调整:
public class Test implements Runnable { private int i = 10; private void sale(){ while (true) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } f(); } } private synchronized void f(){ if (i >0){ System.out.println(Thread.currentThread() + "正在卖第" + i + "张票"); i--; }else return; } @Override public void run() { sale(); } public static void main(String[] args) { Test t = new Test(); Thread t1 = new Thread(t); Thread t2 = new Thread(t); t1.start(); t2.start(); Thread.yield(); } }
这时候的锁是哪个对象呢?
当修饰的方法是类方法时同步锁是该类对应的Class对象;
当修饰普通方法时,该同步锁是当前对象即this。
体会:不要滥用synchronized方法
在平时的编程中为了达到线程同步的目的,在不经认真思考的情况下,经常发生synchronized关键字的滥用,归根结底是没有理解同步的原理本质。
看下面的代码:
public class Test implements Runnable{ @Override public void run() { f(); } public synchronized void f(){ System.out.println(this); } public static void main(String[] args) { Test t1=new Test(); Test t2=new Test(); // f()里面的代码无法达到同步的目的 new Thread(t1).start(); new Thread(t2).start(); } } //Output //Test@2073b879 //Test@d542094
根据打印的结果也可以看出来函数f()是无法同步的,因为这两个线程使用了两个同步锁。这就告诉我们,并不要看到一个方法是synchronized的就想当然的认为它是同步方法就在不同的线程里随便调用。
注:上面的代码里面多次使用到了Thread.sleep(long)方法,是让当前线程睡眠一会,这个方法会让当前线程放弃CPU的执行权,处于Time Waiting状态,CPU不在为其分配时间片。由于机器的不同可能不容易出现我们期望的线程切换,目这样做就可以强制的让线程切换。
另外,在synchronized代码里面使用sleep无效。因为该线程sleep后CPU不在为其分配时间片,但是这个时候线程已经拿到了同步锁,即使睡到天荒地老,它也不会把同步锁交出去,别的线程得到了CPU执行却却苦于没有同步锁而被拒之门外。后面学习线程的状态会讲到这些。
会写代码不一定理解了,理解了不一定能给别人讲清楚。想把一个东西用文字表述清楚真的挺不容易。