java多线程(详解)

215 阅读26分钟

java多线程(详解)

线程-概述

线程(Thread)是一个程序内部的一条执行流程。

 public static void main(String[] args) {
     //代码
     for(int i=0;i<10; i++){
         System.out.println(i);
      }
     //代码
 }

上面的main方法就是一个线程,程序中如果只有一条执行流程,那这个程序就是单线程的程序

那么多线程是什么呢?

多线程是指从软硬件上实现的多条执行流程的技术(多条线程由CPU负责调度执行);

多线程一般都用在我们生活的方方面面,比如12306买车票,每次都是同时有很多人一起访问,所以就会设计多线程,性能更加的好;还有消息通信,淘宝,京东系统都离不开多线程技术

如何在程序中创建出多条线程? Java是通过java.lang.Thread类的对象来代表线程的。

线程的创建有三种方式:

创建方式一:继承Thread类,实现run 方法

  1. 定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法
  2. 创建MyThread类的对象
  3. 调用线程对象的start() 方法启动线程(启动后还是执行run方法的)
//继承Thread类
public class MyThread extends Thread{
    @Override
    public void run() {
//        super.run();
        for (int i = 0; i < 5; i++) {
            System.out.println("新的线程"+i);
        }
    }
}
​
//测试类 ,启用线程
public class demo {
    public static void main(String[] args) {
        //第一种创建线程的方式
        //导致不可以继承其他的类了
      //main是由一条默认的主线程进行执行的
        // 创建MyThread线程类的对象代表一个线程
        Thread t1=new MyThread(); //多态写法
        //启动线程
        t1.start();
​
        for (int i = 0; i <5 ; i++) {
            System.out.println("main主线程"+i);
        }
    }
}
/*执行结果:
main主线程0
main主线程1
main主线程2
main主线程3
新的线程0
新的线程1
新的线程2
新的线程3
新的线程4
main主线程4
​
*/

执行结果每次都是不一样的,因为每个线程都是自己抢资源运行

方式一优缺点:

  1. 优点:编码简单
  2. 缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展。

多线程的注意事项:

  1. 启用线程必须是调用Start方法,不是调用run 方法。

    1. 直接调用run方法会当成普通方法执行,此时相当于还是单线程执行
    2. 只有调用start 方法才是启动一个新的线程执行
  2. 不要把主线程任务放在启动子线程之前

    1. 这样主线程一直都是先跑完,相当于是一个单线程的效果了。

创建方式二:实现Runnable接口

  1. 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
  2. 创建MyRunnable任务对象
  3. 把MyRunnable任务对象交给Thread处理。
Thread类提供的构造器说明
public Thread(Runnable target)封装Runnable对象成为线程对象

调用线程对象的start()方法启动线程

public class mythread2 implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("新的线程"+i);
        }
    }
}
​
//测试类
public class demo2 {
    public static void main(String[] args) {
        // 第二种创建线程方式 实现接口 Runnable
        //创建任务对象
        Runnable my2=new mythread2();  //多态写法
        //把任务对象交个线程对象处理
        new Thread(my2).start();
​
      
    }
}
​
//执行结果:
/*
新的线程0
新的线程1
新的线程2
新的线程3
新的线程4
我是第二个线程
我是线程3
​
*/

方式二的优缺点:

  1. 优点:任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强。
  2. 缺点:需要多一个Runnable对象(其实也不算缺点相比较于第一种的缺点)

创建方式二的匿名内部类写法、然后也可以进一步简写lambda表达式

  //匿名写法,简写
 new Thread(new Runnable() {
    @Override
     public void run() {
     System.out.println("我是第二个线程");
            }
        }).start();
 //简写2. lambda 表达式
   new Thread(()->{
            System.out.println("我是线程3");  
        }).start();

创建方式三:实现Callable接口

多线程的第三种创建方式:利用Callable接口、FutureTask类来实现。

  1. 创建任务对象:

    1. 定义一个类实现Callable接口,重写call方法,封装要做的事情,和要返回的数据。
    2. 把Callable类型的对象封装成FutureTask(线程任务对象)。
  2. 把线程任务对象交给Thread对象。

  3. 调用Thread对象的start方法启动线程。

  4. 、线程执行完毕后、通过FutureTask对象的的get方法去获取线程任务执行的结果。

