Thread线程

目录

需要笔记的可以关注私聊我发给你

Thread

线程的创建方式

方式一(继承Thread类方式)

        方式二(实现Runnable方式)

方式三(实现Callable接口)

获取线程名称: String getName():

Thread类中设置线程的名字:void setName(String name)

获取当前线程的对象:public static Thread currentThread()

线程休眠:

线程有两种调度模型

安全问题

多线程操作共享资源,导致共享资源出错

同步代码块

同步代码块和同步方法的区别

同步方法

Lock锁

线程死锁

产生的条件

示例:

什么是线程死锁?

如何避免死锁?

线程状态

新建状态(NEW)

就绪状态(RUNNABLE)

阻塞状态(BLOCKED)

等待状态(WAITING)

计时状态(TIMED_WAITING)

结束状态(TERMINATED)

线程间的通讯

等待方法

唤醒方法

注意:

什么是线程间通讯?

等待和唤醒的方法调用有什么注意事项?

等待和唤醒方法的调用有什么要求?

线程进入等待使用方法

线程唤醒其他线程的方法是什么

使用等待唤醒机制实现生产者,消费者案例

桌子

厨师

吃货

测试类

线程池

线程使用存在的问题

线程池的认识

线程池使用大致流程:

线程池的好处

线程池API

线程池对象的创建

提交执行任务方法:

关闭线程池方法:

如何定义线程池

线程池处理Runnable任务的大致步骤是怎样的?

Callable与Runnable的不同点:


需要笔记的可以关注私聊我发给你

Thread


线程的创建方式


方式一(继承Thread类方式)

优点:代码实现比较简单那,可以直接使用Thread类中功能
缺点:扩展性比较差,只能继承Thread类,任务执行完毕没有返回值结果,有异常能捕获
        基本步骤 :
        1 创建一个类继承Thread类。
        2 在类中重写run方法(线程执行的任务放在这里)
        3 创建线程对象,调用线程的start方法开启线程。

                    需求 :
        我们启动一个Java程序,其实默认就存在一个主线程(main方法所在线程)
        接下来,我们在主线程启动一个线程,打印1到100的数字,主线程启动完线程后又打印1到100的数字。
        此时主线程和启动的线程在并发执行,观察控制台打印的结果。
         Thread类中设置线程的名字
            void setName(String name):将此线程的名称更改为等于参数 name
            通过构造方法也可以设置线程名称

 

public class MyThread01 {
    public static void main(String[] args) {  // 主线程
        // 3 创建自定义线程类的对象
        MyThread myThread = new MyThread();
        // 4 开启线程
        myThread.start();


        // 主线程中 : 打印1-100数据
        //  Thread.currentThread() : 获取Thread类线程对象
        for (int i = 1; i <= 100; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}

// 1 自定义类继承Thread类
class MyThread extends Thread {
    // 2 重写Thread类中的run方法,在run方法中定义线程执行的任务
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            // Thread类 : getName() ==> 获取线程名字
            System.out.println(getName() + ":" + i);
        }
    }
}


        方式二(实现Runnable方式)

优点:代码实现方式比较简单,扩展性较强还可以继续其他的类,
缺点:不能直接使用Thread类中的功能,任务执行完毕没有返回结果,有异常只能捕获
基本步骤 :
        1 定义任务类,实现Runnable,并重写run方法
        2 创建任务对象
        3 创建Thread类型的对象 , Thread类的构造方法需要接受一个Runnable实现类对象
        4 调用线程的start方法,开启线程
        Thread类
            public Thread(Runnable target) : 接受一个Runnable接口的实现类对象
            public Thread(Runnable target , String name) :
                第一个参数 : 接受一个Runnable接口的实现类对象
                第二个参数 : 给线程设置名字

                    需求 :
        我们启动一个Java程序,其实默认就存在一个主线程(main方法所在线程)
        接下来,我们在主线程启动一个线程,打印1到100的数字,主线程启动完线程后又打印1到100的数字。
        此时主线程和启动的线程在并发执行,观察控制台打印的结果。

