Java多线程

文章目录

  • 一、初始多线程
    • 线程和进程
    • 线程并发、并行、串行
  • 二、创建线程
    • 继承Thread类
    • 实现Runnable接口
    • 实现Callable接口
  • 三、线程的生命周期
    • 线程的6种状态
    • 线程状态图
  • 四、中断线程
    • 中断线程作用
    • 中断线程原理
    • 关于中断的相关方法
  • 五、线程的属性
    • 线程优先级
    • 守护线程
  • 六、线程同步
    • 线程竞争产生的风险
    • 对产生风险的解释
    • synchronized关键字实现线程同步
  • 七、死锁
  • 八、简单实现生产消费者问题
    • 相关方法
    • 代码实现

一、初始多线程

线程和进程

进程是程序向操作系统申请资源的基本单位,线程是进程中可独立执行的最小单位。通常一个进程可以包含多个线程,至少包含一个线程,同一个进程中所有线程共享该进程的资源。

  • 1、根本区别
      进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。

  • 2、资源开销
      每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间(在Java中,线程之间是共享堆区和方法区的资源,但是它们拥有着独立的栈区),每个线程都有自己独立的运行栈和程序计数器,线程之间切换的开销小。

  • 3、包含关系
      一个进程里可以包含多个线程。

  • 4、内存分配
      同一进程的线程共享本进程的地址空间和资源,而线程之间的地址空间和资源是相互独立的。

  • 5、影响关系
      一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

  • 6、执行过程
      每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行。

线程并发、并行、串行

  • 并发
      多个任务在同一个CPU核上,按细分的时间片轮流执行,从逻辑上来看任务是同时执行。
  • 并行
      单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的“同时进行”。
  • 串行
      有n个任务,由一个线程按顺序执行。由于任务、方法都在一个线程执行所以不存在线程不安全情况,也就不存在临界区的问题。
      在这里插入图片描述

二、创建线程

这里讲解创建线程的三种方式。

  • 继承Thread类
  • 实现Runnable接口
  • 实现Callable接口

继承Thread类

使用方法:

  1. 创建一个继承Thread类的类。
  2. 重写Thread类中的run()方法。
  3. 用该类创建线程。
  4. 通过start()方法启动线程。

代码如下:

//创建线程的第一种方式:编写继承Thread的线程类。
package thread;
class myThread extends Thread{
    public void run(){
        for(int i = 0; i < 1000; i++){
            System.out.println("分支线程->" + i);
        }
    }
}
public class ThreadTest01 {
    public static void main(String[] args) {
        myThread t = new myThread();
        t.start();
    }
}

这种方式现在不是很推荐,好的Java程序应该将并行运行的任务与运行机制解耦合。

实现Runnable接口

使用方法:

  1. 创建一个实现(implements)了Runnable接口的类。
  2. 实现Runnable接口中的run()方法。
  3. 创建Thread类对象,将实现了Runnable接口的类对象通过参数的形式传进去。
  4. 通过strat()启动线程。

代码如下:

public class ThreadTest02 {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunable());
        t.start();
        }
}
class MyRunable implements Runnable{
    public void run(){
        for(int i = 0; i < 1000; i++){
            System.out.println("分支线程->" + i);
        }
    }
}

该类较常用,因为Java程序要面向接口编程,实现了接口的类还可以继承其他类,而继承了类的类,不能在继承其他类。

实现Callable接口

使用步骤:

  1. 创建实现Callable接口的类;
  2. 以Callable接口的实现类为参数,创建FutureTask对象;
  3. 将FutureTask作为参数创建Thread对象;
  4. 调用线程对象的start()方法。
    代码如下:
