JavaSE进阶十 线程一

1,关于线程的概述

  • 什么是进程?什么是线程?

    • 进程是一个应用程序。
    • 线程是一个进程中的执行场景/执行单元;一个进程可以启动多个线程。
  • 在java语言中:

    • 多个线程,堆内存和方法区内存共享。
    • 栈内存独立,一个线程一个栈。
    • 假设启动10个线程,会有十个栈内存空间,每个栈之间互不干扰;各自执行各自的,这就是多线程并发。
  • 思考一个问题:

    • 使用了多线程机制之后,main方法结束,是不是有可能程序也不会结束;main方法结束
      只是主线程结束了,主栈空了;其他的栈(线程)可能还在压栈弹栈。
  • 分析一个问题:对于单核CPU来说,真的可以做到真正的多线程并发吗?

    • 对于多核的CPU电脑来说,真正的多线程并发是没问题的。
    • 什么是真正的多线程并发?
      • t1线程执行t1的, t2线程执行t2的,
        t1、t2互不影响;这就是真正的多线程并发。
    • 单核的CPU表示只有一个大脑:
      • 不能做到真正的多线程并发,但可以做到给人一种"多线程并发"的感觉。
        对于单核CPU来说,在某一个时间点上实际上只能处理一件事情,但是由于
        CPU的处理速度极快,多个线程之间频繁切换执行,给人的感觉是:多个事情
        同时在做。
  • java语言中实现线程的两种方式:

    • 1,编写一个类,直接继承java.lang.Thread,重写run方法。
    • 2,编写一个类,实现java.lang.Runnable接口,实现run方法。
    • 注意:第二种方式实现接口比较常用,因为一个类实现了接口,它还可以去继承其他的类,更加灵活。
  • Java支持多线程机制,并且已经将多线程实现了,我们只需要继承就可以了。

  • 线程的生命周期
    • 新建状态:刚new出来的的线程对象;
    • 就绪状态:调用完start()方法;
      • 就绪状态线程又叫可运行状态,表示当前线程具有抢夺CPU时间片的权利(CPU时间片就是执行权);当一个线程抢夺到CPU时间片之后,就开始执行run方法,run方法的开始执行标志着线程进入运行状态。
    • 运行状态:run方法开始执行;
      • run方法的开始执行标志着这个线程进入运行状态,当之前占有的CPU时间片用完之后,会重新回到就绪状态
        继续抢夺CPU时间片,当再次抢到CPU时间片之后,会重新进入run方法接着上一次的代码继续往下执行。
    • 阻塞状态:遇到阻塞事件;
      • 在一个线程运行时,遇到阻塞事件,例如:接收用户键盘输入,或者sleep()方法等,此时线程会进入阻塞状态,
        阻塞状态的线程会放弃之前占有的CPU时间片;当阻塞事件结束,需要再次回到就绪状态重新抢夺CPU时间片。
    • 死亡状态:run方法执行完成(结束)。

1.1,实现线程的第一种方式:

  • 编写一个类,继承java.lang.Thread,重写run方法。

  • start()方法的作用:

    • start()方法的作用:启动一个分支线程,在JVA中开辟一个新的栈空间。
    • 只要新的栈空间开出来,start()方法就结束了;分支线程启动成功了。
    • 启动成功的的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈)。
    • run方法在分支栈的底部,main方法在主栈的栈底部。run和main是平级的。
  • 直接调用run方法,不会启动线程,也不会分配新的分支栈。

代码示例
public class ThreadTest02 {
    public static void main(String[] args) {
        // 这里是main方法,这里的代码属于主线程,在主栈中运行。

        // 新建一个分支线程对象
        MyThread myThread = new MyThread();
        // 启动线程
        myThread.start();// 这个代码执行极快,瞬间就结束了

        // 直接调用run方法,不会启动线程,也不会分配新的分支栈。
        // myThread.run();

        // 这里的代码还是运行在主栈线程中
        for (int i = 0;i < 1000; i++){
            System.out.println("主线程--->" + i);
        }
    }
}

class MyThread extends Thread{
    @Override
    public void run() {
        super.run();

        // 编写程序,这段程序运行在分支线程中(分支栈)
        for (int i = 0;i < 1000; i++){
            System.out.println("分支线程--->" + i);
        }
    }
}

1.2实现线程的第二种方式:

  • 编写一个类,实现java.lang.Runnable接口,实现run方法。
