JavaSE相关概念(12)——线程

一、概述

1.程序

        为解决某种问题,使用计算机语言编写的一系列指令(代码)的集合,本章中的程序,特指的是静态的,安装在硬盘上的代码集合。

2.计算机组成三大件

  • 硬盘,容量大,可以长久保存文件,造价低,速度慢,程序安装在硬盘上;
  • CPU,速度极快,用来执行指令,缓存,寄存器;
  • 内存,是临时储存数据,主要存储的是当前正在运行中的程序数据,运行中的程序,也被称为一个进程;

3.进程

        运行中的程序(被加载到内存中),是操作系统进行资源分配的最小单位。

4.线程

       进程可以进一步细化为线程,是进程内一个最小执行单位,是cpu进行任务调度的最小单位(早期没有线程,早期cpu在执行的时候,是以进程为单位执行,进程单位还是比较大的,当一个进程执行时,其他的进程就不能执行,所以后来,将进程中的多个任务,细化成线程,cpu执行单位,也从进程转为更小的线程)。

注:main也是一个线程叫做主线程,代码只能从上到下的顺序执行。以QQ举例,其为一个进程,操作系统会为这个进程分配内存资源,一个聊天窗口就认为是一个线程,这多个聊天窗口可以同时被cpu执行,但是这些聊天窗口属于进程。

5.进程和线程的关系

  • 一个进程中可以包含多个线程
  • 一个进程只能隶属于一个进程,线程不能脱离进程存在
  • 一个进程中至少有一个线程(即主线程)
  • 在主线程中可以创建并启动其他线程,所有线程都共享进程内存

二、创建线程

1.继承Thread类的方法

     在Java中要实现线程,最简单的方式就是扩展Thread类,重写其中的run方法,其本身并不执行任何操作,当线程启动时,它将执行run方法

定义

public class MyThread extends Thread{

        public void run(){

                //线程操作

        }

}

调用并启动线程

MyThread thread = new MyThread();

thread.start();

注意点

  1. 启用线程后,并不会立即执行,而是会看cpu调度;
  2. 不能直接调用run()方法,并不是启动线程,而要在主线程中调用start()方法;
  3. start()内部调用start0()方法,是本地方法(native)。

2.实现Runnable接口方式(用的多一点)

        可以通过Runnable接口的方式来实现线程,其中仅有一个抽象方法,只要实现其中的run方法。

定义

public class MyTask implements Runnable{

        public void run(){

                //线程操作

        }

}

调用并开启线程 

MyTask myTask = new MyTask();

Thread th = new Thread(myTask);

th.start();

注意点 

  1. 用此接口不能把这个类叫作线程,应该叫任务类;
  2. 此方法避免了单继承的局限性;
  3. 多个线程可以共享同一个接口实现类对象,非常适合多个相同线程来处理同一份资源。

三、Thread类中的常用方法

构造方法:

new Thread(runnable);==>接收一个任务对象

new Thread(runnable,String name);==>接收一个对象并设置名字

  1. void run();==>线程要执行的任务
  2. void start();==>启动线程
  3. static Thread currentThread();==>返回对当前正在执行的线程对象引用
  4. final String getName();==>返回线程的名称
  5. final void setPriority(int newPriority);==>设置线程的优先级(1~10,正常是5)
  6. final int getPriority();==>返回线程的优先级
  7. final void join();==>等待线程终止,即不结束其他线程不能进行
  8. static void sleep(long millis);==>让当前正在执行的线程休眠,时间由millis(毫秒)指定

四、线程优先级——操作系统线程任务任务调度算法

  • 先来先调度(FCFS)
  • 短作业优先(SJF)
  • 优先级
  • 高响应比优先
  • 时间片轮转
  • 多级反馈队列(集优)

五、线程状态

        线程生命周期,线程从创建到销毁,期间经历5个状态。

  1. 新建:new Thread();此状态还不能被执行,调用start();启动线程,让线程进入到就绪状态;
  2. 就绪:当获得到cpu执行权后,线程进入到cpu进行;
  3. 运行:运行中的线程可以被切换,回到就绪状态,也可能因为休眠(sleep)等原因进入阻塞状态;
  4. 阻塞:线程休眠时间到了,返回就绪状态;
  5. 死亡:所有任务执行结束,进入死亡状态。

