Java 线程01:并发并行、进程线程、3种线程创建、线程安全与锁对象


一、线程概念


1、并发与并行

并发:指两个或多个事件在同段时间内发生。

并行:指两个或多个事件在同一时刻同时发生。

并行比并发效率高。

在这里插入图片描述

注意:

单核处理器的计算机肯定是不能并行处理多个任务,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上理解线程是并行的,但从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,把这个情况称之为线程调度。


2、线程与进程

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

简而言之:一个程序运行后至少有一个进程,一个进程至少有一个线程。多线程效率高,多个线程之间互不影响。


3、线程调度

  • 分时调度

    所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间

  • 抢占式调度

    优先让优先级高的线程使用CPU,如果线程优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

    • 可在任务管理器中详细信息一栏设置线程的优先级
    • CPU在多个线程中高速切换。优先级高的线程使用CPU的几率就大很多,使用时间也会多很多。如果CPU是多核多线程的,那么比CPU单核单线程的处理线程效率高。

二、线程

Thread、Runnable、Callable

3种创建方式:

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

注意:

thread不建议使用,因为OOP单继承的局限性。

推荐使用实现runnable接口的方式,创建线程,避免OOP单继承的局限性、灵活方便、方便一个对象被多个线程调用。

Java中默认有2个线程:main线程、GC(垃圾回收)线程


1、主线程

在Java中,main方法就是一个主线程。

单线程程序:java程序中只有一个线程执行main方法开始,从上往下依次执行。当该线程中有一处发生异常,那么该线程就会终止,就不会往后继续执行。

JVM执行main方法,main方法会进入栈内存,JVM会找操作系统开辟一条main方法通向CPU的执行路劲,CPU可通过该路径执行main方法,而这个路径有一个名字叫mian(主)线程

public class Test{
    public static void main(String[] args){
        Person p1 = new Person("xi");
        p1.run();
        System.out.println(0/0); // 在这里会抛出ArithmeticException,就会终止程序,后面代码不会执行
        Person p2 = new Person("wa");
        p2.run();
    }
}

2、多线程原理

当线程对象创建之后,调用start方法,就会启动新的一个线程,JVM就会通知OS给新的线程开辟一条路径,main线程和新的线程同时会抢夺一个CPU,CPU的调度是不能人为干预,都是由CPU随机调取其中一个线程来处理,在两个线程之间来回高速切换。

多线程内存原理

在这里插入图片描述

执行main线程,main方法就会进入到栈内存中,main线程开始执行,第一行代码new了一个线程对象,那么该对象就会存在推内存中;下一行代码调用了该对象的run方法,那么就会将run方法引入到主线程的栈内存中执行;下一行代码调用了start方法,那么就会开辟一个新的栈内存,该栈内存就引入该线程对象里的run方法,在新的栈内存中执行,也就是开启了新的一个线程。CPU就会在多个线程之间进行随机挑选,来回高速切换执行。

简而言之,只要线程对象调用了start方法,就会开辟新的栈内存空间,执行该线程对象的run方法,栈内存之间的执行互不影响,相互独立。

run()与start() 区别

在这里插入图片描述

线程不一定立即执行,由CPU调度安排。


3、Thread类

java.lang.Thread类实现了Runnable接口,代表线程,所有的线程对象都必须是Thread类或其子类的实例或者可以让Thread类做代理。每个线程的作用是完成一定的任务,实际上就是执行一段程序流。Java使用线程执行体来代表这段程序流。

构造方法

常用

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