public class ThreadTest09 {
    public static void main(String[] args) {
        FutureTask futureTask = new FutureTask(new myCallable());
        Thread thread = new Thread(futureTask);
        thread.start();
    }
}
class myCallable implements Callable<Integer>{
    public Integer call(){
        System.out.println("Call----->start");
        try {
            Thread.sleep(1000*3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Call----->end");
        return 1000;
    }
}

使用该方法创建线程时,核心方法是call(),与其他方式最大的不同是:该方法有返回值,其返回值类型就是Callable接口中泛型对应的类型。我们通过FutureTask对象调用get()方法,获得该线程的返回值。但是这个方法也有相应的缺点,调用get()方法后,在主线程获得该线程的返回值前,主线程会进入阻塞状态。

注意:调用get()方法会抛出两个异常:InterruptedExceptionExecutionException

三、线程的生命周期

线程的6种状态

Java中有如下6种状态

  • New(新创建)
  • Runnable(可运行)
  • Blocked(阻塞)
  • Waiting(等待)
  • Timed waiting(计时等待)
  • Terminated(被终止)
  1. 新建状态 :
    使用new关键字创建一个thread对象,刚刚创建出的这个线程就处于新建状态。在这个状态的线程没有与操作系真正的线程产生关联,仅仅是一个java对象。

  2. 可运行:
    正在进行运行的线程,只有处于可运行状态的线程才会得到cpu资源。
    可运行状态可以分为两类理解:就绪状态和运行状态:
    运行状态:拥有抢夺CPU时间片的能力,但还未抢夺成功。
    就绪状态:成功抢夺CPU时间片。

  3. 阻塞 :
    在可运行阶段争抢锁失败的线程就会从可运行—>阻塞

  4. 等待 :
    可运行状态争抢锁成功,但是资源不满足,主动放弃锁(调用wait()方法)。条件满足后再恢复可运行状态(调用notiy()方法)。

  5. 有时限等待:
    类似于等待,不过区别在于有一个等待的时间,到达等待时间后或者调用notiy(),都能恢复为可运行状态。
    有两种方式可以进入有时限等待:wait(Long)和sleep(Long)

  6. 终结 :代码全部执行完毕后,会进入到终结状态,释放所有的资源。

线程状态图

四、中断线程

中断线程作用

Java中可以调用interrupt()向线程发出中断请求,从而使线程中断,中断不会对处于运行状态的线程产生影响,但是可以打断线程的阻塞状态(wait(), sleep()),使其进入就绪状态,并抛出InterruptedException异常。
测试代码如下:

public class ThreadTest06 {
    public static void main(String[] args) {
        Thread t = new Thread(new myRunnable2());
        t.start();
        t.interrupt();
    }
}

class myRunnable2 implements Runnable{
    public void run(){
        System.out.println(Thread.currentThread().getName() + "------>start");
        try {
            Thread.sleep(1000*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "------>end");
    }
}

执行结果如下:
在这里插入图片描述

中断线程原理

每一个线程都有一个boolean类型的中断状态,该中断状态初始为false。当一个线程调用interrupted方法时,线程的中断状态被置为true。对于一个运行状态的线程来说,没有什么影响,但是对于阻塞状态的线程来说,遇到中断状态时(true),会抛出一个InterruptedException,同时将中断状态重新置为false
测试代码如下:

public class ThreadTest06 {
    public static void main(String[] args) {
        Thread t = new Thread(new myRunnable2());
        t.start();
    }
}

class myRunnable2 implements Runnable{
    public void run(){
        System.out.println(Thread.currentThread().getName() + "------>start");
        System.out.println("中断前:" + Thread.currentThread().isInterrupted());
        Thread.currentThread().interrupt();
        System.out.println("中断后(遇到堵塞之前):" + Thread.currentThread().isInterrupted());
        System.out.println();
        try {
            Thread.sleep(1000*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("中断后(遇到堵塞之后):" + Thread.currentThread().isInterrupted());
        System.out.println(Thread.currentThread().getName() + "------>end");
    }
}

运行结果如下:

关于中断的相关方法

  • public void interrupt()
    向线程发出中断请求,线程的中断状态被置为true。

  • public static boolean interrupted()
    测试当前线程(正在执行的线程)是否被中断,并将中断状态重置为false。

  • public boolean isInterrupted()
    测试线程的中断状态,但是并不会重置中断状态。

五、线程的属性

这里会对线程优先级和守护线程进行说明。

线程优先级

Java线程的优先级属性本质上只是一个给线程调度器的提示信息,以便于线程调度器决定优先调度哪些线程运行,也可以理解为,优先级高的线程更容易抢夺到CPU时间片。每个线程的优先级都在1到10之间,1的优先级为最低,10的优先级为最高,在默认情况下优先级都是Thread.NORM_PRIORITY(常数 5)。
  虽然开发者可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。
  
常用方法和属性

  • static int MIN_PRIORITY
    线程的最小优先级,最小优先级为1。
  • static int MAX_PRIORITY
    线程的最大优先级,最大优先级为10。
  • static int NORM_PRIORITY
    线程的默认优先级,默认优先级为5。
  • public final int getPriority()
    获得线程的优先级。
  • public static void yield()
    让当前线程处于让步状态,放弃CPU时间片,回到就绪状态,重新抢夺CPU时间片。
  • public final void setPriority(int newPriority)
    设置线程的优先级,但是必须在允许的范围内。

守护线程

Java 中的线程分为两种:守护线程和用户线程。任何线程都可以设置为守护线程和用户线程,通过方法setDaemon(true)可以把该线程设置为守护线程,反之则为用户线程。
  用户线程:运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程。
  守护线程:运行在后台,为其他前台线程服务,比如垃圾回收线程,JIT(编译器)线程就可以理解为守护线程。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作。
  守护线程应该永远不去访问固有资源 ,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。

注意事项:

  • setDaemon(true)必须在start()方法前执行,否则会抛出IllegalThreadStateException。
  • 在守护线程中产生的新线程也是守护线程。
  • 不是所有的任务都可以分配给守护线程来执行,比如读写操作或者计算逻辑。
  • 守护线程中不能依靠finally块的内容来确保执行关闭或清理资源的逻辑。因为我们上面也说过了一旦所有用户线程都结束运行,守护线程会随JVM一起结束工作,所以守护线程中的finally语句块可能无法被执行。

相关方法

public final void setDaemon(boolean on)
将线程设置为守护线程。

六、线程同步

  • 同步
      多个线程访问共享资源时,只有当一个线程访问结束后其它线程才能继续方法。
  • 异步
     多个线程即使在访问共同资源时也互不影响,各自完成各自的任务。

线程竞争产生的风险

当两个线程存取相同对象,并且每一个线程都调用了修改线程状态的方法,将会发生什么呢?
下面用代码演示:
首先创建一个银行账户类,如下:

public class Account {
    //账号
    private String actno;
    //余额
    private int banlance;
    //构造器
    public Account() {
    }
    public Account(String actno, int banlance) {
        this.actno = actno;
        this.banlance = banlance;
    }

    //setter and getter
    public void setBanlance(int banlance) {
        this.banlance = banlance;
    }

    public int getBanlance() {
        return banlance;
    }
    //取钱操作
    public void takeOut(Account act, int takeOut){
        int newBanlance = act.getBanlance() - takeOut;
        //这里sleep()只是为了让效果更加明显
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        act.setBanlance(newBanlance);
        System.out.println( Thread.currentThread().getName() + "取出:" + takeOut + "   剩余:" + newBanlance);
    }
}

然后我们实现一个Runnable类,如下:

class myRunnable implements Runnable{
	//银行账户对象
    Account act;
    //要取出的金额
    int takeOut;
    //构造器
    public myRunnable(){
    }
    public myRunnable(Account act, int takeOut){
        this.act = act;
        this.takeOut = takeOut;
    }
    public void run(){
        act.takeOut(act,takeOut);
    }
}

然后我们用两个线程同时对一个账户进行取钱操作,如下:

class Test{
    public static void main(String[] args) {
        Account act = new Account("Joken",10000);
        //去五次钱,每次取五千
        Thread t1 = new Thread(new myRunnable(act,5000));
        Thread t2 = new Thread(new myRunnable(act,5000));
        t1.start();
        t2.start();
    }
}

程序运行结果如下:

取了两次钱,但是余额仍然还有5000。

对产生风险的解释

产生以上问题的原因在于:act.takeOut(act,takeOut)并不是一个原子操作

这条指令会被分解为:

  1. int newBanlance = act.getBanlance() - takeOut;
    计算出处取钱后剩余的余额。
  2. act.setBanlance(newBanlance);
    更新余额到对象账户种。
  3. 打印账户余额信息。

注意: 在这里我并未将每一个操作都分解为原子操作,因为这里的样例线程较少,操作也很少,为了方便讲解所以将风险忽略,但是在实际开发中并不能忽略。

现在,线程一执行完了第一步,然后他被剥夺了CPU执行权,线程二又开始执行第一步,虽然线程一取了5000元,但是还没来得及更新,所以线程二在执行时,账户对象的余额仍然是10000元,这样就产生了错误。

synchronized关键字实现线程同步

  • synchronized关键字是用来控制线程同步的。在多线程的环境下,synchronized控制的代码段不被多个线程同时执行,以达到保证并发安全的效果。
  • synchronized关键字实现的锁称为内部锁,内部锁是一种排他锁,能够保证原子性、可见性和有序性。之所以被称为内部锁,是因为线程对内部锁的申请与释放的动作都是由Java虚拟机负责实现的,开发者看不到这个锁的获取和释放过程。

将上部分代码通过synchronized改进,如下:

public void takeOut(Account act, int takeOut){
         synchronized (this){ 
            int newBanlance = act.getBanlance() - takeOut;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            act.setBanlance(newBanlance);
            System.out.println( Thread.currentThread().getName() + "取出:" + takeOut + "   剩余:" + newBanlance);
        }
 }

对以上代码的解释:

  1. synchronized (this){ … },意味着想要执行花括号中的代码,首先要在锁池中找到this的对象锁,找到锁后该线程才会进入可运行状态,没找到锁他就会进入阻塞状态,直到它在锁池中找到这个对象锁。(其它线程将这个锁释放)
  2. 两个线程同时对一个账户对象进行取钱操作,其中一个线程会先到账户对象锁,另一个线程找不到锁就会进入阻塞状态,当获得对象锁的线程将所有指令完成之后就会归还账户对象锁,处于阻塞状态的线程就会进入可运行状态。

**synchronized的三种用法 **

用法一:

synchronized(共享对象){
    //同步代码
}

用法二:

synchronized 加在实例方法上,锁的是this,同步整个方法体。

public synchronized void test(){......}

synchronized 加在静态方法上,找的是类锁,一个类无论new几个对象都只有一个类锁。

用法三:

public static synchronized void test(){......}

七、死锁

死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
在这里插入图片描述
死锁样例:

首先创建两个线程类,如下:

class myThread extends Thread{
    Object o1;
    Object o2;
    public myThread(Object o1, Object o2){
        this.o1 = o1;
        this.o2 = o2;
    }
    public void run(){
        synchronized (o1){
            System.out.println(Thread.currentThread().getName() + "拿到o1的锁,准备拿o2的锁");
            synchronized (o2){
                System.out.println(Thread.currentThread().getName() + "拿到o2的锁");
            }
        }
    }
}

class myThread2 extends Thread{
    Object o1;
    Object o2;
    public myThread2(Object o1, Object o2){
        this.o1 = o1;
        this.o2 = o2;
    }
    public void run(){
        synchronized (o2){
            System.out.println(Thread.currentThread().getName() + "拿到o2的锁,准备拿o1的锁");
            synchronized (o1){
                System.out.println(Thread.currentThread().getName() + "拿到o1的锁");
            }
        }
    }
}

创建这两个线程类对象,并传入相同对象,如下:

public class deadLock {
    public static void main(String[] args) {
        Object o1 = new Object();
        Object o2 = new Object();
        myThread t1 = new myThread(o1,o2);
        myThread2 t2 = new myThread2(o1,o2);
        t1.start();
        t2.start();
    }
}

运行结果如下:

在这里插入图片描述

八、简单实现生产消费者问题

生产消费者问题是多线程经典问题,其中生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据

相关方法

  • void notify()
    随机选择在该对象上调用wait方法的线程,解除其阻塞状态。该方法只能在一个同步方法或同步代码块中调用。如果当前线程不是该对象锁的持有者,该方法将会抛出一个IllegalMonitorStateException

  • void notifyAll()
    解除在该对象上调用wait方法的全部线程,解除其阻塞状态。该方法只能在一个同步方法或同步代码块中调用。如果当前线程不是该对象锁的持有者,该方法将会抛出一个IllegalMonitorStateException

  • void wait()
    使当前线程进入阻塞状态,并释放该对象的锁,直到它被通知(nitify),并且在调用时抛出InterruptedException,如果当前线程不是该对象锁的持有者,该方法将会抛出一个IllegalMonitorStateException

代码实现

public class ThreadApplication {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        ConsumerThread consumer = new ConsumerThread(list);
        ProcuderThread procuder = new ProcuderThread(list);
        consumer.setName("消费者");
        procuder.setName("生产者");
        consumer.start();
        procuder.start();
    }
}
//消费者线程
class ConsumerThread extends Thread{
    List<String> list = new ArrayList<>();
    public ConsumerThread(List<String> list){
        this.list = list;
    }
    public void run(){
        while(true){
            synchronized (list){
                if(list.isEmpty()){
                    try {
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName() + "消费了" + list.remove(0));
                list.notify();
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
//生产者线程
class ProcuderThread extends Thread{
    int i = 1;
    List<String> list = new ArrayList<>();
    public ProcuderThread(List<String> list){
        this.list = list;
    }
    public void run(){
        while(true){
            synchronized (list){
                if(list.size() == 10){
                    try {
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                list.add("产品" + i);
                System.out.println(Thread.currentThread().getName() + "生产了" + list.get(list.size() - 1));
                i++;
                list.notify();
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

编译结果如下:
在这里插入图片描述

你可能感兴趣的:(Java学习笔记,java,经验分享,面试)