如图所示:

JavaSE相关概念(12)——线程_第1张图片

守护线程 

        守护线程也是线程中的一种,区别在于它的结束,如果一个线程是守护线程,那么它会等java中其他的线程任务结束后,自动终止,它为其他线程提供服务,例如jvm的垃圾回收线程就是一个守护线程,守护线程设置要在start()方法之前,方法setDaemon(true)开启守护线程。

六、多线程

        在一个应用程序中,存在多个线程,不同的线程可以并行的执行任务。

优点

  1. 提高程序的处理能力;
  2. 提高了CPU的利用率;
  3. 改善程序结构,将复杂任务分为多个线程,独立运行。

缺点

  1. 多线程需要占用更多的内存资源和CPU资源;
  2. 多线程对同一个共享的资源进行访问,会出现线程安全问题。

1.线程同步

        通过排队+锁的方式能保护共享资源的安全性。

方式一:synchronized关键字

语法一————修饰同步代码块

synchronized (同步对象){

        //要保护的资源

}

同步对象的概念补充: 

        对多个线程对应的对象必须是同一个,在对象的对象头中有一块空间用来记录有没有线程进入到同步代码块中的,同步对象可以是Java中的任何类对象。

语法二————修饰方法

         synchronized放在方法声明中,表示整个方法为同步方法。

public [static] synchronized void show(){

        //要保护的资源

}

注意:

  1. static的添加取决于该线程类是继承Thread还是完善Runnable接口 ,前者在主线程中使用时会new多个对象,要添加static,而后者是任务类,不用添加,它在主线程中作为参数启动线程,不用new多个。
  2. 语法二的锁不需要我们提供了,会默认提供锁对象;
  3. synchronized如果修饰的是非静态方法,锁对象是this,如果修饰的是静态的方法,锁对象是class对象,一个类只有一个class对象;
对synchronized的举例:
import java.util.*;

public class Main {
    public static void main(String[] args) {
        Bank bank = new Bank(0);
        Thread thread = new Thread(bank);
        thread.start();
        Bank bank1 = new Bank(114514);
        bank.deposit(1919);
        bank.withdraw(810);
        System.out.println("银行还有:" + bank1.getBalance());
    }
}
class Bank implements Runnable{
    private int balance;

    public Bank(int balance) {
        this.balance = balance;
    }

    // 存钱
    public synchronized void deposit(int amount) {
        balance += amount;
    }

    // 取钱
    public synchronized void withdraw(int amount) {
        if (balance < amount) {
            throw new IllegalArgumentException("余额不足!");
        }
        balance -= amount;
    }

    public int getBalance() {
        return balance;
    }

    @Override
    public void run() {
        Bank bank = new Bank(114514);
        bank.deposit(1919);
        bank.withdraw(810);
        System.out.println("银行还有:" + bank.getBalance());
    }
}

        synchronized不仅可以修饰方法,还可以修饰代码块,通常是用来控制对某个对象的访问,以避免多个线程同时访问同一个对象所导致的竞争条件。 

        在这个示例中,我们使用了双重检查锁定(Double-checked Locking)的方式来确保单例。在getInstance()方法中,如果instance为空,则会创建一个新的Singleton对象,但是为了避免多个线程同时创建新的实例,我们使用了synchronized关键字来修饰代码块。

这个代码块中的锁是Singleton.class对象,也就是说,当一个线程进入这个代码块时,其他线程不能同时进入该代码块,直到锁被释放。这样可以避免多个线程同时创建Singleton实例的问题,从而保证单例的正确性和线程安全性。

public class Singleton {
    private static Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
 

方式二:用Lock对象充当

        从JDK5.0开始,同步锁使用Lock对象充当,ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,可以显式加锁放锁,其只能对代码块加锁,不能对方法加锁。

常用方法
  1. lock();==>加锁
  2. unlock();==>放锁

ReentrantLock是Java中的一个锁实现,它可以用于线程同步。下面是一个简单的例子,说明如何使用ReentrantLock进行线程同步:

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;

