Java多线程
为什么要有多线程?
进程
进程是程序的基本执行实体
线程
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程的实际运作单位。
简单理解:应用软件中互相独立,可以同时运行的功能
多线程的应用场景:
-
软件中的耗时操作
-
拷贝,迁移大文件
-
加载大量的资源文件
-
所有的聊天软件
-
所有的后台服务器
多线程的作用:提高效率
多线程的两个概念
并发:在同一时刻,有多个指令在单个CPU上交替执行
并行:在同一时刻,有多个指令在多个CPU上同时执行
多线程的实现方式
多线程的实现方式:
-
继承Thread类的方式进行实现
-
实现Runnable接口的方式进行实现
-
利用Callable接口和Future接口方式实现
-
【线程池创建】
多线程的第一种启动方式
继承Thread类的方式进行实现:
-
自己定义一个类继承Thread
-
重写run方法
-
创建子类的对象,并启动该线程
public class MyThread extends Thread{
@Override
public void run(){
//书写线程要执行的代码
for(int i = 0;i < 100;i++){
System.out.printf(getName() + "HelloWorld");
}
}
}
public class ThreadDemo{
public static void main(String[] args){
/*
多线程的第一种启动方式
1. 自己定义一个类继承Thread
2. 重写run方法
3. 创建子类的对象,并启动该线程
*/
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.setName("线程1");
t1.setName("线程2");
t1.start();
t2.start();
}
}
多线程的第二种启动方式
实现Runnable接口的方式进行实现:
-
自己定义一个类实现Runnable接口
-
重写里面的run方法
-
创建自己的类的对象
-
创建一个Thread类的对象,并开启线程
public class MyRun implements Runnable{
@Override
public void run(){
//书写线程要执行的代码
for(int i = 0;i < 100;i++){
//获取到当前线程的对象
/*Thread t = Thread.currentThread();
System.sout.println(t.getName() + "HelloWorld");*/
System.sout.println(Thread.currentThread().getName() + "HelloWorld");
}
}
}
public class ThreadDemo{
public static void main(String[] args){
/*
多线程的第二种启动方式:
1. 自己定义一个类实现Runnable接口
2. 重写里面的run方法
3. 创建自己的类的对象
4. 创建一个Thread类的对象,并开启线程
*/
//创建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();
}
}
多线程的第三种启动方式
利用Callable接口和Future接口方式实现:
特点:可以获取到多线程运行的结果
-
创建一个类 MyCallable 实现 Callable 接口
-
重写 call 【是有返回值的,表示多线程运行的结果】
-
创建 MyCallable 的对象【表示多线程要执行的任务】
-
创建 FutureTask 的对象【作用管理多线程运行的结果】
-
创建 Thread 类的对象,并启动【表示线程】
public class MyCallable iplements Callable<Integer> {
@Override
public Integer call() throws Excepion{
//求1 ~ 100之间的和
int sum = 0;
for(int i = 1;i <= 100;i++){
sum = sum + i;
}
return sum;
}
}
public class ThreadDemo{
public static void main(String[] args){
/*
多线程的第三种启动方式
利用Callable接口和Future接口方式实现:
特点:可以获取到多线程运行的结果
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);
}
}
输出:5050
多线程三种实现方式对比
实现方式 | 优点 | 缺点 |
---|---|---|
继承 Thread 类 | 编程比较简单,可以直接使用 Thread 类中的方法 | 可扩展性较差,不能在继承其他的类 |
实现 Runnable 接口 | 扩展性强,实现该接口的同时还能继承其他的类 | 编程相对复杂,不能直接使用 Thread 类中的方法 |
实现 Callable 接口 | 扩展性强,实现该接口的同时还能继承其他的类 | 编程相对复杂,不能直接使用 Thread 类中的方法 |
常见的成员方法
方法名称 | 说明 |
---|---|
String getName() | 返回此线程的名称 |
void setName(String name) | 设置线程的名字【构造方法也可以设置名字】 |
static Thread currentThread() | 获取当前线程的对象 |
static void sleep(long time) | 让线程休眠指定的时间,单位为毫秒 |
setPriority(int new Priority) | 设置线程的优先级【默认为5,1最低,10最高】 |
final int getPriority() | 获取线程的优先级 |
final void setDaemon(boolean on) | 设置为守护线程 |
public static void yield() | 出让线程/礼让线程 |
public void join() | 插入线程/插队线程 |
演示
public class MyThread extends Thread{
public MyThread(){
}
public MyThread(){
super(name);
}
@Override
public void run(){
for(int i =0;i < 100;i++){
System.out.println(getName() + "@" + i);
}
}
}
public class ThreadDemo{
public static void main(String[] args){
/*
1、String getName() 返回此线程的名称
2、void setName(String name) 设置线程的名字(构造方法也可以设置名称)
细节:
1.如果我们没有给线程设置名字,线程也是有默认名字的
格式:Thread-X(X序号,从0开始)
2.如果我们要给线程设置名字,可以用set方法进行设置,也可以构造方法进行设置
3、static Thead currentThread() 获取当前线程的对象
细节:
当JVM虚拟机启动之后,会自动的启动多条线程
其中有一条线程叫做main线程
它的作用就是去调用main方法,并执行里面的代码
4、static void sleep(long time) 让线程休眠指定的时间,单位为毫秒
细节:
1.哪条线程执行到这个方法,那么哪条线程就会在这停留对应的时间
2.方法的参数:就表示睡眠的时间,单位为毫秒
3.当时间到了之后,线程就会自动醒来,继续执行下面的其他代码
*/
/*
//1.创建线程的对象
MyThread t1 = new MyThread("飞机");
MyThread t2 = new MyThread("坦克");
//2.开启线程
t1.start();
t2.start();
*/
/*
//哪条线程执行到这个方法,此时获取的就是哪条线程的对象
Thread t = Thread.currentThread();
String name = t.getName();
System.out.println(name);//main
*/
/*
System.out.println("睡眠前");
Thread.sleep(5000);
System.out.println("睡眠后");
*/
}
}
线程的调度
非抢占式调度 -
抢占式调度 - 随机性 【Java中】
该线程优先级越高,抢占CPU执行权的机会越大
public class MyRunnable implements Runnable{
@Override
public void run(){
for(int i = 1;i <= 100;i++){
System.out.println(Thread.currentThread().getName + "---" + i);
}
}
}
public class ThreadDemo{
public static void main(String[] args){
/*
5、setPriority(int new Priority) 设置线程的优先级
6、final int getPrioroty() 获取线程的优先级
*/
//创建线程要执行的参数对象
MyRunnable mr = new MyRunnable();
//创建线程对象
Thread t1 = new Thread(mr,"飞机");
Thread t2 = new Thread(mr,"坦克");
/*
System.out.println(t1.getPriority());//5
System.out.println(t1.getPriority());//5
*/
t1.setPriority(1);
t2.setPriority(10);
t1.start();
t2.start();
}
}
守护线程
public class MyThread1 extends Thread{
@Override
public void run(){
for(int i = 1;i <= 10;i++){
System.out.println(getName() + "@" + i);
}
}
}
public class MyThread2 extends Thread{
@Override
public void run(){
for(int i = 1;i <= 100;i++){
System.out.println(getName() + "@" + i);
}
}
}
public class ThreadDemo{
public static void main(String[] args){
/*
7、final void setDaemon(boolean on) 设置为守护线程
细节:
当其他的非守护线程执行完毕之后,守护线程会陆续结束
通俗易懂:
当女神线程结束之后,那么备胎线程也没有存在的必要的,会陆陆续续结束备胎线程
*/
MyThread1 t1 = new MyThread1();
MyThread2 t2 = new MyThread2();
t1.setName("女神");
t2.setName("备胎");
//把第二个线程设置为守护线程【备胎线程】
t2.setDaemon(true);
t1.start();
t2.start();
}
}
出让线程/礼让线程
public class MyThread extends Thread{
@Override
public void run(){
for(int i = 1; i<= 100;i++){
System.out,println(getName() + "@" +i);
//表示出让当前CPU的执行权
Thread.yield();
}
}
}
public class ThreadDemo{
public static void main(String[] args){
/*
8、public static void yield() 出让线程/礼让线程
*/
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.setName("飞机");
t2.setName("坦克");
t1.start();
t2.start();
}
}
插入线程/插队线程
当插入线程之后,这个插入线程执行完才会释放CPU执行权
public class MyThread extends Thread{
@Override
public void run(){
for(int i = 1;i <= 100;i++){
System.out.println(getName() + "@" + i);
}
}
}
public class ThreadDemo{
public static void main(String[] args){
/*
9、public final void join() 插入线程/插队线程
*/
MyThread t = new MyThread();
t.setName("土豆");
t.start();
//表示把t这个线程,插入到当前线程之前
//t:土豆
//当前线程:main线程
t.join();
//执行在main线程当中的
for(int i = 0;i < 10;i++){
System.out.println("main线程" + i);
}
}
}
线程的生命周期
线程的生活周期中,它有五种状态:
-
新建(New)
-
就绪(Runnable)
-
运行(Running)
-
阻塞(Blocked)
-
死亡(Dead)
问:sleep方法会让线程睡眠,睡眠时间到了以后,立马就会执行下面的代码吗?
不会,线程睡眠时间到了之后会重新变为 就绪 状态,要去抢夺CPU的执行权才能执行代码。
线程安全的问题
public class MyThread extends Thread{
//表示这个类所有的对象,都共享ticket数据
static int ticket = 0;//0~99
@Override
public void run(){
while(true){
if(ticket < 100){
try{
Thread.sleep(100);
}catch(InterruptedException e){
e.printSrackTrace();
}
ticket++;
System.out.println(getName() + "正在卖第" +ticket + "张票!!");
}else{
break;
}
}
}
}
public class ThreadDemo{
public static void main(String[] args){
/*
需求:
某电影目前正在上映国产大片,共有100张票,而他有三个窗口卖票,请设计一个程序模拟电影院卖票
*/
//创建线程对象
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
//起名字
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
//开启线程
t1.start();
t2.start();
t3.start();
}
}
买票引发的安全问题
-
相同的票出现多次
-
出现了超出范围的票
同步代码块
把操作共享数据的代码锁起来
格式:
synchronized(锁){
操作共享数据的代码
}
-
特点1:锁默认打开,有一个线程进去了,锁自动关闭。
-
特点2:里面的代码全部执行完毕,线程出来,锁自动打开。
public class MyThread extends Thread{
//表示这个类所有的对象,都共享ticket数据
static int ticket = 0;//0~99
//锁对象,一定要是唯一的
static Object obj = new Object();
@Override
public void run(){
while(true){
//同步代码块
synchronized(obj){
if(ticket < 100){
try{
Thread.sleep(100);
}catch(InterruptedException e){
e.printSrackTrace();
}
ticket++;
System.out.println(getName() + "正在卖第" +ticket + "张票!!");
}else{
break;
}
}
}
}
}
同步代码块的两个小细节
-
synchronized 无法写在循环的外面
-
synchronized(obj) 其中obj 【锁对象】要是唯一的,一般为当前类的字节码文件对象 类名.class
同步方法
就是把synchronized 关键字加到方法上
格式
修饰符 synchronized 返回值类型 方法名(方法参数){...}
特点:
-
同步方法是锁住方法里面的所有代码
-
锁对象不能自己指定,是Java已经规定好的
-
非静态:this
-
静态:当前类的字节码文件对象
-
示例:
public class MyRunnable implements Runnable{
static int ticket = 0;
@Override
public void run(){
//1.循环
while(true){
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 +张票!!!);
}
}
}
public class ThreadDemo{
public static void main(String[] args){
/*
需求:
某电影目前正在上映国产大片,共有100张票,而他有三个窗口卖票,请设计一个程序模拟电影院卖票
利用同步方法完成
技巧:同步代码块
*/
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
Thread t3 = new Thread(mr);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
延申:
String buffer和String builder区别
-
StringBuffer 与 StringBuilder 中的方法和功能完全是等价的 。
-
只是StringBuffer 中的方法大都采用了 synchronized 关键字进行修饰,因此是线程安全的,而 StringBuilder 没有这个修饰,可以被认为是线程不安全的。
-
在单线程程序下,StringBuilder效率更快,因为它不需要加锁,不具备多线程安全而StringBuffer 则每次都需要判断锁,效率相对更低
lock锁
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没直接看到在哪里上的锁,在哪里释放锁,为了更清晰的表达如何加锁和释放锁,JDK5 以后提供了一个新的锁对象Lock
Lock实现提供比使用 synchronized 方法和语句可以获得更广泛的锁定操作
Lock中提供了获得锁和释放锁的方法
void lock();//获得锁,手动上锁
void unlock();//释放锁,手动解锁
Lock 是接口不能直接实例化,这里采用它的实现类 ReentrantLock 来实例化
ReentrantLock 的构造方法
ReentrantLock():创建一个 ReentrantLock 的实例
public class MyRunnable implements Runnable{
static int ticket = 0;
static Lock lock = new ReentrantLock();
@Override
public void run(){
//1.循环
while(true){
//2.同步代码块
//synchronized(MyThread.class){
lock.lock();
try{
//3.判断
if(ticket == 100){
break;
//4.判断
}else{
Thread.sleep(10);
ticket++;
System.out.println(getName + "在卖第" + ticket +张票!!!);
}
//}
}catch(InterruptedException e){
e.printSrackTrace();
}finally{
lock.unlock();
}
}
}
}
}
public class ThreadDemo{
public static void main(String[] args){
/*
需求:
某电影目前正在上映国产大片,共有100张票,而他有三个窗口卖票,请设计一个程序模拟电影院卖票
*/
//创建线程对象
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
//起名字
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
//开启线程
t1.start();
t2.start();
t3.start();
}
}
死锁
以后写锁的时候,千万不能让锁嵌套起来
生产者和消费者
生产者和消费者【等待唤醒机制】
生产者消费者模式是一个十分经典的多线程协作的模式
-
生产者:生产数据
-
消费者:消费数据
消费者等待
-
判断桌子上是否有食物
-
如果没有就等待
-
如果有就开吃
-
吃完之后,唤醒厨师继续做
生产者等待
-
判断桌子上是否有食物
-
有:等待
-
没有:制作食物
-
把食物放在桌子上
-
叫醒等待的消费者开吃
生产者和消费者【常见方法】
方法名称 | 说明 |
---|---|
void wait() | 当前线程等待,直到被其他线程唤醒 |
void notify() | 随即唤醒单个线程 |
void notifyAll() | 唤醒所有线程 |
消费者代码实现
生产者
public class Cook extends Thread{
@Override
public void run(){
/*
1.循环
2.同步代码块
3.判断共享数据是否到了末尾【到了末尾】
4.判断共享数据是否到了末尾【没有到末尾,执行核心逻辑】
*/
while(true){
synchronized(Desk.lock){
if(Desk.count == 0){
System.out.println("不需要再做面条啦!!!");
break;
}else{
//判断桌子上有没有食物
if(Desk.foodFlag == 1){
//如果有就等待
try{
Desk.lock.wait();
}catch(InterruptedException e){
e.printSrackTrace();
}
}else{
//如果没有,就制作食物
System.out.println("厨师做了一碗面条");
//修改桌子上的状态
Desk.foodFlag = 1;
//叫醒等待的消费者开吃
Desk.lock.notifyAll();
}
}
}
}
}
}
消费者
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(foodFlag == 0){
//如果没有,就等待
try{
Desk.lock.wait();//让当前线程跟锁进行绑定
}catch(InterruptedException e){
e.printSrackTrace();
}
}else{
//把吃的总数 -1
Desk.count--;
//如果有,就开吃
System.out.println("吃货还在吃面条,还能再吃" + Desk.count + "碗!!");
//吃完之后,唤醒厨师继续做
Desk.lock.notifyAll();
//修改桌子的状态
Desk.foodFlag = 0;
}
}
}
}
}
}
桌子【中间桥梁】
public class Desk{
/*
作用:
控制生产者和消费者的执行
*/
//是否有面条 0:没有面条 1:有面条
public static int foodFlag = 0;
//总个数
public static int count = 10;
//锁对象
public static Object lock = new Object();
}
测试
public class ThreadDemo{
public static void main(String[] args){
/*
需求:
实现生产者和消费者【等待唤醒机制】的代码
实现线程轮流交替执行的效果
*/
//创建线程的对象
Cook c = new Cook();
Foodie f = new Foodie();
//给线程设置名字
c.setName("厨师");
f.setName("吃货");
//开启线程
c.start();
f.start();
}
}
生产者和消费者【阻塞队列方式实现】
-
PUT数据时:放不进去,会等着,也叫做阻塞。
-
take数据时:取出来第一个数据,取不到会等着,也叫做阻塞。
阻塞队列的继承结构
-
接口
-
Iterable
-
Collection
-
Queue
-
BlockingQueue
-
-
实现类
-
ArrayBlockingQueue:底层是数组,有界
-
LinkedBlockingQueue:底层是链表,无界但不是真正的无界,最大为int的最大值。
-
生产者
public class Cook extends Thread{
ArrayBlockingQueue<String> queue;
public Cook(ArrayBlockingQueue<String> queue){
this.queue = queue;
}
@Override
public void run(){
while(true){
//不断的把面条放到阻塞队列中
try{
queue.put("面条");
System.out.println("厨师放了一碗面条");
}catch(InterruptedException e){
e.printSrackTrace();
}
}
}
}
消费者
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){
e.printSrackTrace();
}
}
}
}
测试
public class ThreadDemo{
public static void main(String[] args){
/*
需求:利用阻塞队列完成生产者和消费者【等待唤醒机制】的代码
细节:
生产者和消费者必须使用同一个阻塞队列
*/
//1.创建阻塞队列的对象
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
//创建线程的对象
Cook c = new Cook(queue);
Foodie f = new Foodie(queue);
//开启线程
c.start();
f.start();
}
}
多线程的六种状态
当一个线程抢到了CPU的执行权的时候,那么虚拟机就会把当前线程就给虚拟机去执行,所以为什么Java没有定义线程的运行
在Java当中只有以下的六种状态
线程池
以前多线程的弊端:
-
用到线程的时候就创建
-
用完之后就消失
线程池 主要核心原理
-
创建一个池子,池子中是空的
-
提交任务时,池子会创建新的线程对象,任务执行完毕,线程归还池子,下回再次提交任务时,不需要创建新的多线程,直接复用已有的线程即可
-
但是如果提交任务时,池子中没有空闲线程,也无法创建新的线程,任务就会排队等待
线程池 代码实现
Executors :线程池的工具类通过调用方法返回不同类型的线程池对象。
方法名称 | 说明 |
---|---|
public static ExecutorService newCachedThreadPool() | 创建一个没有上限的线程池【最大容量为int 最大值】 |
public static ExecutorService newFixedThreadPool(int nThreads) | 创建有上线的线程池 |
代码演示
public class MyRunnable implements Runnable{
@Override
public void run(){
for(int i = 1; i<= 100;i++){
System.out.println(Thread.currentThead().getName() + "---" + i);
}
}
}
public class MyTheadPoolDemo{
/*
public static ExecutorService newCachedThreadPool()创建一个没有上限的线程池【最大容量为int 最大值】 public static ExecutorService newFixedThreadPool(int nThreads)创建有上线的线程池
*/
//1.获取线程池对象
ExecutorService pool1 = Executors.newCachedThreadPool();
ExecutorService pool2 = Executors.newFixedThreadPool(3);//线程池的最大线程数为3
//提交任务
pool1.submit(new MyRunnable);
pool1.submit(new MyRunnable);
}
自定义线程池
自定义线程池 【任务拒绝策略】
任务拒绝策略 | 说明 |
---|---|
ThreadPoolExecutor.AbortPolicy | 默认策略:丢弃任务并抛出RejectedExecutionException异常 |
ThreadPoolExecutor.DiscardPolicy | 丢弃任务,但是不抛出异常,这是不推荐的做法 |
ThreadPoolExecutor.DiscardOldestPolicy | 抛弃队列中等待时间最久的任务,然后把当前任务加入队列中 |
ThreadPoolExecutor.CallerRunsPoolicy | 调用任务的run()方法绕过线程池直接执行 |
public class MyThreadPoolDemo{
public static void main(String[] args){
/*
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor
(核心线程数量,最大线程数量,空闲线程最大存活时间,任务队列,创建线程工厂,任务的拒绝策略)
参数一:核心线程数量 不能小于0
参数二:最大线程数 不能小于等于0,最大数量》=核心线程数量
参数三:空闲线程最大存活时间 不能小于0
参数四:时间单位 用TimeUnit指定
参数五:任务队列 不能为null
参数六:创建线程工厂 不能为null
参数七:任务的拒绝策略 不能为null
*/
ThreadPoolExecutor pool = new ThreadPoolExecutor(
3,//核心线程数量
6,//最大线程数
60,//空闲线程最大存活时间
TimeUnit.SECONDS,//时间单位
new ArrayBlockingQueue<>(3),//任务队列
Executorys.defaultThreadFactory(),//创建线程工厂
new ThreadPoolExecutor.AbortPolicy()//任务拒绝策略
);
}
}