线程与进程
线程:操作系统能够运行调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
进程:程序的基本执行实体。一般而言,一个正在运行的软件就是一个进程。
例如:在一个运行中的杀毒软件中,该软件可以看成一个进程,软件内的不同功能可以视作不同的线程。
多线程程序:cpu可以在多个程序之间来回切换,把等待的时间充分利用起来,提高运行的效率。
应用场景:拷贝、迁移大文件,加载大量资源文件等等
并发和并行
并发:在同一时刻,有多个指令在单个CPU上交替执行。
并行:在同一时刻,有多个指令在多个CPU上同时执行。
我的理解:笔记本内只含有一个CPU,但是不同的CPU有不同的核心数,例如2核4线程、8核16线程等,CPU线程的数量表示最多同时并行的线程数量。当程序的线程多于CPU所支持的最大线程数时,CPU的核心会在所有线程中不断切换,即并发。
多线程的实现方式
①继承Thread类的方式进行实现:
a.自己定义一个类继承Thread,重写run方法
public class MyThread extends Thread{
@Override
public void run(){
//书写线程要执行代码
for (int i = 0; i < 100; i++) {
//因为MyThread是继承Thread的,所以可以直接使用getName方法获取线程名
System.out.println(getName() + "Hello");
}
}
}
b.创建子类的对象,并启动线程
public class 多线程的实现_thread {
public static void main(String[] args) {
/*第一种启动方法
* 1.自己定义一个类继承Thread
* 2.重写run方法
* 3.创建子类的对象,并启动线程
* */
MyThread t1 = new MyThread();
//开启线程,注意要使用start方法,会自动执行类里面的run方法。
t1.setName("线程1");
t1.start();
//当使用多线程的时候,要给个线程起名避免出现认不清的情况
MyThread t2 = new MyThread();
t2.setName("线程2");
t2.start();
}
}
②实现Runnable类的方式进行实现:
a.自己定义一个类实现Runnable接口,然后重写runn方法,要注意getName的使用方法
public class MyRun implements Runnable{
@Override
public void run(){
for (int i = 0; i < 100; i++) {
//获取到当前线程的对象
//当前方法线程的对象是谁,就以谁作为改对象作为返回值
Thread t = Thread.currentThread();
//不能直接使用getName方法的原因是:Runnable接口里面没有getName方法
System.out.println(t.getName()+"Hello2");
}
}
}
b.创建MyRun对象,表示多线程要执行的任务,同时创建一个Thread对象,然后start线程
public class 多线程的实现_Runnable {
public static void main(String[] args) {
/*第二种启动方法
* 1.自己定义一个类实现Runnable接口
* 2.重写run方法
* 3.创建自己的类的对象,并启动线程
* 4.创建一个Thread类的对象,并开启线程
* */
//创建MyRun对象,表示多线程要执行的任务
MyRun mr = new MyRun();
//创建线程对象,需要将创建好的对象作为参数传入Thread
//否则会使用Thread默认的run方法实例化对象
Thread t1 = new Thread(mr);
//给线程设置名字
t1.setName("线程1");
//开启线程
t1.start();
//设置线程2
Thread t2 = new Thread(mr);
t2.setName("线程2");
t2.start();
}
}
③利用Callable接口和Future接口方式实现:
a.创建一个类MyCallable实现Callable接口,然后重写call方法
public class MyCallable implements Callable<Integer> {
//这里的泛型表示结果的类型
@Override
public Integer call() throws Exception {
//求1~100之间的和
int sum = 0;
for (int i = 0; i <= 100; i++) {
sum = sum + i;
}
return sum;
}
}
b.创建MyCallable的对象,用来表示多线程要执行的任务,再创建一个FutureTask对象来管理多线程运行的结果,最后创建Thread类的对象来启动线程
public class 多线程的实现_callable {
public static void main(String[] args) throws ExecutionException, InterruptedException {
/*第三种启动方法
* 特点:可以获取到多线程运行的结果
*
* 1.创建一个类MyCallable实现Callable接口
* 2.重写call方法(存在返回值,表示多线程的运行结果)
* 3.创建MyCallable的对象(表示多线程要执行的任务)
* 4.创建FutureTask的对象(作用:管理多线程运行的结果)
* 5.创建Thread类的对象,并启动线程
* */
//创建MyCallable的对象(表示多线程要执行的任务)
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);
}
}
总结
①继承Thread类
优点:编程简单,可以直接使用Thread类中的方法
缺点:扩展性较差,不能再继承其他的类
②实现Runnable接口和Callable接口
优点:扩展性强,实现接口的同时还可以继承其他的类
缺点:编程相对复杂,不能直接使用Thread类中的方法
注:三种方法中只有Callable接口可以获取线程运行结果,其他都不能
创建一个线程的步骤:1.写一个循环 2.同步代码块 3.判断共享数据是否到了末尾 4.到了,结束,未到,执行核心代码
线程的常见成员方法:
String getName():返回此线程的名称(若没setName,则返回默认名称Thread-num)
Void setName(String name):设置线程的名字(构造方法也可以设置名字)
static Thread currentThread():获取当前线程的对象(在哪个线程调用这个方法,就返回对应线程的对象)
static void sleep(long time):让线程休眠指定的时间,单位为毫秒
setPriority(int newPriority):设置线程的优先级
final int getPriority():获取线程的优先级
final void setDaemon(boolean on):设置为守护线程
public static void yield():出让/礼让线程
public static void jion():插入线程
细节:
①main也是一个线程,称为主线程
②线程的优先级不是绝对的,高优先级的线程不一定每次都能首先执行结束
③当其他非守护线程执行完毕之后,守护线程会陆续结束。如:聊天窗口为线程1,传输文件为线程2,且线程2为守护线程,当聊天窗口关闭时,即线程1结束时,传输文件就没有必要了,因此线程2也随之结束
④当前线程执行到出让线程方法时,会让出当前CPU的执行权,重新与其他线程进行CPU执行权的争抢。(该方法影响的时线程占用CPU执行权的时间)
⑤线程1在线程2的代码中执行了插入线程,即线程1作为插入线程,线程2作为被插入线程,此时线程2会暂停,直到线程1全部执行完毕后,才会继续执行。
线程的生命周期
①线程调用了start方法时,该线程具有执行资格(抢CPU执行权的资格),但是还没有执行权,只有抢到CPU的执行权,该线程才会运行。(调用sleep方法或者被其他阻塞式方法时会暂时剥夺该线程的执行资格,无法抢夺CPU执行权)
②当线程执行完run方法时,线程死亡(结束),变成垃圾。
线程的安全问题
小知识:类里面定义变量时,前面加个static,表示所有类共享这个变量。
同步代码块
格式:
static Object obj = new Object();
public void run(){
synchronized(boj){
//代码体
}
}
特点:
①锁默认打开,有一个线程进去了,锁自动关闭
②里面的代码全部执行完毕,线程出来,锁自动打开
③synchronized写在循环里面,否则容易出现一个线程
④尽可能使用同一把锁
同步方法:把修饰符synchronized关键字加到方法上
修饰符 synchronized 返回值类型 方法名(方法参数){ …… }
特点:①同步方法是锁住方法里面所有的代码
②锁对象不能自己指定(由java规定,非静态方法的锁对象为this,静态方法的锁对象为当前类的字节码文件对象)
也可采用Lock锁的方法
线程等待唤醒机制(生产者和消费者)
特点:①生产者:生产数据 ②消费者:消费数据
核心思想:通过某一个中介平台控制线程的执行
具体方法:
void wait():当前线程等待,直到被其他线程唤醒
void notify():随机唤醒单个线程
void notifyAll():唤醒所有线程
代码实现(厨师吃货为例):
①创建“厨师”线程
public class Cook extends Thread {
@Override
public void run() {
while (true) {
synchronized (Desk.Lock) {
if (Desk.count == 0) {
break;
}
else {
if (Desk.foodFlag == 1) {
//如果食物满了,则等待
try {
Desk.Lock.wait();
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
else {
//食物还没有满,则继续生产
System.out.println("厨师在制作面条,还需制作" + Desk.count + "碗,吃货才能吃饱");
//生产完之后唤醒吃货来吃
Desk.Lock.notify();
//修改桌子的状态
Desk.foodFlag = 1;
}
}
}
}
}
}
②创建“吃货”线程
public class Foodie extends Thread {
@Override
public void run() {
/*1.循环
* 2.同步代码块
* 3.判断共享数据是否到了末尾
* 4.到了,结束,未到,执行核心代码
* */
while (true) {
synchronized (Desk.Lock) {
//判断是否还能吃
if (Desk.count == 0) {
break;
}
else {
//先判断桌子上是否有面条
if (Desk.foodFlag == 0) {
//如果没有,就等待
try {
Desk.Lock.wait();
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
else{
//如果有,则开吃
//食物个数-1
Desk.count--;
System.out.println("吃货在吃面条,还能再吃" + Desk.count + "碗!");
//吃完之后,唤醒厨师继续做
Desk.Lock.notify();
//修改桌子的状态
Desk.foodFlag = 0;
}
}
}
}
}
}
③创建中间平台
public class Desk {
/*作用:用来控制生产者和消费者*/
//判断标志,0表示没有面条,1表示有面条
public static int foodFlag = 0;
//消费者能吃的食物个数
public static int count = 10;
//定义一把锁
public final static Object Lock = new Object();
}
④创建测试类
public class Demotext {
public static void main(String[] args) {
//创建线程对象
Cook c = new Cook();
Foodie f = new Foodie();
//设置名字
c.setName("厨师");
f.setName("吃货");
//开启线程
c.start();
f.start();
}
}
运行结果:
厨师在制作面条,还需制作10碗,吃货才能吃饱
吃货在吃面条,还能再吃9碗!
厨师在制作面条,还需制作9碗,吃货才能吃饱
吃货在吃面条,还能再吃8碗!
厨师在制作面条,还需制作8碗,吃货才能吃饱
吃货在吃面条,还能再吃7碗!
厨师在制作面条,还需制作7碗,吃货才能吃饱
吃货在吃面条,还能再吃6碗!
厨师在制作面条,还需制作6碗,吃货才能吃饱
吃货在吃面条,还能再吃5碗!
厨师在制作面条,还需制作5碗,吃货才能吃饱
吃货在吃面条,还能再吃4碗!
厨师在制作面条,还需制作4碗,吃货才能吃饱
吃货在吃面条,还能再吃3碗!
厨师在制作面条,还需制作3碗,吃货才能吃饱
吃货在吃面条,还能再吃2碗!
厨师在制作面条,还需制作2碗,吃货才能吃饱
吃货在吃面条,还能再吃1碗!
厨师在制作面条,还需制作1碗,吃货才能吃饱
吃货在吃面条,还能再吃0碗!
阻塞队列
继承结构:
①接口:Iterable,Collection,Queue,Blocking,Queue
②实现类:ArrayBlockingQueue,LinkedBlockingQueue
ArrayBlockingQueue:底层是数组,有界
LinkedBlockingQueue:底层是链表,无界(最大值为int的最大值,非常大)
利用阻塞队列实现等待唤醒机制:
①生产者和消费者必须使用同一个阻塞队列
②阻塞队列一般在中介平台上生成
代码实现:
①创建Cook类
public class Cook extends Thread {
ArrayBlockingQueue<String> queue;
public Cook(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run(){
while (true) {
try{
//put方法里面已经包含了锁对象了,所以无需使用synchronized
queue.put("面条");
System.out.println("厨师放了一碗面条");
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
②创建Foodie类
public class Foodie extends Thread {
ArrayBlockingQueue<String> queue;
public Foodie(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true) {
try {
String food = queue.take();
System.out.println(food);
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
③创建测试类
public class Demotext {
public static void main(String[] args) {
//创建线程对象
Cook c = new Cook();
Foodie f = new Foodie();
//设置名字
c.setName("厨师");
f.setName("吃货");
//开启线程
c.start();
f.start();
}
④运行结果:
厨师放了一碗面条
面条
厨师放了一碗面条
面条
面条
厨师放了一碗面条
厨师放了一碗面条
面条
厨师放了一碗面条
面条
厨师放了一碗面条
厨师放了一碗面条
面条
面条
…………
小结:其实阻塞队列就相当于中介平台,通过类的构造方法,在实例化类的过程中传入相同的阻塞队列,从而达到利用阻塞队列实现等待唤醒机制
缺点:由运行结果可以看出,会出现连续打印同一条信息的情况,出现这种情况的原因是:阻塞队列中的put和take方法里面都内含锁,而且是在方法里面完成上锁解锁的操作,print语句是在锁外执行的,加上线程的随机性,就会出现连续打印同一条信息的情况。
线程的状态
线程池
核心原理:
①创建一个空池子
②提交任务时,池子会创建新的线程对象,任务执行完毕,线程归还池子,下次再次提交任务时不需要创建新的线程,直接复用已有的线程即可
③如果提交任务过程中,池子没有空闲线程,也无法创建新的线程,任务就会排队等待
代码实现:
①创建线程池
Executors:线程池的工具类通过调用方法返回不同类型的线程池对象
//创建一个没有上限的线程池
public static ExecutorService newCachedThreadPool();
//创建一个上限为nThreads的线程池
pubilc static ExecutorService newFixedThreadPool(int nThreads);
②线程池其他方法
//提交任务
public viod submit(new Runnable{代码体});
//销毁线程池
public viod submit();
自定义线程池
1.核心元素:
①核心线程数量
②线程池中最大线程的数量
③临时线程空闲时间(数值)
④临时线程空闲时间(单位)
⑤阻塞队列
⑥创建线程的方式
⑦任务拒绝服务策略(有四种,常用的策略是直接舍弃)
注意:
①核心线程在线程池中会一直存在,不会被销毁
②临时线程创建之后,若系统没有分配任务,则空闲一定时间后自动销毁
③最大线程数量 = 核心线程数 + 临时线程数
④临时线程只有在阻塞队列满了之后才会创建
⑤当提交的任务数量 >(最大线程数量 + 阻塞队列长度)时,会处罚任务拒绝策略,一般是直接舍弃后提交的任务
代码实现:
/*
ThreadPoolExcutor threadPoolExecutor =new ThreadPoolExcutor
(核心线程数量,最大线程数,空闲线程最大存活时间,时间单位,任务队列,创建线程工厂,任务的拒绝策略);
*/
ThreadPoolExecutor pool = new ThreadPoolExecutor(
3, //核心线程数量,能小于0
6, //最大线程数,不能小于0,最大数量 >= 核心线程数量
60, //空闲线程最大时间(数值)
TimeUnit.SECONDS, //时间单位
new ArrayBlockingQueue<>(3), //任务队列
Executors.defaultThreadFactory(), //创建线程工厂
new ThreadPoolExecutor.AbortPolicy() //任务的拒绝策略
);
线程池大小的选择
1.CPU密集型运算:最大并行数+1
注:在四核八线程的CPU中,最大并行数为8
小知识:可以通过java获取处理器数目
//获取java虚拟机可使用的处理器数目
public int availableProcessors();
2. I / O密集型运算:最大并行数 * 期望CPU利用率 * (总时间 / CPU计算时间)
注:总时间 = CPU计算时间 + 等待时间
3.理解
①CPU密集型运算:项目中计算操作比较多,读取本地文件或者读取数据库的操作比较少的运算
②I / O密集型运算:读取操作比较多,计算操作比较少的运算
③正在运行的线程数量不能大于CPU最大并行数,当线程数量比较多的时候,其他未运行的线程则会处于等待系统调度的状态,容易与正在运行的线程争夺CPU使用权,即线程竞争