文章目录
概要
1.什么是多线程
有了多线程,可以让我们的程序同时做多件事情
2.多线程的作用
提高效率
3.多线程的应用场景
只要你想让多个事情同时运行,就需要用到多线程。比如:软件中的耗时操作、所有的聊天软件、所有的服务器。
例如:
QQ中,聊天的同时可以传输文件,这就是用了多线程。
并发和并行
并发:在同一时刻,有多条指令在单个CPU上交替执行。(单核CPU)
并行:在同一时刻,有多条指令在多个CPU上同时执行。(多核CPU)
多线程的实现方式
表头 | 优点 | 缺点 |
---|---|---|
继承Thread类 | 编程比较简单可以直接使用,无法获取多线程的返回结果 | 扩展性较差 |
实现Runna接口 | 扩展性强,实现该接口的同时还可以继承其他类 ,无法获取多线程的返回结果 | 编程相对复杂,不能直接使用Thread类中的方法 |
实现Callable接口 | 扩展性强,实现该接口的同时还可以继承其他类。但是可以获取多线程的返回结果 | 编程相对复杂,不能直接使用Thread类中的方法 |
1. 继承Thread类的方式实现
定义一个类,并且继承Thread类,重写run()方法
public class MyThread extends Thread{
@Override
public void run() {
//书写线程要执行的代码
for (int i = 0; i < 10; i++) {
//获取线程名字
System.out.println(getName()+"Hello world");
}
}
}
再定义一个启动类用来测试
1.用对象名.start()来启动线程
2.区分线程可以用**对象名.setName()**来给线程起一个名字
public class A01_ThreadDemo1 {
public static void main(String[] args) {
/*多线程的第一种启动方式
1.自己定义一个类继承Thread
2.重写run方法
3.创建子类的对象并启动线程
*/
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
//给线程起个名字(用来区分线程)
t1.setName("线程一:t1 ");
t2.setName("线程二:t2 ");
//开启线程 不能直接调用run方法
t1.start();
t2.start();
}
}
2. 实现Runnable接口的方式进行实现
public class ThreadDemo {
public static void main(String[] args) {
/*多线程的第二种启动方式
1.自己定义一个类实现Runnable接口
2.重写里边的run方法
3.创建自己的这个类的对象
4.创建一个Thread对象 并开启多线程
*/
//创建MyRunnable对象
//表示多线程要执行的任务
MyRunnable myRunnable = new MyRunnable();
//创建线程Thread对象
Thread t1 = new Thread(myRunnable);
Thread t2 = new Thread(myRunnable);
//重命名线程
t1.setName("线程1:");
t2.setName("线程2:");
//开启线程
t1.start();
t2.start();
}
}
自己创建一个类实现Runnable接口 并重写run()方法。
注意:这个方法不能像第一种 直接获得线程名称,因为这个类并没有继承Thread类,所以要先获得当前执行的线程对象,在获取线程名称
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
//获取当前执行的线程对象
Thread thread = Thread.currentThread();
//再用对象获取线程的名字
System.out.println(thread.getName()+"Hello world");
}
}
}
3. 利用Callable接口和Future接口方式实现
public class ThreadDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
/*
多线程的第三种方式
特点:可以获取多线程运行的结果
1.创建一个类MyCallable实现Callable接口
2.重写call方法(是有返回值的,表示多线程的返回结果)
3.创建MyCallable的对象(表示多线程要执行的任务)
4.创建FutureTask对象(作用:管理多线程运行的结果)
5.创建Thread对象 并启动(表示线程)
*/
//创建MyCallable的对象(表示多线程要执行的任务)
MyCallable myCallable = new MyCallable();
//创建FutureTask对象(作用:管理多线程运行的结果)
FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
//创建线程的对象
Thread thread1 = new Thread(futureTask);
//启动多线程
thread1.start();
//获取方法返回值的结果
Integer result = futureTask.get();
System.out.println(result);
}
}
写一个类用来实现Callable接口,泛型的类型要和方法的类型保持一致
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 += i;
}
return sum;
}
}
多线程中常用成员方法
1、设置线程名字和休眠线程
方法名称 | 说明 |
---|---|
String Name() | 返回线程的名字 |
void setName() | 设置线程的名字(构造方法也可以设置名字) |
static Thread currentThread() | 获取当前线程对象 |
static void sleep(long time) | 让线程休眠指定时间,单位为毫秒 |
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
/*
String getName() 返回此线程的名称
void setName(String name) 设置线程的名字(构造方法也可以设置线程名字)
细节:
1、如果我们没有给线程设置名字,线程也是有默认的名字的
格式:Thread-X(X是序号,从0开始的)
2、如果我们要给线程设置名字,可以用set方法进行设置,也可以构造方法设置
static Thread currentThread() 获取当前线程的对象
细节:
当JVM虚拟机启动之后,会自动的启动多条线程
其中有一条线程就叫做main线程
他的作用就是去调用main方法,并执行里面的代码
在以前,我们写的所有的代码,其实都是运行在main线程当中
static void sleep(long time) 让线程休眠指定的时间,单位为毫秒
细节:
1、哪条线程执行到这个方法,那么哪条线程就会在这里停留对应的时间
2、方法的参数:就表示睡眠的时间,单位毫秒
1 秒= 1000毫秒
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("11111111111");
Thread.sleep(5000);
System.out.println("22222222222");*/
}
}
public class MyThread extends Thread{
public MyThread() {
}
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + "@" + i);
}
}
}
2、线程优先级
方法名称 | 说明 |
---|---|
setPriority(int newPriority) | 设置线程的优先级,线程默认优先级是5,线程优先级的范围是:1-10 |
final int getPriority() | 获取线程的优先级 |
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "---" + i);
}
return "线程执行完毕了";
}
}
public class Demo {
public static void main(String[] args) {
//优先级: 1 - 10 默认值:5
MyCallable mc = new MyCallable();
FutureTask<String> ft = new FutureTask<>(mc);
Thread t1 = new Thread(ft);
t1.setName("飞机");
t1.setPriority(10);
//System.out.println(t1.getPriority());//5
t1.start();
MyCallable mc2 = new MyCallable();
FutureTask<String> ft2 = new FutureTask<>(mc2);
Thread t2 = new Thread(ft2);
t2.setName("坦克");
t2.setPriority(1);
//System.out.println(t2.getPriority());//5
t2.start();
}
}
3、守护线程
方法名 | 说明 |
---|---|
void setDaemon(boolean on) | 将此线程标记为守护线程,当运行的线程都是守护线程时,Java虚拟机将退出 |
应用场景:例如QQ一边聊天一边传输文件,当你把聊天框关闭了,文件传输也会断掉,此时文件传输就是守护线程。
public class MyThread1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName() + "---" + i);
}
}
}
public class MyThread2 extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + "---" + i);
}
}
}
public class Demo {
public static void main(String[] args) {
MyThread1 t1 = new MyThread1();
MyThread2 t2 = new MyThread2();
t1.setName("女神");
t2.setName("备胎");
//把第二个线程设置为守护线程
//当普通线程执行完之后,那么守护线程也没有继续运行下去的必要了.
t2.setDaemon(true);
t1.start();
t2.start();
}
}
线程安全问题
同步代码块和同步方法的区别
1.同步代码块 synchronized()
提示:
同步代码块就是把synchronized()写在方法内,将需要上锁的代码包裹起来
注意:
- synchronized(锁对象):锁对象是唯一的 一般采用当前类的字节码文件。比如MyThread.class
- synchronized(锁对象):同步代码块不能写在循环外边,会导致第一个进去的线程就全部执行完了,例如案例的第一个线程就把票卖完了
public class MyThreadDemo {
public static void main(String[] args) {
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
MyThread myThread3 = new MyThread();
myThread1.setName("窗口1:");
myThread2.setName("窗口2:");
myThread3.setName("窗口3:");
myThread1.start();
myThread2.start();
myThread3.start();
}
}
class MyThread extends Thread{
//用static修饰 表示这个类的所有对象,都共享ticket对象
static int ticket = 0;
@Override
public void run() {
while (true){
//同步代码块
// synchronized (obj) 锁 锁代码默认开启 当有一个线程进去执行的时候
//就会锁起来 其他的线程就只能在外边等待 里边的线程执行完 锁才会打开。
synchronized (MyThread.class){
if (ticket<100){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ticket++;
System.out.println(getName()+"正在卖第:"+ticket+"张票!!!");
}else {
break;
}
}
}
}
}
2.同步方法 synchronized
同步方法就是把synchronized关键字加到方法上
格式:修饰符 synchronized 返回值类型 方法名(方法参数){...}
小技巧:
不熟练的话 可以先把要锁住的代码 写在同步代码块内,然后再抽取出来,形成一个同步方法
特点:
- 同步方法会锁住方法里面的所有代码
- 锁对象不能自己指定 非静态:
this
,静态:当前类的字节码文件对象 (如MyThread.class)
public class ThreadDemo {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread1 = new Thread(myRunnable,"窗口1:");
Thread thread2 = new Thread(myRunnable,"窗口2:");
Thread thread3 = new Thread(myRunnable,"窗口3:");
thread1.start();
thread2.start();
thread3.start();
}
}
class MyRunnable implements Runnable {
int ticket = 0;
@Override
public void run() {
//1.循环
//2.同步代码块(同步方法)
//3.判断共享数据是否到了末尾,如果到了末尾
while (true) {
if (method()) break;
}
}
//非静态的 锁对象是this
private synchronized boolean method() {
if (ticket < 100) {
ticket++;
System.out.println(Thread.currentThread().getName() + "正在卖第:" + ticket + "张票!!!");
} else {
return true;
}
return false;
}
}
3.Lock锁【应用】
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化
-
ReentrantLock构造方法
方法名 说明 ReentrantLock() 创建一个ReentrantLock的实例 -
加锁解锁方法
方法名 说明 void lock() 获得锁 void unlock() 释放锁 -
代码演示 这里尽量使用try catch finally 这样才能保证释放锁一定会被执行
public class Ticket implements Runnable { //票的数量 private int ticket = 100; private Object obj = new Object(); private ReentrantLock lock = new ReentrantLock(); @Override public void run() { while (true) { //synchronized (obj){//多个线程必须使用同一把锁. try { lock.lock(); if (ticket <= 0) { //卖完了 break; } else { Thread.sleep(100); ticket--; System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticket + "张票"); } } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } // } } } } public class Demo { public static void main(String[] args) { Ticket ticket = new Ticket(); Thread t1 = new Thread(ticket); Thread t2 = new Thread(ticket); Thread t3 = new Thread(ticket); t1.setName("窗口一"); t2.setName("窗口二"); t3.setName("窗口三"); t1.start(); t2.start(); t3.start(); } }
4.死锁【应用】
-
概述
线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行
注意:
写锁的时候尽量不要让锁进行嵌套,这样就会尽量避免死锁 -
什么情况下会产生死锁
- 资源有限
- 同步嵌套
-
代码演示 :当线程一执行到了objA 锁,此时objA 关闭,但在这是执行权被线程二抢到,线程二执行到objB 锁,然后objB 锁也关闭;此时两把锁都关闭了,线程一 进不去objB 锁,线程二也进不去objA 锁。程序就会被卡住,这就是死锁
public class Demo {
public static void main(String[] args) {
Object objA = new Object();
Object objB = new Object();
new Thread(()->{
while(true){
synchronized (objA){
//线程一
synchronized (objB){
System.out.println("小康同学正在走路");
}
}
}
}).start();
new Thread(()->{
while(true){
synchronized (objB){
//线程二
synchronized (objA){
System.out.println("小薇同学正在走路");
}
}
}
}).start();
}
}
生产者消费者
-
概述
生产者消费者模式是一个十分经典的多线程协作的模式,弄懂生产者消费者问题能够让我们对多线程编程的理解更加深刻。
所谓生产者消费者问题,实际上主要是包含了两类线程:
一类是生产者线程用于生产数据
一类是消费者线程用于消费数据
为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库
生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为
消费者只需要从共享数据区中去获取数据,并不需要关心生产者的行为
-
Object类的等待和唤醒方法
方法名 | 说明 |
---|---|
void wait() | 导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法 |
void notify() | 唤醒正在等待对象监视器的单个线程 |
void notifyAll() | 唤醒正在等待对象监视器的所有线程 |
-
案例需求
-
桌子类(Desk):定义表示包子数量的变量,定义锁对象变量,定义标记桌子上有无包子的变量
-
生产者类(Cooker):实现Runnable接口,重写run()方法,设置线程任务
1.判断是否有包子,决定当前线程是否执行
2.如果有包子,就进入等待状态,如果没有包子,继续执行,生产包子
3.生产包子之后,更新桌子上包子状态,唤醒消费者消费包子
-
消费者类(Foodie):实现Runnable接口,重写run()方法,设置线程任务
1.判断是否有包子,决定当前线程是否执行
2.如果没有包子,就进入等待状态,如果有包子,就消费包子
3.消费包子后,更新桌子上包子状态,唤醒生产者生产包子
-
测试类(Demo):里面有main方法,main方法中的代码步骤如下
创建生产者线程和消费者线程对象
分别开启两个线程
-
代码实现
public class Desk {
//定义一个标记
//true 就表示桌子上有汉堡包的,此时允许吃货执行
//false 就表示桌子上没有汉堡包的,此时允许厨师执行
public static boolean flag = false;
//汉堡包的总数量
public static int count = 10;
//锁对象
public static final Object lock = new Object();
}
public class Cooker extends Thread {
// 生产者步骤:
// 1,判断桌子上是否有汉堡包
// 如果有就等待,如果没有才生产。
// 2,把汉堡包放在桌子上。
// 3,叫醒等待的消费者开吃。
@Override
public void run() {
while(true){
synchronized (Desk.lock){
if(Desk.count == 0){
break;
}else{
if(!Desk.flag){
//生产
System.out.println("厨师正在生产汉堡包");
Desk.flag = true;
Desk.lock.notifyAll();
}else{
try {
Desk.lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
}
public class Foodie extends Thread {
@Override
public void run() {
// 1,判断桌子上是否有汉堡包。
// 2,如果没有就等待。
// 3,如果有就开吃
// 4,吃完之后,桌子上的汉堡包就没有了
// 叫醒等待的生产者继续生产
// 汉堡包的总数量减一
//套路:
//1. while(true)死循环
//2. synchronized 锁,锁对象要唯一
//3. 判断,共享数据是否结束. 结束
//4. 判断,共享数据是否结束. 没有结束
while(true){
synchronized (Desk.lock){
if(Desk.count == 0){
break;
}else{
if(Desk.flag){
//有
System.out.println("吃货在吃汉堡包");
Desk.flag = false;
Desk.lock.notifyAll();
Desk.count--;
}else{
//没有就等待
//使用什么对象当做锁,那么就必须用这个对象去调用等待和唤醒的方法.
try {
Desk.lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
}
public class Demo {
public static void main(String[] args) {
/*消费者步骤:
1,判断桌子上是否有汉堡包。
2,如果没有就等待。
3,如果有就开吃
4,吃完之后,桌子上的汉堡包就没有了
叫醒等待的生产者继续生产
汉堡包的总数量减一*/
/*生产者步骤:
1,判断桌子上是否有汉堡包
如果有就等待,如果没有才生产。
2,把汉堡包放在桌子上。
3,叫醒等待的消费者开吃。*/
Foodie f = new Foodie();
Cooker c = new Cooker();
f.start();
c.start();
}
}
阻塞队列
-
常见BlockingQueue:
ArrayBlockingQueue: 底层是数组,有界LinkedBlockingQueue: 底层是链表,无界.但不是真正的无界,最大为int的最大值
-
BlockingQueue的核心方法:
方法名 | 说明 |
---|---|
put(anObject) | 将参数放入队列,如果放不进去会阻塞 |
take() | 取出第一个数据,取不到会阻塞 |
public class ThreadDemo {
public static void main(String[] args) {
/*
* 需求:利用阻塞队列完成生产者和消费者(等待唤醒机制)的代码
* 细节:
* 生产者和消费者必须使用同一个阻塞队列
* */
//创建阻塞队列的对象 必须是唯一的
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
//创建线程的对象 并把阻塞对象传过去
Cook c = new Cook(queue);
Foodie f = new Foodie(queue);
c.start();
f.start();
}
}
public class Foodie extends Thread{
ArrayBlockingQueue<String> queue;
public Foodie(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true){
//不断地把面条放到阻塞队列中
try {
queue.put("面条");
System.out.println("厨师放了一晚面条");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class Cook extends Thread{
ArrayBlockingQueue<String> queue;
public Cook(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);
}
}
}
}