java多线程、同步、线程的六种状态

异常处理方式
    * JVM处理:将异常信息打印在控制台上并退出JVM结束程序运行。
    * 手动处理
        
手动处理异常方式
    * 捕获处理
    * 抛出处理

捕获处理的格式
    try{
        // 可能会出现异常的代码
    } catch(异常类名 变量名){
        // 出现异常之后要执行的代码
    } ....
    finally{
        // 不管是否出现异常都要执行的代码,一般编写释放资源相关的代码
    }

声明处理和抛出处理
    throw关键字
        * 作用:将异常对象抛出抛给方法调用者并结束当前方法的运行。
        * 格式:throw new 异常类名("异常信息字符串");
        * 位置:使用方法体中
    throws关键字
        * 作用:将方法中可能会出现的异常标识处理报告给方法调用者,让方法调用者注意处理异常。
        * 格式:方法声明()  throws 异常类名1,....{}
        * 位置:使用方法声明上
        
 自定义异常步骤
     * 创建一个类,类名:XxxxException
     * 继承系统的异常类
         * Exception:编译时异常
         * RuntimeException:运行时异常
     * 提供构造方法
         * 有参数构造:public XxxxException(String message){ super(message);}
        * 无参数构造:public XxxxException(){}
进程的概念
    * 一个正在运行中的程序
线程的概念
    * 进程中的一个独立的执行路径

开启线程的方式1:继承Thread
    * 创建类继承Thread类
    * 重写run方法:将线程任务相关的代码写在该方法中
    * 创建Thread类子类对象,调用start方法开启线程。

  •  线程的相关概念
  • 线程的创建方式
  • 线程安全
  • 线程的状态(生命周期)—面试题

1.1 主线程和子线程概述

  • 什么是主线程
  1. 程序启动过程中自动创建并执行main方法的线程则称为主线程。
  2. Java程序启动过程中默认会创建两个线程:一个是主线程,一个是垃圾回收器线程(默认忽略)。
  •  主线程的执行路径
  1. 从main方法开始直到main方法结束
  • 什么是子线程
  1. 除了主线程以外的所有线程都是子线程
  • 子线程执行路径
  1. 从run方法开始直到run方法结束

1.2 线程的运行模式:

  • 分时式模式:每个线程平均分配CPU使用权,每个线程使用CPU的时间是相同。
  • 抢占式模式:优先级高的线程抢到CPU的概率会高,如果优先级相同,则所有线程一起去抢夺CPU,哪个线程抢到CPU就执行哪个线程的任务。Java程序的线程运行模式就属于该种模式。 

1.3 多线程内存图解

  • 每个线程都会有自己独立的栈空间。
  • 在同一线程中,代码是从上往下按顺序执行的。
  • 同一进程中的线程共享进程的堆空间

 

//自定义线程类
public class MyThread extends Thread {
    public MyThread(String name) {
        super(name);
    }
    //重写run方法
    public void run(){
        for (int i = 0; i < 20; i++) {
            //该getName()方法来自父亲
            System.out.println(getName()+i);
        }
    }
}



public class Demo {
    public static void main(String[] args) {
        System.out.println("这里是main线程:");
        MyThread mt=new MyThread("小李");
        mt.start();//开启了一个新线程
        for (int i = 0; i < 20; i++) {
            System.out.println("小明"+i);
        }
    }
}

1.4 Thread类常用方法

构造方法

  • public Thread():分配一个新的线程对象。
  • public Thread(String name):分配一个指定名字的新的线程对象。
  • public Thread(Runnable target):分配一个带有指定目标新的线程对象。
  • public Thread(Runnable target,String name):分配一个带有指定目标新的线程对象并指定名字。

常用方法: 

  • String getName():获得线程名称,默认名称:Thread-序号
  • void setName(String name):设置线程名称
  • void start():开启线程,在新的执行路径中执行run方法。
  • static Thread currentThread():获得执行当前方法的线程对象
  • static void sleep(long millis):让线程休眠指定的毫秒值

总结:创建线程的方式总共有两种,一种是继承Thread方式,一种是实现Runnable接口方式。 