FutureTask提供的码疸命说明
public FutureTask<>(Callable call)把callable对象封装成FutureTask对象。
FutureTask提供的方法说明
public v get() throws Exception获取线程执行call方法返回的结果。

实现Callable 的类

public class myCallable   implements Callable<String> {
    private int n;
​
    public myCallable(int n) {
        this.n = n;
    }
​
    @Override
    public String call() throws Exception {
       int sum=0;
        for (int i = 0; i <= n; i++) {
            sum+=i;  //求1-n的和
        }
        return "最后的结果为:"+sum;
    }
}

测试类

public class demo3 {
    public static void main(String[] args) throws Exception {
        //第三种创建方式 callable
        //创建一个Callable 的对象
        Callable<String>  c1=new myCallable(100);
        String name=Thread.currentThread().getName();
        System.out.println(name);  //main
        //把callable 对象封装成为 FutureTesk 未来任务对象
        //FutureTask 实现了Runnable 接口的所以是一个任务对象
        FutureTask<String> ft=new FutureTask<>(c1);
        // 把任务对象交给一个Thread对象
        new Thread(ft).start();
        String name1=Thread.currentThread().getName();
        System.out.println(name1);//main
​
        //线程执行完了就可以通过FutureTesk中的 get 方法获取线程返回的值了
        System.out.println(ft.get()); //最后的结果为:5050
​
    }
}

线程创建方式三的优缺点:

优点∶线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;可以在线程执行完毕后去获取线程执行的结果。

缺点∶编码复杂一点。

线程-常用方法

Thread提供的常用方法

Thread提供的常用方法说明
public void run()线程的任务方法
public void start()启动线程
public string getName()获取当前线程的名称,线程名称默认是Thread-索引
public void setName(String name)为线程设置名称
public static Thread currentThread()获取当前执行的线程对象
public static void sleep(long time)让当前执行的线程休眠多少毫秒后,再继续执行
public final void join()...让调用当前这个方法的线程先执行完!

Thread提供的常用构造器

Thread提供的常见构造器说明
public Thread( String name)可以为当前线程指定名称
public Thread(Runnable target)封装Runnable对象成为线程对象
public Thread(Runnable target,String name)封装Runnable对象成为线程对象,并指定线程名称

为线程设置名字和获取线程名字

// 创建MyThread线程类的对象代表一个线程
Thread t1=new MyThread(); //多态写法
t1.setName("1号线程"); //为线程设置名字
System.out.println(t1.getName()); // 1号线程

join方法作用:让当前调用这个方法的线程先执行完

Thread t1=new MyThread();
t1.start();
t1.join();
t2.start(); //例如下面又启动了一个t2线程,这就会让t1线程执行完后,才会开始做任务

interrupt():线程中断

public class ThreadTest08 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable2());
t.setName("t");
t.start();
    // 希望5秒之后,t线程醒来(5秒之后主线程手里的活儿干完了。)
    try {
        Thread.sleep(1000 * 5);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    // 终断t线程的睡眠(这种终断睡眠的方式依靠了java的异常处理机制。)
    t.interrupt();
}
}
class MyRunnable2 implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "---> begin");
try {
// 睡眠1年
Thread.sleep(1000 * 60 * 60 * 24 * 365);
} catch (InterruptedException e) {
e.printStackTrace();
}
//1年之后才会执行这里
System.out.println(Thread.currentThread().getName() + "---> end");
}

yield(): 让位,当前线程暂停,回到就绪状态,让给其他线程

  public class ThreadTest12 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable6());
t.setName("t");
t.start();
    for(int i = 1; i <= 10000; i++) {
        System.out.println(Thread.currentThread().getName() + "--->" + i);
    }
}
}
class MyRunnable6 implements Runnable {
    @Override
public void run() {
    for(int i = 1; i <= 10000; i++) {
        //每100个让位一次。
        if(i % 100 == 0){
            Thread.yield(); // 当前线程暂停一下,让给主线程。
        }
        System.out.println(Thread.currentThread().getName() + "--->" + i);
    }
}
}

注意: 并不是每次都让成功的,有可能它又抢到时间片了。

守护线程

java语言中线程分为两大类:

  • 一类是:用户线程
  • 一类是:守护线程后台线程

其中具有代表性的就是:垃圾回收线程(守护线程)

守护线程的特点:

一般守护线程是一个死循环所有的用户线程只要结束,守护线程自动结束

注意:主线程main方法是一个用户线程。

