JavaSE部分多线程基础总结

介绍

  • 多线程是Java程序设计语言的一个亮点,它使用户可以很方便地编写多线程程序。虽然编写多线程程序需要考虑诸如安全、死锁、资源共享的问题,但是总体上讲Java在编写多线程程序时比其他语言都要简洁。
  • 多线程技术可以模拟多处理器的效果,对用户而言,计算机同时完成一个程序的多个任务。而实际上该机制使得计算机把CPU周期按照一定策略分配给每一个线程,而高速的CPU使用户觉得计算机在同时完成多个任务。

线程

线程是操作系统的概念,线程也称为轻量级进程(light-weight process,LWP),是CPU的基本使用单元,它的轻量级名称是和进程相关的。线程由线程ID、程序计数器、寄存器和堆栈组成,多个线程可以共享代码段、数据段和诸如打开的文件等系统资源。而传统的进程其实就是单线程控制程序,每个进程都有自己的代码段、数据段和其他系统资源。这无疑使得每个进程管理更多的内容,从而称 为重量级进程。“轻量”是指线程没有独自的存储空间,而是和同一个进程的多个线程共享存储空间
在这里插入图片描述

java多线程

线程调度

  • 分时调度:所有线程轮流调度,按时间分配
  • 抢占式调度:按照优先级进行调度

主线程

JVM执行main方法,main方法会进入到栈内存,jvm会通过操作系统开辟main方法通向cpu的一条路径。cup通过该路径来执行main方法,而这个路径有一个名字,叫做主线程。

创建线程

方法1----继承Thread

步骤:
①、定义类继承Thread;
②、复写Thread类中的run方法;
目的:将自定义代码存储在run方法,让线程运行
③、调用线程的start方法:
该方法有两步:启动线程,调用run方法。

package thread01;
//继承了Thread接口
public class CreateThread01 extends Thread{
    //重写run方法,自定义业务逻辑
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+"运行的结果:"+i);
        }
    }
    public static void main(String[] args) {
        CreateThread01 thread01 = new CreateThread01();
        Thread thread = new Thread(thread01,"extendsThread");
        thread.start();
    }
}

方法2----实现Runnab接口

接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为run 的无参方法。
实现步骤:
①、定义类实现Runnable接口
②、覆盖Runnable接口中的run方法,将线程要运行的代码放在该run方法中。
③、通过Thread类建立线程对象。
④、将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。
自定义的run方法所属的对象是Runnable接口的子类对象。所以要让线程执行指定对象的run方法就要先明确run方法所属对象
⑤、调用Thread类的start方法开启线程并调用Runnable接口子类的run方法。

package thread01;
//实现Runnable接口
public class CreateThread02 implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+"运行的结果:"+i);
        }
    }
    public static void main(String[] args) {
        CreateThread02 thread02 = new CreateThread02();
        Thread thread1 = new Thread(thread02,"implementsRunnable1");
        //lambda表达式来实现
        Thread thread2 = new Thread(()->{
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName()+"运行的结果:"+i);
            }
        },"implementsRunnable2");
        thread1.start();
        thread2.start();
    }
}

通过Callable和Future创建线程:

实现步骤:
①、创建Callable接口的实现类,并实现call()方法,这个方法将作为线程执行体,且具有返回值。
②、创建Callable实现类的实例,使用FutrueTask类进行包装Callable对象,FutureTask对象封装了Callable对象的call()方法的返回值
③、使用FutureTask对象作为Thread对象启动新线程。
④、调用FutureTask对象的get()方法获取子线程执行结束后的返回值。

package thread01;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class CreateThread03 implements Callable<String> {
    @Override
    public String call() throws Exception {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+"运行的结果:"+i);
        }
        return "执行完了...";
    }
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CreateThread03 thread03 = new CreateThread03();
        FutureTask<String> task = new FutureTask<String>(thread03);
        Thread thread = new Thread(task,"implementsCallable");
        thread.start();
        System.out.println(task.get());
    }
}

