【java 多线程笔记】

一、概念    

  •         java 给多线程编程提供了内置的支持。 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
  • 多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。
  • 这里定义和线程相关的另一个术语
  •          进程:一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。
  •         多线程能满足程序员编写高效率的程序来达到充分利用 CPU 的目的。
  • 单线程:一个进程中包含一个顺序控制流(一条执行路径)
  • 多线程:一个进程中包含多个顺序控制流(多条执行路径)
  •         在java语言中:线程A和线程B,堆内存和方法区内存共享。但是栈内存独立,一个线程一个栈。假设启动10个线程,会有10个栈空间,每个栈和每个栈之间,互不干扰,各自执行各自的,这就是多线程并发。
  •         java中之所以有多线程机制,目的就是为了提高程序的处理效率。对于单核的CPU来说,不能够做到真正的多线程并发,但是可以做到给人一种“多线程并发”的感觉。对于单核的CPU来说,在某一个时间点上实际上只能处理一件事情,但是由于CPU的处理速度极快,多个线程之间频繁切换执行,跟人来的感觉是多个事情同时在做。

二、线程的生命周期

线程是一个动态执行的过程,它也有一个从产生到死亡的过程。

下图显示了一个线程完整的生命周期

  • 新建状态:

    使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。

  • 就绪状态:

    当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

  • 运行状态:

    如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

  • 阻塞状态:

    如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:

    • 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。

    • 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。

    • 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。

  • 死亡状态:

    一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

三、创建线程的方法

创建一个线程,最简单的方法是创建一个实现 Runnable 接口的类。

为了实现 Runnable,一个类只需要执行一个方法调用 run(),声明如下:

1、自定义一个MyRunnable类来实现Runnable接口

2、在MyRunnable类中重写run()方法

3、创建Thread对象,并把MyRunnable对象作为Tread类构造方法的参数传递进去

4、启动线程

public class Demo02 {
    public static void main(String[] args) {
        MyRunnable myRun = new MyRunnable();//将一个任务提取出来,让多个线程共同去执行
        //封装线程对象
        Thread Thread01 = new Thread(myRun, "线程01");
        Thread Thread02 = new Thread(myRun, "线程02");
        Thread Thread03 = new Thread(myRun, "线程03");
        //开启线程
        Thread01.start();
        Thread02.start();
        Thread03.start();
        //通过匿名内部类的方式创建线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 20; i++) {
                    System.out.println(Thread.currentThread().getName() + " - " + i);
                }
            }
        },"线程04").start();
    }
}
//自定义线程类,实现Runnable接口
//这并不是一个线程类,是一个可运行的类,它还不是一个线程。
class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            System.out.println(Thread.currentThread().getName() + " - " + i);
        }
    }
}

Java 提供了三种创建线程的方法:

  • 通过实现 Runnable 接口;
  • 通过继承 Thread 类本身;
  • 通过 Callable 和 Future 创建线程              

通过实现 Runnable 接口来创建线程 

继承Thread类创建线程实例     

public class Demo {
    public static void main(String[] args) {
        //通过new对象创建线程
        MyThread Thread1 = new MyThread();
        MyThread Thread2 = new MyThread();
        MyThread Thread3 = new MyThread("线程03");
 
        Thread1.start();
        Thread2.start();
        Thread3.start();
        //设置线程名(补救的设置线程名的方式)
        Thread1.setName("线程01");
        Thread2.setName("线程02");
        //设置主线程名称
        Thread.currentThread().setName("主线程");
        for (int i = 0; i < 50; i++) {
            
            System.out.println(Thread.currentThread().getName() + ":" + i);//获取当前正在执行线程的对象 
        }
    }
}
class MyThread extends Thread{
    public MyThread() {
    }
 
    public MyThread(String name) {
        super(name);
    }
 
    //需要实现该run方法
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
//循环打印出此时的线程名
            System.out.println(this.getName() + ":" + i);
        }
    }
}

     此处最重要的为start()方法。单纯调用run()方法不会启动线程,不会分配新的分支栈。start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了。线程就启动成功了。启动成功的线程会自动调用run方法(由JVM线程调度机制来运作的),并且run方法在分支栈的栈底部(压栈)。run方法在分支栈的栈底部,main方法在主栈的栈底部。run和main是平级的。单纯使用run()方法是不能多线程并发的。

Thread

上述方法是被 Thread 对象调用的,下面表格的方法是 Thread 类的静态方法。

序号方法描述
1public static void yield()
暂停当前正在执行的线程对象,并执行其他线程。
2public static void sleep(long millisec)
在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
3public static boolean holdsLock(Object x)
当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。
4public static Thread currentThread()
返回对当前正在执行的线程对象的引用。
5

public static void dumpStack()
将当前线程的堆栈跟踪打印至标准错误流。

上述方法是被 Thread 对象调用的,下面表格的方法是 Thread 类的静态方法。

序号方法描述
1public static void yield()
暂停当前正在执行的线程对象,并执行其他线程。
2public static void sleep(long millisec)
在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
3public static boolean holdsLock(Object x)
当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。
4public static Thread currentThread()
返回对当前正在执行的线程对象的引用。
5public static void dumpStack()
将当前线程的堆栈跟踪打印至标准错误流。

 

通过 Callable 和 Future 创建线程

​​
  • 1. 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。

  • 2. 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。

  • 3. 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。

  • 4. 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。

  • 这种方式的优点:可以获取到线程的执行结果。这种方式的缺点:效率比较低,在获取t线程执行结果的时候,当前线程受阻塞,效率较低。

 

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

