更新时间:2020/7/13 17:32,更新了锁
更新时间:2020/7/12 22:29,更新原理解读和线程池
更新时间:2020/7/11 22:35,更新了入门系列
记录java多线程学习的相关知识,本文会持续更新,不断地扩充
本文仅为记录学习轨迹,如有侵权,联系删除
下面主要介绍几种多线程的创建方式,列举了3种,实际上是4种,另一种这里就不介绍了
package com.zsc;
public class Thread01 extends Thread {
private String name;
public Thread01(String name){
this.name = name;
}
/**
* run()方法,作为线程 的操作主体
*/
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(this.name+": "+i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class ThreadDome01
{
public static void main(String[] args) {
Thread01 t1 = new Thread01("线程1");
Thread01 t2 = new Thread01("线程2");
Thread01 t3 = new Thread01("线程3");
//通过start调用线程主体,其中线程主体是run方法
t1.start();
t2.start();
t3.start();
}
}
package com.zsc;
public class Thread02 implements Runnable {
private String name;
public Thread02(String name){
this.name = name;
}
/**
* 实训run()方法,作为线程 的操作主体
*/
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(this.name+": "+i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class ThreadDome02
{
public static void main(String[] args) {
Thread01 t01 = new Thread01("线程1");
Thread01 t02 = new Thread01("线程2");
Thread01 t03 = new Thread01("线程3");
Thread t1 = new Thread(t01);
Thread t2 = new Thread(t02);
Thread t3 = new Thread(t03);
//通过start调用线程主体,其中线程主体是run方法
t1.start();
t2.start();
t3.start();
}
}
注意:线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。
Java通过Executors提供四种线程池,分别为:
线程池 | 说明 |
---|---|
newSingleThreadExecutor | 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。 |
newFixedThreadPool | 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。 |
newScheduledThreadPool | 创建一个可定期或者延时执行任务的定长线程池,支持定时及周期性任务执行。 |
newCachedThreadPool | 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。 |
newFixedThreadPool
package com.zsc.thread03;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* newFixedThreadPool
* 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
*/
public class ThreadPool01 {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 50; i++) {
executorService.execute( new MyThread01("线程"+i));
}
executorService.shutdown();
}
}
class MyThread01 implements Runnable
{
private String flag;
public MyThread01(String flag){
this.flag = flag;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+": "+this.flag);
//while (true){}
}
}
运行结果
可以看到这里代码设置了线程池的数量为3,所以运行结果中只有3个线程,pool-1-thread-1到pool-1-thread-3,超出的线程会在队列中等待,直到线程池中有空闲线程后才会给到队列中的线程,如果这3个线程一直在忙的话,其余超出的线程只能一直在队列中等待,直到线程池中有空闲线程为止,下面模拟线程池的线程一直在忙的情况。
可以看到当线程池里面的所有线程都在忙的情况下,其余超出的线程只能一直等待,直到线程池有空闲线程为止。
newCachedThreadPool
package com.zsc.thread03;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* newSingleThreadExecutor
* 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
*/
public class ThreadPool03 {
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
executorService.execute( new MyThread03("标志"+i));
}
executorService.shutdown();
}
}
class MyThread03 implements Runnable
{
private String flag;
public MyThread03(String flag){
this.flag = flag;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+": "+this.flag+"="+i);
}
}
}
运行结果
可以看到至始至终只有pool-1-thread-1这一个线程,而且保证了顺序执行
newScheduledThreadPool
package com.zsc.thread03;
import java.util.Calendar;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* newScheduledThreadPool
* 创建一个定长线程池,支持定时及周期性任务执行。
*/
public class ThreadPool04 {
public static void main(String[] args) {
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
scheduledExecutorService.schedule( new MyThread04("5秒后执行该线程"),5,TimeUnit.SECONDS);
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
executorService.execute( new MyThread04("线程"+i));
}
scheduledExecutorService.scheduleAtFixedRate( new MyThread04("5秒后执行该线程,以后每隔两秒执行一次"),5,2,TimeUnit.SECONDS);
}
}
class MyThread04 implements Runnable
{
private String flag;
public MyThread04(String flag){
this.flag = flag;
}
@Override
public void run() {
System.out.println(Calendar.getInstance().getTime()+": "+Thread.currentThread().getName()+": "+this.flag);
}
}
首先来看Thread类和和Runnable接口,查看Thread类
该类也是继承了Runnable接口,接着往下看,我们知道在创建完线程后,调用的是start()方法,然后该方法会调用run()方法,我们创建完线程后,一些业务的逻辑都是在重写的run方法里面的实现的
查看start()方法后发现里面并没有调用run()方法,但有start0()方法
然而start0()方法并看不到实现的方法,可以注意到返回值是native,关于native关键字有必要说一下,被native关键字修饰的方法叫做本地方法,本地方法和其它方法不一样,本地方法意味着和平台有关,因此使用了native的程序可移植性都不太高。另外native方法在JVM中运行时数据区也和其它方法不一样,它有专门的本地方法栈。native方法主要用于加载文件和动态链接库,由于Java语言无法访问操作系统底层信息(比如:底层硬件设备等),这时候就需要借助C/C++来完成了。被native修饰的方法可以被C语言重写。
所以得出结论就是被native修饰的start0()方法底层实现看不了,因为创建线程是计算机底层的事情,而java是不能直接调用底层的资源的,但是C++可以直接调用,于是用native修饰,表示java通过C++调用底层资源,然后再调用run()方法,这个过程具体的实现是看不到的,所以在start()方法里面看不到调用run方法,OK,理清了start方法后,重点就应该看run()方法了
run()方法也简单,其中涉及到Runnable接口,我们知道第一种多线程是需要重写run()方法,那第二种创建方式呢,这其中有点东西需要讲清除,继承Thread重写run()方法是一种方式,还有就是实现接口Runnable,实现接口里面的run()方法;这就是区别,一个是重写,一个是实现。
所以看run()方法的代码实现就知道,实现接口Runnable的方式创建多线程的方式中,当我们实现了Runnable接口并实现了run方法后,调用的是Runnable接口中被实现的run方法。
这其中会引出一个问题:Runnable可以用来在多线程间共享对象,而Thread不能共享对象
这个问题有的人持反对意见,有的人则赞同,这里我引用一个大佬的回答
到处为止,关于入门系列的前两种多线程创建的部分源码解析就到这里。
在入门系列中,简单介绍了如何使用4种线程池,下面则是对线程池的进行深入的解读,包括一些线程池常见的一些问题讨论。首先需要明确下面的几个问题。
什么是线程池
线程池其实是一种多线程处理形式,也可以看成是多个线程的集合,将任务(线程)添加到队列中,然后在创建线程后自动启动这些任务。比如把线程池看成一个容器,集中管理线程。线程使用完不会销毁,会先储存在线程池中。
线程池的作用
(1)线程是稀缺资源,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以重复使用。
(2)提高线程的可管理性。所有的线程都在线程池里面,方便对线程的管理。
(3)提高性能,避免线程过多导致内存不足,出现oom的情况
总结:使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或 者“过度切换”的问题。
队列
队列是常见的一种数据结构,在线程池里面的核心参数中有涉及到阻塞队列,主要有以下这些队列
SynchronousQueue
直接提交,不会保存任何任务,一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。
LinkedBlockingQueue
基于链表的无界阻塞队列(其实最大容量为Interger.MAX),对于新加来的任务全部存入队列中,量大可能会导致oom,按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到maxPoolSize,因此使用该工作队列时,参数maxPoolSize其实是不起作用的。
ArrayBlockingQueue
基于数组的有界阻塞队列,队列有一个最大值,超过最大值的任务交给拒绝策略处理,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。
PriorityBlockingQueue
具有优先级的无界阻塞队列,优先级通过参数Comparator实现。
拒绝策略
当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢。这里的拒绝策略,就是解决这个问题
AbortPolicy
该策略下,当线程池中的数量等于最大线程数时对于新传入的任务,直接丢弃任务,并抛出RejectedExecutionException异常。
DiscardPolicy
该策略下,当线程池中的数量等于最大线程数时直接丢弃任务,什么都不做。
CallerRunsPolicy
该策略下,在调用者线程中直接执行被拒绝任务的run方法,除非线程池已经shutdown,则直接抛弃任务。
DiscardOldestPolicy
该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列
注意:现在线程池默认的是第一种拒绝策略,直接抛异常
点进去看源码,发现这几种线程池都是new的ThreadPoolExecutor类,里面的参数就是核心参数,应该有7个核心参数,具体可以查看ThreadPoolExecutor类
进入ThreadPoolExecutor可以看到最多的里面有7个核心参数,上面都有相应的英语注释
参数 | 说明 |
---|---|
corePoolSize (核心线程数量) |
表示核心线程数量,线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会 被销毁,除非设置了allowCoreThreadTimeOut。这里的最小线程数量即是corePoolSize。 |
maximumPoolSize (线程池最大数量) |
表示线程池的最大数量,一个任务被提交到线程池后,首先会缓存到工作队列(后面会介绍)中,如果工作队列满了,则会创建一个新线程,然后从工作队列中的取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize来指定。 |
keepAliveTime (空闲线程存活时间) |
表示空闲线程存活时间,一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定 |
unit (空间线程存活时间单位) |
表示空间线程存活时间单位,keepAliveTime的计量单位 |
workQueue (阻塞队列) |
表示工作队列,或者叫阻塞队列,新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列, |
threadFactory (线程工厂) |
表示一个线程工厂, |
handler (拒绝策略) |
当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢。这里的拒绝策略,就是解决这个问题的,jdk中提供了4中拒绝策略 |
关于阻塞队列和拒绝策略上面已经有介绍了,就不再展开了。
ThreadFactory
关于线程工程需要学习一下,ThreadFactory是一个接口,里面只有一个newThread方法用来创建线程
在Executor这个类中,我们可以发现,里面有一个类是实现了线程工厂这个接口,我们可以简单想一下,既然Executor可以用来通过new ThreadPoolExecutor的方式来创建线程的话,里面必然要实现ThreadFactory接口,因为它是ThreadPoolExecutor里面的一个核心参数
后面需要自定义线程池的时候可以从这里直接copy,定制属于自己规范的线程池。
RejectedExecutionHandler
拒绝策略定义在ThreadPoolExecutor这个类中,需要可以自己从这个类中获取
首先是为什么要自定义线程池,使用Executors来创建线程池不是挺方便的吗,这个时候可以看一下阿里的开发手册
所以,我们需要自己去自定义线程池,下面给出两种自定义线程池的代码
package com.zsc.thread03;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class CustomizeThreadPool {
public static void main(String[] args) {
// MyThreadFactory1Demo();
MyThreadFactory2Demo();
}
//自定义线程池1
static void MyThreadFactory1Demo(){
ExecutorService pool = new ThreadPoolExecutor(
5, //核心线程数量
10, //最大线程数量
60, //存活时间
TimeUnit.SECONDS, //时间单位
new LinkedBlockingQueue<Runnable>(100), //阻塞队列,这里限制队列长度100
new MyThreadFactory1(), //线程池工厂
new ThreadPoolExecutor.AbortPolicy() //拒绝策略
);
for (int i = 0; i < 20; i++) {
pool.execute(() -> {
System.out.println(Thread.currentThread().getName() + ": hello");
});
}
}
//自定义线程池2
static void MyThreadFactory2Demo(){
ExecutorService pool = new ThreadPoolExecutor(
5, //核心线程数量
10, //最大线程数量
60, //存活时间
TimeUnit.SECONDS, //时间单位
new LinkedBlockingQueue<Runnable>(100), //阻塞队列,这里限制队列长度100
new MyThreadFactory2(new ThreadGroup("线程组1"),"商品线程"), //线程池工厂
new ThreadPoolExecutor.AbortPolicy() //拒绝策略
);
for (int i = 0; i < 20; i++) {
pool.execute(() -> {
System.out.println(Thread.currentThread().getName() + ": hello");
});
}
}
}
//自定义线程池工厂方式1:直接复制Exceutors类的默认线程池工厂即可
class MyThreadFactory1 implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
MyThreadFactory1() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
//自定义线程池工厂方式2:定制自己的线程池工厂
class MyThreadFactory2 implements ThreadFactory {
private AtomicInteger number = new AtomicInteger(0);
private ThreadGroup group;
private String namePrefix;
public MyThreadFactory2(ThreadGroup group, String namePrefix) {
this.group = group;
this.namePrefix = namePrefix;
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r, namePrefix + "" + number.getAndIncrement());
if (t.isDaemon()) {
t.setDaemon(false);
}
if (t.getPriority() != Thread.NORM_PRIORITY) {
t.setPriority(Thread.NORM_PRIORITY);
}
return t;
}
}
开发的时候需要用到多线程的话,可以自己定义线程池工厂,定义线程的名字,方便错误的排查。
多线程在运行时会出现一些线程安全问题,主要访问了共享变量导致的线程安全问题,以最常见的买票为例
package com.zsc.security;
import java.util.concurrent.ThreadFactory;
/**
* 买票案例
*/
public class SecurityQuestion01 {
public static void main(String[] args) {
SellTicket s = new SellTicket();
//4个线程同时跑
new Thread(s, "窗口1").start();
new Thread(s, "窗口2").start();
new Thread(s, "窗口3").start();
new Thread(s, "窗口4").start();
}
}
class SellTicket implements Runnable {
//10张票
private int ticketNum = 10;
@Override
public void run() {
while (ticketNum > 0) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖出一张票,票号为" + ticketNum);
ticketNum--;
}
}
}
运行结果
不仅出现了重复卖票的问题,还出现了-1和-2的问题,这就是线程安全问题,解决的方法主要采用锁机制。
synchronized是Java 所提供的一种内置的互斥锁, 主要的使用方式有两种:synchronized方法(同步方法)和synchronized块(同步块)
synchronized方法
在涉及到共享数据的代码,即临界区所在的方法加上synchronized关键字,此时该方法所对应的类的整个实例对象都会被锁住,即synchronized(this)
运行结果
synchronized块
同步块的使用方式也简单:synchronized(Obj){ 临界区 }
其中Obj叫做同步监视器,它可以是任何对象,不过建议将共享资源作为Obj
运行结果
从jdk5.0开始,java提供了更强大的线程同步机制,显示的定义同步锁,对象是Lock,它的实现类是ReentrantLock,一般也是通过该类实现代码同步,操作有加锁和解锁。
运行结果