public class MyThread  extends Thread{
    @Override
    public void run(){
        try {
            //让线程休眠1s
        Thread.sleep(1000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        for (int i = 0; i < 10; i++) {
            System.out.println("run="+i);
        }
    }

}



public class ThreadDemo01 {
    public static void main(String[] args) throws InterruptedException {
        //创建线程对象
        MyThread mt=new MyThread();
        System.out.println(mt.hashCode());

        //给线程设置名字
        mt.setName("下载文件A线程");
        //输出线程名字
        System.out.println(mt.getName());
        //开启线程
        mt.start();
        System.out.println("------------");
        //让当前线程休眠1s
        mt.sleep(1000);
        for (int i = 0; i < 10; i++) {
            System.out.println("main="+i);
        }
        //获得主线程对象
       Thread t=Thread.currentThread();
        //获得主线程名称
        System.out.println(t.getName());
    }
    private void test01(){
        //获得执行当前方法的线程对象
        Thread t=Thread.currentThread();
        //获得主线程名称
        System.out.println(t.getName());
    }
}

 1.5 创建线程方式二

  • 创建类实现Runable接口,重写run方法:将线程任务相关的代码写在该方法中
  • 创建实现类对象,根据实现类对象创建Thread对象
  • 调用Thread类的start方法开启线程

 

 1.6 实现Runnable的好处

  • 解决了Java类单继承的局限性。
  • 可以更好的在多个线程之间共享数据。
  • 将线程和任务进行了分离,降低了程序的耦合性。
  • 为线程池提供前提条件。

解析:1.如果一个类继承Thread,则不适合资源共享(因为Java是单继承的,如果已经继承了Thread,想要继承其他类则不能实现了)。但是如果实现了Runnable接口的话,则很容易实现资源共享。 

2.线程池只能放入实现Runnable或callable类线程,不能直接放入继承Thread的类。

1.7 匿名内部类方式实现线程的创建

  •  创建线程的方式3:使用匿名内部类
  • 匿名内部类的语法:

new 类名或接口名(){

     //重写方法

}

  1. 直接创建已知类的子类对象
  2. 直接创建已知接口的实现类对象
  • 什么时候使用匿名内部类创建线程:如果任务只需要执行一次时,可以考虑使用匿名内部类,否则不推荐使用。
public class ThreadDemo02 {
    public static void main(String[] args) {
        //使用匿名内部类的方法创建线程对象
        /*
            class XxxThread extends Thread{
                public void run(){

                }
            }

            Thread t=new XxxThread();
         */
        Thread t=new Thread(){
          @Override
            public void run(){
              System.out.println("这里是子线程..."+getName());
          }
        };
        t.start();

        //简化版
        new  Thread(){
            @Override
            public void  run(){
                System.out.println("这里是子线程..."+getName());
            }
        }.start();

        //方式2:创建Runnable接口的实现类对象
        /*
            class XxxRunnable implements Runnable{
                public void run(){

                }
            }
            Runnable target=new XxxRunnable();
         */
        Runnable target=new Runnable(){
            @Override
            public void run(){
                System.out.println("这里是子线程..."+Thread.currentThread().getName());
            }
        };
        //根据target创建Thread对象
        Thread t1=new Thread(target);
        t1.start();

        //简化版
        new Thread(new Runnable(){
            @Override
            public void run(){
                System.out.println("这里是子线程..."+Thread.currentThread().getName());
            }
        }).start();
    }
}

2.1 线程安全概念

  • 什么是线程安全:两个或两个以上的线程在执行任务过程中操作共享资源仍然能得到正确的结果则称为线程安全。
  • 线程安全案例:
  1. 卖火车票逻辑
  • 定义一个变量记录总票数
  • 判断是否有余票,如果有则卖一张,票数减一
  • 如果没有剩余票数了,则提示用户票没了

注意:卖火车票有多个窗口,相当于开了多个线程,一个窗口代表一个线程,特别注意在实际的开发过程中,先保证一个线程是正确的,在开其它线程,执行相同的代码。 

思考步骤1:先不开辟子线程,在main方法中写,main方法默认是主线程,保证卖票逻辑能正常执行。

public class ThreadDemo01 {
    public static void main(String[] args) {
        //定义一个变量记录总票数
        int tickets=100;
        //判断是否有剩余票
        if(tickets > 0){
            //有则卖一张,票数减一
            System.out.println("卖了一张票,还剩"+(--tickets)+"张");
        }else {
            System.out.println("票没了...");
        }
    }
}

思考步骤2:发现只能卖一张,程序就死了,我们要将票全部卖完,则需要利用循环,因为只有一个线程(main方法),相当于一个窗口卖完了所有的票。

public class ThreadDemo01 {
    public static void main(String[] args) {
        //定义一个变量记录总票数
        int tickets=100;
        //使用死循环保证票能够全部卖完
        while (true){
            //判断是否有剩余票
            if(tickets > 0){
                //有则卖一张,票数减一
                System.out.println("卖了一张票,还剩"+(--tickets)+"张");
            }else {
                System.out.println("票没了...");
                break;
            }
        }
    }
}

思考步骤3: 这时候已经能够保证一个线程能够正常运行,就需要开辟新的线程了。就是把上面的代码放到run方法中去,那么下面将演示使用Runnable接口开辟线程的方式。

public class TicketThread implements Runnable {
    @Override
    public void run() {
        //定义一个变量记录总票数
        int tickets=100;
        //使用死循环保证票能够全部卖完
        while (true){
            //判断是否有剩余票
            if(tickets > 0){
                //有则卖一张,票数减一
                System.out.println(Thread.currentThread().getName()+"卖了一张票,还剩"+(--tickets)+"张");
            }else {
                System.out.println("票没了...");
                break;
            }
        }
    }
}



public class ThreadDemo01 {
    public static void main(String[] args) {
        //创建Runnable接口的实现类对象
        TicketThread target=new TicketThread();

        //创建两个线程
        Thread t1=new Thread(target);
        Thread t2=new Thread(target);

        t1.setName("美女A");
        t2.setName("美女B");

        //开启线程
        t1.start();
        t2.start();
    }
}

思考步骤4:输出结果发现票卖了200张,原因是tickets=100张放在了run方法内,每个线程都有自己的独立栈空间,执行run方法的时候每个线程都有自己的局部变量tickets,开了两个线程每个线程都有自己的100,所以需要让两个线程共用一个tickets

public class TicketThread implements Runnable {
    //定义一个变量记录总票数
    int tickets=100;
    @Override
    public void run() {
        //使用死循环保证票能够全部卖完
        while (true){
            //判断是否有剩余票
            if(tickets > 0){
                //有则卖一张,票数减一
                System.out.println(Thread.currentThread().getName()+"卖了一张票,还剩"+(--tickets)+"张");
            }else {
                System.out.println("票没了...");
                break;
            }
        }
    }
}


public class ThreadDemo01 {
    public static void main(String[] args) {
        //创建Runnable接口的实现类对象
        TicketThread target=new TicketThread();

        //创建两个线程
        Thread t1=new Thread(target);
        Thread t2=new Thread(target);

        t1.setName("美女A");
        t2.setName("美女B");

        //开启线程
        t1.start();
        t2.start();
    }
}

补充:用第一种方式做存在缺点,下面进行演示。(用实现Runnable接口的好处之一是可以更好的在多个线程之间共享数据),而如果采用继承的方式,由于Java的单继承特性则不能更好的实现共享数据,则创建的每个对象都有自己的target(成员变量),如果要用继承实现,则需要使用static关键字,因为static修饰静态成员变量该变量的值会被该类的所有对象共享,则可以实现共享(静态成员有一个特点是跟随类的加载而加载,跟随类的消失而消失,所以这种方式不好,周期会延长),而实现Runnable接口只有创建对象的时候才存在。

public class TicketThread extends Thread {
    public static void main(String[] args) {
       TicketThread t1=new TicketThread();
       TicketThread t2=new TicketThread();
       t1.start();
       t2.start();
    }
    //定义一个变量记录总票数
    static int tickets=100;
    @Override
    public void run() {
        //使用死循环保证票能够全部卖完
        while (true){
            //判断是否有剩余票
            if(tickets > 0){
                //有则卖一张,票数减一
                System.out.println(Thread.currentThread().getName()+"卖了一张票,还剩"+(--tickets)+"张");
            }else {
                System.out.println("票没了...");
                break;
            }
        }
    }
}

思考步骤5:此时还不严谨,模拟卖票网络延迟,加上休眠部分,可能会出现负数(并发执行导致)和重复票数(并行执行导致)的错误结果。

public class TicketThread implements Runnable {
    //定义一个变量记录总票数
    int tickets=100;
    @Override
    public void run() {
        //使用死循环保证票能够全部卖完
        while (true){
            //判断是否有剩余票
            if(tickets > 0){
                //模拟休眠
                try{
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //有则卖一张,票数减一
                System.out.println(Thread.currentThread().getName()+"卖了一张票,还剩"+(--tickets)+"张");
            }else {
                System.out.println("票没了...");
                break;
            }
        }
    }
}

思考步骤6: 那么如何解决呢?实现线程安全,则需要保证同一时间不能有两个线程同时进去。需要用到线程同步技术

  • 线程同步:多个线程之间要按顺序执行(不能同时执行)
  • 异步:可以同时执行,多线程的代名词。
  • 实现线程安全的方式:
  1. synchronized关键字
  2. Lock接口
  • synchronized关键字实现线程安全方式
  1. 同步代码块
  2. 同步方法
  •   同步代码块
  • 同步代码块实现线程安全
  • 同步代码块的格式

    synchronized(锁对象){
        // 操作共享资源的代码
    }

  • 同步代码块的原理

    * 能够保证同一时间只能有一个线程执行代码块中的代码。

  • 同步代码块注意事项

    * 锁对象可以是任意类型的对象
    * 锁对象必须被所有线程共享(所有线程必须共用一个锁对象)如://定义一个变量记录总票数
    int tickets=100;
    //创建锁对象
    Object lockobj=new Object();而不能放在while循环中

public class TicketThread implements Runnable {
    //定义一个变量记录总票数
    int tickets=100;
    //创建锁对象
    Object lockobj=new Object();

    @Override
    public void run() {
        //使用死循环保证票能够全部卖完
        while (true){
            //使用同步代码块实现线程安全
            synchronized (lockobj){
                //判断是否有剩余票
                if(tickets > 0){
                    //模拟休眠
                    try{
                        Thread.sleep(20);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //有则卖一张,票数减一
                    System.out.println(Thread.currentThread().getName()+"卖了一张票,还剩"+(--tickets)+"张");
                    continue;
                }
            }
                System.out.println("票没了...");
                break;
        }
    }
}




/*
    实现线程安全之同步代码块
 */
public class ThreadDemo01 {
    public static void main(String[] args) {
        //创建Runnable接口的实现类对象
        TicketThread target=new TicketThread();

        //创建两个线程
        Thread t1=new Thread(target);
        Thread t2=new Thread(target);

        t1.setName("美女A");
        t2.setName("美女B");

        //开启线程
        t1.start();
        t2.start();
    }
}


同步方法实现线程安全

  • 同步方法的格式:

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

     }

  • 同步方法的原理:

    * 能够保证同一时间只有一个线程执行方法体的代码

  • 同步方法的锁对象:(默认有)

    * 非静态同步方法:锁对象是:this(即方法调用者,下面案例的锁对象为target)
    * 静态同步方法:锁对象是:类名.class
        类名.class获得的是Class对象,字节码对象
        1.每一个类都会有一个Class对象
        2.每一个类的Class对象都是唯一的

  • 静态同步方法和非静态同步方法的选择:

    * 如果方法体中需要访问非静态成员,则定义为非静态同步方法。
    * 如果方法体中不需要访问任何非静态成员,则可以将该方法定义静态同步方法。

public class TicketThread implements Runnable {
    //定义一个变量记录总票数
    int tickets = 100;

    @Override
    public void run() {
        //使用死循环保证票能够全部卖完
        while (tickets > 0) {
            //调用方法卖票
            saleTicket();
        }
    }
        /*
            每调用一次该方法就卖一张票
         */
        public synchronized void saleTicket() {
            //判断是否有剩余票
            if (tickets > 0) {
                //模拟休眠
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //有则卖一张,票数减一
                System.out.println(Thread.currentThread().getName() + "卖了一张票,还剩" + (--tickets) + "张");
        }
    }
}

Lock接口实现线程安全

  •  Lock接口概述:

    * JDK1.5新特性
    * 专门用来实现线程安全的技术

  • Lock接口常用实现类:

    * ReentrantLock:互斥锁

  • Lock接口常用方法:

    * void lock(); 获取锁
    * void    unlock(); 释放锁

  • Lock接口注意事项:

    * 获取锁和释放锁的代码必须成对出现。

  • synchronized和Lock接口的选择

    * 如果资源竞争不激烈,则选择谁都差不多。
    * 如果资源竞争很激烈,则选择Lock接口的效率会高于synchronized关键字。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TicketThread implements Runnable {
    //定义一个变量记录总票数
    int tickets=100;
    //创建互斥锁对象
    Lock lockObj=new ReentrantLock();

    @Override
    public void run() {
        //使用死循环保证票能够全部卖完
        while (true){
            //获取锁
            lockObj.lock();
            try{
                //判断是否有剩余票
                if(tickets > 0){
                    //模拟休眠
                    Thread.sleep(20);
                    //有则卖一张,票数减一
                    System.out.println(Thread.currentThread().getName()+"卖了一张票,还剩"+(--tickets)+"张");
                    continue;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                //释放锁
                lockObj.unlock();
            }
            //如果没有剩余票数了,则提示用户票没了
            System.out.println("票没了...");
            break;
        }
    }
}




public class ThreadDemo01 {
    public static void main(String[] args) {
        //创建Runnable接口的实现类对象
        TicketThread target=new TicketThread();

        //创建两个线程
        Thread t1=new Thread(target);
        Thread t2=new Thread(target);

        t1.setName("美女A");
        t2.setName("美女B");

        //开启线程
        t1.start();
        t2.start();
    }
}


 线程的六种状态:(面试题)

  • NEW:新建状态 刚刚创建出来,还没有调用start方法之前的状态。
  • RUNNABLE:可运行状态,可能正在执行,也可能不是正在执行,只有在该种状态下的线程才有资格抢CPU。
  • BLOCKED:锁阻塞状态  线程要等待另一个线程释放锁对象。
  • WAITING:无限等待  线程调用了wait()方法进入的状态,需要其它线程调用notify方法唤醒。
  • TIMED_WAITING:计时等待状态  线程调用了sleep方法获wait(long time)方法进入的状态。
  • TERMINATED:死亡状态  线程任务执行完毕或调用了stop方法。

猜你喜欢

转载自blog.csdn.net/Huangyuhua068/article/details/81434878