public class Demo03 {
    public static void main(String[] args) throws Exception {
 
        // 第一步:创建一个“未来任务类”对象。
        // 参数非常重要,需要给一个Callable接口实现类对象。
        FutureTask task = new FutureTask(new Callable() {
            @Override
            public Object call() throws Exception { // call()方法就相当于run方法。只不过这个有返回值
                // 线程执行一个任务,执行之后可能会有一个执行结果
                // 模拟执行
                System.out.println("call method begin");
                Thread.sleep(1000 * 10);
                System.out.println("call method end!");
                int a = 100;
                int b = 200;
                return a + b; //自动装箱(300结果变成Integer)
            }
        });
 
        // 创建线程对象
        Thread t = new Thread(task);
 
        // 启动线程
        t.start();
 
        // 这里是main方法,这是在主线程中。
        // 在主线程中,怎么获取t线程的返回结果?
        // get()方法的执行会导致“当前线程阻塞”
        Object obj = task.get();
        System.out.println("线程执行结果:" + obj);
        // main方法这里的程序要想执行必须等待get()方法的结束
        // 而get()方法可能需要很久。因为get()方法是为了拿另一个线程的执行结果
        // 另一个线程执行是需要时间的。
        System.out.println("hello world!");
    }
}

线程控制

  • void yield()    使当前线程让步,重新回到争夺CPU执行权的队列中,暂停当前正在执行的线程对象,并执行其他线程yield()方法不是阻塞方法。让当前线程让位,让给其它线程使用。yield()方法的执行会让当前线程从“运行状态”回到“就绪状态”。注意:在回到就绪之后,有可能还会再次抢到
  • static void sleep(long ms)    使当前正在执行的线程停留指定的毫秒数
  • void join()    等死(等待当前线程销毁后,再继续执行其它的线程)
  • void interrupt()    终止线程睡眠

线程的调度

线程调度模型

  1. 均分式调度模型:所有的线程轮流使用CPU的使用权,平均分配给每一个线程占用CPU的时间。
  2. 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么就会随机选择一个线程来执行,优先级高的占用CPU时间相对来说会高一点点。

Java中JVM使用的就是抢占式调度模型

  1. getPriority():获取线程优先级
  2. setPriority:设置线程优先级

线程的安全

数据安全问题

  • 是否具备多线程的环境
  • 是否有共享数据
  • 是否有多条语句操作共享数据

例如:我和小明同时取一个账户的钱,我取钱后数据还没返回给服务器,小明又取了,这个时候小明的余额还是原来的。

如何解决?线程排队执行(不能并发),线程同步机制。

变量对线程安全的影响

  • 实例变量:在堆中。
  • 静态变量:在方法区。
  • 局部变量:在栈中。

 以上三大变量中:

  • 局部变量永远都不会存在线程安全问题。因为局部变量不共享。(一个线程一个栈)局部变量在栈中。所以局部变量永远都不会共享。
  • 实例变量在堆中,堆只有1个。
  • 静态变量在方法区中,方法区只有1个。
  • 堆和方法区都是多线程共享的,所以可能存在线程安全问题。

 解决方法:线程同步,给线程加上锁,修饰符 synchronized 

        

    线程同步的利弊

  • 好处:解决了线程同步的数据安全问题

  • 弊端:当线程很多的时候,每个线程都会去判断同步上面的这个锁,很耗费资源,降低效率

  1. 第一种方案:尽量使用局部变量代替“实例变量和静态变量”。  
  2. 第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了。(一个线程对应1个对象,100个线程对应100个对象, 对象不共享,就没有数据安全问题了。)
  3. 第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synchronized了。线程同步机制。

守护线程

java语言中线程分为两大类:
            一类是:用户线程
            一类是:守护线程(后台线程)
            其中具有代表性的就是:垃圾回收线程(守护线程)。

        守护线程的特点:
            一般守护线程是一个死循环,所有的用户线程只要结束,
            守护线程自动结束。
        
注意:主线程main方法是一个用户线程。

        守护线程用在什么地方呢?
            每天00:00的时候系统数据自动备份。
            这个需要使用到定时器,并且我们可以将定时器设置为守护线程。
            一直在那里看着,每到00:00的时候就备份一次。所有的用户线程
            如果结束了,守护线程自动退出,没有必要进行数据备份了。

线程池

 - 概念
        线程池就是首先创建一些线程,他们的集合称之为线程池。线程池在系统启动时会创建大量空闲线程,程序将一个任务传递给线程池,线程池就会启动一条线程来执行这个任务,执行结束后线程不会销毁(死亡),而是再次返回到线程池中成为空闲状态,等待执行下一个任务。

- 线程池的工作机制
        在线程池的编程模式下,任务是分配给整个线程池的,而不是直接提交给某个线程,线程池拿到任务后,就会在内部寻找是否有空闲的线程,如果有,则将任务交个某个空闲线程。

 - 使用线程池的原因
        多线程运行时,系统不断创建和销毁新的线程,成本非常高,会过度的消耗系统资源,从而可能导致系统资源崩溃,使用线程池就是最好的选择。

 - 可重用线程

方法名说明
Executors.newCacheThreadPoll();创建一个可缓存的线程池
execute(Runnable run)启动线程池中的线程
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 

public class ExecutorsTest {
    public static void main(String[] args) {
        //创建线程池
        ExecutorService threadPoll = Executors.newCachedThreadPool();
 
        for (int i = 0; i < 10; i++) {
            //如果不睡眠,那么第一个执行完的线程无法及时成为空闲线程,那么线程池就会让一个新的线程执行
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //每次循环都会开启一个线程
            threadPoll.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "正在被执行!~");
                }
            });
        }
        threadPoll.shutdown();//关闭线程池
        //线程池是无限大,当执行当前任务时,上一个任务已经完成,会重复执行上一个任务的线程,而不是每次使用新的线程
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值