线程

线程是操作系统中能够进行运算调度的最小单位。它被包含在进程中,是进程中的实际运作单位
进程是程序的基本执行实体
多线程可以让程序同时做多件事情,其本质是提高效率
应用场景:想要让多个事情同时运行时(宏观上),可以采用多线程

并发和并行

并发:在同一时刻,有多个指令在单个CPU上交替执行
并行:在同一时刻,有多个指令在多个CPU上同时执行

多线程的实现方式

继承Thread类的方式进行实现

1.自己定义一个类继承Thread
2.重写run方法(线程要执行的任务)
3.创建子类对象,并启动线程

1
2
MyThread t1 = new MyThread();
t1.start();

实现Runnable接口的方式进行实现

1.自己定义一个类实现Runable接口
2.重写run方法
3.创建子类对象
4.创建一个Thread类的对象,并启动线程

1
2
3
4
5
6
7
8
//测试类中的代码
//创建NyRun的对象
//表示多线程要执行的任务
MyRun mr = new MyRun();
//创建线程对象
Thread t1 = new Thread(mr);
//开启线程
t1.start();

注意:此时定义的类并不继承自Thread类,因此不能直接使用Thread中的方法如setName,此时可以在run方法中用Thread.currentThread()来获取当前线程的对象,从而使用Thread中的方法

利用Callable接口和Future接口方式实现

特点:可以获取到多线程运行的结果
1.创建一个类MyCallable实现Callable接口
2.重写call(是有返回值的。表示多线程运行的结果)
3.创建MyCallable的对象(表示多线程要执行的任务)
4.创建FutureTask的对象(作用:管理多线程运行的结果)
5.创建Thread类的对象,并启动(表示线程)

1
2
3
4
5
6
7
8
9
10
11
//创建Nycallable的对象(表示多线程要执行的任务)
MyCallable mc = new MyCallable();
//创建FutureTask的对象(作用管理多线程运行的结果)
FutureTask<Integer> ft= new FutureTask<>(mc);
//创建线程的对象
Thread t1 = new Thread(ft);
//启动线程
t1.start();
//获取多线程运行的结果
Integer result= ft.get();
System.out.println(result);

多线程三种实现方式对比

图片损坏

多线程中的常见成员方法

图片损坏

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
2
3
4
5
6
//同步代码块
//obj是锁对象,可以是任意的对象,但一定要是唯一的,一般可以用该类的字节码文件(.class)
synchronized(obj){
//被锁起来的代码
...
}

同步方法

修饰符 sunchronized 返回值类型 方法名(方法参数){…}
特点:同步方法是锁住方法里面所有的代码,并且锁对象不能自己指定
如果是非静态方法,为this,静态方法是当前类的字节码文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public void run(){
//1.循环
while (true){
//2.同步代码块(同步方法)
if (method()) break;
}
}

//this
private synchronized boolean method(){
//3.判断共享数据是否到了末尾,如果到了末尾,跳出循环
if (ticket == 100){
return true;
}else {
//4.判断共享数据是否到了末尾,如果没有到末尾,执行代码逻辑
try {
Thread.sleep(10);
}catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(Thread.currentThread().getName() +"在卖第” + ticket +"张票!!!");
}
return false;
}

Lock锁

虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock
Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作
Lock中提供了获得锁和释放锁的方法,可以手动上锁、手动释放锁
void lock():获得锁
void unlock():释放锁
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化ReentrantLock的构造方法
ReentrantLock():创建一个ReentrantLock的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
while(true){
//synchronized(MYThread.class){//同步代码块表述
lock.lock();
try{
if (ticket == 100){
return true;
}else {
//4.判断共享数据是否到了末尾,如果没有到末尾,执行核心逻辑
Thread.sleep(10);
ticket++;
System.out.println(Thread.currentThread().getName() +"在卖第” + ticket +"张票!!!");
}
}catch(InterruptedException e){
e.printStackTrace();
}finally{
//释放锁一定要运行,放在finally中
lock.unlock();
}
}

死锁

在程序中出现了锁的嵌套时,会出现死锁,导致线程都进入循环等待的状态

等待唤醒机制

生产者、消费者问题

常见方法:
图片损坏
这几个方法都是通过锁对象调用(锁在资源类中创建,资源类用来控制生产者和消费者的执行)

阻塞队列

放数据时,放不进去,会等着,即阻塞
取数据时,取不到,会等着,也会阻塞
阻塞队列实现的四个接口:Iterable、Collection、Quene、BlockingQuene
创建阻塞队列的实现类对象:ArrayBlockingQuene、LinkedBlockingQuene
其中前者的底层时数组,有界;后者的底层是链表,无界,但是最大为int的最大值
生产者和消费者必须使用同一个阻塞队列,所以阻塞队列对象应该在测试类中创建,但是生产者和消费者的类中应该给出阻塞队列的定义(但不赋值)
注意take方法的底层也是有锁的,因此不能再自己加锁,否则可能死锁

线程的状态

图片损坏 线程共有七种状态,但java中只定义了除运行外的其他六种状态,这是因为运行时java将线程交给操作系统了,不再管理

线程池

原理

1.创建一个池子,池子中是空的
2.提交任务时,池子会(自动)创建新的线程对象,任务执行完毕,线程归还给池子,下回再次提交任务时,不需要创建新的线程,直接复用已有的线程即可
3.如果提交任务时,池子中没有空闲线程,也无法创建新的线程,任务就会排队等待
创建线程池->提交任务->所有任务全部执行完毕,关闭线程池

代码实现

图片损坏
1
2
3
4
5
6
7
//1.获取线程池对象
ExecutorService pool1 = Executors.newCachedThreadPool();
//2.提交任务
//在MyRunable类中的run方法写任务
pool1.submit(new MyRunnable());
//3.销毁线程池
pool1.shutdown();

自定义线程池

当一些任务想要被线程运行时,线程池的分配顺序如下:
核心线程开始运行,剩下的任务进入排队队列,若还有剩下的任务,则进入临时线程(最大线程数量减核心线程数量),若还有剩下的任务,则被拒绝
代码实现:
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor
(核心线程数量,最大线程数量,空闲线程最大存活时间,任务队列,创建线程工厂,任务的拒绝策略);
参数一:核心线程数量 不能小于0
参数二:最大线程数 不能小于等于0.最大数量>=核心线程数量
参数三:空闲线程最大存话时间 不能小于0
参数四:时间单位 用TimeUnit指定
参数五:任务队列 不能为null
参数六:创建线程工厂 不能为null
参数七:任务的拒绝策略 不能为null

1
2
3
4
5
6
7
8
9
10
ThreadPoolExecutor pool = new ThreadPoolExecutor(
3, //核心线程数量。不能小于0
6//最大线程数,不能小于0.最大数量>=核心线程数量
60, //空闲线程最大存活时间
TimeUnit.SECONDS , //时间单位
new ArrayBlockingQueue<>(3), //任务队列
Executors.defaultThreadFactory(),//创建线程工厂
new ThreadPoolExecutor.AbortPolicyo(),//任务的拒绝策略
);
//任务的拒绝策略是ThreadPoolExecutor的静态内部类,这是因为拒绝策略为线程池服务,并且是一个独立的个体

四种拒绝策略:
图片损坏

最大并行数

四核八线程(超线程技术)的最大并行数就是8
Runtime.getRuntime.().availableProcessors()可以获取可用处理器数目即最大并行数
线程池的大小:
CPU密集型运算:最大并行数+1(+1即替补)
I/O密集型运算:最大并行数* 期望CPU利用率* (总时间即CPU计算时间+等待时间)/CPU计算时间(利用工具thread dump获取数据)