守护线程用在什么地方呢?

每天00:00的时候系统数据自动备份。

这个需要使用到定时器,并且我们可以将定时器设置为守护线程。

一直在那里看着,没到00:00的时候就备份一次。所有的用户线程如果结束了,守护线程自动退出,没有必要进行数据备份了。

方法名作用
void setDaemon(boolean on)on 为true时表示吧线程设置为守护线程
public class ThreadTest14 {
    public static void main(String[] args) {
        Thread t = new BakDataThread();
        t.setName("备份数据的线程");
​
        // 启动线程之前,将线程设置为守护线程
        t.setDaemon(true);
​
        t.start();
​
        // 主线程:主线程是用户线程
        for(int i = 0; i < 10; i++){
            System.out.println(Thread.currentThread().getName() + "--->" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
​
class BakDataThread extends Thread {
    public void run(){
        int i = 0;
        // 即使是死循环,但由于该线程是守护者,当用户线程结束,守护线程自动终止。
        while(true){
            System.out.println(Thread.currentThread().getName() + "--->" + (++i));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
​

线程-线程优先级

常量:

常量名备注
static int MAX_PRIORITY最高优先级(10)
static int MIN_PRIORITY最低优先级(1)
static int NORM_PRIORITY默认优先级(5)

方法

方法名最高优先级(10)
int getPriority()获得线程优先级
void setPriority(int newPriority)设置线程优先级

基本使用:

public class ThreadTest11 {
    public static void main(String[] args) {
        System.out.println("最高优先级:" + Thread.MAX_PRIORITY);//最高优先级:10
        System.out.println("最低优先级:" + Thread.MIN_PRIORITY);//最低优先级:1
        System.out.println("默认优先级:" + Thread.NORM_PRIORITY);//默认优先级:5
        
        // main线程的默认优先级是:5
        System.out.println(hread.currentThread().getName() + "线程的默认优先级是:" + currentThread.getPriority());
​
        Thread t = new Thread(new MyRunnable5());
        t.setPriority(10);
        t.setName("t");
        t.start();
​
        // 优先级较高的,只是抢到的CPU时间片相对多一些。
        // 大概率方向更偏向于优先级比较高的。
        for(int i = 0; i < 10000; i++){
            System.out.println(Thread.currentThread().getName() + "-->" + i);
        }
    }
}
​
class MyRunnable5 implements Runnable {
    @Override
    public void run() {
        for(int i = 0; i < 10000; i++){
            System.out.println(Thread.currentThread().getName() + "-->" + i);
        }
    }
}

注意:

  • main线程的默认优先级是:5
  • 优先级较高的,只是抢到的CPU时间片相对多一些,大概率方向更偏向于优先级比较高的

线程安全

什么是线程安全问题?

多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题。

可以通过一个案例来理解

场景:小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,如果小明和小红同时来取钱,并且2人各自都在取钱10万元,可能会出现什么问题呢?

线程111.png

所以线程安全问题出现的原因就知道了?

  1. 存在多个线程在同时执行
  2. 同时访问一个共享资源
  3. 存在修改该共享资源

用程序模拟线程安全问题

需求:小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,模拟2人同时去取钱10万。

分析:

①:需要提供一个账户类,接着创建一个账户对象代表2个人的共享账户。

②:需要定义一个线程类(用于创建两个线程,分别代表小明和小红)。

③:创建2个线程,传入同一个账户对象给2个线程处理。

④:启动2个线程,同时去同一个账户对象中取钱10万。

账户类:account

public class Account {
    private String cardId; //卡号
    private double money; //余额
​
    public Account() {
    }
​
    public Account(String cardId, double money) {
        this.cardId = cardId;
        this.money = money;
    }
​
    //取钱
    public void getAccountmoney(double money){
        if(this.money>=money){
            //余额够的话就可以实现取钱
            String name=Thread.currentThread().getName();
            System.out.println(name+"成功取钱!"+money);
            this.money-=money;
            System.out.println("剩余的余额为"+this.money);
        }else{
            System.out.println("取钱失败,余额不足"+this.money);
        }
    }
    public String getCardId() {
        return cardId;
    }
​
    public void setCardId(String cardId) {
        this.cardId = cardId;
    }
​
    public double getMoney() {
        return money;
    }
​
    public void setMoney(double money) {
        this.money = money;
    }
}
​

线程类:

public class mythreads extends Thread{
    private Account acc;
​
    public mythreads(Account acc,String name) {
        super(name);  //设置线程的名字
        this.acc=acc;
    }
​
    @Override
    public void run() {
        acc.getAccountmoney(100000);
    }
}

测试类;

public class demo33 {
    public static void main(String[] args) {
        //先创建一个账户对象
        Account acc=new Account("icbc-1234",100000);
        //创建两个线程共享一个账户取钱
        Thread ming=new mythreads(acc,"小明");
        Thread hong=new mythreads(acc,"小红");
        ming.start();
        hong.start();
    }
}

运行结果

小红成功取钱!100000.0 小明成功取钱!100000.0 剩余的余额为0.0 剩余的余额为-100000.0

所以需要线程同步来解决

线程同步

线程同步就是解决安全问题的方案

线程同步的思想:让多个线程实现先后依次的访问共享资源,这样就解决了安全问题

线程同步的常用方案就是加锁

加锁: 每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来。

加锁总共有三种方式:

方式一:同步代码块

作用: 把访问共享资源的核心代码给上锁,以此保证线程安全。

Synchronized(同步锁){
    //访问共享资源的核心代码
}

原理: 每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行。

同步锁的注意事项

对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象) ,否则会出bug.

下面还是用的上面的线程安全问题改造的加锁,实现线程同步

//取钱
public void getAccountmoney(double money){
   synchronized (this){  //多个线程操作一个对象,所以让对象本身作为锁对象,最好
       String name=Thread.currentThread().getName();
       if(this.money>=money){
           //余额够的话就可以实现取钱
           this.money-=money;
           System.out.println(name+"成功取钱!"+money);
           System.out.println("剩余的余额为"+this.money);
       }else{
           System.out.println(name+"取钱失败!!!");
           System.out.println("取钱失败,余额不足"+this.money);
       }
   }
}

执行结果:

小明成功取钱!100000.0 剩余的余额为0.0 小红取钱失败!!! 取钱失败,余额不足0.0

锁对象随便选择一个唯一的对象好不好呢? 当然是不可以的,会影响其他无关线程的执行

锁对象的使用规范

  1. 建议使用共享资源作为锁对象,对于实例方法建议使用this作为锁对象。
  2. 对于静态方法建议使用字节码 (类名.class) 对象作为锁对象。

方式二:同步方法

作用: 把访问共享资源的核心方法给上锁,以此保证线程安全。

修饰符  synchronized  返回值类型 方法名称(形参列表){
    //操作共享资源的代码
}

原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。

默认锁对象是this,只是你自己看不到啦

//取钱  //在方法上加锁
public synchronized void getAccountmoney(double money){
    if(this.money>=money){
        //余额够的话就可以实现取钱
        String name=Thread.currentThread().getName();
        System.out.println(name+"成功取钱!"+money);
        this.money-=money;
        System.out.println("剩余的余额为"+this.money);
    }else{
        System.out.println("取钱失败,余额不足"+this.money);
    }
}

执行结果:

小明成功取钱!100000.0 剩余的余额为0.0 小红取钱失败!!! 取钱失败,余额不足0.0

同步方法底层原理

同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。

如果方法是实例方法:同步方法默认用this作为的锁对象。

如果方法是静态方法:同步方法默认用类名.class作为的锁对象。

方式三:Lock锁

Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁。更灵活、更方便、更强大

Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象

构造器说明
public ReentrantLock()获得Lock锁的实现类对象

Lock的常用方法:

方法名称说明
void lock()获得锁
void unlock()释放锁

基本用法:

先定义锁对象,然后在需要的代码前加锁,一般用捕获,把关锁放在Finally 里面,反之后面线程进不来

    private Lock lk=new ReentrantLock(); //创建一个锁对象
//取钱
public void getAccountmoney(double money){
    lk.lock(); //加锁
    try {
        if(this.money>=money){
            //余额够的话就可以实现取钱
            String name=Thread.currentThread().getName();
            System.out.println(name+"成功取钱!"+money);
            this.money-=money;
            System.out.println("剩余的余额为"+this.money);
        }else{
            System.out.println("取钱失败,余额不足"+this.money);
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        //防止前面出现异常,导致后面的线程无法进入
        lk.unlock(); //解锁
    }
   
}

线程通信

当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以相互协调,并避免无效的资源争夺。

线程通信的常见模型(生产者与消费者模型)

  1. 生产者线程负责生产数据
  2. 消费者线程负责消费生产者生产的数据。
  3. 注意:生产者生产完数据应该等待自己,通知消费者消费;消费者消费完数据也应该等待自己,再通知生产者生产!

程序实现生产消费模型:

场景:3个生产者线程,负责生产包子,每个线程每次只能生产1个包子放在桌子上2个消费者线程负责吃包子,每人每次只能从桌子上拿1个包子吃。

在这之前我们应该知道:

Object类的等待和唤醒方法:

方法名称
void wait()让当前线程等待并释放所占锁,直到另一个线程调用notify()方法或notifyAl1()方法
void notify()唤醒正在等待的单个线程
void notifyAll()唤醒正在等待的所有线程

注意:

上述方法应该使用当前同步锁对象(就是的到锁的对象)进行调用

实现场景:

桌子类:

public class Desk {
    //桌子用来放包子
    //定义一个集合用来放包子
    private List<String> list=new ArrayList<>();
​
    //放包子
    public synchronized void put(){
        try {
            String name=Thread.currentThread().getName();
            if(list.size()==0){
                //桌子上的没有包子,那么厨师就要生成一个包子,放在桌子上
                list.add("做个一个肉包子");
                System.out.println(name+"做了一个肉包子!!");
                Thread.sleep(2000);  //做东西要两秒
                notifyAll();
                wait();
            }else{
                //桌子上有包了 就唤醒别人,自己等待
                notifyAll();
                wait();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    //吃包子
    public synchronized void getbao(){
        try {
            String name=Thread.currentThread().getName();
            //如果桌子上有包子就直接吃
            if(list.size()!=0){
                list.clear(); //吃空桌上的食物
                Thread.sleep(2000); //吃东西要两秒
                System.out.println(name+"吃了一个肉包子");
                //吃完了就唤醒其他人,自己等待
                notifyAll();
                wait();
            }
            else{
                //如果没有包子直接等待
                notifyAll();
                wait();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

测试类:

public class demo1 {
    public static void main(String[] args) {
        //生产消费模型
        Desk desk=new Desk();
​
        //三个厨师,两个吃货 ,一共五个线程
        new Thread(()->{
            while (true){
                desk.put();
            }
        },"厨师一号").start();
        new Thread(()->{
            while (true){
                desk.put();
            }
        },"厨师二号").start();
        new Thread(()->{
            while (true){
                desk.put();
            }
        },"厨师三号").start();
​
        new Thread(()->{
            while (true){
                desk.getbao();
            }
        },"吃货一号").start();
        new Thread(()->{
            while (true){
                desk.getbao();
            }
        },"吃货二号").start();
​
    }
}

执行结果:

厨师一号做了一个肉包子!! 吃货二号吃了一个肉包子 厨师三号做了一个肉包子!! 吃货一号吃了一个肉包子 厨师一号做了一个肉包子!! 吃货一号吃了一个肉包子 厨师三号做了一个肉包子!!

..............

这些线程就会一直做一个吃一个做一个吃一个......

线程池

什么是线程池

线程池就是一个可以复用线程的技术。

那么为什么要使用线程池呢?如果不使用线程池用户每发起一个请求,后台就需要创建一个新线程来处理,下次新任务来了肯定又要创建新线程处理的,而创建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程出来,这样会严重影响系统的性能

线程池的工作原理

线程222.png

使用线程池就不会来一个任务就创建一个线程,而是原先定义的几个线程循环的使用的。可以减少内存的开销

如何创建线程池

JDK5.0起提供了代表线程池的接口:ExecutorService

如何的得到线程池对象?

方式一: 使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象

ThreadPoolExecutor构造器

public ThreadPoolExecutor(int corePoolSize,
                       int maximumPoolSize,
                       long keepAliveTime,
                       TimeUnit unit,
                       BlockingQueue<Runnable> workQueue,
                       ThreadFactory threadFactory,
                       RejectedExecutionHandler handler) 

参数一:corepoolSize:指定线程池的核心线程的数量

参数二:maximumPoolSize:指定线程池的最大线程数量

参数三:keepAliveTime:指定临时线程的存活时间

参数四:unit:指定临时线程存活的时间单位(秒、分、时、天)

参数五:workQueue:指定线程池的任务队列

参数六:threadFactory:指定线程池的线程工厂

参数七:handler:指定线程池的任务拒绝策略(线程都在忙,任务队列也满了的时候,新任务来了该怎么处理)

可以把构造器想象成为一个餐馆:

参数一:正式工3

参数二:最大员工数量:5 临时工就为:2

参数三:临时工空闲多久被开除

参数四:临时工空闲时间的单位

参数五:客人排队的地方

参数六:负责招聘员工的(hr)

参数七:忙不过来了怎么处理新来的客人?

ExecutorService pool=new ThreadPoolExecutor(3,5,
        8, TimeUnit.SECONDS,new ArrayBlockingQueue<>(4),
        Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());

方式二: 使用Executors(线程池的工具类) 调用方法返回不同特点的线程池对象

**Executors**是一个线程池的工具类,提供了很多静态方法用于返回不同特点的线程池对象。

方法名称说明
public static ExecutorService newFixedThreadPool(int nThreads)创建固定线程数量的线程池,如果某个线程因为执行异e而结束,那么线程池会补充一个新线程替代它
public static Executorservice newsingleThreadExecutor()创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程。
public static ExecutorService newCachedThreadPool()线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了6es则会被回收掉。
public static scheduledExecutorService newScheduledThreadPool(int corePoolsize)创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。

注意∶这些方法的底层,都是通过线程池的实现类ThreadPoolExecutor创建的线程池对象。

//通过Executors创建一个线程池对象
ExecutorService pool=Executors.newFixedThreadPool(3);
​
  //注意该出ScheduledExecutorService继承了ExecutorService。
        ScheduledExecutorService pool1=Executors.newScheduledThreadPool(3);
        //延迟执行任务
        pool1.schedule(new MyRunnable(),10,TimeUnit.SECONDS);
        //定时执行任务,没隔多长时间久执行一次
        //第一次打印出来是用来2s中,以后开始每5s打印一次。
        pool1.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                System.out.println("定时任务执行了!!!");
            }
        },2,5,TimeUnit.SECONDS);

线程池的注意事项:

  1. 临时线程什么时候创建?

    1. 新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
  2. 什么时候会开始拒绝新任务?

    1. 核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务

线程池处理Runnable 任务

ExecutorService的常用方法

方法名称说明
void execute( Runnable command)执行Runnable 任务
Future submit(Callable task)执行callable 任务,返回未来任务对象,用于获取线程返回的结果
void shutdown()等全部任务执行完毕后,再关闭线程池!
List shutdownNow()立刻关闭线程池,停止正在执行的任务,并返回队列中未执行的任务
public class createThread11 {
    public static void main(String[] args) {
        //创建线程池的第一种方式
        //创建一个线程池对象
        ExecutorService pool=new ThreadPoolExecutor(3,5,
                8, TimeUnit.SECONDS,new ArrayBlockingQueue<>(4),
                Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
        //使用线程池处理runnable任务
        //创建runnable任务对象
        Runnable target=new MyRunnable();
        pool.execute(target);  //线程池会自动创建一个新的线程,自动处理这个任务,自动执行的
        pool.execute(target); //线程池会自动创建一个新的线程,自动处理这个任务,自动执行的
        pool.execute(target); //线程池会自动创建一个新的线程,自动处理这个任务,自动执行的
        pool.execute(target);   //等待前面3个线程执行完了,有空了就找我,
        pool.execute(target);   //等待前面3个线程执行完了,有空了就找我
        pool.execute(target);   //等待前面3个线程执行完了,有空了就找我
        pool.execute(target);   //等待前面3个线程执行完了,有空了就找我,任务队列满了
        //如果现在还有任务需要进入到任务队列中,那么久需要创键临时对象了
        pool.execute(target);
        pool.execute(target);
        //所有线程都在忙了,任务队列也满了,就到了拒绝新任务的时机
        pool.execute(target);
        //等待所有任务执行完成,就关闭线程池,不然程序会一直执行的
        pool.shutdown();
​
    }
}

可能不太理解创建线程池的最后一个参数怎么写?

下面是新任务拒绝策略

策略详解
ThreadPoolExecutor.AbortPolicy丢弃任务并抛出RejectedExecutionException异常。是默认的策略
ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常这是不推荐的做法
ThreadPoolExecutor.Discard0ldestPolicy抛弃队列中等待最久的任务然后把当前任务加入队列中
ThreadPoolExecutor.CallerRunsPolicy由主线程负责调用任务的run()方法从而绕过线程池直接执行

使用线程池处理Callable 任务

主要用 Future<T> submit(Callable<T> tesk)

案例:计算1-n的和

public class demo22 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建线程池的第一种方式
        //创建一个线程池对象
        ExecutorService pool=new ThreadPoolExecutor(3,5,
                8, TimeUnit.SECONDS,new ArrayBlockingQueue<>(4),
                Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
        //使用线程池处理callable 任务
        Future<String> fu1=pool.submit(new myCalls(100));
        Future<String> fu2=pool.submit(new myCalls(200));
        Future<String> fu3=pool.submit(new myCalls(300));
        Future<String> fu4=pool.submit(new myCalls(400));
        
        //获取任务对象返回的值
        System.out.println(fu1.get());  //这是线程锁计算出来的数:5050
        System.out.println(fu2.get());  //这是线程锁计算出来的数:20100
        System.out.println(fu3.get());  //这是线程锁计算出来的数:45150
        System.out.println(fu4.get());  //这是线程锁计算出来的数:80200
​
        pool.shutdown(); //任务执行完后,关闭线程池
    }
}
​
//callable 任务类 计算1- n 的和
public class myCalls implements Callable<String> {
    private int n;
    public myCalls(int n) {
        this.n=n;
    }
​
    @Override
    public String call() throws Exception {
        String name1=Thread.currentThread().getName();
        System.out.println(name1);
        int sum=0;
        for (int i = 0; i <= n; i++) {
            sum+=i;
        }
        return "这是线程锁计算出来的数:"+sum;
    }
}
​

核心线程数量如何配置

计算密集型的任务:核心线程数量=CPU的核数+1 (比如上面的例子就是计算密集型任务,我的计算机CPU核数为16,所以我最好是设计核心线程数量为17)

IO密集型的任务:核心线程数量=CPU核数*2

注意事项executors

线程池最好不要使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

说明:Executors返回的线程池对象的弊端如下︰

  1. FixedThreadPool和SingleThreadPool :允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM;
  2. CachedThreadPool :允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM(内存用完了)

并发、并行

首先要知道什么是进程、线程?

正在运行的程序(软件)就是一个独立的进程。

线程是属于进程的,一个进程中可以同时运行很多个线程。

进程中的多个线程其实是并发和并行执行的

线程333333.png

并发的含义

进程中的线程是由CPU负责调度执行的,但CPU能同时处理线程的数量有限,为了保证全部线程都能往前执行,CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。

线程4444.png

并行的理解

在同一时刻,同时有多个线程在被CPU调度

线程5555555.png

所以多线程是并发和并行同时进行的

线程的生命周期

人类的生命周期:

婴儿----- 单身-----婚后-----死去;大概是这样从生到死,而且在其中也会有一些状态的改变,比如婚后也可能又变回单身等。

那么线程的生命周期也就是线程从生到死的过程中,经历的各种状态及状态转换。

理解线程这些状态有利于提升并发编程的理解能力。

Java线程的状态:Java总共定义了6种状态,6种状态都定义在Thread类的内部枚举类中。

public enum State {
​
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}

线程66666.png

线程状态说明
NEW(新建)线程刚被创建,但是并未启动。
Runnable(可运行)线程已经调用了start(),等待CPU调度
Blocked(锁阻塞)线程在执行的时候未竞争到锁对象,则该线程进入Blocked状态;。
waiting(无限等待)一个线程进入Waiting状态,另一个线程调用notify或者notifyAll方法才能够唤醒
Timed waiting(计时等待)同waiting状态,有几个方法(sleep,wait)有超时参数,调用他们将进入Timed waiting状态。
Teminated(被终止)因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

悲观锁和乐观锁

悲观锁:一上来就加锁,没有安全感。每次只能一个线程进入访问完毕后,再解锁。线程安全,性能较差!

乐观锁:一开始不上锁,认为是没有问题的,大家一起跑,等要出现线程安全问题的时候才开始控制。线程安全,性能较好。

就比如:一个公共厕所,有很多人去上厕所,一个人进入厕所感觉没有安全感关上门(上锁),别人就进不来了,这就是悲观锁;然而如果这个人不关门,其他的人是可以进来的就是看着你上厕所,但是真正上厕所的(处理资源)的只有一个人(乐观锁)

乐观锁,就是每次拿数据的时候都假设为别人不会修改,所以不会上锁;只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新了,要么报错,要么自动重试。