常用方法

  • public String getName() 获取线程的名称
  • public void setName() 改变线程的名称
  • public void start() 导致此线程开始执行; Java虚拟机调用此线程的run方法。线程只能启动一次,多次启动非法。
  • public void run() 此线程要执行的任务代码
  • public static void sleep(long millis) throws InterruptedException 使当前正在运行的线程暂时停止执行的毫秒数。报中断异常
  • public static Thread currentThread() 返回对当前正在执行的线程对象的引用
  • public static void yield() 线程礼让,退出线程,进入可运行状态,并且线程调度的时候也可能会被cpu选中。yield()方法会通知线程调度器放弃对处理器的占用,并不会释放锁,但调度器可以忽视这个通知。yield()方法主要是为了保障线程间调度的连续性,防止某个线程一直长时间占用cpu资源。但是他的使用应该基于详细的分析和测试。这个方法一般不推荐使用,它主要用于debug和测试程序,用来减少bug以及对于并发程序结构的设计。(可释放cpu资源,不释放锁)
  • public final void join() throws InterruptedException 强行插队,线程调用这个方法,就会先执行该线程,并该线程死亡了其他线程才能继续执行。

Java通过继承Thread类来创建启动多线程的步骤如下:

  1. 定义Thread类的子类,并重写run()方法,该run()方法的方法体就是代表了线程需要完成的任务,因此run()方法称为线程执行体;
  2. 创建Thread子类的实例,即创建了线程对象;
  3. 调用线程对象的start()方法来启动该线程。
// 使用继承Thread类,通过实例调用start方法开启一个新的线程
public class ThreadTest extends Thread {

    public ThreadTest() {
    }

    public ThreadTest(String name) {
        super(name); // 将给线程设置的名称传给父类进行设置
    }
    
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println(getName() + i + "次");
        }
    }

    public static void main(String[] args) {
        // ThreadTest threadTest = new ThreadTest("heroC");
        ThreadTest threadTest = new ThreadTest();
        threadTest.setName("heroC"); // 给该线程设置名称
        threadTest.start();
        for (int i = 0; i < 1000; i++) {
            System.out.println("main线程执行" + i + "次");
        }
        System.out.println(Thread.currentThread().getName());// 输出 main;获取当前线程的名称。Thread.currentThread()获取到了当前线程的对象,然后在调用getName()获取该线程的名称
    }
}

练习:通过继承线程类,开启多线程下载网络图片

该练习,要使用到FileUtils类,该类为io工具类,是apache旗下的。

import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;

public class DownPricFileUtis extends Thread{
    private String url;
    private String name;

    public DownPricFileUtis(String url, String name) {
        this.url = url;
        this.name = name;
    }

    @Override
    public void run() {
        Downloader d1 = new Downloader();
        d1.downLoader(url,name);
        System.out.println(name + "已下载完成!");
    }

    public static void main(String[] args) {
        DownPricFileUtis downP1 = new DownPricFileUtis("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1582892937486&di=1f14ad0c117a0695ff8d4701d91a1b9f&imgtype=jpg&src=http%3A%2F%2Fimg3.imgtn.bdimg.com%2Fit%2Fu%3D637719543%2C1600461480%26fm%3D214%26gp%3D0.jpg","1.jpg");
        DownPricFileUtis downP2 = new DownPricFileUtis("https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=1858174317,3137906118&fm=26&gp=0.jpg","2.jpg");
        System.out.println("正在下载,请稍后...");

        // 开启2条线程下载,会自动执行重写的run方法
        downP1.start();
        downP2.start();
    }
}

class Downloader{
    public void downLoader(String url, String name){
        try {
            FileUtils.copyURLToFile(new URL(url), new File(name));
        } catch (IOException e) {
            System.out.println("IO异常,downLoader方法");
        }
    }
}

4、Runable接口

java.lang.Runnable 创建一个线程是声明实现类Runnable接口。该接口只有一个run方法。

推荐使用Runnable对象,因为Java单继承的局限性

实现线程步骤:

  1. 定义Runnable接口的实现类,并重写该接口的run方法;
  2. 创建Runnable实现类的实例对象,并以此实例作为Thread的target来创建Thread对象,该Thread才是真正的线程对象;
  3. 调用线程的start方法来启动创建一个新的线程。