代码示例
public class ThreadTest03 {
    public static void main(String[] args) {
        // 创建一个可运行的对象
        MyRunnable r = new MyRunnable();
        // 将可运行的对象封装成一个线程对象
        Thread t = new Thread(r);

        // 合并代码
        // Thread t = new Thread(new MyRunnable());

        //启动线程
        t.start();

        for (int i = 0;i < 100; i++){
            System.out.println("主线程--->" + i);
        }

        // --------使用匿名内部类------------------------------------------------------

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                // 编写程序,这段程序运行在分支线程中(分支栈)
                for (int i = 0;i < 100; i++){
                    System.out.println("分支线程(匿名内部类)--->" + i);
                }
            }
        });
        // 启动线程
        t2.start();

        for (int i = 0;i < 100; i++){
            System.out.println("main主线程--->" + i);
        }
    }
}

// 这并不是一个线程类,是一个可运行的类;它还不是一个线程
class MyRunnable implements Runnable{

    @Override
    public void run() {

        // 编写程序,这段程序运行在分支线程中(分支栈)
        for (int i = 0;i < 100; i++){
            System.out.println("分支线程--->" + i);
        }
    }
}

1.3 线程中常用的三个方法

  • 获取当前线程对象:
  •   static native Thread currentThread()
    
  • 获取线程对象的名字:
  •   final String getName()
    
  • 修改线程对象的名字
  • setName(String name)
    
  • 当线程线程没有设置名字的时候,默认名字是:
  •    Thread-0
      Thread-1
      Thread-2
      Thread-3
      ...
    
代码示例
public class ThreadTest04 {
    public static void main(String[] args) {
        // 获取当前线程
        // 这个代码出现在main方法中,当前线程就是主线程
        Thread mt = Thread.currentThread();
        // 获取线程名字
        System.out.println(mt.getName()); // main

        //=================================================================

        // 创建线程对象
        MyThread2 t = new MyThread2();
        // 获取线程名字
        System.out.println(t.getName());// Thread-0
        // 设置线程的名字
        t.setName("t1");
        // 获取线程名字
        System.out.println(t.getName());// t1
        // 启动线程
        t.start();

        // 创建线程对象
        MyThread2 t2 = new MyThread2();
        t2.setName("t2");
        // 启动线程
        t2.start();
    }
}

class MyThread2 extends Thread{
    @Override
    public void run() {
        // 获取当前线程
        Thread currentThread = Thread.currentThread();

        // 编写程序,这段程序运行在分支线程中(分支栈)
        for (int i = 0;i < 100; i++){
            System.out.println(currentThread.getName() + "-->" + i);
        }
    }
}

2 线程线程的休眠和唤起(终止)

2.1 关于线程的sleep方法:

  • Static void sleep (long millis)

    • 1,静态方法:Thread.sleep(1000);
    • 2,参数是毫秒。
    • 3,作用是让"当前线程"进入休眠,进入"阻塞状态";放弃占有的CPU时间片,让其他线程使用。
  • 强行终止一个线程的执行:

    • void interrupt();这种终止睡眠的方式依靠java异常处理机制。
代码示例
public class ThreadTest05 {
    public static void main(String[] args) {
        System.out.println("hello");

        Thread t = new Thread(new MyRunnable2());
        t.setName("tt");
        t.start();

        // 希望五秒之后,t线程醒来。
        try {
            Thread.sleep(1000 * 5);// 休眠5秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 五秒钟之后执行下面代码
        System.out.println("word");

        // 终止t线程的睡眠
        // 这种终止睡眠的方式依靠java异常处理机制
        t.interrupt();
    }
}

class MyRunnable2 implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "-->" + "begin");
        try {
            // 睡眠一年
            Thread.sleep(1000 * 60 * 60 * 24 *365);
        } catch (InterruptedException e) {
            // 打印异常信息
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + "-->" + "end");

    }
}

2.2 合理的终止一个线程的执行:(很常用)

public class ThreadTest06 {
    public static void main(String[] args) {
        MyRunnable3 runnable3 = new MyRunnable3();
        Thread t = new Thread(runnable3);
        t.setName("t3");
        t.start();
        // 希望五秒之后,t线程醒来。
        try {
            Thread.sleep(1000 * 5);// 休眠5秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //终止线程 把runBool设为:false
        runnable3.runBool = false;
    }
}

class MyRunnable3 implements Runnable{
    // 打一个布尔标记
    boolean runBool = true;
    @Override
    public void run() {

        for (int i = 0;i < 100;i++){

            if (runBool){
                System.out.println(Thread.currentThread().getName() + "-->" + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }else {
                // 终止当前线程
                return;
            }
        }
    }
}

3,关于线程的调度

  • 1,常见的线程调度模型:(属于了解内容)
    • 抢占式调度模型:
      • 哪个线程的优先级比较高,抢到的CPU时间片的概率就高一些/多一些。
      • java采用的就是抢占式调度模型。
    • 均分式调度模型:
      • 平均分配CPU时间片,每个线程占有的CPU时间片时间长度一样;平均分配,一切平等。
  • java中和线程调度有关系的方法:
    • 实例方法:

