p.s.这是萌新自己自学总结的笔记,如果想学习得更透彻的话还是请去看大佬的讲解
目录
- 进程和线程的概念
- 并发与并行
- 多线程的实现方式
- 继承Thread类的方式进行实现
- 使用Runnable接口的方式进行实现
- 使用Callable接口和Future接口方式进行实现
- 多线程常用的成员方法
- 同步代码块
- 同步方法
- lock锁
- 死锁
- 等待唤醒机制(生产者和消费者)
- 服务员代码实现
- 消费者代码实现
- 生产者代码实现
- 测试类
- 等待唤醒机制(阻塞队列方式实现)
- 生产者代码实现
- 消费者代码实现
- 测试类
- 线程池
- 任务
- 线程池
进程和线程的概念
进程是程序的基本执行实体。一个软件运行后其就是一个进程,
而线程是操作系统能够进行运算调度的最小单位,它被包含在进程中,是进程的实际运作单位
简单来说线程就是应用软件中相互独立,可以同时运行的功能
但这些功能比较多的时候,就形成了多线程
.
比如360。当360运行的时候其就是一个进程,在这个360进程中我们可以使用其功能,比如清理垃圾、木马查杀。使用一个功能其实就是开启了一个线程,当我们既进行清理垃圾又进行木马查杀的时候其实我们就是开启多线程来在同一时间处理我们需要解决的多个问题
而有了多线程我们就可以让程序同时做多件事情,提高了效率
应用场景,只要想让多个事情同时运行就需要用到多线程。比如软件中的耗时操作、所有的聊天软件、所有的服务器
并发与并行
并发:在同一时刻,有多个指令在单个CPU上交替执行
并行:在同一时刻,有多个指令在多个CPU上同时执行
多线程的实现方式
继承Thread类的方式进行实现
public class Demo1 {public static void main(String[] args) {//多线程的第一种启动方式//1.定义一个类继承Thread//2.重写run方法//3.创建子类的对象,并启动线程MyThread t1 = new MyThread();MyThread t2 = new MyThread();t1.setName("线程1");t2.setName("线程2");//开启线程t1.start();t2.start();}
}
class MyThread extends Thread{@Overridepublic void run() {//书写线程要执行的代码for (int i = 0; i < 100; i++) {System.out.println(getName()+"你好世界");}}
}
使用Runnable接口的方式进行实现
public class Demo2 {public static void main(String[] args) {//多线程的第二种启动方式//1.定义一个类继承Runnable接口//2.重写run方法//3.创建自己的类的对象//4.创建一个Thread类的对象,并启动线程//3.创建MyRun的对象//表示多线程要执行的任务MyRun mr = new MyRun();Thread t1 = new Thread(mr);Thread t2 = new Thread(mr);//给线程设置名字t1.setName("线程1");t2.setName("线程2");t1.start();t2.start();}
}
class MyRun implements Runnable{@Overridepublic void run() {//书写线程要执行的代码for (int i = 0; i < 100; i++) {//获取到当前线程的对象Thread thread = Thread.currentThread();System.out.println(thread.getName()+"你好世界");}}
}
使用Callable接口和Future接口方式进行实现
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;public class Demo3 {public static void main(String[] args) throws ExecutionException, InterruptedException {//多线程的第三种实现方式//特点:可以获取到多线程运行的结果//1.定义一个类继承Callable接口//2.重写call方法(有返回值,即多线程运行的结果)//3.创建自己的类的对象//4.创建FutureTask的对象(作用管理多线程运行的结果)//5.创建一个Thread类的对象,并启动线程//3.创建MyCallable的对象//表示多线程要执行的任务MyCallable mc = new MyCallable();//4.创建FutureTask的对象(作用管理多线程运行的结果)FutureTask<Integer> ft = new FutureTask<>(mc);//创建线程的对象Thread t1 = new Thread(ft);Thread t2 = new Thread(ft);t1.start();//获取多线程的结果Integer result = ft.get();System.out.println(result);}
}
class MyCallable implements Callable<Integer> {@Overridepublic Integer call() throws Exception {//求1-100之间的和int sum = 0;for (int i = 0; i <= 100; i++) {sum+=i;}return sum;}
}
多线程常用的成员方法
注意
setName方法如果我们没有给线程设置名字,线程也是有默认名字的;格式:Thread-x(x序号,从0开始)
如果我们要给线程设置名字,可以用set方法进行设置,也可以使用构造方法进行设置
.
currentThread方法,当JVM虚拟机启动之后,会自动地启动多条线程
其中有一条线程就叫做main线程
其作用就是去调用main方法,并执行里面的代码
在以前我们写的代码其实都是运行在main线程当中
.
sleep方法哪条线程执行到这个方法,那么哪条线程就会在这里停留对应的时间
方法的参数:就表示睡眠的时间,单位毫秒
当时间到了之后,线程就会自动地醒来,继续执行下面的代码
.
线程的优先级分为10级(1-10),默认为5。优先级越大,先运行的概率越高(也就是有可能优先级低的先运行完毕,但概率很小)
.
当其他的非守护线程执行完毕后,守护线程会陆续结束
.
礼让线程用于使多线程的结果尽量更均匀
.
插入线程用于使某线程在插入到某线程之前执行
同步代码块
同步代码块,即把操作共享数据的代码锁起来
格式
synchronized (锁){操作共享数据的代码}
锁默认打开,,当有一个线程进去了,锁就会默认关闭
当里面的代码全部执行完毕,线程出来,锁就会自动打开了
锁对象一定要是唯一的
距离
import static study.threaded.Demo1.obj;
import static study.threaded.Demo1.sum;public class Demo1 {static int sum = 0;public static void main(String[] args) {MyThread1 t1 = new MyThread1();MyThread1 t2 = new MyThread1();t1.setName("线程1");t2.setName("线程2");//开启线程t1.start();t2.start();}
}
class MyThread1 extends Thread{@Overridepublic void run() {while (true){synchronized (MyThread.class){//synchronized要写在循环里面if (sum<100){try {Thread.sleep(50);} catch (InterruptedException e) {throw new RuntimeException(e);}sum++;System.out.println(getName()+":"+sum);}else {break;}}}}
}
同步方法
就是将synchronized关键字加到方法上
格式修饰符 synchronized 返回值类型 方法名(方法参数){...}
同步方法是锁住方法里面的所有代码,
并且锁对象不能自己指定:非静态方法的锁为this
,静态方法则为当前类的字节码文件对象
lock锁
虽然我们可以理解同步代码块和同步方法的锁对象问题
但是我们并没有直接看到在哪里上了锁,在哪里释放了锁
为了更加清晰地表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock
Lock实现提供了更广泛的锁定操作:
比如void lock(); 上锁、void unlock(); 开锁
Lock是一个接口不能直接实例化,要采用其实现类ReentrantLock来实例化
举例
package study.threaded;import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;import static study.threaded.Demo1.sum;public class Demo5 {static int sum = 0;public static void main(String[] args) {MyThread2 t1 = new MyThread2();MyThread2 t2 = new MyThread2();t1.setName("线程1");t2.setName("线程2");//开启线程t1.start();t2.start();}
}
class MyThread2 extends Thread{//用static修饰,让所有线程都共同使用这个lock对象static Lock lock = new ReentrantLock();@Overridepublic void run() {while (true){lock.lock();try {if (sum<100){Thread.sleep(50);sum++;System.out.println(getName()+":"+sum);//lock.unlock();这里上锁行不通}else {//lock.unlock();这里上锁行不通break;}} catch (InterruptedException e) {throw new RuntimeException(e);}finally {//在finally里写,保证锁一定会被释放lock.unlock();}}}}
死锁
死锁,即锁的嵌套
这种情况是一个错误,死锁会导致两个线程都一直等着对方先释放锁,则会导致程序卡在这里
详细解释看这里
等待唤醒机制(生产者和消费者)
生产者消费者机制是一个非常经典的多线程协作的模式
其结果为两条线程交替执行
.
其中的一条线程称为生产者,负责生产数据;另一条为消费者,负责消费数据
如果将生产者比作厨师、消费者比作顾客,那的等待唤醒机制的核心就是二者之间传递食物(数据)的服务员
.
理想情况下刚开始服务员应该在厨师处(即生产者抢到了CPU的执行权),然后厨师将食物(数据)给服务员,服务员再递交给顾客,顾客吃完后(处理完数据)服务员又回到厨师处循环往复
.
第二种情况 (消费者等待) 是一开始服务员在顾客处,这时顾客只能等待(wait)服务员上菜,结果等太久睡着了。当厨师把菜做好给服务员后,看到顾客睡着了,便用大嗓门将顾客唤醒(notify)了。
这种情况下,消费者要进行判断:服务员如果没有上菜就等待(结果等睡着了)。而生产者则需要:做菜、把菜给服务员、最后唤醒顾客
.
第三种情况 (生产者等待) 是一开始服务员在厨师那里,厨师做好菜后交给服务员,但结果服务员端着菜出去之后不知道为什么又回到了厨师处。没办法,厨师也只能等待(wait)服务员又一次去上菜,结果也睡着了。顾客吃完后还没饱,看到厨师也睡着了,于是将厨师 唤醒(notify)了。
.
这种情况下,生产者要进行判断:服务员如果手上有菜就等待(结果等睡着了),没有就开始做菜。而消费者则需要:吃菜、唤醒厨师
.
所以等待唤醒机制的双方的流程为:
生产者:1.判断服务员手上是否有菜,有则等待,无则做菜;2.把菜给服务员;3.唤醒顾客吃菜
消费者:1.判断服务员手上是否有菜,有则吃菜,无则等待;2.吃完唤醒厨师做菜
.
等待与唤醒的方法为
服务员代码实现
public class Waiter {//作用:控制生产者和消费者的执行//有食物为1,无食物为0public static int foodFlog = 0;//顾客最多能吃的食物数量public static int count = 10;//锁对象public static Object lock = new Object();
}
消费者代码实现
public class Foodie extends Thread{@Overridepublic void run() {//书写多线程的步骤//1.循环//2.同步代码块//3.判断共享数据是否到了末尾(到了末尾)//4.判断共享数据是否到了末尾(没有到末尾,执行核心思想)while (true){synchronized (Waiter.lock){if (Waiter.count == 0){break;}else {//先判断服务员是否有食物if (Waiter.foodFlog == 0 ){//没有就等待try {Waiter.lock.wait();//让当前线程跟锁进行绑定} catch (InterruptedException e) {throw new RuntimeException(e);}}else {//有就吃//把吃的总数-1Waiter.count--;//有就吃System.out.println("顾客正在吃食物,还能吃"+Waiter.count+"份");//吃完之后唤醒厨师继续做Waiter.lock.notifyAll();//修改服务员状态Waiter.foodFlog = 0;}}}}}
}
生产者代码实现
public class Cook extends Thread{@Overridepublic void run() {//书写多线程的步骤//1.循环//2.同步代码块//3.判断共享数据是否到了末尾(到了末尾)//4.判断共享数据是否到了末尾(没有到末尾,执行核心思想)while (true){synchronized (Waiter.lock){if (Waiter.count==0){break;}else {//判断桌子上是否有食物if (Waiter.foodFlog == 1){//如果有则等待try {Waiter.lock.wait();//让当前线程跟锁进行绑定} catch (InterruptedException e) {throw new RuntimeException(e);}}else {//没有则制作食物System.out.println("厨师做了一份食物");//修改桌子上的食物状态Waiter.foodFlog = 1;//叫醒等待的消费者开吃Waiter.lock.notifyAll();}}}}}
}
测试类
public class ThreadDemo {public static void main(String[] args) {//创建线程的对象Cook cook = new Cook();Foodie foodie = new Foodie();//给线程设置名字cook.setName("顾客");foodie.setName("厨师");//开启线程cook.start();foodie.start();}
}
等待唤醒机制(阻塞队列方式实现)
阻塞队列方式实现,即去除了服务员,改为一条传送带来实现食物的传递
可以自定义传送带上最多放的食物数量
生产者代码实现
package study.threaded;import java.util.concurrent.ArrayBlockingQueue;public class Cook extends Thread{ArrayBlockingQueue<String> queue;public Cook(ArrayBlockingQueue<String> queue) {this.queue = queue;}@Overridepublic void run() {while (true){//不断地把食物放在阻塞队列当中try {queue.put("食物");System.out.println("厨师放了一碗面条");} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
消费者代码实现
package study.threaded;import java.util.Queue;
import java.util.concurrent.ArrayBlockingQueue;public class Foodie extends Thread{ArrayBlockingQueue<String> queue;public Foodie(ArrayBlockingQueue<String> queue) {this.queue = queue;}@Overridepublic void run() {while (true){while (true){//不断地从阻塞队列中获取食物try {String food = queue.take();System.out.println(food);} catch (InterruptedException e) {throw new RuntimeException(e);}}}}
}
测试类
package study.threaded;import java.util.concurrent.ArrayBlockingQueue;public class ThreadDemo {public static void main(String[] args) {//注意:生产者和消费者要使用同一个阻塞队列//阻塞队列要在测试类中创建ArrayBlockingQueue queue = new ArrayBlockingQueue<>(5);//创建线程的对象,并把阻塞队列传递过去Cook cook = new Cook(queue);Foodie foodie = new Foodie(queue);//给线程设置名字cook.setName("顾客");foodie.setName("厨师");//开启线程cook.start();foodie.start();}
}
线程池
之前写多线程的时候由于使用线程时必须先创建,并且用完后线程就会消失。这样会浪费系统资源
于是我们可以将线程存到线程池中,要用时就取出来,并且用完后还不会消失,从而节约了系统资源
当我们将任务给线程池的时候,线程池会自动创建一个线程来执行该任务,执行完后再将线程存回线程池中;在提交第二个任务的时候就不用再创建一个新线程,而是直接使用线程池中已有的线程
线程池的最大容量数可以自定义
线程池主要核心原理
1.创建一个空线程池
2.提交任务时,线程池会创建一个新的线程对象,任务执行完毕后,线程会归还给线程池;下回再提交任务时就不需要创建新的线程了,直接复用已有的线程就行了
3.但是如果提交任务时,线程池中没有空闲的线程,也无法创建新的线程时,任务就会排队等待
任务
public class MyRunnable implements Runnable {@Overridepublic void run() {System.out.println(Thread.currentThread().getName());}
}
线程池
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class Demo6 {public static void main(String[] args) throws InterruptedException {//获取线程池的对象//所有的任务全部执行完毕,关闭线程池ExecutorService pool1 = Executors.newCachedThreadPool();//提交任务pool1.submit(new MyRunnable());Thread.sleep(1000);pool1.submit(new MyRunnable());Thread.sleep(1000);pool1.submit(new MyRunnable());Thread.sleep(1000);pool1.submit(new MyRunnable());Thread.sleep(1000);pool1.submit(new MyRunnable());//销毁线程池pool1.shutdown();}
}