public class MyThread02 {
    public static void main(String[] args) {// 主线程
        // 3 创建任务类对象
        MyRunnable runnable = new MyRunnable();
        // 4 创建线程对象(Thread类对象) , 把任务类对象作为参数传递给Thread类的构造方法
        Thread t = new Thread(runnable);
        // 5 开启线程
        t.start();

        // 在main线程中打印1-100
        for (int i = 1; i <= 100; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}

// 1 自定义类 实现 Runnable接口
// 任务类
class MyRunnable implements Runnable {
    // 2 重写Runnable接口中的抽象run方法 , 在run方法中定义线程要执行的任务
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            // Thread类 ===> getName()
            // Thread.currentThread() : 获取线程Thread对象
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}

方式三(实现Callable接口)

优点:扩展性比较强,还可以继承其他的类,任务执行完毕会有一个返回结果,如果任务中出现了异常可以抛出可以捕获
缺点:实现太过复杂,不能直接使用Thread类中的功能
基本步骤 :
        1 定义任务类,实现Callable接口,并重写call方法,并有返回值
        2 创建任务对象
        3.定义桥梁 , 属于Thread 和 Callable 之间桥梁
        FutureTask<String> task = new FutureTask<>(myCallable);
        4 创建Thread类型的对象 , Thread类的构造方法需要接受一个FutureTask实现类对象
        5 调用线程的start方法,开启线程
        Thread类
            public Thread(FutureTask task) : 接受一个FutureTask接口的实现类对象
        6.实现线程第三种方式Callable  , 线程执行完毕会有一个返回值 , 获取的方式通过FutureTask对象调用get方法
        System.out.println(task.get());// 阻塞

 

package com.itheima.thread_demo;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/*
    实现线程的第三种方式 : 实现Callable接口
 */
public class MyThread03 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 3 创建任务对象
        MyCallable myCallable = new MyCallable();
        // 4 定义桥梁 , 属于Thread 和 Callable 之间桥梁
        FutureTask<String> task = new FutureTask<>(myCallable);
        // 5 创建一个线程
        Thread t = new Thread(task);
        t.setName("新线程");
        // 6 开启线程
        t.start();

        // 实现线程第三种方式Callable  , 线程执行完毕会有一个返回值 , 获取的方式通过FutureTask对象调用get方法
        System.out.println(task.get());// 阻塞

        // main线程中打印一个话
        for (int i = 0; i < 100; i++) {
            System.out.println("main方法执行...");
        }
    }
}


// 1 自定义一个类(任务类) ,实现Callable接口
class MyCallable implements Callable<String> {
    // 2 重写Callable接口中call方法
    @Override
    public String call() throws Exception {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
        return "线程任务执行完毕!";
    }
}

    常用功能


获取线程名称: String getName():


            返回此线程的名称

       MyThread myThread = new MyThread();
        // 设置线程名
       System.out.println(getName())


Thread类中设置线程的名字:void setName(String name)


            将此线程的名称更改为等于参数name

       MyThread myThread = new MyThread();
        // 设置线程名
        myThread.setName("新线程");


            构造方法也可以设置线程名称
    public Thread(Runnable target,String name)
                

Thread t=new Thread(myRunnable ,"新线程")


获取当前线程的对象:public static Thread currentThread()


            返回对当前正在执行的线程对象的引用
           

Thread.currentThread() : 获取当先线程对象 , 返回的是Thread类的对象
            System.out.println(Thread.currentThread().getName())


线程休眠:

public static void sleep(long time)

    让线程休眠指定的时间,单位为毫秒

        public static void sleep(long time) : 当前线程进行休眠 , time代表的是毫秒值
        Thread.sleep(5000);


public void join():

    具备阻塞作用,等待这个线程死亡,才执行其他线程
              

 thread.join();


线程有两种调度模型


        1 分时调度模型   :所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
        2 抢占式调度模型 :让优先级高的线程优先使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程
           获取的 CPU 时间片相对多一些
            public final void setPriority(int newPriority)    设置线程的优先级
       

public final void setPriority(int newPriority)    设置线程的优先级
        // 优先级从低到高 1-10
        thread.setPriority(1);


            public final int getPriority()        获取线程的优先级 , 线程不指定优先级,默认优先级为5


安全问题


多线程操作共享资源,导致共享资源出错


            出现线程安全问题的条件
                1.多线程程序
                2.有共享资源
                3.有多条语句操作共享资源

解决方案:
       java允许多线程并发执行,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证该变量的唯一性和准确性。

同步代码块


锁住多条语句操作共享数据,可以使用同步代码块实现
格式:
       synchronized(任意对象) { 
               多条语句操作共享数据的代码 
       }
    
   默认情况锁是打开的,只要有一个线程进去执行代码了,锁就会关闭
   当线程执行完出来了,锁才会自动打开
   锁对象可以是任意对象 , 但是多个线程必须使用同一把锁