    public void increment() {
        lock.lock();
        try {
            count++; // 访问共享变量
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        lock.lock();
        try {
            return count; // 访问共享变量
        } finally {
            lock.unlock();
        }
    }
}

在上面的例子中,ReentrantLock被用来保护一个共享变量(count),确保多个线程同步访问该变量。

在increment方法中,lock.lock()方法会获取锁,如果锁已经被其他线程占用,则等待直到获得锁。在锁内部,可以访问共享变量,然后在finally块中调用lock.unlock()方法释放锁。

在getCount方法中,同样也需要获取锁,确保只有一个线程访问共享变量count,然后在finally块中释放锁。

这样可以保证多个线程并发操作共享变量时,不会出现数据竞争和错误结果,从而保证线程安全。

七、线程通讯

线程通讯指的是多个线程通过相互牵制,相互调度,即线程间的相互作用。

涉及三个方法:

  1. .wait一旦执行此方法,当前线程就进入阻塞状态,并释放同步锁对象。
  2. .notify一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait, 就唤醒优先级高的那个。
  3. .notifyAll一旦执行此方法,就会唤醒所有被wait的线程。

注意: .wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。

Java线程通信机制可以使用wait、notify和notifyAll方法,让不同线程之间协调工作。下面是一个简单的例子,说明这三个方法的用法:

public class ThreadCommunicationExample {
    public static void main(String[] args) {
        Message message = new Message();

        Thread producer = new Thread(new Producer(message));
        Thread consumer = new Thread(new Consumer(message));

        producer.start();
        consumer.start();
    }
}

class Message {
    private String content;
    private boolean empty = true;

    public synchronized String read() {
        while (empty) {
            try {
                wait(); // 等待消息的通知
            } catch (InterruptedException e) {}
        }
        empty = true;
        notifyAll(); // 通知生产者可以生产消息了
        return content;
    }

    public synchronized void write(String content) {
        while (!empty) {
            try {
                wait(); // 等待消费者的通知
            } catch (InterruptedException e) {}
        }
        empty = false;
        this.content = content;
        notifyAll(); // 通知消费者可以消费消息了
    }
}

class Producer implements Runnable {
    private Message message;

    public Producer(Message message) {
        this.message = message;
    }

    @Override
    public void run() {
        String[] messages = {"Hello", "World", "Good", "Morning"};
        for (String message : messages) {
            this.message.write(message);
            System.out.println("Produced " + message);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {}
        }
        this.message.write("Finished");
    }
}

class Consumer implements Runnable {
    private Message message;

    public Consumer(Message message) {
        this.message = message;
    }

    @Override
    public void run() {
        String message = null;
        while ((message = this.message.read()) != "Finished") {
            System.out.println("Consumed " + message);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {}
        }
    }
}

在上面的例子中,Message类表示一个消息对象,其中包含一个字符串和一个变量empty,用于表示消息对象是否为空。read方法表示消费者从消息对象中读取消息,如果消息对象为空,则等待生产者生产消息的通知。同时,消费者线程调用了wait()方法,使得自己进入阻塞状态,等待生产者线程调用notify()或者是notifyAll()方法来通知消费者线程可以消费消息了。

write方法表示生产者向消息对象中写入消息,如果消息对象不为空,则等待消费者消费消息的通知。同时,生产者线程调用了wait()方法,使得自己进入阻塞状态,等待消费者线程调用notify()或者是notifyAll()方法来通知生产者线程可以生产消息了。

在Producer和Consumer类中,分别模拟生产者和消费者。生产者线程不断向消息对象中写入消息,然后休眠1秒钟,消费者线程不断从消息对象中读取消息,然后休眠1秒钟,直到读取的消息为"Finished"时结束。

通过wait、notify和notifyAll方法的配合使用,生产者和消费者之间可以协调工作,保证线程安全。

特殊情况:

在使用ReentrantLock时,可以使用Condition对象来进行线程通信。Condition是一个基于锁的等待/通知机制,与Object中的wait、notify和notifyAll方法类似,但是更灵活。下面是一个简单的例子,说明ReentrantLock和Condition如何进行线程通信:

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

public class ThreadCommunicationExample {
    public static void main(String[] args) {
        Message message = new Message();

        Thread producer = new Thread(new Producer(message));
        Thread consumer = new Thread(new Consumer(message));

        producer.start();
        consumer.start();
    }
}

class Message {
    private String content;
    private boolean empty = true;
    private ReentrantLock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public String read() {
        lock.lock();
        try {
            while (empty) {
                condition.await(); // 等待消息的通知
            }
            empty = true;
            condition.signalAll(); // 通知生产者可以生产消息了
            return content;
        } catch (InterruptedException e) {
            return null;
        } finally {
            lock.unlock();
        }
    }