三种方式实现的优劣势

  • 继承Thread:线程代码存放在Thread子类run方法中。
    优势:编写简单,可直接用this.getname()获取当前线程,不必使用Thread.currentThread()方法。
    劣势:已经继承了Thread类,无法再继承其他类。
  • 实现Runnable:线程代码存放在接口的子类的run方法中。
    优势:避免了单继承的局限性、多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。
    劣势:比较复杂、访问线程必须使用Thread.currentThread()方法、无返回值。
  • 实现Callable:
    优势:有返回值、避免了单继承的局限性、多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。
    劣势:比较复杂、访问线程必须使用Thread.currentThread()方法

线程的生命周期

  • 在Java中线程的执行过程稍微有些复杂,但线程对象创建后并不是立即执行,需要做些准备工作才有执行的权利,而一旦抢占到CPU周期,线程就可以执行,但CPU周期结束则线程必须暂时停止,或当线程执行过程中的某个条件无法满足时也会暂时停止,只有等待条件满足时才会继续执行,最后从run()方法返回后,线程退出。可以看出,在线程的执行过程中涉及一些状态,线程就在这些状态之间迁移。

  • Java规范中只定义了线程的4种状态:新建状态、可运行状态、阻塞状态和死亡状态。为了更清晰地说明线程的状态变化过程,我们认为划分为5个状态更好理解,这里把可运行状态(Runnable)分解为就绪状态和运行状态,可以更好地理解可运行状态的含义。

  • 1)新建状态:线程对象(通过new关键字)已经建立,在内存中有一个活跃的对象,但是没有启动该线程,所以它仍然不能做任何事情,此时线程处在新建状态,程序没有运行线程中的代码。如果线程要运行,需要处于就绪状态。

  • 2)就绪状态:一个线程一旦调用了start()方法,该线程就处于就绪状态。此时线程等待CPU时间片,一旦获得CPU周期线程就可以执行。这种状态下的任何时刻线程是否执行完全取决于系统的调度程序。

  • 3)运行状态:一旦处于就绪状态的线程获得CPU周期,就处于运行状态,执行多线程代码部分的运算。线程一旦运行,只是在CPU周期内获得执行权利,而一旦CPU的时间片用完,操作系统会给其他线程运行的机会,而剥夺当前线程的执行。在选择哪个线程可以执行时,操作系统的调度程序会考虑线程的优先级。

  • 4)阻塞状态:该状态下线程无法执行,必须满足一定条件后方可执行。如果线程处于阻塞状态,JVM的调度机不会为其分配CPU周期。而一旦线程满足一定的条件就解除阻塞,线程处于就绪状态, 此时就获得了被执行的机会。 当发生以下情况时会使得线程进入阻塞状态:
    线程正等待一个输入、输出操作,该操作完成前不会返回其调用者。
    线程调用了wait()方法或sleep()方法。
    调用了线程的suspend()方法,该方法已经不推荐使用。
    线程需要满足某种条件才可以继续执行。 如果线程处于被阻塞状态,其他线程就可以被CPU执行。而当一个线程解除了阻塞状态,如线程等待输入、输出操作而该操作已经完成,线程调度机会检查该线程的优先权,如果优先权高于当前的运 行线程(如就绪状态的线程和运行状态的线程),该线程将抢占当前线程的资源并开始运行。

  • 5)死亡状态:线程一旦退出run()方法就处于死亡状态。在Java2中通过调用stop()方法和destroy()方法使线程死亡,但这些方法都会引起程序的不稳定。由于stop()方法已经过时了(事实证明该方法容易造成程序的混乱)
    在这里插入图片描述

线程的优先级

  • 线程的优先级表示一个线程被CPU执行的机会多少。注意,这里用“机会多少”而不是用“先后顺序”来表达。在Java中虽然定义了设置线程优先级高低的方法,但是优先级低并不意味着在不同优先 级的线程中就不会被执行,优先级低只说明该线程被执行的概率小,同理优先级高的线程获得CPU周期的概率就大。
  • 通过Thread类的setPriority()方法设置线程的优先级,该方法的参数为int型。其实Java提供了3个优先级别,都为Thread类的常量,从高到低依次为Thread.MAX_PRIORITY、Thread.NORM_PRIORITY、Thread.MIN_PRIORITY。这里再次重申,优先级低并不意味着线程得不到执行,而是线程被执行的概率小。这也说明设置线程的优先级不会造成死锁的发生。
