一个进程正在运行时至少会有1个线程的运行,这种情况在Java中也是存在的。
每一个进程至少有一个线程,这些线程都是在后台默默地执行,比如调用public static void main()的线程就是这样的,它是由JVM创建的。示例如下:
代码清单1 main方法
运行结果如图1所示。
图1 运行结果
控制台输出线程名为main,但是main与mian方法仅仅是名字相同而言。
常见实现多线程编程的方式主要有两种,一种是继承Thread类,另一个是实现Runnable接口。
Thread类的结构如下:public class Thread implements Runnable
从上面的源代码中可以发现,Thread类实现了Runnable接口,它们之间具有多态关系。
使用继承Thread类的方式创建新线程时,最大的局限就是不支持多继承。因为Java语言的特点就是单根继承,所以为了支持多继承,完全可以实现Runnable接口的方式,一边实现一边继承。但用着两种方式创建的线程在工作时的性质是一样的,没有本质区别。
继承Thread类
继承自Thread,重写run方法。
代码清单2 继承Thread类
运行结果如图2所示。
图2 myThread运行结果
从运行结果来看,MyThread.java类中的run方法执行时间比较晚,这也说明在使用多线程技术时,代码的运行结果与代码执行顺序或调用顺序无关。
注意:如果重复调用start方法调用start()方法,则会出现Exception in thread "main" MyThread java.lang.IllegalThreadStateException
异常。
演示线程的随机性,如代码清单3所示。
代码清单3 randomThread
在代码中,为了展现出线程具有随机性,所以使用随机数的形式来使线程得到挂起的效果,从而表现出CPU执行哪个线程具有不确定性。
Thread类中的start()方法通知“线程规划器”此线程已经准备就绪,等待调用线程对象的run()方法。这个过程其实就是让系统安排一个时间来调用Thread中的run方法,也就是使线程得到运行,启动线程,具有异步执行的效果。如果调用代码thread.run()就不是异步执行了,而是同步,那么此线程对象并不交给“线程规划器”来进行处理,而是由main主线来调用run()方法,也就是必须等run()方法中的代码执行完成才可以执行后面的代码。
异步的方法运行效果如图3所示。
图3 随机被执行的线程
另外还需要注意一下,执行start()方法的顺序不代表线程启动的顺序。如代码清单4所示。
代码清单4 RankThread
线程启动顺序与start()执行顺序无关,如下图。
图4 线程启动顺序与start()执行顺序无关
实现Runnable接口
如果要创建的线程类已经有一个父类了,这时就不能再继承自Thread类了,因为Java不支持多继承,所以就需要实现Runnable接口来应对这种情况。
代码清单5 MyRunnable实现Runnable接口
如何使用MyRunnable.Java类呢?这就要看一下Thread.java的构造函数了如图5所示。
图5 Thread构造函数
构造函数Thread(Runnable target)可以传递Runnable接口,说明构造函数支持传入一个Runnable接口的对象。运行类代码如下。
代码清单6 运行类
运行结果如图6所示。
图6 运行结果
构造函数Thread(Runnable target)不光可以传入Runnable接口对象,还可以传入一个Thread类的对象,这样做完全可以将一个Thread对象中的run方法交由其他的线程进行调用。
实例变量与线程安全
自定义线程类中的实例变量针对其他线程可以有共享与不共享之分,这在多个线程之间进行交互时是很重要的一个技术点。
不共享数据情况
不共享数据的情况如图7所示。
图7 不共享数据
代码清单7 不共享情况
运行结果如图8所示,每个线程都有各自的count变量,自己减少自己的count变量的值。这样的情况就是变量不共享,此例并不存在多个线程访问同一个实例变量的情况。
图8 不共享数据运行结果
共享数据的情况
共享数据的情况如图9所示。
图9 共享数据
共享数据就是多个线程可以访问同一个变量,代码如下。
代码清单8 共享情况
共享数据运行结果如图10所示。
图10 共享数据运行结果
从图10可以看到线程D和E打印出的count都是0,说明D和E同时对count进行处理,产生了“非线程安全”问题。而我们想要得到的打印结果却不是重复的,而是依次递减的。
在某些JVM中,i–的操作要分成如下3步:
- 取得原有i值
- 计算i-1
- 对i进行赋值
在这3个步骤中,如果有多个线程同时访问,那么一定会出现非线程安全问题。
典型案例
5个销售员卖同一个货物,必须在每一个销售员卖完后其他销售员才可以在新的剩余物品数上减1操作。这时需要使多个线程之间进行同步,也就是用按顺序排队的方式进行减1操作。更改上面代码可得如下。
代码清单9 SynchronizedThread
共享数据运行结果如图11所示。
图11 共享数据运行结果
通过run方法前加入synchronized关键字,使多个线程在执行run方法时,以排队的方式进行处理。当一个线程调用run前,先判断run方法有么有上锁,如果上锁,说明其他方法正在调用run方法,必须等待其他线程对run方法调用结束后才可以执行run方法。这样也就实现了排队调用run方法的目的,也就达到了按顺序对count变量减1的效果了。synchronized可以在任意对象及方法上加锁,而加锁的这段代码称为“互斥区”或“临界区”。
当一个线程想要执行同步方法里面的代码时,线程首先尝试去拿这把锁,如果能够拿到这把锁,那么这个线程就可以执行synchronized里面的代码。如果不能拿到这把锁,那么这个线程就会不断地尝试拿这把锁,直到能够拿到为止,而且是有多个线程同时去争抢这把锁。
非线程安全
非安全安全主要是多个线程对同一个对象中的同一个实例变量进行操作时会出现值被更改、值不同步的情况,进而影响程序的执行流程。下面为一个如何解决“非线程安全”的问题。
代码清单10 非线程安全
非线程安全运行结果如图12所示。
图12 非线程安全运行结果
解决这个“非线程安全”的方法也就是使用synchronized关键字。代码如代码清单10所示,选用注释掉那部分代码。
图13 排队进入运行结果
留意i–与System.out.println()的异常
本节将通过程序案例细化一下println方法与i++联合使用时“有可能”出现的另外一种异常情况,并说明其原因。
代码清单11 println方法与i++联合
线程运行后根据概率还是会出现非线程安全问题,如图14所示。
图14 出现非线程安全
本实验的测试目的是:虽然println()方法在内部都是同步的,但是i–的操作却是在进入println()之前发生的,所以有发生非线程安全问题的概率,如图15所示。
图15 println内部同步
所以,为了防止发生非线程安全问题,还是应继续使用同步方法。