多线程
实现方法
- 1.继承Thread类的方式进行实现
- 2.实现Runnable接口的方式进行实现
- 3.利用Callable接口和Future接口方式实现
继承Thread类的方式进行实现
步骤
- 1.定义一个类继承Thread
- 2.重写run方法
- 3.创建子类对象,并启动线程(start方法
代码
//在类中
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("hello world!"+this.getName());
}
}
}
//在main中
public class Main {
public static void main(String[] args) {
MyThread mt1 = new MyThread();
MyThread mt2 = new MyThread();
mt1.setName("t1");
mt2.setName("t2");
mt1.start();
mt2.start();
}
}
实现Runnable接口的方式进行实现
步骤
- 1.自己定义一个类实现Runnable接口
- 2.重写里面的run方法
- 3.创建自己的类的对象
- 4.创建一个Thread类的对象,并开启线程
代码
//在类中
public class MyThread implements Runnable {
@Override
public void run() {
//获取当前线程的对象
Thread t = Thread.currentThread();
//执行逻辑
for (int i = 0; i < 10; i++) {
System.out.println("hello world!"+t.getName());
}
}
}
//在main中
public class Main {
public static void main(String[] args) {
//创建MyThread对象
MyThread mt1 = new MyThread();
MyThread mt2 = new MyThread();
//创建线程对象
Thread t1 = new Thread(mt1);
Thread t2 = new Thread(mt2);
//给线程设置名字
t1.setName("t1");
t2.setName("t2");
//开启线程
t1.start();
t2.start();
}
}
利用Callable接口和Future接口方式实现
- 特点
- 对上述方法的补充
- 可以获取多线程运行的结果
步骤
- 1.创建一个类实现Callable接口
- 2.重写call(是有返回值的,表示多线程运行的结果)
- 3.创键实现Callable接口的对象(表示多线程要执行的任务
- 4.创建FutureTask对象(作用管理多线程运行的结果
- 5.创建Thread类的对象,并启动(表示线程
代码
//实现Callable的类
public class MyThread implements Callable {
//返回值只能写包装类
@Override
public Integer call() throws Exception {
int sum =0;
for (int i = 0; i < 20; i++) {
sum+=i;
}
return sum;
}
}
//main
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创键实现Callable接口的对象
MyThread mt = new MyThread();
//创建实现Future接口的对象
FutureTask<Integer> ft = new FutureTask<>(mt);
//创建线程的对象
Thread t1 = new Thread(ft);
//启动线程
t1.start();
//获取多线程运行的结果
Integer result = ft.get();
System.out.println(result);
}
三种方法对比
方法 | 优点 | 缺点 |
---|---|---|
Thread类 | 编程简单 | 可拓展性差,使得类不能继承其他类 |
Runnable类 | 拓展性强,类可以继承其他类的同时实现该接口 | 编程较为复杂 |
Callable类 | 拓展性强,类可以继承其他类的同时实现该接口,可以返回多线程的处理结果 | 编程较为复杂 |
常见的成员方法
方法名称 | 说明 |
---|---|
String getName() | 返回此线程的名称 |
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 join() | 插入线程/插队线程 |
- 解释:
- 守护进程:守护进程就是在其他进程结束之后,再陆续结束(及时守护进程代码是循环1000次,也会及时结束
- 例如:就像骑士守护公主一样,一旦公主不存在了,骑士也就没有了存在的意义。
- 出让线程:出让CPU的执行权
- 插入线程:就是在运行别的线程时优先运行插入的线程,当插入的线程运行完毕后再运行之前的线程。
- 守护进程:守护进程就是在其他进程结束之后,再陆续结束(及时守护进程代码是循环1000次,也会及时结束
线程的声明周期
线程安全问题
-
例如一个影院分三个窗口,出售100张电影票。如果用之前学习到的方法写一个程序,你会发现有一些问题。例如:窗口出售重复的票,出售的票超过100张等问题。之类问题属于资源竞争问题,就是100张票的变量被三个进程访问,票数来不及更新就被下一个进程引用,从而出现上述问题。
-
代码
//线程类
public class MyThread extends Thread {
static int tickets = 100;
@Override
public void run() {
//获取当前线程信息
Thread t = Thread.currentThread();
while(tickets>0){
tickets--;
System.out.println(t.getName() + "获取电影票,剩余" + tickets + "张!");
}
}
}
//main
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread mt1 = new MyThread();
MyThread mt2 = new MyThread();
MyThread mt3 = new MyThread();
mt1.setName("1");
mt2.setName("2");
mt3.setName("3");
mt1.start();
mt2.start();
mt3.start();
}
}
- 部分结果
3获取电影票,剩余97张!
1获取电影票,剩余97张!
2获取电影票,剩余97张!
1获取电影票,剩余96张!
3获取电影票,剩余96张!
2获取电影票,剩余95张!
...
2获取电影票,剩余2张!
1获取电影票,剩余1张!
3获取电影票,剩余0张!
2获取电影票,剩余-1张!
1获取电影票,剩余-2张!
解决方法
同步代码块
-
将操作共享数据的代码锁起来(实现在并发执行过程中,同一时刻只有一个线程能访问程序)
-
特点:
- 1.锁默认打开,有一个线程进去了,所自动关闭。
- 2.里面的代码全部执行完毕,线程出来,锁自动打开。
-
语法:
-
synchronized(锁对象){} //锁的对象一定要是惟一的 //例如: static Object Obj = new Object(); synchronized(Obj){}
-
-
上述代码加锁结果:
-
1获取电影票,剩余99张! 1获取电影票,剩余98张! 1获取电影票,剩余97张! 1获取电影票,剩余96张! 1获取电影票,剩余95张! 1获取电影票,剩余94张!
-
-
同步代码块的细节:
synchronized(锁对象){}
不要写在循环的外面。- 锁的对象一定要是唯一的
同步方法
-
把
synchronized
关键字加到方法上。 -
语法格式:
修饰符 synchronized 返回值类型 方法名(方法参数){...}
-
特点:
-
- 同步方法时锁住方法里面所有的代码
- 锁对象不能自己指定
-
-
代码:(将之前同步代码块的抽取成一个方法体
-
public class MyThread extends Thread { static int tickets = 0; private static final int MaxTickets = 100; static Object obj = new Object(); @Override public void run() { while(true) { if (sellTicket()) break; } } private synchronized boolean sellTicket() { if (tickets >= 100) { return true; } else { tickets++; System.out.println(Thread.currentThread().getName() + "获取电影票,剩余" + tickets + "张!"); } return false; } }
-
-
结果
-
1获取电影票,剩余1张! 2获取电影票,剩余2张! 3获取电影票,剩余3张! 2获取电影票,剩余5张! 1获取电影票,剩余4张! 2获取电影票,剩余7张! 3获取电影票,剩余6张!
-
Lock锁
-
特点:
-
Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作。
-
Lock中提供获取锁和释放锁的方法
-
-
语法:
- void lock(); 获取锁
- void unlock();释放锁
-
细节:
- Lock是接口不能直接实例化,需要采用实现类ReentrantLock来实例化
- ReentrantLock的构造方法
- ReentrantLock();创建一个ReentrantLock()的实例
- 检查程序退出前时,是否进行释放锁的操作
-
代码
-
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class MyThread extends Thread { static int tickets = 0; private static final int MaxTickets = 100; //创建锁对象 /* 注意:lock需要用static修饰(指明锁是所有对象共享的) * */ static Lock lock = new ReentrantLock(); @Override public void run() { while(true) { //上锁 lock.lock(); if (tickets >= MaxTickets) { //释放锁 lock.unlock(); break; } else { tickets++; System.out.println(Thread.currentThread().getName() + "获取电影票,剩余" + tickets + "张!"); } //释放锁 lock.unlock(); } } }
-
-
改进(上述写法需要考虑lock.unlock()的位置,引入try…catch…finally{}改进)
-
finally{}中的代码在程序退出前,一定会执行。
-
public void run() { while(true) { //上锁 lock.lock(); try { if (tickets >= MaxTickets) { break; } else { tickets++; System.out.println(Thread.currentThread().getName() + "获取电影票,剩余" + tickets + "张!"); } }finally { lock.unlock(); } } }
-
死锁
- 死锁(Deadlock)是指在一个多进程(或多线程)的并发环境中,两个或多个进程(或线程)因争夺资源而造成的一种互相等待的现象,使得它们中的每一个都无法继续执行下去,在开发过程中应该避免死锁出现。
等待唤醒机制
常见方法
-
方法名称 说明 void wait()
当前线程等待,直到被其他线程唤醒。 void notify()
随机唤醒单个线程 void notifyAll()
唤醒所有线程 -
细节:
- 上述方法在使用的时候需要锁对象调用,例如:
lock.wait(),lock.notifyAll()
,原因方便系统知道该唤醒哪些线程,而不是唤醒所有的线程。
- 上述方法在使用的时候需要锁对象调用,例如:
-
生产者消费者模型代码
//消费者 public class Consumer extends Thread{ @Override public void run() { DoConsume(); } //消费者消费逻辑 public void DoConsume() { while(true){ synchronized (Desk.lock){ if(Desk.MaxNum!=0){ //有食物就消耗 if(Desk.ProduceExit){ Desk.MaxNum--; Desk.ProduceExit = false; System.out.println("消费者消费了一份食物!"); //通知生产者 Desk.lock.notifyAll(); }else{ try{ Desk.lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } }else{ break; } } } } } //生产者 public class Producer extends Thread { @Override public void run() { DoProduce(); } //生产者逻辑 public void DoProduce(){ while(true){ synchronized(Desk.lock){ if(Desk.MaxNum!=0){ //没有食物就生产食物 if (!Desk.ProduceExit){ Desk.ProduceExit=true; System.out.println("生产者生产一份食物!"); //通知消费者 Desk.lock.notifyAll(); }else{ //有食物就进行等待 try{ Desk.lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } }else{ break; } } } } } //中间变量Desk public class Desk { //产品最大数量 static int MaxNum = 3; //是否存在产品 static boolean ProduceExit = false; //创建一个锁对象 static final Lock lock = new ReentrantLock(); } //Main函数 public class Main { public static void main(String[] args) throws ExecutionException, InterruptedException { Consumer c = new Consumer(); Producer p = new Producer(); p.start(); c.start(); } }
-
结果
生产者生产一份食物! 消费者消费了一份食物! 生产者生产一份食物! 消费者消费了一份食物! 生产者生产一份食物! 消费者消费了一份食物!
等待唤醒机制(阻塞队列方法
-
LinkedBlockingQueue
底层是链表 -
ArrayBlockingQueue
底层是数组 -
LinkedBlockingQueue<类型> name = new LinkedBlockingQueue<>(cap);
类型
:指定阻塞队列中的数据类型name
:阻塞队列的名称cap
:指定队列的最大容量
-
常用方法
-
方法 说明 void put(参数) 向阻塞队列存入数据 返回值类型 take() 向阻塞队列拿取数据 -
细节:
- 阻塞队列中,在底层源码已经利用锁控制同步代码块了。所以我们不需要利用锁、同步代码块等操作同步我们的代码。