四、多线程的同步
1、同步问题的引出
在前面分析售票系统的时候,有如下代码
if(count>0){
System.out.println(Thread.currentThread().getName()+" count = "+count--);
}
这种情况下会碰到一些意外,同一张票被打印两次获多次,票号为0或者负数的情况。
假设tickets值为1的时候,线程1刚执行完
if(count>0)这段代码,此时cpu切换到了线程2上,tickets值仍为1,线程2执行完上面两行代码,tickets值变为0后,cpu又切回到线程1上,其不会在执行
if(count>0),继续执行下一段代码,此刻tickets的值变为0,屏幕上打印出来的将是0.
我们可以使用Thread.sleep()方法来可以造成这种意外,迫使线程执行到该处后暂停执行,让出cpu给别的线程,在指定的时间后,cpu回到刚才暂停的线程上执行。
范例1:
public class Test {
public static void main(String args[]){
TestThread t = new TestThread();
Thread mTestThread1 = new Thread(t,"A");
Thread mTestThread2 = new Thread(t,"B");
Thread mTestThread3 = new Thread(t,"C");
mTestThread1.start();
mTestThread2.start();
mTestThread3.start();
}
}
class TestThread implements Runnable{
private int tickets = 5;
public void run(){
String ThreadName = Thread.currentThread().getName();
while(true){
if(tickets>0){
try{
Thread.sleep(100);
}catch(Exception e){}
System.out.println(ThreadName+" sales tickets "+tickets--);
}
}
}
}
运行结果:
B sales tickets 5
A sales tickets 3
C sales tickets 4
B sales tickets 2
A sales tickets 1
C sales tickets 0
B sales tickets -1
从运行的结果可以看到,票号被打印出来了负数,说明同一张票被卖了3次的意外发生。造成这种意外的根本原因就是因为资源数据反问不同步引起的。下面引入同步的概念。
2、同步代码块
如何避免上述意外?如何保证开发出的程序是线程安全的?这就要涉及到线程间的同步问题。
再看下面这段代码
if(count>0){
System.out.println(Thread.currentThread().getName()+" count = "+count--);
}
即当一个线程运行到
if(count>0)后,cpu不去执行其他线程中的可能影响当前线程中的下一句代码的执行结果的代码块,必须等到下一句执行完后才能执行其他线程中的有关代码块。这段代码好比一座独木桥,任何时刻都只能有一个人在桥上行走,即程序中不能有多个线程同时这两句代码之间执行,这就是线程同步。
语法如下
......
synchronized(对象){
需要同步的代码;
}
.....
范例2:
public class Test {
public static void main(String args[]){
TestThread t = new TestThread();
Thread mTestThread1 = new Thread(t,"A");
Thread mTestThread2 = new Thread(t,"B");
Thread mTestThread3 = new Thread(t,"C");
mTestThread1.start();
mTestThread2.start();
mTestThread3.start();
}
}
class TestThread implements Runnable{
private int tickets = 20;
public void run(){
String ThreadName = Thread.currentThread().getName();
while(true){
synchronized(this){
if(tickets>0){
try{
Thread.sleep(100);
}catch(Exception e){}
System.out.println(ThreadName+" sales tickets "+tickets--);
}
}
}
}
}
运行结果如下:
B sales tickets 20
B sales tickets 19
B sales tickets 18
B sales tickets 17
B sales tickets 16
B sales tickets 15
B sales tickets 14
B sales tickets 13
B sales tickets 12
B sales tickets 11
B sales tickets 10
B sales tickets 9
B sales tickets 8
B sales tickets 7
B sales tickets 6
B sales tickets 5
B sales tickets 4
B sales tickets 3
B sales tickets 2
B sales tickets 1
由于数据较小,只有线程B,使用大量数据会有多个线程交替
3、同步方法
除了可以对代码块同步,也可以对方法实现同步,只要在需要同步的方法定义前面加上synchronized关键字即可,语法如下
访问控制符
synchronized 返回值类型 方法名称(参数){
.........;
}
范例3:
public class Test {
public static void main(String args[]){
TestThread t = new TestThread();
Thread mTestThread1 = new Thread(t,"A");
Thread mTestThread2 = new Thread(t,"B");
Thread mTestThread3 = new Thread(t,"C");
mTestThread1.start();
mTestThread2.start();
mTestThread3.start();
}
}
class TestThread implements Runnable{
private int tickets = 20;
public void run(){
while(true){
sale();
}
}
public synchronized void sale(){
if(tickets>0){
try{
Thread.sleep(100);
}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+" sales tickets "+tickets--);
}
}
}
编译运行结果与上述同步同步代码块一致,所以说使用同步方法也可以实现线程间的同步
在同一类中,使用
synchronized关键字定义的若干方法,可以在多个线程之间同步,当有一个线程进入有
synchronized修饰的方法时,其他线程就不能进入同一个对象使用
synchronized来修饰所以方法,直到第一个线程执行完它所进入的
synchronized修饰的方法为止。
五、死锁
一旦有多个进程,且他们都要征用对多个锁的独占访问,那么就有可能发生死锁。如果有一组进程或线程,其中每个都在等待一个只有其他进程或线程才可以进行的操作,那么就称他们被死锁了。
最常见的死锁形式是:线程1持有对象A上的锁,而且正在等待对象B上的锁;而线程2
持有对象B上的锁,而且正在等待对象A上的锁。这两个线程拥有不会获得第二个锁,或是释放第1个锁,所以它们只会永远等待下去。
范例1:
class A{
synchronized void funA(B b){
String name = Thread.currentThread().getName();
System.out.println(name+" enter A function");
try{
Thread.sleep(1000);
}catch(Exception e){}
System.out.println(name+" get class B last()");
b.last();
}
synchronized void last(){
System.out.println("class A last()");
}
}
class B{
synchronized void funB(A a){
String name = Thread.currentThread().getName();
System.out.println(name+" enter B function");
try{
Thread.sleep(1000);
}catch(Exception e){}
System.out.println(name+" get class A last()");
a.last();
}
synchronized void last(){
System.out.println("class B last()");
}
}
class Test implements Runnable{
A a = new A();
B b = new B();
Test(){
Thread.currentThread().setName("Main Thread");
Thread t = new Thread(this);
t.start();
a.funA(b);
System.out.println("main thread run finish");
}
public void run(){
Thread.currentThread().setName("Test Thread");
b.funB(a);
System.out.println("other thread run finish");
}
public static void main(String args[]){
new Test();
}
}
运行结果:
Main Thread enter A function
Test Thread enter B function
Main Thread get class B last()
Test Thread get class A last()
从结果来看,main thread进入了a的监视器,并等待b的监视器;同时test thread进入了b的监视器,然后又在等待a的监视器。这个程序永远不会完成。
要避免死锁,应该确保在获取多个锁时,在所有的线程中都以相同的顺序获取锁,所以我们使用同步时一定要注意。
六、线程的状态
线程在一定条件下,状态会发生变化。线程变化的状态转换图如下:
1、新建状态(New):新创建了一个线程对象。
2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。
(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。