多线程基础

1. 线程相关的概念

  1. 程序:是为了完成特定的任务,用某种语言编写的一组指令的集合,简单的来说就是我们的代码。
  2. 进程:进程时指运行中的程序,比如QQ,启动QQ就启动了一个进程,操作系统就会为该进程分配内存空间。进程时程序的一次执行过程,或是正在运行的一个程序。是动态的过程,它有自身的产生、存在、消亡的过程。
  3. 线程:线程由进程创建,是进程的一个实体,一个进程可以拥有多个线程。
  4. 单线程:同一时刻,只允许执行一个线程。
  5. 多线程:同一时刻,可以执行多个线程,例如QQ可以打开多个窗口。一个迅雷可以下载多个文件/软件
  6. 并发:同一时刻,多个任务交替执行,造成一种”貌似同时“的错觉,简单来说,单核cpu实现的多任务就是并发
  7. 并行:同一时刻,多个任务同时执行,多核cpu可以实现并行

1.1 用java查看cpu

public class CpuNum {
    public static void main(String[] args) {
        Runtime runtime = Runtime.getRuntime();
        int number = runtime.availableProcessors();
        System.out.println("个数为"+number);
    }
}

请添加图片描述

2. 创建线程的方式

2.1 继承Thread类,重写run方法

Thread类实际上也实现了Runnable接口
请添加图片描述

2.1.1 题目

开启一个线程,要求该线程每隔一秒在控制台输出 ”喵喵,我是小猫咪“