// 实现Runnable接口
public class Test implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            // 获取当前线程的引用之后,获取当前线程的名字
            System.out.println(Thread.currentThread().getName() + ": " + i);
        }
    }

    public static void main(String[] args) {
        Test test = new Test();
        // 通过Thread类将Runnable实现类传过去,并给线程命名,并启动
        new Thread(test,"heroC").start();
        for (int i = 0; i < 1000; i++) {
            System.out.println(Thread.currentThread().getName() + ": " + i);
        }
    }
}

5、Thread类 与 Runnable接口 的区别

Thread类实现了Runnable接口。如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的类,因为不是继承了Thread类,就不是真正的线程代码,则很多类都可以使用该类的代码,达到资源共享的目的。

【总结】

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

  1. 适合多个相同的程序代码的线程去共享同一个资源;

    (如果多个程序需要同一个线程的代码,直接用Runnable实现的类去写run方法,然后不同程序将该类引用过去,new一个Thread方法将该类创建成一个线程执行run代码)

  2. 可以避免java中的单继承的局限性;

    (很多时候当你继承了一个父类,你这个类又要使用Thread类,因不能多继承,所以无法实现相应功能,为了解决该问题,就可以使用线程代理Runnable来创建一个线程)

  3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立;

  4. 线程池只能放入实现Runanble或Callable类线程,不能直接放入继承Thread的类。

扩充:

在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每个JVM其实是在操作系统中启动了一个进程。


6、匿名内部类实现线程创建

匿名内部类,就是没有名字的类。只是把类的方法,提到了new对象的后面来写了,直接使用接口或者父类来创建一个匿名的类。

格式1(使用有参构造方法)避免错,推荐使用格式:

new 父类/接口(构造方法的参数){
	重写方法/实现方法
};

格式2(使用无参构造方法):

new 父类( () - > {
	重写方法/实现方法
});

实战:

public class NoNameTest {
    public static void main(String[] args) {
        
        // 创建了一个匿名类,该匿名类继承了Thread类,并重写了run方法,调用了star方法启动线程
        new Thread("Thread"){
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName() + ": " + i);
                }
            }
        }.start();

        
        // 创建了一个匿名类,实现了Runnable接口,并重写了run方法,调用了start方法启动线程
        /*Runnable runnable = new Runnable(){   // 多态,接口变量与一个接口的实现类
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName() + ": " + i);
                }
            }
        };*/
        // 可简写成:
        Runnable runnable = () -> {  // 多态,接口变量与一个接口的实现类
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        };
        new Thread(runnable,"Runnable").start();

        // Runnable还可以简化:
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName() + ": " + i);
                }
            }
        },"RunableSimple").start();
    }
}

7、Callable<V> 接口

java.util.concurrent Interface Callable<V>

只有一个抽象类,V call() trows 异常

好处:

  1. 可以返回值
  2. 可以抛出异常
public class CallableTest implements Callable<Boolean> {
    // 重写call方法
    @Override
    public Boolean call() throws Exception {
        System.out.println(Thread.currentThread().getName()+"   Callable执行的线程...");
        return true;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建线程池
        ExecutorService executorService = Executors.newFixedThreadPool(3);// 不建议这样创建线程池,会出现OOM,具体见JUC

        // 提交线程,创建线程
        Future<Boolean> s1 = executorService.submit(new CallableTest());
        Future<Boolean> s2 = executorService.submit(new CallableTest());
        Future<Boolean> s3 = executorService.submit(new CallableTest());

        // 获取线程返回值
        Boolean sr1 = s1.get();
        Boolean sr2 = s2.get();
        Boolean sr3 = s3.get();

        // 关闭线程池服务
        executorService.shutdownNow();
    }
}

三、线程安全


1、线程安全

有多个线程在同时运行,而这些线程都是相互独立,互不影响的。假如多个线程同时买100张电影票,每个线程运行结果就如同单线程运行的结果一样,每个线程都会去卖这100张票,就会出现线程安全问题,多个线程会出现卖出同一张票的情况以及会出现卖出负数票的情况。