什么叫代码的同步?

        使用同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证该变量的唯一性和准确性。
同步代码块的定义格式?
 

        synchronized(同步锁){   
        多个线程访问共享数据的代码
}


同步代码块中的锁是什么有什么要求?
         是一个对象 可以是任意类型任意对象
          多个线程需要使用同一个锁对象
同步的好处和弊端    
好处:解决了多线程的数据安全问题
弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率

                        锁住多条语句操作共享数据,可以使用同步代码块实现
格式:
       

synchronized(任意对象) { 
               多条语句操作共享数据的代码 
       }


    
   默认情况锁是打开的,只要有一个线程进去执行代码了,锁就会关闭
   当线程执行完出来了,锁才会自动打开
   锁对象可以是任意对象 , 但是多个线程必须使用同一把锁

同步代码块和同步方法的区别


同步代码块可以锁住指定代码,同步方法是锁住方法中所有代码
 同步代码块可以指定锁对象,同步方法不能指定锁对象


同步方法


同步方法:就是把synchronized关键字加到方法上,保证线程执行该方法的时候,其他线程只能在方法外等着
格式:
       

修饰符 synchronized 返回值类型 方法名(方法参数) {    }


注意 : 同步方法时不能指定锁对象的 , 但是有默认存在的锁对象的。
对于非static方法,同步锁就是this。       
对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。   Class类型的对象

同步方法是如何定义的,有什么特点?

使用关键字synchronized关键字修饰的方法就是同步方法,该方法多个线程并发访问时只能唯一访问

同步方法的锁对象是什么?
对于非static方法,同步锁就是this。        
对于static方法,我们使用当前方法所在类的字节码对象(类名.class) ,  Class类型的对象

Lock锁


虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,
为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock

Lock中提供了获得锁和释放锁的方法
void lock():获得锁
void unlock():释放锁

Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化
ReentrantLock的构造方法
ReentrantLock​():创建一个ReentrantLock的实例


注意:多个线程使用相同的Lock锁对象,需要多线程操作数据的代码放在lock()和unLock()方法之间。一定要确保unlock最后能够调用

创建Lock锁对象

private static final ReentrantLock lock =new ReentrantLock();


调用lock()方法获得锁

lock.lock()


调用unLock()释放锁

lock.unlock()


方法二:

Object lock =new Object();
synchronized (lock){
    
}


线程死锁


        死锁是一种少见的,而且难于调试的错误,在两个线程对两个同步锁对象具有循环依赖时,就会大概率的出现死锁。我们要避免死锁的产生。否则一旦死锁,除了重启没有其他办法的。


产生的条件


1.多个线程
2.存在锁对象的循环依赖

示例:


        如左下图代码中,当线程一获取了锁A,进一步要获取锁B才能继续执行。于此同时线程二获取了锁B,那么此时线程一就得等待线程二的锁B释放了,然而线程二也在等待线程一的锁A,因此出现了相互等待的现象,程序不动了

            


什么是线程死锁?


            简单来说 : 同步代码块的锁进行嵌套使用 , 就会大概率产生死锁


如何避免死锁?


            不使用锁的嵌套。


线程状态


新建状态(NEW)


                创建线程对象


就绪状态(RUNNABLE)


                start方法


阻塞状态(BLOCKED)


                无法获得锁对象


等待状态(WAITING)


                wait方法


计时状态(TIMED_WAITING)


                sleep对象


结束状态(TERMINATED)


                全部代码运行完毕


线程间的通讯


        线程间的通讯技术就是通过等待和唤醒机制,来实现多个线程协同操作完成某一项任务,例如经典的生产者和消费者案例。等待唤醒机制其实就是让线程进入等待状态或者让线程从等待状态中唤醒


等待方法


            两个方法调用会导致当前线程释放掉锁资源。   
void wait()   让线程进入无限等待。 

package com.itheima.waitnotify_demo;

/*
    Object类 :  wait()

    1 线程进入无限等待 : wait()方法
        注意:进入无限等待需要使用锁在同步代码中调用wait方法

        wait() 和 notify() 方法 必须使用锁对象调用
 */
