一、基本概念
并发和并行:
- 并发:同一时刻,多个指令在CPU上交替运行
- 并行:同一时刻,多个指令在CPU上同时运行
二、多线程的实现方式(重点)
1.继承Thread类
- 要重写run方法,里面书写线程要执行的代码
- 然后创建这个类的对象,调用start方法,就可以打开这个线程(注意别直接调用run方法,直接调用就相当于用对象的方法,不是线程)
2.实现Runnable接口
- 也要重写run方法,里面书写要执行的代码
- 创建实现了Runnable接口的对象,这个对象表示任务
- 创建Thread类对象,把任务对象传入,调用start方法启动线程
测试类测试代码:
//主函数代码
public class ThreadTest1 {
public static void main(String[] args) {
//创建任务对象
MyThread1 m1 = new MyThread1();
//创建线程对象
Thread t1 = new Thread(m1);//任务传入
Thread t2 = new Thread(m1);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
实现了Runnable接口的类的代码:
public class MyThread1 implements Runnable {
@Override
public void run() {
//线程要执行的代码
for (int i = 0; i < 50; i++) {
//这里用不了getName方法,因为是在Thread类中的
//所以我们要先获取当前线程对象
System.out.println(Thread.currentThread().getName() + "114514");
}
}
}
3.利用Callable接口和Future接口实现
- 这种方式能获得多线程运行的结果
步骤:
- 创建一个类MyCallable实现Callable接口(注意这个接口有泛型)
- 重写call方法(有返回值,表示多线程的运行结果)
- 创建MyCallable对象(表示多线程的任务)
- 创建FutureTask对象(管理多线程的运行结果)
- 创建Thread类对象,启动多线程
4.三种方法的优缺点
- 第一种:代码简单,但是可扩展性弱,不能再继承其他类,但是可以直接使用Thread类中的方法
- 第二种和第三种:扩展性强,代码可能复杂一些,而且不能直接用Thread类中的方法
三、常见的成员方法
1.4个基本方法
public class ThreadTest1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//1.获取线程名字,如果没设置,线程有默认名字
System.out.println(t1.getName());
//2.设置线程名字
//构造方法中也能设置名字,但是注意是Thread中的构造方法,子类要用super调用这个方法
t1.setName("线程四");
//3.获取当前线程对象
Thread t = Thread.currentThread();
System.out.println(t);
//4.睡眠()ms
//时间到后,线程会自动醒来,继续往下运行
System.out.println(114);
Thread.sleep(1000);
System.out.println(514);
}
}
2.线程的优先级
线程的调度方法:
- 抢占式调度:线程随机执行(JAVA中是这种)
- 线程的优先级越高,越容易抢到
- 默认优先级是5,最低是1,最高是10
- 非抢占式调度:线程轮流执行
两个成员方法:
getPriority() 获取优先级
setPriority() 设置优先级
3.守护线程
成员方法:
setDeamon() 设置为守护线程
作用:
- 当其他非守护线程结束后,守护线程也会陆续结束(不管有没有执行完,都会陆续结束,但注意这个结束需要时间)
4.出让线程和插入线程(用的很少)
yield() 出让线程,即让出CPU的占有
join() 插入线程,把调用这个方法的线程,插入到当前线程之前
(当前线程就看写在哪)
四、同步代码块
- 在操作共享数据(static)时,多个线程可能得到重复/超出的结果,这是因为在执行中,需要执行的语句未被完全执行,CPU执行权就被抢走了
- 同步代码块就可以进行锁的操作,只有锁中的代码全部被执行完了,其他线程才能抢CPU
锁的特点:
- 锁默认打开,有一个线程进入后,锁自动关闭
- 锁内代码全部执行完毕,线程出来后,锁自动打开
格式:
synchronized(锁的对象){
//代码
}
//锁对象是任意的,写Object都可以
//但是锁对象一定要是唯一的,即加static修饰
//我们可以把这个对象当成锁的钥匙来理解
//我们一般用当前类的字节码文件当做锁对象,即xxx.class
五、同步方法
- 把synchronized关键字加到方法上
- 特点:
- 同步方法锁住方法里的所有代码
- 锁的对象不能自己指定
- 非静态方法:this
- 静态方法:当前类的字节码文件
Javabean类:
public class MyRun implements Runnable {
int ticket = 0;
@Override
public void run() {
while(true){
if (extracted()) break;
}
}
private synchronized boolean extracted() {
if(ticket == 100){
return true;
}else{
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ticket++;
System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票");
}
return false;
}
}
测试类:
- 因为我们采用多线程实现的第二种方式,是把一个任务传入多个线程,这个任务是确定唯一的,所以无需再声明为static了。(如果用第一种,创建三个线程对象,公共数据就要声明为static)
public class ThreadTest2 {
public static void main(String[] args) {
MyRun mr = new MyRun();
//创建线程
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();
}
}
六、Lock锁(JDK5以后,能够手动上锁)
- Lock是一个接口,创建对象时要用它的实现类ReenTrantLock
- 一定要注意unlock的位置,如果在解锁前出现了return,break这类语句,可能导致一条线程一直独占(没解锁),解决方法之一是用try,catch包围,finally里面解锁
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyRun implements Runnable {
int ticket = 0;
Lock lock = new ReentrantLock();
@Override
public void run() {
while(true){
lock.lock();
try {
if (extracted()) break;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
}
private boolean extracted() {
if(ticket == 100){
return true;
}else{
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ticket++;
System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票");
}
return false;
}
}
死锁:出现了锁的嵌套,容易出BUG,在用到synchronized和lock时都要注意
七、等待唤醒机制(生产者消费者模式)
- 核心思想:线程分成消费者和生产者,通过第三者(用桌子比喻)来控制二者轮流执行
1.消费者:
- 判断桌子上是否有数据
- 如果没有就等待
- 如果有就消费数据
- 消费完成后,唤醒生产者继续生产
2.生产者:
- 判断桌子上是否有数据
- 如果有就等待
- 如果没有就生产数据
- 把数据放到桌子上
- 唤醒消费者来消费
3.涉及的方法
wait() 当前线程等待,直到被唤醒
notify() 随机唤醒单个线程
notifyAll() 唤醒全部线程
4.一个简单例子:
生产者类:
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.foodflag = 1;
Desk.lock.notifyAll();
}
}
}
}
}
}
消费者类:
public class Foodie1 extends Thread{
@Override
public void run() {
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{
//总数减少,消耗了一个数据
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 final Object lock = new Object();
}
测试类:
public class ThreadTest3 {
public static void main(String[] args) {
Foodie1 f = new Foodie1();
Cook c = new Cook();
c.setName("生产者");
f.setName("消费者");
c.start();
f.start();
}
}
八、等待唤醒机制的阻塞队列式实现
阻塞队列的顶层接口:
Iterable
Collection
Queue
BlockingQueue
实现类:
ArrayBlockingQueue 底层是数组,有界
LinkedBlockingQueue 底层是链表,相对无界,但也不超过int最大值
小例子:
测试类:
public class ThreadTest4 {
public static void main(String[] args) {
ArrayBlockingQueue<String> q1 = new ArrayBlockingQueue<>(1);//这里要传上界
Cook c = new Cook(q1);
Foodie f = new Foodie(q1);
c.start();
f.start();
}
}
生产者类:
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("诶!给你整碗面吃");//注意锁在put里,而这条语句在锁外面,不过数据是安全的,只是我们打印出来看着不对劲
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
消费者类:
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);
}
}
}
}
九、初识线程池
- 上面我们写的线程代码都是一次性的,每个线程用完就没用了,这样很浪费资源,线程池就能帮我们解决这个问题
原理:
- 创建一个空的线程池
- 提交任务时会创建新的线程对象,任务执行完毕,把线程还给池子,下次再提交任务时就还用这个线程
- 如果提交任务时,池子中没有空的线程,那就创建新的
- 如果没有空线程也无法创建,那就排队等待
代码实现:
1.创建线程池:工具类Executors
public static ExecutorService newCachedThreadPool(); //没有上线
public static ExecutorService newFixedThreadPool(int nThreads); //有上限
2.提交任务
submit()
3.关闭线程池(服务器上24小时运行,所以一般不会关)
shutdown()
小例子:
public class MyThreadPool {
public static void main(String[] args) {
//1.获取线程池对象
ExecutorService pool1 = Executors.newCachedThreadPool();
//2.提交任务
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
//3.销毁线程池
pool1.shutdown();
}
}
十、自定义线程池——创建ThreadPoolExecutor对象
重点考虑七个参数
- 1.核心线程数量(不能小于0)
- 2.线程池中最大线程数量
- 3.空闲时间(值)
- 4.空闲时间(单位)
- 要用TimeUnit指定
- 5.阻塞队列(不能为null)
- 6.创建线程的方式(不能为null)
- 7.执行的任务过多时的解决方案(不能为null)
- 在ThreadPoolExecutor的静态内部类AbortPolicy中
执行过程:
- 任务加入线程池时,创建核心线程,当任务数比核心线程数大时,会进入阻塞队列来排队,当排队的任务达到阻塞队列最大值时,新的任务会进入临时线程来执行,当临时线程也满时,新的任务会被拒绝服务
小例子:
public class MyThreadPool2 {
public static void main(String[] args) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(
3, //核心线程数;
6, //最大线程数
60, //60
TimeUnit.SECONDS, //秒
new ArrayBlockingQueue<>(3),
Executors.defaultThreadFactory(), //默认线程创建
new ThreadPoolExecutor.AbortPolicy() //拒绝策略
);
}
}