        void setPriority(int newPriority)  设置线程的优先级
        int getPriority() 获取线程优先级  最低优先级是1 默认优先级是5 最高优先级是10
        void join() 合并线程;
        在当前t1线程中new一个t2线程,t2.join(); 然后当前t1线程进入阻塞;t2线程执行,
        直到t2线程结束;当前t1线程才可以继续执行。
      
    • 静态方法:

       static void yield()
        让位方法;暂停当前正在执行的线程对象,并执行其他线程;
        yield()方法不是阻塞方法,让当前线程让位,让给其他线程使用;
        yield()方法的执行会让当前线程从"运行状态"回到"就绪状态"。
      
代码示例

以下代码是包含了:线程优先级、线程让位、合并线程;大家复制代码运行时,最好根据"=====..."分割线,注释一部分执行看效果。

public class ThreadTest08 {
    public static void main(String[] args) {
        System.out.println("最高优先级:" + Thread.MAX_PRIORITY);
        System.out.println("最低优先级:" + Thread.MIN_PRIORITY);
        System.out.println("默认优先级:" + Thread.NORM_PRIORITY);

        // 获取当前线程对象
        Thread tc = Thread.currentThread();
        // 获取当前线程的优先级
        System.out.println(tc.getName() +"线程的优先级:" + tc.getPriority());//5
        // 更改当前线程优先级
        tc.setPriority(6);
        System.out.println(tc.getName() +"线程的优先级:" + tc.getPriority());//6

        // ===================线程让位================================================================
        System.out.println("===================线程让位==========================");
        Thread tc1 = new Thread(new MyRunnable4());
        tc1.setName("tc1");
        tc1.start();

        for (int i = 0;i < 1000; i++){
            System.out.println(Thread.currentThread().getName() + "-->" + i);
        }

        // ===================合并线程================================================================
        System.out.println("===================合并线程=============================");

        System.out.println("main begin");
        Thread tc2 = new Thread(new MyRunnable5());
        tc2.setName("tc2");
        tc2.start();

        // 合并线程
        try {
            tc2.join();// 合并到当前线程中,当前线程阻塞,tc2线程执行直到结束。
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("main over");

    }
}

class MyRunnable4 implements Runnable{

    @Override
    public void run() {
        for (int i = 0;i < 1000; i++){
            // 每50个让位一次
            if ( i % 50 == 0){
                Thread.yield();// 当前线程暂停一下,让给主线程
            }
            System.out.println(Thread.currentThread().getName() + "-->" + i);
        }
    }
}

class MyRunnable5 implements Runnable{

    @Override
    public void run() {
        for (int i = 0;i < 100; i++){
            System.out.println(Thread.currentThread().getName() + "-->" + i);
        }
    }
}

4,关于线程安全

  • 什么时候数据在多线程并发的环境下会存在安全问题呢

    • 1,多线程并发
    • 2,有共享数据。
    • 3,共享数据有修改的行为。
    • 满足以上3个条件之后,就会存在线程安全问题。
  • 怎么解决线程安全问题

    • 当多线程并发的环境下,有共享数,并且这个数据还会被修改,就会存在线程
    • 安全问题,如何解决:
      • 线程排队执行。(不能并发)
      • 用排队执行解决线程安全问题。
      • 这种机制称为:线程同步机制。
  • 线程同步机制涉及两个专业术语:

    • 同步编程模型:(同步就是排队)
      • 线程t1和线程t2:t1线程必须等待t2线程执行结束再执行,或者t2线程必须等待t1线程执行结束再执行,
        两个线程之间发生了等待关系。这就是同步编程模型,效率较低,线程排队执行
    • 异步编程模型:(异步就是并发)
      • 线程t1和线程t2:各自执行各自的互不影响,谁也不等谁,这种编程模型叫做:异步编程模型。
  • java语言中的三大变量谁可能存在线程安全问题:

    • 实例变量:在堆中;堆只有一个,是多线程共享的,所以可能存在线程安全问题。
    • 静态变量:在方法区中;方法区只有一个,是多线程共享的,所以可能存在线程安全问题。
    • 局部变量:在栈中;一个线程一个栈,所以局部变量永远不会有线程安全问题。(局部变量不共享)
  • 开发中如何解决线程安全问题?

    • 第一种方案:尽量使用局部变量代替"实例变量"和"静态变量"。
    • 第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内容就不共享了。
      (一个线程对应一个对象)
    • 第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synchronized了,
      线程同步机制。

4.1,模拟多线程高并发Demo

  • Account类的withDraw方法的两种写法:
    • 1,不使用线程同步机制,多线程对同一银行账户进行取款,出现线程安全问题。
    • 2,使用线程同步机制,多线程对同一银行账户进行取款,解决线程安全问题。(synchronized三种写法如下:)
      • 2.1,synchronized同步代码块:

