【Java】多线程

1 概述

并发与并行:

  • 并发:指两个或多个事件在同一个时间段内发生。
  • 并行:指两个或多个事件在同一时刻发生(同时发生)。

在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行(CUP时间片),只不过是给人的感觉是同时运行,因为分时交替运行的时间非常短。

线程与进程:

  • 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
  • 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

💎一个程序运行后至少有一个进程,一个进程中可以包含多个线程

Java中提供两种方式实现线程,分别为:继承java.lang.Thread类与实现java.lang.Runnable接口

2 继承Thread类

构造方法:

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

常用方法:

  • public String getName() :获取当前线程名称。
  • public void start():导致此线程开始执行; Java虚拟机调用此线程的run方法。
  • public void run() :此线程要执行的任务在此处定义代码。
  • public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
  • public static Thread currentThread() :返回对当前正在执行的线程对象的引用。

创建并启动多线程步骤:

  1. 定义Thread类的子类,并重写run()方法,该run()方法的方法体即为线程需要完成的任务,因此把run()方法称为线程执行体。
public class MyThread extends Thread {
    @Override
    public void run() {
    }
}
  1. 创建Thread子类的实例,即创建了线程对象。
  2. 调用线程对象的start()方法来启动该线程。
public class Test1(){
    public static void main(String[] args) {
        MyThread mt = new MyThread("线程一");
        mt.start();
    }
}

3 实现Runnable接口

由于Java不支持多继承(如一个扩展JFrame类的GUI程序不可能再继承Thread类),这时该类就需要实现Runnable接口使其具有多线程功能。
实现Runnable接口的程序会创建一个Thread对象,并将Runnable对象与Thread对象关联。构造方法:

  • public Thread(Runnable target)
  • public Thread(Runnable target, String name)

以上两种构造方法参数中都存在Runnable实例,使用构造方法可以将Runnable与Thread实例相关联。

步骤:

  1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
public class MyRunnable implements Runnable{
    @Override
    public void run() {
    }
}
  1. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
  2. 调用线程对象的start()方法来启动线程。
public class Test2(){
    public static void main(String[] args) {
        MyRunnable mr = new MyRunnable();
        Thread t = new Thread(mr, "线程二");
        t.start();
    }
}

通过实现Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个执行目标。所有的多线程代码都在run方法里面。Thread类实际上也是实现了Runnable接口的类。
在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。
实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是继承Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的。

4 Thread和Runnable的区别

如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。

实现Runnable接口比继承Thread类所具有的优势:

  1. 适合多个相同的程序代码的线程去共享同一个资源。
  2. 可以避免java中的单继承的局限性。
  3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
  4. 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。

5 线程同步

当使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。例如:

/*例如电影院售票问题,总共只有100张票,有多个窗口在同时卖电影票*/
public class Ticket implements Runnable{
    private int ticket = 100; //总票数
    private int sum = 0;      //售出票数

    @Override
    public void run() { 
        while (true) {
            if (ticket > 0) {//有票可卖
                //出票,使用sleep模拟出票时间
                try{
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println(Thread.currentThread().getName()
                        + "正在卖...余票:" + --ticket + ",共售出票数:" + ++sum);
            }
        }
    }
}

public class Test3_Security {
    public static void main(String[] args) {
        //创建线程任务对象
        Ticket ticket = new Ticket();
        //创建三个窗口对象
        Thread t1 = new Thread(ticket,"窗口1");
        Thread t2 = new Thread(ticket,"窗口2");
        Thread t3 = new Thread(ticket,"窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

执行结果:
...
窗口3正在卖...余票:3,共售出票数:104
窗口3正在卖...余票:2,共售出票数:105
窗口1正在卖...余票:0,共售出票数:107
窗口2正在卖...余票:1,共售出票数:106

要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制(synchronized)来解决。

窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结束,窗口1和窗口2和窗口3才有机会进入代码去执行。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。

有三种方法完成同步操作:

  1. 同步代码块
  2. 同步方法
  3. 锁机制

5.1 同步代码块

synchronized关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。格式:

synchronized(同步锁) {
    需要同步操作的代码
}

使用同步代码块解决:

public class Ticket implements Runnable {
    Object lock = new Object();

    @Override
    public void run() { 
        while (true) {
            synchronized (lock) {
                //卖票操作
            }
        }
    }
}

执行结果:
...
窗口2正在卖...余票:3,共售出票数:97
窗口3正在卖...余票:2,共售出票数:98
窗口1正在卖...余票:1,共售出票数:99
窗口1正在卖...余票:0,共售出票数:100

5.2 同步方法

使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。格式:

public synchronized void method(){
    可能会产生线程安全问题的代码
}

使用同步方法解决:

ublic class Ticket implements Runnable{
    @Override
    public void run() { //卖票操作
        while (true) {
            sellTicket();
        }
    }

    public synchronized void sellTicket(){
        //卖票操作
    }
}

执行结果:
...
窗口2正在卖...余票:3,共售出票数:97
窗口3正在卖...余票:2,共售出票数:98
窗口1正在卖...余票:1,共售出票数:99
窗口1正在卖...余票:0,共售出票数:100

5.3 Lock锁

java.util.concurrent.locks.Lock 机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
Lock锁也称同步锁,加锁和释放锁方法化了,如下:

  • public void lock() :加同步锁。
  • public void unlock() :释放同步锁。

使用Lock锁解决:

public class Ticket implements Runnable{
    Lock lock = new ReentrantLock();

    @Override
    public void run() { //卖票操作
        while (true) {
            lock.lock();
            //卖票操作
            lock.unlock();
        }
    }
}

执行结果:
...
窗口3正在卖...余票:3,共售出票数:97
窗口3正在卖...余票:2,共售出票数:98
窗口2正在卖...余票:1,共售出票数:99
窗口1正在卖...余票:0,共售出票数:100
posted @ 2020-09-17 23:17  KJee  阅读(112)  评论(0编辑  收藏  举报