通过卖票案例,演示线程出现安全问题:

public class TicketSalesProblem implements Runnable{
    // 有100张票
    private int tickets = 100;

    /*
    执行买票操作
     */
    @Override
    public void run() {
        while (tickets > 0){
            // 为了提高安全问题的出现率,每次让线程暂停再执行,模拟处理器处理速度满的情况,
            // 这时就会出现大量重复票和负数票
            try {
                TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                System.out.println("中断异常");
            }
            System.out.println( Thread.currentThread().getName() + "已售出第" + tickets-- + "号票");
        }
    }

    public static void main(String[] args) {
        // 一个Runnable实例对象,开启3个线程
        TicketSalesProblem ticketSalesProblem = new TicketSalesProblem();
        new Thread(ticketSalesProblem,"售票窗口A  ").start();
        new Thread(ticketSalesProblem,"售票窗口B  ").start();
        new Thread(ticketSalesProblem,"售票窗口C  ").start();
    }
}

// 该案例输出了大量重复的票和负数票
/*
...
售票窗口A  已售出第44号票
售票窗口B  已售出第44号票
售票窗口C  已售出第43号票
...
售票窗口C  已售出第2号票
售票窗口C  已售出第1号票
售票窗口B  已售出第0号票
售票窗口A  已售出第-1号票
...
*/

出现0号票和负数票的原因:
在这里插入图片描述

出现重复票的原因:

在执行过程中,该电脑是4核的,可并行执行线程,所以难免两个线程会同时执行输出语句,这时候tickets的值都是一样的,还没有发生变化,所以在这一时刻,会卖出同一个号码的票。

注意:

线程安全问题是不能产生的,解决方案是,当线程在访问共享资源的时候,无论该线程是否失去了CPU执行权,其他线程都必须等待,等待当前线程完成所有操作,其他线程才可以操作。只要有一个进程在操作该共享资源,其他线程都必须等待。保证,始终只有一个线程在执行共享资源代码就可。


2、线程同步

当我们使用多个线程访问同一资源时候,且多个线程中对资源有写的操作或更改的操作,就容易出现线程安全问题。要解决多线程并发访问一个资源的安全性问题,Java中提供了同步(synchronized)机制来解决。

根据卖票案例简述:

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

为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。

有3种方式完成同操作:

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

1)同步代码块

使用关键字synchronized的代码块,表示只对这个区块的资源实行互斥访问。

格式:

synchronized(锁对象){
	可能会出现线程安全问题的代码
}

同步锁:

对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁。

  1. 锁对象 可以是任意类型
  2. 多个线程对象 要使用同一把锁

注意:

在任何时候,最多允许一个线程有同步锁,谁拿到这个锁就可以进入代码块,其他线程只能在外面等着(BLOCKED)。

缺点:

​ 程序频繁的判断锁,获取锁,释放锁,程序的效率会降低。

使用同步代码块解决线程安全:

public class SolveTicketSalesProblem implements Runnable{
    // 有100张票
    private int tickets = 100;
    // 创建一个锁对象
    Object object = new Object();
    /*
    执行买票操作
     */
    @Override
    public void run() {
        while (true){
            // 同步代码块
            synchronized (object){
                // 为了提高安全问题的出现率,每次让线程暂停再执行,模拟处理器处理速度满的情况
                // 因为有同步代码块,锁对线程的标记,所以不会出现线程安全问题
                // 一定要在锁里面判断票是否还有
                if(tickets > 0){
                    try {
                        TimeUnit.MILLISECONDS.sleep(500);
                    } catch (InterruptedException e) {
                        System.out.println("中断异常");
                    }
                    System.out.println( Thread.currentThread().getName() + "已售出第" + tickets-- + "号票");
                }
            }
        }
    }