        synchronized (){
              // 线程同步代码块
          }
        synchronized后面小括号中传的"数据"是十分关键的,这个数据必须是多线程贡献的数据, 才能达到多线程排队。
        synchronized()小括号中写什么?
            要看你想让哪些线程同步。
            假设t1,t2,t3,t4,t5,有五个线程;你希望t1,t2,t3排队,t4,t5不需要排队;
            你一定要在()中写一个t1,t2,t3共享的对象;这个对象对于t4,t5不共享。
        
      • 2.2,在实例方法上使用synchronized

        • 表示共享对象一定是this,并且同步代码块是整个方法体。
      • 2.3,在静态方法上使用synchronized

        • 表示找类锁,类锁永远只有一把,就算创建了100个对象,类锁也只有一把。
代码示例
public class ThreadSafeTest01 {
    public static void main(String[] args) {
        // 创建账户对象(只创建一个)
        Account act = new Account("act-001",10000);
        // 创建两个线程
        Thread t1 = new AccountThread(act);
        Thread t2 = new AccountThread(act);
        // 设置线程名字
        t1.setName("t1");
        t2.setName("t2");

        // 启动线程取款
        t1.start();
        t2.start();
    }
}

class AccountThread extends Thread{
    // 两个线程必须共享同一个账户对象
    private Account act;

    // 通过构造方法传递过来账号对象
    public AccountThread(Account act){
        this.act = act;
    }

    @Override
    public void run() {
        super.run();
        // 这里执行取款操作
        double money = 5000;

        // 也可以采用这种写法;去掉方法里面写的synchronized。
        // 多线程并发执行withDraw整个方法;
        // 这种写法会扩大同步范围,效率变得更低了。
        // synchronized (act){
        //    act.withDraw(money);
        // }

        act.withDraw(money);
        System.out.println(Thread.currentThread().getName() + "对" + act.getActno() + "取款成功:" + money + ";剩余余额:" + act.getBalance());
        /*
        采用withDraw方法的第一中方式:
        出现了高并发(取款了2次5000,余额应为0)
        打印结果余额缺为:5000:
        t1对act-001取款成功:5000.0;剩余余额:5000.0
        t2对act-001取款成功:5000.0;剩余余额:5000.0
        */
    }
}

class Account{

    // 账号
    private String actno;
    // 余额
    private double balance;

    // 创建一个实例对象
    Object obj = new Object();

    public Account() {
    }

    public Account(String actno, double balance) {
        this.actno = actno;
        this.balance = balance;
    }

    public String getActno() {
        return actno;
    }

    public void setActno(String acton) {
        this.actno = acton;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    public Account(double balance) {
        this.balance = balance;
    }

    // 取款方法
    // 1,不使用线程同步机制,多线程对同一银行账户进行取款,出现线程安全问题。
//    public void withDraw(double money){
//        // 取款之前的余额
//        double before = this.getBalance();
//        // 取款之后的余额
//        double after = before - money;
//
//        // 模拟一下网络延迟
//        try {
//            Thread.sleep(1000);
//        } catch (InterruptedException e) {
//            e.printStackTrace();
//        }
//        // 更新余额
//        this.setBalance(after);
//    }

    // 2,使用线程同步机制,多线程对同一银行账户进行取款,解决线程安全问题。
    public void withDraw(double money){
        // 这里的共享对象是:账户对象,那么this就是账户对象。
        // synchronized (this){
        synchronized (obj){ // 实例对象只有一个 也属于共享对象
            // 取款之前的余额
            double before = this.getBalance();
            // 取款之后的余额
            double after = before - money;

            // 模拟一下网络延迟
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 更新余额
            this.setBalance(after);
        }
    }

    /*
        2.1,在实例方法上使用synchronized,表示整个方法体都需要同步,
        可能会无故扩大同步的范围,会导致程序的执行效率降低。这种方式不常用。

        在实例方法上使用synchronized使用在实例方法上的优点:
            代码写的少了,节俭了。

        如果共享的对象就是this,并且需要同步的代码块是整个方法体,建议使用这种方式。
    */
//    public synchronized void withDraw(double money){
//        // 取款之前的余额
//        double before = this.getBalance();
//        // 取款之后的余额
//        double after = before - money;
//
//        // 模拟一下网络延迟
//        try {
//            Thread.sleep(1000);
//        } catch (InterruptedException e) {
//            e.printStackTrace();
//        }
//        // 更新余额
//        this.setBalance(after);
//    }
}

上篇:JavaSE进阶九 IO流二
下篇:JavaSE进阶十 线程二

你可能感兴趣的:(JavaSE进阶十 线程一)