public class Test1 {
    public static void main(String[] args) {
        Object lock = new Object();

        new Thread(() -> {
            synchronized (lock) {
                System.out.println("A线程开始执行");
                System.out.println("A线程进入无线等待状态");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();

    }
}


void wait(long timeout)   让线程进入计时等待
 

 package com.itheima.waitnotify_demo;

/*
    3 线程进入计时等待并唤醒
        注意:
            1 进入计时等待的线程,时间结束前可以被其他线程唤醒。时间结束后会自动唤醒
            2 sleep方法只有时间到了 , 自动苏醒

    void wait(long timeout) : 让线程进入计时等待 , timeout代表long类型毫秒值

    wait(5000) : 5秒钟之内还可以被唤醒
    sleep(5000) : 只能5秒钟才可以自动醒
 */
public class Test3 {
    public static void main(String[] args) {
        Object lock = new Object();

        new Thread(() -> {
            synchronized (lock) {
                System.out.println("A线程开始执行");
                System.out.println("A线程进入计时等待状态");
                try {
                    lock.wait(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("A线程自动苏醒");
            }
        }, "A").start();
    }
}


唤醒方法


                两个方法调用不会导致当前线程释放掉锁资源。
                    void notify()  随机唤醒在此对象监视器(锁对象)上等待的单个线程。
                    void notifyAll() 唤醒在此对象监视器上等待的所有线程。 


注意:

等待和唤醒的方法,都要使用锁对象调用(需要在同步代码块中使用)。
等待和唤醒方法应该使用相同的锁对象调用。

什么是线程间通讯?

线程间的通讯技术就是通过等待和唤醒机制,来实现多个线程协同操作完成某一项任务,例如经典的生产者和消费者案例


等待和唤醒的方法调用有什么注意事项?


            等待的方法会释放锁,唤醒的方法不会释放锁
            等待和唤醒的方法,都要使用锁对象调用(需要在同步代码块中使用)。
            等待和唤醒方法应该使用相同的锁对象调用。


等待和唤醒方法的调用有什么要求?


            需要使用锁对象调用,需要在同步代码中完成


线程进入等待使用方法


            wait进入无限等待,wait(时间)计时等待


线程唤醒其他线程的方法是什么


            notify , notifyAll


使用等待唤醒机制实现生产者,消费者案例

桌子

package 生产者和消费者;

import java.util.concurrent.locks.Lock;

//桌子
public class Desk {
    //汉堡个数
    public static int hanbao=1;
    //定义唯一的lock锁对象
    public static final Object lock = new Object();
}

厨师

 package 生产者和消费者;

import java.awt.*;

/**
 * 消费者步骤:
 * 1,判断桌子上是否有汉堡包。
 * 2,如果没有就等待。
 * 3,如果有就开吃
 * 4,吃完之后,桌子上的汉堡包就没有了
 *      叫醒等待的生产者继续生产
 *      汉堡包的总数量减一
 *
 *      生产者:厨师
 */
public class Cooker implements Runnable {

    @Override
    public void run() {
        //死循环:让程序一直执行,方便查看
        while (true) {
            //同步代码块
           synchronized (Desk.lock){
               //1.判断桌子上是否有汉堡
               if (Desk.hanbao == 1) {
                   //有汉堡:就等待吃货来吃
                   try {
                       Desk.lock.wait();//等待
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               } else {
                   //没有汉堡,就开始生产
                   System.out.println(Thread.currentThread().getName()+"生产了一个汉堡");
                   Desk.hanbao=1;//汉堡+1,放在桌子上
               } Desk.lock.notify();//叫醒消费者开始吃
           }
        }
    }
}

吃货

package 生产者和消费者;

import java.awt.*;

/**
 * 消费者步骤:
 * 1,判断桌子上是否有汉堡包。
 * 2,如果没有就等待。
 * 3,如果有就开吃
 * 4,吃完之后,桌子上的汉堡包就没有了
 *      叫醒等待的生产者继续生产
 *      汉堡包的总数量减一
 *
 *     消费者:吃货
 */
public class Foodie implements Runnable {
    @Override
    public void run() {
        //死循环:让程序一直执行,方便查看
        while (true) {
            //同步代码块
            synchronized (Desk.lock) {
                //判断桌子上是否有食物
                if (Desk.hanbao == 1) {
                    //有汉堡就吃
                    System.out.println(Thread.currentThread().getName() + "吃了一个汉堡");
                    Desk.hanbao = 0;//修改桌子上汉堡数量
                } else {
                    //没有汉堡,等待(厨师制作)
                    try {
                        Desk.lock.wait();//等待
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                Desk.lock.notify();//唤醒厨师
            }
            try {
                Thread.sleep(1000);//睡眠,方便观察
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}

测试类

 package 生产者和消费者;

public class Test {
    public static void main(String[] args) {
        Cooker cooker = new Cooker();
        new Thread(cooker, "厨师").start();

        Foodie foodie = new Foodie();
        new Thread(foodie, "吃货").start();
    }
}

线程池


线程使用存在的问题


            如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。


如果大量线程在执行,会涉及到线程间上下文的切换,会极大的消耗CPU运算资源。

线程池的认识


            其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

线程池使用大致流程:


            1.创建线程池指定线程开启的数量
            2.提交任务给线程池,线程池中的线程就会获取任务,进行处理任务。
            3.线程处理完任务,不会销毁,而是返回到线程池中,等待下一个任务执行。
            4.如果线程池中的所有线程都被占用,提交的任务,只能等待线程池中的线程处理完当前任务。

线程池的好处


            降低资源消耗
                减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
            提高响应速度
                当任务到达时,任务可以不需要等待线程创建 , 就能立即执行。
            提高线程的可管理性
                可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存 , 服务器死机 (每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

线程池API


线程池对象的创建

package com.itheima.threadpool_demo;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/*
    1 需求 :
        使用线程池模拟游泳教练教学生游泳。
        游泳馆(线程池)内有3名教练(线程)
        游泳馆招收了5名学员学习游泳(任务)。

    2 实现步骤:
        1 创建线程池指定3个线程
        2 定义学员类实现Runnable,
        3 创建学员对象给线程池
 */
public class Test1 {
    public static void main(String[] args) {
        // 获取Java提供的线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(3);

        // 给线程提交任务
        // threadPool.submit(Runnable接口实现类对象);
        threadPool.submit(new Student("小红"));
        threadPool.submit(new Student("小刚"));
        threadPool.submit(new Student("小丽"));
        threadPool.submit(new Student("小明"));
        threadPool.submit(new Student("小黑"));

        // 关闭线程池
        // threadPool.shutdown();
    }
}

// 任务类
class Student implements Runnable {
    private String name;

    public Student(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "教" + name + "学习游泳");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(name + "学习完毕");
    }
}


 java.util.concurrent.ExecutorService 是线程池接口类型。使用时我们不需自己实现,JDK已经帮我们实现好了。
获取线程池我们使用工具类java.util.concurrent.Executors的静态方:

public static ExecutorService newFixedThreadPool (int num) 指定线程池最大线程池数量获取线程池

提交执行任务方法:


<T> Future<T>  submit(Callable<T> task) Future<?> submit(Runnable task)


关闭线程池方法:


       (一般不使用关闭方法,除非后期不用或者很长时间都不用,就可以关闭)
void shutdown()  启动一次顺序关闭,执行以前提交的任务,但不接受新任务。

如何定义线程池


                使用工具类Executors的静态方法:
public static ExecutorService newFixedThreadPool (int num) 指定线程池最大线程池数量获取线程池

线程池处理Runnable任务的大致步骤是怎样的?


1.指定线程数量获取线程池
2.定义Runnable任务类型
3.创建任务对象,提交给线程池

Callable与Runnable的不同点:


                    Callable可以 抛出异常,Runnable不行
                    Callable支持结果返回,Runnable不行
                
MyCallable

package 线程池Callable;

import javax.sql.rowset.CachedRowSet;
import java.util.concurrent.Callable;

public class MyCallable implements Callable<Integer> {
    private  int n;
    //构造器给成员变量传参赋值
    public MyCallable(int n) {
        this.n = n;
    }
    @Override
    public Integer call() throws Exception {

        int sum=0;//求和变量
        for (int i = 0; i <= n; i++) {
            sum+=i;
        }
        return sum;
    }
}

Test

 package 线程池Callable;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(3);//三个线程
        MyCallable myCallable = new MyCallable(100);//创建线程实现类并传入参数
        Future<Integer> future = pool.submit(myCallable);//提交线程任务
        System.out.println(future.get());
        pool.shutdown();//关闭线程池
    }
}

 并发工具类Semaphore

      控制线程并发量
        public Semaphore(int permits)
            创建信号量,指定并发线程数量
                

public static Semaphore semaphore =new Semaphore(2)


        public void acquire()
            获取许可证(获取锁)
                

semaphore.acquire()


        public void release()
            释放许可证(释放锁)
                

semaphore.release()


版权声明:本文为anyi2351033836原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。