package thread02;
public class MyPriorityThread extends Thread{
    private int id;
    //利用构造方法来设置线程的优先级
    public MyPriorityThread(int level,int id){
        this.id = id;
        //设置其优先级
        setPriority(level);
    }
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(id+"线程运行"+i+"---->"+this.getPriority());
        }
    }
    public static void main(String[] args) {
        new Thread(new MyPriorityThread(Thread.MAX_PRIORITY,1)).start();
        new Thread(new MyPriorityThread(Thread.MIN_PRIORITY,2)).start();
        //new Thread(new MyPriorityThread(Thread.MIN_PRIORITY,3)).start();
    }
}

线程的同步

在多线程中经常遇到的一个问题就是资源共享问题。假设两个线程同时访问一个数据区,一个读数据,一个写数据,在一个线程读数据前另一个线程修改了数据,则读数据线程读到的不是原始数据而 是被修改过的数据,显然这样是不允许的。而在多线程编程中经常会遇到访问共享资源的问题,这些资源可以是数据、文件、一块内存区或是外围设备的访问等。所以必须解决在多线程编程中实现资源共 享的问题,在Java中称为线程的同步问题。在多数的编程语言中解决共享资源冲突的方法是采用顺序机制(Serialize),通过为共享资源加锁的方法实现资源的顺序访问。
例如:

package thread02;
public class QuestionThread {
    private static int card = 10;
    static class RunnableImpl implements Runnable{
        @Override
        public void run() {
            while (true){
                if (card>0){
                    System.out.println(Thread.currentThread().getName()+"处理数据"+card);
                    card--;
                }else {
                    break;
                }
            }
        }
    }
    public static void main(String[] args) {
        new Thread(new RunnableImpl(),"thread01").start();
        new Thread(new RunnableImpl(),"thread02").start();
    }
}

同步方法

synchronized关键字

1.同步代码块
synchornized可以用于方法中的某个区块,表示只对这个区块的资源实行互斥访问。
同步锁:对象的同步锁只是一个概念,理解为对象上标记的一个锁。
对上述代码优化:

				synchronized (lock){
                    if (card>0){
                        System.out.println(Thread.currentThread().getName()+"处理数据"+card);
                        card--;
                    }else {
                        break;
                    }
                }

2.同步方法

package thread02;
public class QuestionThread {
    private static int card = 10;
    static class RunnableImpl implements Runnable{

        @Override
        public void run() {
            getCard();
        }
    }
    //同步方法
    public synchronized static void getCard(){
        while (true){
            if (card>0){
                System.out.println(Thread.currentThread().getName()+"处理数据"+card);
                card--;
            }else {
                break;
            }
        }
    }
    public static void main(String[] args) {
        new Thread(new RunnableImpl(),"thread01").start();
        new Thread(new RunnableImpl(),"thread02").start();
    }
}

3.锁机制

