多线程编程
- 写在前面
在描述多线程程序之前,首先回顾一下我们经常写的单线程程序。做一件事,计算一个等式,或者描述一个算法,通常都是在单线程的情况下执行的,它们的效率十分的固定并且只要规则恰当输入和输出都是符合恒等式的。引入多线程的原因个人理解大概是因为人类想把做一件事的空闲时间去做另外一件事,以达到提高效率的效果。比如我煮饭的过程中可以炒菜,可以炖汤,在人力劳动固定的情况下提高生产的效率,这大概就是多线程的意义了。
- 多线程编程总纲
多线程编程的基本元素是线程,一个程序算是上层容器的一个进程,进程里面可以开启多个线程进行计算和完成IO。所以对于基本元素线程的研究大概会有:线程的状态、线程的优先级、对临界区资源的操作。多线程与操作系统的进程调度是很类似的,在单核机器上体现为对时间片的抢占。单核机器不停切换执行多线程任务的指令代码,看似一次并行计算,其实只是单核切换执行多个线程任务。由于上述背景,线程的状态必须具备:预备执行、正在执行、挂起、等待、执行终了。
所有的行为都具有一定的代价消耗,多线程在新建线程的时候必然会消耗系统资源(内存),与数据库的链接类似,频繁的创建销毁线程十分影响程序的性能,所以对线程的复用是必然的,因此线程池作为线程复用工具处于十分重要的地位。
在多任务协作的环境下,必然涉及到对临界区资源的操作,有时候这个线程要读取信息,另外一个线程要写入信息,在写入还未结束的时候信息被读取,则可能就得到了不合理的信息。所以线程间的协作,主要体现在同步和锁这方面技术上。
多线程的意义,主要体现在如今的多线程协作甚至是分布式上。对于一时刻多条请求进入请求队列中,在队列头处理出队请求并返回给客户这样的模型中,因为客户请求的内容有所区别所以会有中间件负责请求的转发,采用生产者消费者模型设计的程序就是一个协作的例子。请求由中间件分发到各个处理中心,然后返回中间件最后返回到客户端,每个线程完成一次这样的任务,然后回到线程池继续处理下一次客户的请求,由此可见线程作为执行任务的基本元素,处于非常关键的地位。在另一个场景中,某张数据库表由多个线程操作,如果放任线程的执行必然会导致数据损坏。要想保持数据的一致性,则需要对线程操作的排他性进行约束,比如读写锁,在请求队列中读与读是不阻塞的,因为它们并不会损坏数据,而读和写或者写和读为相邻请求的队列中,则必须有阻塞功能,读取的时候加锁,读取完成之后才能进行写入,反之亦然。
多线程运算模型的拓展,就如现在多机合作,集群合作这些类似的场景。多个机子如何处理数据,比如一个用户注册,邮箱验证中心和手机验证中心是两个集群,在用户输入完信息之后进入缓冲区,中间件会把数据拆分转发,或者不拆分转发给各个中心,最后汇总成反馈信息返回客户端。
-
多线程编程的基本语法
Java语言实现多线程的方式就是它提供了Thread类和Runnable接口,继承类或者实现接口并重写run方法就可以实现一个线程程序。
package Base; /** * 基础多线程方法 * @author ctk * */ public class BaseMethod implements Runnable{ @Override public void run() { } public static void main(String[] args) { BaseMethod b = new BaseMethod(); Thread t1 = new Thread(b); t1.start(); } }
package Base; /** * 基础多线程方法 * @author ctk * */ public class BaseMethod extends Thread{ @Override public void run() { } public static void main(String[] args) { BaseMethod b = new BaseMethod(); b.start(); } }
C/C++提供了一个pthread.h的头文件,并提供了创建线程的方法。
pthread_create(<#pthread_t _Nullable * _Nonnull#>, <#const pthread_attr_t * _Nullable#>, <#void * _Nullable (* _Nonnull)(void * _Nullable)#>, <#void * _Nullable#>)
// // ThreadDemo.cpp // ForPractice // // Created by MacBook on 2017/3/14. // Copyright © 2017年 MacBook. All rights reserved. // #include "ThreadDemo.hpp" void *thread1(void *ptr){ int temp = *(int *)ptr; for(int i=0;i<3;i++){ sleep(1); printf("this is a pthread :%d\n",temp); } *(int *)ptr = *(int *)ptr - 1; return ptr; } void *thread2(void *ptr){ int temp = *(int *)ptr; for(int i=0;i<3;i++){ sleep(1); printf("this is a pthread :%d\n",temp); } *(int *)ptr = *(int *)ptr - 1; return ptr; } void testDemo(){ pthread_t id1; pthread_t id2; int temp1 = 5; int temp2 = 6; void *id1return; void *id2return; //创建线程:线程地址、线程属性、运行函数run、传入run的参数 int ret1 = pthread_create(&id1, NULL,thread1,&temp1); int ret2 = pthread_create(&id2, NULL,thread2,&temp2); if(ret1){ printf("Create pthread error!\n"); return ; } if(ret2){ printf("Create pthread error!\n"); return ; } for(int i=0;i<2;i++){ printf("This is the main process.\n"); sleep(1); } //等待id执行完再结束 执行完后id将会被回收 第二个参数是返回值 int wait1 = pthread_join(id1, &id1return); int wait2 = pthread_join(id2, &id2return); int *r1 = (int*)id1return; int *r2 = (int*)id2return; if(wait1 == 0 && wait2 == 0) printf("the result in the end id1:%d,id2:%d\n",*r1,*r2); }
与Java很类似,不过C++可以自己定义线程执行函数并传入函数指针,执行函数可以传入参数进行计算。对于C++的多线程编程更类似于一种任务,Java更具有面向对象的意味。
- Java多线程的一些方法
打开Thread类的JDK源码,里面规定了线程有可能产生的行为,除了getter/setter和构造函数之外,就是线程将会执行的行为。其中比较常用的是:start(开始线程执行),sleep(休眠,不释放资源),wait(暂停,释放锁和对象监视器),notify(唤醒具有同一个锁的线程),interrupt(中断线程,释放资源),join(等待此线程执行完毕,再执行下一条语句),yeild(让出时间片进入竞争队列,很大几率再次获得时间片的占有权),suspend(挂起,不会释放监视器),stop(停止线程)。
通常,在run方法中使用sleep来模拟任务执行的时间,有些场景也需要使用sleep来设计程序。
@Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }
package Base; /** * 中断线程 * @author ctk * 调用interrupt方法会把线程的锁顺带释放 */ public class InterruptSample implements Runnable{ public static Object obj = new Object(); @Override public void run(){ synchronized (obj) { try{ System.out.println("I am "+Thread.currentThread().getName()); Thread.sleep(4000); }catch (Exception e) { System.out.println("Interrupt when sleep"); Thread.currentThread().interrupt(); } } } public static void main(String[] args) { InterruptSample i1 = new InterruptSample(); Thread t1 = new Thread(i1); Thread t2 = new Thread(i1); t1.start();t2.start(); t1.interrupt(); } }
在中断之前,可以在run方法判断是否已中断,从而设计平稳结束的线程程序。
package Base; //中断线程 /** * @author ctk * interrupt函数设置了一个信号 * 需要在run逻辑里面得到当前线程是否需要中断的逻辑 */ public class InterruptThread implements Runnable{ public static int i=0; @Override public void run() { while(true){ if(Thread.currentThread().isInterrupted()) break; System.out.println(i++); Thread.yield(); } } public static void main(String[] args) throws InterruptedException { Thread t = new Thread(new InterruptThread()); t.start(); System.out.println("中断前"); Thread.sleep(2000); System.out.println("中断之后"); t.interrupt(); } }
使用join方法,是阻塞在这行代码上,直到调用join的这个线程对象执行完毕之后,才会执行下一句代码。
package Base; /** * join方法demo * yield方法 当前线程让出cpu占有权 * @author ctk * */ public class JoinSample { public volatile static int i = 0; public static class AddThread extends Thread{ @Override public void run(){ for(i=0;i<100000;i++); } } public static void main(String[] args) throws InterruptedException { AddThread add = new AddThread(); add.start(); add.join(); System.out.println(i); } }
如果去掉join,则会打印小于100000的数字。start方法的理解可以与join相反,执行到start的代码之后,程序就不管它的结果直接执行下一句代码了。
除此之外,线程还和数据库一样,有自己的id和name属性,在复杂业务场景中,为了查清楚哪个业务发生了问题,最好在新建线程的同时给他附上名字。
- 原子操作
原子操作表示一个操作不具有可分解性,比如1+1是一个原子操作,而(1+1)*2则是一个可分解的操作。原子操作表示这个操作执行不会受到别的操作的影响,在声明变量为volatile之后,告诉编译器要格外小心这个变量,每次操作这个变量的时候都要去读取它的值。
package Base; /** * 多个线程操作临界区资源 * @author ctk * 不加锁的时候两个线程都能取到i 虽然都进行了++,但是存入的时候很大可能两个线程把相同结果存入i中。 */ public class AcountingVol implements Runnable{ static AcountingVol instance = new AcountingVol(); static volatile int i = 0; public static void increase(){ i++; } @Override public void run() { for(int j=0;j<100000;j++) //加锁之后只有得到对象的锁才能操作i synchronized (instance) { increase(); } } public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(instance); Thread t2 = new Thread(instance); t1.start();t2.start(); t1.join(); t2.join(); System.out.println(i); } }
- 一个简单的线程池Demo
线程池的意义如前面所述,它启动的时候创建好设置的线程数目,线程执行完回收到线程池而不会释放掉,等待下一次调用,由此来实现线程的复用,节省系统由于创建销毁线程而产生的开销。
package ThreadPool; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * 线程池demo * @author ctk * */ public class ThreadPoolDemo { public static class MyTask implements Runnable{ @Override public void run() { System.out.println(System.currentTimeMillis() + ":Thread ID " + Thread.currentThread().getId()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { MyTask task = new MyTask(); ExecutorService es = Executors.newFixedThreadPool(5); for (int i = 0; i < 10; i++) { es.submit(task); } } }
- 结语
对于多线程计算模型,由小见大,工业界的设计都是此等计算模型,集群、分布式都是讲求协作,为的是提高效率和保持数据一致性,并且具有一定的可拓展性。多线程的水还很深,笔者这能慢慢潜入。在concurrent包下的工具类,可以慢慢的研究了。