思考一下什么情况下需要使用到多线程,首先是同时处理多个不同的任务,比如视频会议系统,其次是同时处理多个相同的任务,比如上节中实现小球的无序移动,可以创建5000个小球,用5个线程对象分别管理运行其中的1000个即可。除了以上两种情况,还有一种情况为流水线模式,需要用到多线程,比如一个任务的任务链可以分为A,B,C,我们可以创建3个线程对象T1 T2 T3分别负责A B C任务,前提是T2需要等T1执行完后才执行,T3需要等T2执行完成后才执行,举例说明如下。
对于实现流水线模式,首先我们们创建一个任务类Task,类中定义属性public int num=1;然后定义类中的方法taskA、taskB、taskC,分别有不同的方法体。接着对应的我们就要写三个线程,T1、T2和T3。以T1为例,我们用它继承Thread类,然后创建Task类属性t,然后在run方法中使用t进行任务A,同时这里使用内部类,代码为class T1 extends Thread{Task t;public void run(){t.taskA();}},然后同时创建T2线程,并完成对其run方法的重写,代码为
class T2 extends Thread{Task t;public void run(){t.taskB();}},接着创建T3线程分管任务C,代码为class T3 extends Thread{Task t;public void run(){t.taskC();}}。
最后写一个测试类,把主函数放到其中即可,分别创建TI、T2、T3的新对象,使用构造方法把Task类的属性t传进来,然后分别调用start方法即可,然后这里为了保证T2是在T1结束后才执行的,T3是在T2结束后才执行,我们依次使用线程对象调用Thread类中的join方法,确保A,B,C任务的顺序。此时Test类代码如图所示
这样写,有一定几率得到正确结果,但有时候会出现一些问题,所以继续优化代码。首先为了验证三个线程的开始和结束情况,在run方法中加入输出语句。接着设置flag变量作为标志,分别为flag1、flag2和flag3。当T1完成run方法,将flag1变成true,然后在T2的run方法中,完成num取值后更改flag2,最后在T3的run方法中,最后修改flag3的值,然后用flag作为各个线程的启动判定标志。附上此时的详细代码,可以发现加上flag变量后,那么join方法不用也没有关系。
package com.ms.ThreadGameV3;
public class Task {
boolean flag1 =false;
boolean flag2 =false;
boolean flag3 =false;
int num=1;
public void taskA(){
num *=200;
}
public void taskB(){
num/=2;
flag2=true;
}public void taskC(){
num+=600;
flag3=true;
}
}
class T1 extends Thread{
public Task task;
public T1(Task task){
this.task=task;
}
@Override
public void run() {
System.out.println("T1 start");
while(true) {
if (task.flag2==false&& task.flag3==false){
task.taskA();
task.flag1=true;
System.out.println("T1 end");
break;
}
}
}
}
class T2 extends Thread{
public Task task;
public T2(Task task){
this.task=task;
}
@Override
public void run() {
System.out.println("T2 start");
while(true){
if (task.flag1==true){
task.taskB();
task.flag2=true;
System.out.println("T2 end");
break;
}
}
}
}
class T3 extends Thread{
public Task task;
public T3(Task task){
this.task=task;
}
@Override
public void run() {
System.out.println("T3 start");
while(true){
if (task.flag2==true) {
task.taskC();
task.flag3=true;
System.out.println("T3 end");
break;
}
}
}
}
class Test{
public static void main(String[] args) {
Task t =new Task();
T1 t1 = new T1(t);
T2 t2 = new T2(t);
T3 t3 = new T3(t);
t1.run();
t2.run();
t3.run();
System.out.println(t.num);
//这里先写出所有线程对象的run方法也可以,然后依次写出join方法,但有一定几率得到错误答案。
}
}
以上修改后的代码会一直输出正确结果,在这个例子汇总,结果为700,如图所示
以上讲的是流水线的单任务模式,问题来自于操作时各个任务链的顺序不同而导致的问题,即只给了一个Task类的对象,解决这一个数的计算问题,用三个线程跑出得到了结果,接下来我们讲解一下多任务的情况。创建类Task2来说明问题,该类继承Thread,并重写run方法,进行10000次while循环,对num进行叠加。代码如下
package com.ms.ThreadGameV3;
public class Task2 extends Thread{
static int num =0;
@Override
public void run() {
for (int i=0;i<10000;i++){
num++;
}
}
public static void main(String[] args) {
Task2 t1 =new Task2();
Task2 t2 =new Task2();
Task2 t3 =new Task2();
t1.start();
t2.start();
t3.start();
try {
t1.join();
t2.join();
t3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(num);
}
}
如图理应得到结果为30000,但是不是每次都能得到理想结果,这就体现出了问题,这个问题就是线程安全问题,并发情况下,多个线程操作同一资源,引起线程内存中操作出现重复操作。
如图,可能存在多个线程对num的同时复制的问题,这里不加解释的说明一下,这种情况下,多个线程不能同时改变一个变量,比如这里的num。在java语言中,对变量的操作可以分为两种,赋值和使用,两个线程如果对变量都是读那么不会产生线程安全问题,如果两个线程的操作为读写或者写写那么就会出现问题,这两种情况要规避。
为了解决以上问题,提出锁的概念,锁是对资源的一种操作,分为上锁lock和解锁unlock。上锁即为某个线程抢到资源(即本例中的num)后,进行上锁操作,然后其余有需要的线程此时就会不断请求,知道抢到资源的线程使用完后进行解锁操作,如此循环。这样就保证了只有一个线程在使用资源。接下来就在代码中加锁。我们用到synchronized(){},它的参数即为加锁的资源,但num作为基本数据类型变量不时对象,所以我们创建了一个Object类的对象object,锁住了object就是锁住了num,这里理解为object作为一个标识,Task2中如果有其他的资源也会被锁住,然后在方法体中写入num++,即当执行完了这句代码我们就解锁。即当进入synchronized(){}就是上锁,然后执行完方法体后就是解锁。最后就得到了不会出错的结果。附上修改后的代码
package com.ms.ThreadGameV3;
public class Task2 extends Thread{
static int num =0;
static Object object =new Object();
@Override
public void run() {
for (int i=0;i<10000;i++){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (object){
num++;
}
}
}
public static void main(String[] args) {
Task2 t1 =new Task2();
Task2 t2 =new Task2();
Task2 t3 =new Task2();
t1.start();
t2.start();
t3.start();
try {
t1.join();
t2.join();
t3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(num);
}
}