package thread02;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class QuestionThread {
    private static int card = 10;
    static Lock lock=new ReentrantLock();//获取lock对象。多态的运用,lock接口指向了他的实现类。
    static class RunnableImpl implements Runnable{
        @Override
        public void run() {
            while (true){
                try {
                    lock.lock();
                    if (card>0){
                        System.out.println(Thread.currentThread().getName()+"处理数据"+card);
                        card--;
                    }else {
                        break;
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        }
    }
    public static void main(String[] args) {
        new Thread(new RunnableImpl(),"thread01").start();
        new Thread(new RunnableImpl(),"thread02").start();
    }
}

线程的控制

线程是一个相对独立的执行单元,完成一个具体的任务。线程有被创建、执行、阻塞、恢复执行、结束等行为,这些行为组成了线程的控制机制

  1. 启动线程:
    无论是通过继承Thread类实现多线程还是通过实现Runnable接口实现多线程,如果要启动线程都需要Thread类的start()方法。该方法完成线程执行的一些初始化工作。
  2. 线程的休眠
    Java提供了一种控制线程的方法sleep(int miliseconds)这里称为线程的休眠,它将线程停止一段时间。该时间由方法的参数决定,当时间结束时线程进入就绪状态,可以抢占CPU的时间周期
  3. 等待和通知
    等待和通知实现了线程之间的协调机制,使得线程之间可以建立“和谐”的协作关系
    Java提供了线程对象的wait()、notifty()或notifyAll()方法来实现这种协作,wait()方法使线程挂起一段时间,而 notifty()或notifyAll()方法使线程从wait()方法调用的状态中恢复到就绪状态
    wait()和sleep()方法相似,都是让线程暂时挂起,都可以接受一个时间参数来确定线程挂起时间。但是wait()方法有如下特殊之处:
    1)线程一旦调用wait()方法,线程中同步方法的锁被释放,其他线程可以调用该线程中相应的同步方法
    2)使用wait()方法的线程可以使用notifty()或notifyAll()方法获得执行的权利,即获得抢占CPU周期的权利。 wait()可以使线程在等待外部输入条件时,让线程暂时休眠,等待notify()或notifyAll()方法来唤醒线程以检查是否有外部条件的输入。可见wait()方法为线程之间的同步提供了方法。
  4. 结束线程:
    如果想要安全地结束线程又不使用stop()方法,则只有通过某种替代方法了。这里提供一种通过线程间协作终止线程的间接方法:在线程内部设计一个变量和一个可以设置该变量的方法,而该变量的 取值作为结束线程的标志。

stop方法不建议使用

当调用stop()方法终止一个线程时,会释放该线程持有的所有锁,而问题是用户无法知道代码目前的工作内容,这是导致stop()方法不安全的因素。如果通过谨慎的设计,或许可以安全地使用该方法。 如果用户知道在调用该方法时,线程没有处在处理或更新其他对象或数据的状态,则可以安全地使用该方法,但是有多少程序员会遇到这种情况呢?非常少!多数情况下,用户认为自己安全地使用了 stop()方法来结束线程,往往会造成不可预知的后果。

package thread03;

public class ThreadWait {
    static Object obj = new Object();
    public static class Thread01 extends Thread{
        @Override
        public synchronized void run() {
            synchronized (obj){
                try {
                    System.out.println(this.getName()+"等待");
                    obj.wait();
                    System.out.println(this.getName()+"被唤醒了");
                    obj.notify();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public static class Thread02 implements Runnable{
        @Override
        public  void run() {
            synchronized (obj){
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                obj.notify();
                System.out.println(Thread.currentThread().getName()+"等待");
                try {
                    obj.wait();
                    System.out.println(Thread.currentThread().getName()+"被唤醒了");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public static void main(String[] args) {
        new Thread(new Thread01()).start();
        new Thread(new Thread02()).start();
    }
}

线程之间的通信

  • 线程间进行输入/输出通信最常用的方式是“管道”方式。Java线程支持这种形式的通信,即一个线程从管道一端写入数据,另一个线程从管道对端读出数据。用户不必关心管道是如何传输数据和如何实现管道两端的线程通信的。在Java的输入/输出类库中两个类PipedWriter和PipedReader都支持管道通信方式。前者允许向管道写数据,后者允许不同的线程从同一个管道读数据。

PipedWriter

  • public void connect(PipedReader reader)throws IOException:该方法使PipedWriter对象链接PipedReader对象,如果该PipedWriter对象已经链接到其他PipedReader对象,则抛出IOException异常。如果Reader是未链接的对象,而且PipedWriter类对象writer也是未链接的对象,二者可以使用如下方式建立链接且效果相同。
    writer.connect(reader) 或者: reader.connect(writer)
  • public int write()throws IOException:该方法的作用是向管道中写(输入)字符。
  • public int write(char[]cbuf,int off,int length)throws IOException:该方法的作用是从数组中读取字符,从数组cbuf中第off个字符开始,长度为length。
  • public Boolean flush()throws IOException:该方法的作用是把输出流中的数据全部输出,并且强迫缓冲区中的所有数据全部写入输出流。
  • public void close()throws IOException:该方法关闭管道流,并且释放和管道流相关的链接资源。如果有输入/输出错误,则抛出IOException异常。

PidedReader

  • public void connect(PipedWriter writer)throws IOException:该方法使PipedReader对象链接PipedWriter对象,如果该PipedReader对象已经链接到其他PipedWriter对象,则抛出IOException异常。如果writer是未链接的对象,而且PipedReader类对象reader也是未链接的对象,二者可以使用如下方式建立链接且效果相同。
    reader.connect(writer) 或者: writer.connect(reader)
  • public int read()throwsIOException:该方法的作用是读取来自管道的字符流的下一个字符。如果读完管道字符流而无法再获得数据,则返回值为-1。该方法在可以获得数据前或异常发生时会一直阻塞。如果一个线程要提供字符数据给已经建立链接的PipedWriter对象,但是此时该线程死亡,则也会抛出IOException异常。该函数的返回值是字符流的下一个字符。
  • public int read(char[]cbuf,int off,int length)throws IOException:该方法的作用是读取字符流中从第off个字符开始的length个字符,并存储到字符数组cbuf中。如果字符流中的字符总数少于10个,则该方法读取字符流中的所有字符。
  • public Boolean ready()throws IOException:该方法的作用是判断是否准备好读取已经建立的管道字符流中的字符,如果循环缓冲区不空则说明该流已经准备好链接。
  • public void close()throws IOException:该方法关闭管道流,并且释放和管道流相关的链接资源。如果有输入/输出错误,则抛出IOException异常。
package thread03;

import java.io.IOException;
import java.io.PipedReader;
import java.io.PipedWriter;

public class ThreadConnection {
    static PipedWriter writer = new PipedWriter();
    static PipedReader reader = new PipedReader();
    public static class ThreadWriter extends Thread{
        @Override
        public void run() {
            try {
                writer.connect(reader);
                writer.write("hello,are you ok?");
                writer.flush();
                writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    public static class ThreadReader implements Runnable{

        @Override
        public  void run() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            char [] chars = new char[1024];
            int len = 0;
            try {
                while ((len=reader.read(chars))!=-1){
                    System.out.println(new String(chars));
                }
                reader.close();
            }catch (Exception e){
                System.out.println(e);
            }

        }
    }
    public static void main(String[] args) {
        new Thread(new ThreadWriter()).start();
        new Thread(new ThreadReader()).start();
    }
}

死锁问题

由于线程会进入阻塞状态,并且由于对象同步锁 的存在,使得只有获得对象的锁才能访问该对象,因此很容易发生循环死锁。如线程A等待线程B释放锁,而线程B等待线程C释放锁,线程C又等待线程A释放锁,这样就造成一个轮回等待,3个线程都无法 继续运行。

避免死锁的基本原则:
1)避免使用suspend()和resume()方法,这些方法与生俱来具有容易产生死锁的缺点。

2)不要对长时间I/O操作的方法施加锁。

3)使用多个锁时,确保所有线程都按相同的顺序获得锁。

线程池

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。————线程池来解决
可以参照这篇文章

package thread03;

import java.util.concurrent.*;

public class ThreadPool {

    public static void main(String[] args) {
        ExecutorService es = Executors.newFixedThreadPool(2);
        es.submit(new ThreadTest());
        es.submit(new ThreadTest());
        es.shutdown();//关闭
    }
}
class ThreadTest implements Callable<String>{
    @Override
    public String call() throws Exception {
        System.out.println(Thread.currentThread().getName()+"执行");
        return Thread.currentThread().getName();
    }
}

关于多线程的基本介绍就到这了

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值