    public static void main(String[] args) {
        SolveTicketSalesProblem salesProblem = new SolveTicketSalesProblem();
        new Thread(salesProblem,"售票窗口A").start();
        new Thread(salesProblem,"售票窗口B").start();
        new Thread(salesProblem,"售票窗口C").start();
    }
}

同步代码块的原理

多个线程执行一个共享资源,当某个线程抢到了CPU执行权就会进入run方法开始执行,当执行到synchronized同步代码块时,就会检查有没有锁对象,如果有锁对象,就会携带者锁对象进入到同步代码块进行执行,如果没有锁对象,那么就会在同步代码块外便阻塞等待,直到其他线程释放了锁对象,该线程拿到锁对象为止。所以,同步代码块中,永远只能一个线程在执行。

总结:同步中的线程,没有执行完不会释放锁对象,同步外的线程没有锁对象进不去同步。


2)同步方法

使用关键字synchronized修饰的方法,叫做同步方法,保证A线程执行该方法的时候,其他线程都在方法外等着。

格式:

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

同步方法的同步锁是谁?

如果是static修饰的方法,使用的是当前方法所在类的字节码对象(类.class)

如果非static修饰的方法,同步锁就是使用了this对象(多个线程都是操作的一个对象,所以this对象也是唯一的)

使用同步方法代码如下:

public class SolveTicketSalesProblem2 implements Runnable {
    // private int tickets = 100;
    private static int tickets = 100;

    @Override
    public void run() {
        while (true){
            // buyTickets();
            buyTicketsStatic();
        }
    }

    // 非静态方法 同步锁就是给该类实例化的对象, this
    // 有锁对象的线程才能执行该同步方法,否则在该同步方法外等待
    /*public synchronized void buyTickets(){
        if(tickets > 0){
            try {
                TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                System.out.println("中断异常");
            }
            System.out.println( Thread.currentThread().getName() + "已售出第" + tickets-- + "号票");
        }
    }*/

    // 静态方法 同步锁是本类的class属性,就是class文件
    // 因为静态方法创建与类同步的,早于实例之前,所以使用class文件对象作为锁对象
    public static synchronized void buyTicketsStatic(){
        if(tickets > 0){
            try {
               TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                System.out.println("中断异常");
            }
            System.out.println( Thread.currentThread().getName() + "已售出第" + tickets-- + "号票");
        }
    }

    public static void main(String[] args) {
        SolveTicketSalesProblem2 salesProblem2 = new SolveTicketSalesProblem2();
        new Thread(salesProblem2,"售票窗口A").start();
        new Thread(salesProblem2,"售票窗口B").start();
        new Thread(salesProblem2,"售票窗口C").start();
    }
}

3)Lock 接口 JDK 1.5

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

Lock锁也称之为同步锁。加锁和释放锁被方法化了,如下:

  • public void lock() 获得同步锁
  • public void unlock() 释放同步锁

java.util.concurrent.locks.ReentrantLock 类实现了Lock接口。可通过该类使用lock方法和unlock方法。

使用如下:

public class SolveTicketSalesProblem3 implements Runnable{
    private int tickets = 100;

    Lock lock = new ReentrantLock(); // 创建一个锁对象(多态)

    @Override
    public void run() {
        while (true){
            lock.lock(); // 获取同步锁
            if(tickets > 0){
                try {
                    TimeUnit.MILLISECONDS.sleep(500);
                    System.out.println( Thread.currentThread().getName() + "已售出第" + tickets-- + "号票");
                } catch (InterruptedException e) {
                    System.out.println("中断异常");
                }finally {
                    lock.unlock(); // 无论代码是否发生异常,都会执行finally,将锁释放
                }
            }
        }
    }

    public static void main(String[] args) {
        SolveTicketSalesProblem2 salesProblem2 = new SolveTicketSalesProblem2();
        new Thread(salesProblem2,"售票窗口A").start();
        new Thread(salesProblem2,"售票窗口B").start();
        new Thread(salesProblem2,"售票窗口C").start();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值