    public void write(String content) {
        lock.lock();
        try {
            while (!empty) {
                condition.await(); // 等待消费者的通知
            }
            empty = false;
            this.content = content;
            condition.signalAll(); // 通知消费者可以消费消息了
        } catch (InterruptedException e) {
        } finally {
            lock.unlock();
        }
    }
}

class Producer implements Runnable {
    private Message message;

    public Producer(Message message) {
        this.message = message;
    }

    @Override
    public void run() {
        String[] messages = {"Hello", "World", "Good", "Morning"};
        for (String message : messages) {
            this.message.write(message);
            System.out.println("Produced " + message);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {}
        }
        this.message.write("Finished");
    }
}

class Consumer implements Runnable {
    private Message message;

    public Consumer(Message message) {
        this.message = message;
    }

    @Override
    public void run() {
        String message = null;
        while ((message = this.message.read()) != "Finished") {
            System.out.println("Consumed " + message);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {}
        }
    }
}

在上面的例子中,Message类中定义了一个ReentrantLock对象lock和一个Condition对象condition,用于进行线程通信。read和write方法中使用lock.lock()和lock.unlock()来控制同步代码块,使用condition.await()和condition.signalAll()来进行线程等待和通知。

在Producer和Consumer类中,分别模拟生产者和消费者。生产者线程不断向消息对象中写入消息,然后休眠1秒钟,消费者线程不断从消息对象中读取消息,然后休眠1秒钟,直到读取的消息为"Finished"时结束。

通过ReentrantLock对象和Condition对象的配合使用,生产者和消费者之间可以协调工作,保证线程安全。

简单的来说就是在使用ReentrantLock时,可以使用Condition对象来进行线程通信,且:

  1. condition.signal(); == notify();
  2. condition.signalAll(); == notifyAll();
  3. condition.await(); == wait();

八、新增创建线程方式——Callable接口

import java.util.concurrent.Callable;

public class Example implements Callable {
    private String message;

    public Example(String message) {
        this.message = message;
    }

    public String call() throws Exception {
        Thread.sleep(1000);
        return "The message is: " + message;
    }

    public static void main(String[] args) {
        Example task = new Example("Hello, world!");
        FutureTask Ftask = new FutureTask(task);
        Thread thread = new Thread(Ftask);
        thread.start();
        String str = new String(Ftask.get());
    }
}
 

这段代码中,我们创建了一个实现了Callable接口的Example类,并在构造函数中传入一个字符串类型的参数,表示消息。

在call()方法中,我们使用Thread.sleep()模拟了一个耗时的操作,并在1秒后返回一个带有消息的字符串。

在main()方法中,我们首先创建了一个Example实例task,然后使用该实例创建了一个FutureTask对象Ftask,将该FutureTask对象传入一个新的线程中启动。

接着,我们调用Ftask.get()方法来获取call()方法的返回值,并将其保存在一个字符串变量str中。需要注意的是,Ftask.get()是一个阻塞操作,在线程执行call()方法的过程中,主线程会一直等待直到call()方法返回结果。如果call()方法抛出了异常,Ftask.get()也会抛出相应的异常。

因此,整个程序的流程是:

  1. 创建Example实例,并传入一个消息参数
  2. 使用Example实例创建一个FutureTask对象Ftask
  3. 将Ftask传入一个新的线程中启动
  4. 在主线程中调用Ftask.get()方法等待call()方法的执行结果
  5. call()方法执行完成并返回结果,Ftask.get()方法返回该结果并保存在一个字符串变量中

优点

  1. 有返回值;
  2. 方法可以抛出异常;
  3. 支持泛型的返回值;
  4. 需要借助FutureTask类,获得返回效果。

你可能感兴趣的:(Java,java)