public class Thread01 {
    public static void main(String[] args) {
        Cat cat = new Cat();
        cat.start();
    }
}
// 当一个类继承了Thread类,该类就可以当作线程使用
class Cat extends Thread{
    // 重写Run方法,在里面写上自己的业务逻辑
    @Override
    public void run() {
        while (true) {
            // 每隔1秒输出语句
            System.out.println("喵喵,我是小猫咪");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

请添加图片描述
对上题进行改进:当输出80次后,结束该线程。

public class Thread01 {
    public static void main(String[] args) {
        Cat cat = new Cat();
        cat.start();
    }
}
// 当一个类继承了Thread类,该类就可以当作线程使用
class Cat extends Thread{
    int times = 0;
    // 重写Run方法,在里面写上自己的业务逻辑
    @Override
    public void run() {
        while (true) {
            // 每隔1秒输出语句
            System.out.println("喵喵,我是小猫咪");
            times++;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (times==80){
                // 推出while循环,此时线程也就退出了
                break;
            }
        }
    }
}

注意:当我们启动main方法的时候,会进入到一个进程,然后执行main方法,其实就是启动了一个线程,之后在main方法中执行了start方法,这时就启动了一个线程。

看上述代码,当我们执行start方法的时候,主线程不会阻塞(即不会等到cat.start方法执行完了之后再执行下面的代码),它还会执行下面的方法。

例子:

public class Thread01 {
    public static void main(String[] args) throws InterruptedException {
        Cat cat = new Cat();
        cat.start();
        for (int i=0;i<100;i++){
            System.out.println("这是主线程"+i+"主线程的名称"+Thread.currentThread().getName());
            Thread.sleep(1000);
        }
    }
}
// 当一个类继承了Thread类,该类就可以当作线程使用
class Cat extends Thread{
    int times = 0;
    // 重写Run方法,在里面写上自己的业务逻辑
    @Override
    public void run() {
        while (true) {
            // 每隔1秒输出语句
            System.out.println("喵喵,我是小猫咪"+"我的线程名称是"+Thread.currentThread().getName());
            times++;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (times==80){
                // 推出while循环,此时线程也就退出了
                break;
            }
        }
    }
}

请添加图片描述
所以 主线程和子线程是交替执行的

我们将主线程的for循环调整至10次,继续执行

请添加图片描述
重要:得到的结论就是当主线程结束的时候,如果还有其余线程没有结束,那么整个应用程序是不会结束的

2.1.2 JConsole查看线程的执行情况

执行main方法后切换到控制台执行命令jconsole即可

请添加图片描述

2.1.3 为什么是执行start

当执行start方法后会启动线程是因为内部调用run方法,为什么不直接使用run方法呢?

直接使用run方法

public class Thread01 {
    public static void main(String[] args) throws InterruptedException {
        Cat cat = new Cat();
        cat.run();
        for (int i=0;i<10;i++){
            System.out.println("这是主线程"+i+"主线程的名称"+Thread.currentThread().getName());
            Thread.sleep(1000);
        }
    }
}
// 当一个类继承了Thread类,该类就可以当作线程使用
class Cat extends Thread{
    int times = 0;
    // 重写Run方法,在里面写上自己的业务逻辑
    @Override
    public void run() {
        while (true) {
            // 每隔1秒输出语句
            System.out.println("喵喵,我是小猫咪"+"我的线程名称是"+Thread.currentThread().getName());
            times++;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (times==10){
                // 推出while循环,此时线程也就退出了
                break;
            }
        }
    }
}

请添加图片描述
**可以看到执行cat.run方法的时候直接阻塞在这个方法,而且线程是main线程,也就是主线程,而不是Thread01,直到cat.run方法执行结束后才执行下面的for循环。 由此可见,只有start方法才是真正启动线程,run方法只是一个普通的方法 **

源码查看:

public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

    private native void start0();

可以看到里面调用了strat0方法,这是native方法,不是由java语言编写,真正实现多线程的是start0方法。

start方法调用start0方法后,该线程并不会立马执行,只是将线程变成了可运行状态,具体什么时候执行,取决于cpu,由cpu统一调度。

2.2 实现Runnable接口,重写run方法

说明

  1. java是单继承的,在某些情况下一个类可能已经继承了某个父类,这时在用继承Thread类的方法来创建线程显然是不可能了
  2. java提供了另一个方式创建线程,就是通过实现Runnable接口来创建线程

2.2.1 代码演示

案例:编写程序,使得该程序每隔1秒在控制台输出“hi”,当输出10次后自动退出。

public class Thread02 {
    public static void main(String[] args) {
        T2 t2 = new T2();
        
    }
}
class T2 implements Runnable{
    
    int count = 0;

    @Override
    public void run() {
        while(true){
            System.out.println("hi,我的线程名称是"+Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (count>=10){
                break;
            }
        }
    }
}

此时一个问题出来了,Runnable接口没有start方法,只有run方法,但是通过上述例子已经得知run方法不能创建一个线程,run方法只是一个普通的方法,那么应该如果进行开启一个线程呢?

public class Thread02 {
    public static void main(String[] args) {
        T2 t2 = new T2();
        Thread thread = new Thread(t2);
        thread.start();
    }
}
class T2 implements Runnable{

    int count = 0;

    @Override
    public void run() {
        while(true){
            System.out.println("hi,我的线程名称是"+Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (count>=10){
                break;
            }
        }
    }
}

这里我们创建Thread实例,调用其一个构造器,将t2实例放入构造器中,再调用Thread实例的start方法

为什么呢?因为这里底层使用了静态代理模式

2.2.2 代码模拟,实现Runnable接口开发线程的机制

创建一个类ThreadProxy来模拟极简的Thread类

public class Thread02 {
    public static void main(String[] args) {
        Tiger tiger = new Tiger();// 实现Runnable接口
        ThreadProxy threadProxy = new ThreadProxy(tiger);
        threadProxy.start();
    }
}

// 你可以把Proxy当作Thread类来对待,你把他看作一个Thread类,它是线程代理类,模拟了极简的Thread类
class ThreadProxy implements Runnable {

    // 熟悉targer,类型是Runnable
    private Runnable target = null;

    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

    public ThreadProxy(Runnable target){
        this.target = target;
    }

    public void start(){
        start0();
    }

    public void start0(){
        run();
    }
}

class Animal{

}
class Tiger extends Animal implements Runnable{

    @Override
    public void run() {
        System.out.println("老虎嗷嗷叫");
    }
}

首先明白什么是代理模式,通俗语言来说就是我没有一个东西,但是你有,所以就需要你来帮忙做。

上述代码中,Runnable接口中没有start方法,所以我们再ThreadProxy类中接收实现Runnable接口的类,之后在ThreadProxy类中创建start方法,在start方法立马执行实现Runnable接口类的run方法。这就是代理模式,即tiger类实现Runnable没有start方法,我们创建ThreadProxy类,将tiger传进ThreadProxy中,在ThreadProxy类中执行start方法,但是此start方法执行的run方法还是tiger自己的。

2.3 多个子线程案例

案例:编写一个程序,创建两个线程,一个线程每隔1秒输出 “hello world” 输出10次,退出。另一个线程每隔1秒输出 “hi” 输出5次退出。要求用实现Runnable方式实现。

public class Thread03 {
    public static void main(String[] args) {
        Thread thread = new Thread(new T1());
        Thread thread1 = new Thread(new T3());
        thread.start();
        thread1.start();
    }
}

class T1 implements Runnable{

    int count = 0;
    @Override
    public void run() {
        while(true) {
            System.out.println("hello,world");
            count++;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (count >= 10) {
                break;
            }
        }
    }
}

class T3 implements Runnable{
    int count = 0;
    @Override
    public void run() {
        while(true) {
            System.out.println("hi");
            count++;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (count >= 10) {
                break;
            }
        }
    }
}

请添加图片描述
我们看到貌似输出没有规律。后面会对此进行说明

2.4 继承Thread和实现Runnable的区别

  1. 从java的设计来看,通过继承Thread或者实现Runnable接口来创建线程本质上没有区别,因为Thread类本身就实现了Runnable接口
  2. 实现Runnable接口更适合多个线程共享一个资源的情况,而且避免了单继承的限制性
public class Thread04 {
    public static void main(String[] args) {
        T5 t5 = new T5();
        Thread thread = new Thread(t5);
        Thread thread1 = new Thread(t5);
        thread.start();
        thread1.start();
    }
}
class T5 implements Runnable{

    @Override
    public void run() {
        
    }
}

我们创建一个类实现Runnable,在main方法中,创建T5实例,然后创建多个Thread类实例,调用start方法,就是启动多个线程,但是因为传入的都是T5实例,所以其实多个线程执行的run方法都是同一个run方法

2.5 多线程售票

案例:编程模拟三个售票窗口同时售票100张,分别使用继承Thread类和实现Runnable接口的方式,并分析会有什么问题

2.5.1 继承Thread类

public class SellTicket {
    public static void main(String[] args) {
        SellTicket01 sellTicket01 = new SellTicket01();
        SellTicket01 sellTicket02 = new SellTicket01();
        SellTicket01 sellTicket03 = new SellTicket01();
        sellTicket01.start();
        sellTicket02.start();
        sellTicket03.start();
    }
}

class SellTicket01 extends Thread{
    private static int ticketNumber = 100;
    @Override
    public void run() {
        while(true){
            if (ticketNumber<=0){
                System.out.println("售票结束,退出程序");
                break;
            }
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ticketNumber--;
            System.out.println("当前线程 "+Thread.currentThread().getName()+"售出了一张票 "+"剩余票数 "+ticketNumber);
        }
    }
}

请添加图片描述
问题来了,超卖了

为什么会出现现在这种情况呢?

因为现在有三个线程,在共同抢夺一个资源ticketNumber,三个线程都在while循环,假如此时tickerNumber只剩1张了,现在三个线程都同时进入到判断ticketNumber是否<=0,且都通过了,所以三个线程一起减票,所以就出现超卖的情况。

2.5.2 实现接口的方式

public class SellTicketByRunnable {
    public static void main(String[] args) {
        SellTicketByRunnable2 sell = new SellTicketByRunnable2();
        Thread thread = new Thread(sell);
        Thread thread2 = new Thread(sell);
        Thread thread3 = new Thread(sell);
        thread.start();
        thread2.start();
        thread3.start();
    }
}
class SellTicketByRunnable2 implements Runnable{
    private  int ticketNumber = 100;
    @Override
    public void run() {
        while(true){
            if (ticketNumber<=0){
                System.out.println("售票结束,退出程序");
                break;
            }
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ticketNumber--;
            System.out.println("当前线程 "+Thread.currentThread().getName()+"售出了一张票 "+"剩余票数 "+ticketNumber);
        }
    }
}

请添加图片描述

其实还是出现了超卖的现象

3. 线程终止

基本说明:

  1. 当线程完成任务后,会自动退出
  2. 还可以通过控制变量来控制run方法退出的方式来停止线程

案例:启动线程 T 要求在main线程中去停止 T 请编程实现

4. 线程的常用方法

第一组:

  1. setName
  2. getName
  3. start
  4. run
  5. setPriority设置线程优先级 MAX_PRIORITY:10 ,MIN_PRIORITY:1 ,NORM_PRIORITY:5
  6. getPriority:获取线程优先级
  7. sleep
  8. interrupt:中断线程,注意中断线程不是终止线程。所以一般用于中断正在休眠的线程

演示interrupt

public class ThreadExit_ {
    public static void main(String[] args) throws InterruptedException {
        T t = new T();
        t.start();
        t.interrupt();
        
    }
}
class T extends Thread{
    @Override
    public void run() {
        while(true){
            try {
                System.out.println("线程还在运行中");
                Thread.sleep(50000);
            } catch (InterruptedException e) {
                System.out.println("被Interrupted了");
            }

        }
    }
}

请添加图片描述
后面在一直sleep,因为我们只调用了一次interrupt方法

第二组:

  1. yield:线程的礼让。让出cpu资源,让其他线程执行,但是礼让的时间不确定,所以也不一定礼让成功。
  2. join:线程的插队,插队的线程一旦插队成功,则肯定先执行完插队的线程的所有任务。

案例:创建一个子线程,每隔1s输出hello,输出20次,主线程每隔1s,输出hi,输出20次。要求两个线程同时执行,当主线程输出5次后,就让子线程全部运行完毕,主线程再继续输出。

演示join

public class YieldJoin {
    public static void main(String[] args) throws InterruptedException {
        T t = new T();
        t.start();
        for (int i=0;i<20;i++){
            System.out.println("hi");
            Thread.sleep(1000);
            if (i==4){
                t.join();// 让子线程插队
            }
        }
    }
}

class T extends Thread {
    int count = 0;
    @Override
    public void run() {
        while (true) {
            try {
                System.out.println("hello");
                count++;
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (count >= 20) {
                break;
            }
        }
    }
}

请添加图片描述

演示yield方法

public class YieldJoin {
    public static void main(String[] args) throws InterruptedException {
        T t = new T();
        t.start();
        for (int i=0;i<20;i++){
            System.out.println("hi");
            Thread.sleep(1000);
            if (i==4){
                Thread.yield();
            }
        }
    }
}

class T extends Thread {
    int count = 0;
    @Override
    public void run() {
        while (true) {
            try {
                System.out.println("hello");
                count++;
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (count >= 20) {
                break;
            }
        }
    }
}

因为礼让不一定成功,礼让是和cpu内核相关,如果cpu资源丰富,那就基本不会礼让,只有在资源紧缺的是时候才会体现出来礼让
请添加图片描述

5. 用户线程和守护线程

  1. 用户线程:也叫工作线程,当线程的任务执行完或通知方式结束
  2. 守护线程:一般是为工作线程服务的,当所有的用户线程结束,守护线程会自动结束
  3. 最常见的守护线程:垃圾回收机制

5.1 将用户线程设置为守护线程

public class ThreadMethod03 {
    public static void main(String[] args) throws InterruptedException {
        T2 t2 = new T2();
        t2.start();
        for (int i=1;i<=10;i++){
            System.out.println("主线程");
            Thread.sleep(1000);
        }
    }
}

class T2 extends Thread {
    @Override
    public void run() {
        while (true) {
            try {
                System.out.println("hello");
                
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

上述代码中,主线程结束后,子线程还在继续执行。所以子线程不是守护线程,因为守护线程它是所有线程结束后就结束

那么如何设置用户线程为守护线程?

setDaemon方法可以将用户线程设置为守护线程

public class ThreadMethod03 {
    public static void main(String[] args) throws InterruptedException {
        T2 t2 = new T2();
        t2.setDaemon(true);// 将用户线程设置为守护线程
        t2.start();
        for (int i=1;i<=5;i++){
            System.out.println("主线程");
            Thread.sleep(1000);
        }
    }
}

class T2 extends Thread {
    @Override
    public void run() {
        while (true) {
            try {
                System.out.println("hello");

                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

请添加图片描述

6. 线程的生命周期

  1. JDK中用Thread.State枚举表示了线程的 6 种状态(官方)
    请添加图片描述

  2. 线程状态迁移图
    请添加图片描述

7. 线程同步机制

根据之前的买票问题,会出现超卖的问题

线程同步机制:

  1. 在多线程编程,一些敏感的数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何同一时刻,最多有一个线程访问,以此来保证数据的完整性。
  2. 也可以理解:线程同步,即当有一个线程在对内存进行操作时,其他线程都不能对这个内存地址进行擦欧总,直到该线程完成操作,其他线程才能对该内存地址进行操作。

7.1 Synchronized

使用方法:

  1. 同步代码块
synchronized (对象){ // 这个对象就相当于是一把锁
	// 需要被同步的代码
}
  1. synchronized声明整个方法
public synchronized void m (String name){
	// 需要被同步的代码
}

7.2 解决售票问题

public class SellTicketByRunnable {
    public static void main(String[] args) {
        SellTicketByRunnable2 sell = new SellTicketByRunnable2();
        Thread thread = new Thread(sell,"窗口1");
        Thread thread2 = new Thread(sell,"窗口2");
        Thread thread3 = new Thread(sell,"窗口3");
        thread.start();
        thread2.start();
        thread3.start();
    }
}
class SellTicketByRunnable2 implements Runnable{
    private  int ticketNumber = 100;
    private boolean loop = true;
    private static Object object;
    public synchronized   void sell(){
        if (ticketNumber<=0){
            System.out.println("售票结束,退出程序");
            loop=false;
            return;
        }
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
            ticketNumber--;
            System.out.println("当前线程 " + Thread.currentThread().getName() + "售出了一张票 " + "剩余票数 " + ticketNumber);

    }
    @Override
    public  void run() {
        while(loop){
            sell();
        }
    }
}

将sell方法单独抽出为卖票方法,并且给该方法加上synchronized
请添加图片描述
请添加图片描述

这是优化实现RUnnable接口的售票,我们只是实例化一个SellTicketByRunnable2 对象,然后创建三个Thread类并且传入SellTicketByRunnable2 对象。所以本质上执行的还是一个对象的方法,而且锁对象也是同一个。但是如果是继承Thread类呢?还能这样子写吗

我们使用继承Thrad类的类.class即可

7.3 互斥锁

  1. java语言中引入互斥锁的概念来保证共享数据操作的完整性
  2. 每个对象都对应于一个可称为 “互斥锁” 的标记,这个标记用来保证在任意时刻,只有一个线程能访问该对象
  3. 关键字synchronized来与对象的互斥锁联系,当某个对象用synchronized修饰时,表明该对象在任一时刻只能由一个线程访问
  4. 同步的局限性:导致程序执行效率降低
  5. 同步方法(非静态)的锁可以是this,也可以是其他对象(要求是同一个对象)
  6. 同步方法(静态的)的锁为当前类本身

注意事项:

  1. 同步方法如果没有使用static修饰,默认锁对象为this
  2. 如果方法使用static修饰,默认锁对象为 当前类.class
  3. 实现的落地步骤:1:需要分析上锁的代码 2:选择同步代码块或者同步方法 3:要求多个线程的锁对象为同一个即可

8. 线程死锁

基本结束: 多个线程都占用了对方的锁资源,但是不肯相让,导致了死锁,在编程中一定要避免死锁的发生。

public class DeadLock_ {
    public static void main(String[] args) {
        DeadLockDemo deadLockDemo = new DeadLockDemo(true);
        DeadLockDemo deadLockDemo2 = new DeadLockDemo(false);
        deadLockDemo.start();
        deadLockDemo2.start();
    }
}
class DeadLockDemo extends Thread{
    static Object o1 = new Object();
    static Object o2 = new Object();
    boolean flag;
    public DeadLockDemo(boolean flag){
        this.flag = flag;
    }


    @Override
    public void run() {
        if (flag){
            synchronized (o1){
                System.out.println(Thread.currentThread().getName()+"进入1");
                synchronized (o2){
                    System.out.println(Thread.currentThread().getName()+"进入2");
                }
            }
        }else {
            synchronized (o2){
                System.out.println(Thread.currentThread().getName()+"进入3");
                synchronized (o1){
                    System.out.println(Thread.currentThread().getName()+"进入4");
                }
            }
        }
    }
}

请添加图片描述
出现死锁,所以这种写法一定要避免

9. 释放锁

以下操作会释放锁:

  1. 当前线程的同步方法,同步代码块执行结束
  2. 当前线程在同步代码块,同步方法中遇见break,return
  3. 当前线程在同步代码块,同步方法中出现了未处理的error和exception,导致异常结束
  4. 当前线程在同步代码块,同步方法中执行了线程对象的wait方法,当前线程暂停,并释放锁

以下操作不会释放锁:

  1. 线程执行同步代码块或者同步方法时,程序调用Thread.sleep或者Thread.yield方法暂停当前线程执行,不会释放锁
  2. 线程执行同步代码块时,其他线程调用了该线程的suspend方法将该线程挂起,该线程不会释放锁
  3. 理应避免suspend和resume方法来控制线程,不推荐

10. sleep和wait的异同

相同:一旦执行方法,都可以使得当前线程进入阻塞状态
不同:

  1. 两个方法声明的位置不同,Thread类中声明sleep,object类中声明wait
  2. 调用的要求不同,sleep可以在任何需要的场景下调用,wait必须在同步代码块中进行调用
  3. 如果两个方法都使用在同步代码块或方法中,sleep不会释放锁,wait会释放锁

11. Lock

  1. 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
  2. java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
  3. ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁

使用方法

class A {
    private final ReentrantLock lock = new ReenTrantLock();

    public void m() {
        lock.lock();
        try {
//保证线程安全的代码; }
        finally{
            lock.unlock();
        } 
    }
}

11.1 synchronized 与 Lock 的对比

  1. Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放
  2. Lock只有代码块锁,synchronized有代码块锁和方法锁
  3. 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)

使用优先级:
Lock > 同步代码块(已经进入了方法体,分配了相应资源) > 同步方法(在方法体之外)

12. 创建线程的另外两种方式

12.1 实现Callable

与使用Runnable相比, Callable功能更强大些

  1. 相比run()方法,可以有返回值
  2. 方法可以抛出异常
  3. 支持泛型的返回值
  4. 需要借助FutureTask类,比如获取返回结果

Future接口

  1. 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
  2. FutrueTask是Futrue接口的唯一的实现类
  3. FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值

实例:

public class Callable_ {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建实现了Callable的接口的实例
        NumThread numThread = new NumThread();
        // 获取call方法返回值,如果不需要的返回值,就不需要创建FutureTask
        FutureTask futureTask = new FutureTask(numThread);
        // 真正的执行还是需要创建Thread
        Thread thread = new Thread(futureTask);
        
        thread.start();
        // 获取call方法的结果
        Object sum = futureTask.get();
        
        System.out.println(sum);
    }
}

class NumThread implements Callable {

    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}

请添加图片描述

12.2 使用线程池

背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。

思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。

好处:

  1. 提高响应速度(减少了创建新线程的时间)
  2. 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
  3. 便于线程管理
public class ThreadPool {
    public static void main(String[] args) {
        // 创建提供指定线程数量的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        // 适合使用Runnable
//        executorService.execute();
        // 适合使用于Callable
        executorService.submit(new NumThread1());
        // 关闭线程池
        executorService.shutdown();
    }
}
class NumThread1 implements Callable {

    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}

创建线程池的七个参数

  1. corePoolSize:核心线程数,也是线程池中常驻的线程数,线程池初始化时默认是没有线程的,当任务来临时才开始创建线程去执行任务
  2. maximumPoolSize:最大线程数,在核心线程数的基础上可能会额外增加一些非核心线程,需要注意的是只有当workQueue队列填满时才会创建多于corePoolSize的线程(线程池总线程数不超过maxPoolSize)
  3. keepAliveTime:非核心线程的空闲时间超过keepAliveTime就会被自动终止回收掉,注意当corePoolSize=maxPoolSize时,keepAliveTime参数也就不起作用了(因为不存在非核心线程);
  4. unit:keepAliveTime的时间单位
  5. workQueue:用于保存任务的队列,可以为无界、有界、同步移交三种队列类型之一,当池子里的工作线程数大于corePoolSize时,这时新进来的任务会被放到队列中
  6. threadFactory:创建线程的工厂类,默认使用Executors.defaultThreadFactory(),也可以使用guava库的ThreadFactoryBuilder来创建
  7. handler:线程池无法继续接收任务(队列已满且线程数达到maximunPoolSize)时的饱和策略,取值有AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值