多线程
线程
线程是操作系统中能够进行运算调度的最小单位。它被包含在进程中,是进程中的实际运作单位
进程是程序的基本执行实体
多线程可以让程序同时做多件事情,其本质是提高效率
应用场景:想要让多个事情同时运行时(宏观上),可以采用多线程
并发和并行
并发:在同一时刻,有多个指令在单个CPU上交替执行
并行:在同一时刻,有多个指令在多个CPU上同时执行
多线程的实现方式
继承Thread类的方式进行实现
1.自己定义一个类继承Thread
2.重写run方法(线程要执行的任务)
3.创建子类对象,并启动线程
1 | MyThread t1 = new MyThread(); |
实现Runnable接口的方式进行实现
1.自己定义一个类实现Runable接口
2.重写run方法
3.创建子类对象
4.创建一个Thread类的对象,并启动线程
1 | //测试类中的代码 |
注意:此时定义的类并不继承自Thread类,因此不能直接使用Thread中的方法如setName,此时可以在run方法中用Thread.currentThread()来获取当前线程的对象,从而使用Thread中的方法
利用Callable接口和Future接口方式实现
特点:可以获取到多线程运行的结果
1.创建一个类MyCallable实现Callable接口
2.重写call(是有返回值的。表示多线程运行的结果)
3.创建MyCallable的对象(表示多线程要执行的任务)
4.创建FutureTask的对象(作用:管理多线程运行的结果)
5.创建Thread类的对象,并启动(表示线程)
1 | //创建Nycallable的对象(表示多线程要执行的任务) |
多线程三种实现方式对比
多线程中的常见成员方法
1.getName()
如果没有给线程设置名字,线程有默认的名字,
格式为:Thread-X(X是从零开始的序号)
2.setName(String name)
如果要给线程设置名字,可以用setName方法,也可以用构造方法。
但是使用构造方法时要注意,子类继承父类时,不会继承构造方法,
因此要重写构造方法,并且在方法体中使用super调用父类构造方法
3.currentThread()
当JVM虚拟机启动之后,会自动的启动多条线程
其中有一条线程就叫main线程,它的作用是调用main方法,并执行里面的代码
在以前我们写的所有的代码,其实都是运行在main线程当中
4.sleep(long time)
哪条线程执行到这个方法,那么哪条线程就在这里停留对应的时间
方法的参数:就表示睡眠的时间,单位毫秒
当时间到了之后,线程会自动的醒来,继续执行下面的其他代码
5.setPriority(int newPriority)和getPriority()
最小是1,最大是10,默认为5
优先级越高,抢到CPU的概率越高,但不是一定
6.setDaemon(boolean on)
当其他的非守护线程执行完毕以后,守护线程会陆续结束(即使没有执行完毕),但不是立即结束,可能有较短的时间在继续执行
7.yield()
静态方法,用类名调用,相对于单一进程,一般在run()中使用
某线程执行完一轮后,将CPU执行权交出,所有线程重新抢夺,该线程有可能再次得到CPU执行权,该方法可以尽可能使进程间均匀执行
8.join()
相对于进程之间,由某个进程调用,可以使该进程插入到当前线程之前
(如在main线程中调用,则先执行该线程,结束后执行main线程中的其他代码)
线程的生命周期
线程的安全问题
同步代码块
把操作共享数据的代码锁起来
特点:锁默认打开,有一个线程进去了,锁自动关闭,里面的代码全部执行完毕,锁自动打开
1 | //同步代码块 |
同步方法
修饰符 sunchronized 返回值类型 方法名(方法参数){…}
特点:同步方法是锁住方法里面所有的代码,并且锁对象不能自己指定
如果是非静态方法,为this,静态方法是当前类的字节码文件
1 | public void run(){ |
Lock锁
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock
Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作
Lock中提供了获得锁和释放锁的方法,可以手动上锁、手动释放锁
void lock():获得锁
void unlock():释放锁
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化ReentrantLock的构造方法
ReentrantLock():创建一个ReentrantLock的实例
1 | while(true){ |
死锁
在程序中出现了锁的嵌套时,会出现死锁,导致线程都进入循环等待的状态
等待唤醒机制
生产者、消费者问题
常见方法:
这几个方法都是通过锁对象调用(锁在资源类中创建,资源类用来控制生产者和消费者的执行)
阻塞队列
放数据时,放不进去,会等着,即阻塞
取数据时,取不到,会等着,也会阻塞
阻塞队列实现的四个接口:Iterable、Collection、Quene、BlockingQuene
创建阻塞队列的实现类对象:ArrayBlockingQuene、LinkedBlockingQuene
其中前者的底层时数组,有界;后者的底层是链表,无界,但是最大为int的最大值
生产者和消费者必须使用同一个阻塞队列,所以阻塞队列对象应该在测试类中创建,但是生产者和消费者的类中应该给出阻塞队列的定义(但不赋值)
注意take方法的底层也是有锁的,因此不能再自己加锁,否则可能死锁
线程的状态
线程共有七种状态,但java中只定义了除运行外的其他六种状态,这是因为运行时java将线程交给操作系统了,不再管理线程池
原理
1.创建一个池子,池子中是空的
2.提交任务时,池子会(自动)创建新的线程对象,任务执行完毕,线程归还给池子,下回再次提交任务时,不需要创建新的线程,直接复用已有的线程即可
3.如果提交任务时,池子中没有空闲线程,也无法创建新的线程,任务就会排队等待
创建线程池->提交任务->所有任务全部执行完毕,关闭线程池
代码实现
1 | //1.获取线程池对象 |
自定义线程池
当一些任务想要被线程运行时,线程池的分配顺序如下:
核心线程开始运行,剩下的任务进入排队队列,若还有剩下的任务,则进入临时线程(最大线程数量减核心线程数量),若还有剩下的任务,则被拒绝
代码实现:
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor
(核心线程数量,最大线程数量,空闲线程最大存活时间,任务队列,创建线程工厂,任务的拒绝策略);
参数一:核心线程数量 不能小于0
参数二:最大线程数 不能小于等于0.最大数量>=核心线程数量
参数三:空闲线程最大存话时间 不能小于0
参数四:时间单位 用TimeUnit指定
参数五:任务队列 不能为null
参数六:创建线程工厂 不能为null
参数七:任务的拒绝策略 不能为null
1 | ThreadPoolExecutor pool = new ThreadPoolExecutor( |
四种拒绝策略:
最大并行数
四核八线程(超线程技术)的最大并行数就是8
Runtime.getRuntime.().availableProcessors()可以获取可用处理器数目即最大并行数
线程池的大小:
CPU密集型运算:最大并行数+1(+1即替补)
I/O密集型运算:最大并行数* 期望CPU利用率* (总时间即CPU计算时间+等待时间)/CPU计算时间(利用工具thread dump获取数据)