0
点赞
收藏
分享

微信扫一扫

Java进阶知识复习笔记(二):多线程

凛冬已至夏日未远 2022-03-20 阅读 95
java

文章目录


关于进程、线程的概念:我在操作系统中这篇博客讲过,建议先把这一章内容看一看,有助于理解多线程。

main 方法执行的便是一个主线程,而所谓的多线程,即是在主线程执行的过程中,同时执行其他的线程。

一、多线程的实现方式

1.创建多线程的方式一:继承Thread类

package wang.test;

public class Demo {
    public static void main(String[] args) {
        Thread MyThread = new MyThread();
        //启动线程
        MyThread.start();
    }
}

//继承Thread
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("hello myThread" + Thread.currentThread().getName());
    }
}

在这里插入图片描述

2.创建多线程的方式二:实现Runnable接口

如果创建的线程类已经含有父类时候,此时由于 Java 语法结构不支持多继承的原因,不能够再次继承 Thread 类,此时则需要使用实现 Runnable 接口的方式来应对如此场景。

package wang.test;

public class Demo {
    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        //启动线程
        thread.start();
    }
}

//实现Runnable接口
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("hello myThread" + Thread.currentThread().getName());
    }
}

3.传参:有参构造

无论是哪种线程创建方式,如果需要传参,都需要在实现线程的类里通过有参构造的方式传参。

package wang.test;

public class Demo {
    public static void main(String[] args) {
        MyRunnable runnable=new MyRunnable(6);
        Thread thread = new Thread(runnable);
        //启动线程
        thread.start();
    }
}

//实现Runnable接口
class MyRunnable implements Runnable {
    private int num;
    public MyRunnable(int num){
        this.num=num;
    }
    @Override
    public void run() {
        System.out.println("num:"+num);
        System.out.println("hello myThread" + Thread.currentThread().getName());
    }
}

在这里插入图片描述

4.创建多线程的方式三(有返回值):实现 Callable接口

当需要使用返回值时,需要通过这种方式实现多线程。

package wang.test;

import java.util.concurrent.Callable;

public class Demo {
    public static void main(String[] args) {
        MyCallable callable=new MyCallable(6);
        //启动线程
        int ans=callable.call();
        System.out.println("ans:"+ans);
    }
}

//实现Callable接口
class MyCallable implements Callable<Integer> {
    private int num;
    public MyCallable(int num){
        this.num=num;
    }
    @Override
    public Integer call() {
        System.out.println("num:"+num);
        System.out.println("hello myThread" + Thread.currentThread().getName());
        return num;
    }
}

在这里插入图片描述

5.start() 和 run() 之间的区别

start() 函数用来启动线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕,可以直接继续执行下面的代码;通过调用 Thread 类的 start() 方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。 然后通过此 Thread 类调用方法 run() 来完成其运行操作的, 这里方法 run() 称为线程体,它包含了要执行的这个线程的内容。 Run 方法运行结束, 此线程终止。然后 CPU 再调度其它线程。

run() 函数只是类的一个普通函数而已,如果直接调用 run 方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待 run 方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。

注意:重复调用start()方法会抛出IllegalComponentStateException异常,重复调用run()不会出错。

二、共享实例变量

线程的实例变量有共享和不共享之分。

  • 不共享:
package wang.test;

import java.util.concurrent.Callable;

public class Demo {
    public static void main(String[] args) {
        Thread threadA=new MyThread("A");
        Thread threadB=new MyThread("B");
        Thread threadC=new MyThread("C");
        //启动线程
        threadA.start();
        threadB.start();
        threadC.start();
    }
}

//实现Thread接口
class MyThread extends Thread {
    private int num=6;

    public MyThread(String name){
        super();
        this.setName(name);
    }

    @Override
    public void run() {
        while(num>0){
            System.out.println(num--+" hello " + Thread.currentThread().getName());
        }
    }
}

在这里插入图片描述

  • 共享:
package wang.test;

import java.util.concurrent.Callable;

public class Demo {
    public static void main(String[] args) {
        Thread thread=new MyThread();

        Thread threadA=new Thread(thread,"A");
        Thread threadB=new Thread(thread,"B");
        Thread threadC=new Thread(thread,"C");
        //启动线程
        threadA.start();
        threadB.start();
        threadC.start();
    }
}

//实现Thread接口
class MyThread extends Thread {
    private int num=6;

    @Override
    public void run() {
        while(num>0){
            System.out.println(num--+" hello " + Thread.currentThread().getName());
        }
    }
}

在这里插入图片描述

可以看到,共享的实例变量共享了线程实例中的成员变量,但是这么做容易带来一些数据安全问题。

使用synchronized上锁

  • A.给实例方法上锁:

当一个线程想要执行同步方法里面的代码之前,会先判断 run 方法有没有被上锁,如果被上锁,说明有其他线程正在调用 run 方法,必须等其它线程对 run 方法调用结束后才可以执行 run 方法。这样也就实现了排队调用 run 方法的目的,也就达到了解决实例变量共享的安全问题的目的。

package wang.test;

import java.util.concurrent.Callable;

public class Demo {
    public static void main(String[] args) {
        Thread thread=new MyThread();

        Thread threadA=new Thread(thread,"A");
        Thread threadB=new Thread(thread,"B");
        Thread threadC=new Thread(thread,"C");
        //启动线程
        threadA.start();
        threadB.start();
        threadC.start();
    }
}

//实现Thread接口
class MyThread extends Thread {
    private int num=20;

    //方法内部被上了锁,被称为互斥区、临界区
    @Override
    synchronized public void run() {
        while(num>0){
            System.out.println(num--+" hello " + Thread.currentThread().getName());
        }
    }
}

在这里插入图片描述

可以看到,给代码上锁可以有效解决线程共享数据带来的数据安全问题。

  • B.给静态方法上锁:
class MyService{
    public /*static*/ synchronized void myMethod() {
        try {
            Thread.sleep(30000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

不过值得注意的是,static synchronized 的效率很低,全局情况下每个线程都需要进行等待,可想而知如果 5000 个用户,同时等待某款热门抢购产品,若那个产品的剩余数量被 static synchronized 修饰了,那么每个人每次刷新页面就需要等待几千个线程读取该产品的剩余数量,若访问量过大的话导致服务器崩溃都有可能。

  • C.给代码块上锁:

在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,可能会得不偿失,此时我们可以使用同步代码块的方法对需要同步的代码进行包裹,这样就无需对整个方法进行同步操作了,锁的对象是变量代码。

用法一:成员锁,锁住的对象是变量

class myLock{
    public static final String lock = new String("0");
}

class MyThread extends Thread{
    @Override
    public void run() {
        synchronized (myLock.lock) {
            try {
                myLock.lock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

用法二:实例对象锁,this 代表当前实例

synchronized(this) {
    for (int j = 0; j < 100; j++) {
        i++;
    }
}

用法三:当前类的 class 对象锁

synchronized(AccountingSync.class) {
    for (int j = 0; j < 100; j++) {
        i++;
    }
}

三、两个重要方法

1.isAlive()

此方法用于判断线程是否处于运行中。

package wang.test;

import java.util.concurrent.Callable;

public class Demo {
    public static void main(String[] args) {
        Thread threadA=new MyThread("A");

        System.out.println("start前isActive:"+threadA.isAlive());
        threadA.start();
        System.out.println("start后isActive:"+threadA.isAlive());
    }
}

//实现Thread接口
class MyThread extends Thread {
    private int num=5;

    public MyThread(String name){
        super();
        this.setName(name);
    }

    @Override
    synchronized public void run() {
        while(num>0){
            System.out.println(num--+" hello " + Thread.currentThread().getName());
        }
    }
}

在这里插入图片描述

2.sleep()

此方法可让线程休眠。

package wang.test;

import java.util.concurrent.Callable;

public class Demo {
    public static void main(String[] args) {
        Thread threadA=new MyThread("A");

        System.out.println("start前isActive:"+threadA.isAlive());
        threadA.start();
        System.out.println("start后isActive:"+threadA.isAlive());
        try{
            threadA.sleep(1000);
        }catch(Exception e){
            e.printStackTrace();
        }
        System.out.println("sleep后isActive:"+threadA.isAlive());
    }
}

//实现Thread接口
class MyThread extends Thread {
    private int num=5;

    public MyThread(String name){
        super();
        this.setName(name);
    }

    @Override
    synchronized public void run() {
        while(num>0){
            System.out.println(num--+" hello " + Thread.currentThread().getName());
        }
    }
}

在这里插入图片描述

四、线程的生命周期

1.Thread.State.NEW尚未启动

new Thread() 之后还没 start () 的时候即是 Thread.State.NEW 状态。

2.Thread.State.RUNNABLE可运行

前文里面提到的用 synchronized 同步锁,锁住该线程之后,其它线程等待这个线程时的等待状态。

3.Thread.State.BLOCKED阻塞

前文里面提到的用 synchronized 同步锁,锁住该线程之后,其它线程等待这个线程时的等待状态。

4.Thread.State.WAITING等待

指等待线程的线程状态。调用以下方法之一,线程处于等待状态:

处于等待状态的线程正在等待另一个线程执行特定操作。例如,对某个对象调用了 Object.wait() 的线程正在等待另一个线程对该对象调用 Object.notify() 或 Object.notifyAll() 。调用了 thread.join() 的线程正在等待指定线程终止。

  • wait()函数:

wait 属于 Object 的成员方法,一旦一个对象调用了 wait 方法,必须要采用 notify() 和 notifyAll() 方法唤醒该进程;如果线程拥有某个或某些对象的同步锁,那么在调用了 wait() 后,这个线程就会释放它持有的所有同步资源,而不限于这个被调用了 wait() 方法的对象。wait() 方法也同样会在 wait 的过程中有可能被其他对象调用 interrupt() 方法而产生。

synchronized (obj) {
    while (condition)
        obj.wait();
    // Perform action appropriate to condition
}
  • wait() 函数与 sleep() 函数的区别:

关于异常

wait() 函数不需要捕获异常
sleep() 函数必须捕获异常

关于入参

wait() 函数可以传入参数,也可以不传入参数,传入参数就是在参数结束的时间后开始等待,不传入参数就是直接等待。
sleep() 函数必须传入参数,参数就是休眠时间,时间到了就会自动醒来。

关于 API

sleep() 是 Thread 类的函数,导致此线程暂停执行指定时间,给其他线程执行机会,但是依然保持着监控状态,过了指定时间会自动恢复,调用 sleep() 方法不会释放锁对象。当调用 sleep 方法后,当前线程进入阻塞状态。目的是让出 CPU 给其他线程运行的机会。但是由于 sleep() 方法不会释放锁对象,所以在一个同步代码块中调用这个方法后,线程虽然休眠了,但其他线程无法访问它的锁对象。这是因为 sleep() 方法拥有 CPU 的执行权,它可以自动醒来无需唤醒。而当 sleep() 结束指定休眠时间后,这个线程不一定立即执行,因为此时其他线程可能正在运行。

wait() 方法是 Object 类里的方法,当一个线程执行到 wait() 方法时,它就进入到一个和该对象相关的等待池中,同时释放了锁对象,等待期间可以调用里面的同步方法,其他线程可以访问,等待时不拥有 CPU 的执行权,否则其他线程无法获取执行权。当一个线程执行了 wait() 方法后,必须调用 notify 或者 notifyAll 方法才能唤醒,而且是随机唤醒,若是被其他线程抢到了 CPU 执行权,该线程会继续进入等待状态。由于锁对象可以是任意对象,所以 wait() 方法必须定义在 Object 类中,因为 Obeject 类是所有类的基类。

作用范围

sleep() 是静态方法,也就是说它只对当前对象有效。通过 对象名.sleep() 想让该对象线程进入休眠是无效的,它只会让当前线程进入休眠。

wait 、 notify 和 notifyAll 方法只能在同步方法或者同步代码块中使用,而 sleep 方法可以在任何地方使用。

调用者的区别

sleep() 方法是让某个线程暂停运行一段时间,其控制范围是由当前线程决定,运行的主动权是由当前线程来控制(拥有 CPU 的执行权)。

wait() 方法是使一个线程进入等待状态,并且释放其所持有的锁对象,notify 方法是通知等待该锁对象的线程重新获得锁对象,然而如果没有获得锁对象,wait 方法和 notify 方法都是没有意义的,因此必须先获得锁对象再对锁对象进行进一步操作于是才要把 wait 方法和 notify 方法写到同步方法和同步代码块中了。所以 wait 、 notify 和 notifyAll 方法要和 synchronized 关键字一起使用。由此可知 wait 和 notify 、 notifyAll 方法是由确定的对象即锁对象来调用的,锁对象就像一个传话的人,他对某个线程说停下来等待,然后对另一个线程说你可以执行了(实质上是被捕获了),这一过程是线程通信。

本质区别

其实两者的区别都是让线程暂停运行一段时间,但本质的区别:

sleep() 是线程的运行状态控制

wait() 是线程间的通信。

5.Thread.State.TIMED_WAITING计时等待

具有指定等待时间的等待线程的线程状态。调用以下方法后线程会进入此状态:

调用以下方法之一,线程处于定时等待状态,时间到了就自动退出这个状态:

6.Thread.State.TERMINATED终止

指终止线程的线程状态。

7.线程状态转变NEW、RUNNABLE、TERMINATED

在这里插入图片描述

五、线程的停止

使用优雅的停止方式停止线程是非常有必要的,优雅指当前线程结束本任务的操作后,停止并收回线程,而像 Thread.stop() 函数,不管当前行代码是否结束操作,都会收回线程。因此,对于Thread.stop()我们尽可能地不使用它。

1.使用标志位终止线程

在某些特殊的情况下,run() 方法会被一直执行。比如在服务端程序中可能会使用 while(true) { … } 这样的循环结构来不断的接收来自客户端的请求。此时就可以用修改标志位的方式来结束 run() 方法。

package wang.test;

public class Demo {
    public static void main(String[] args) {
        MyThread threadA=new MyThread("A");

        threadA.start();
        System.out.println("标志位前isActive:"+threadA.isAlive());
        threadA.setExit(true);
        System.out.println("标志位后isActive:"+threadA.isAlive());
    }
}

//实现Thread接口
class MyThread extends Thread {
    public boolean exit = false;

    public MyThread(String name){
        super();
        this.setName(name);
    }

    public void setExit(boolean exit){
        this.exit=exit;
    }

    @Override
    public void run() {
        while(exit){
            System.out.println(" hello " + Thread.currentThread().getName());
        }
    }
}

在这里插入图片描述

2.使用 interrupt() 中断线程

interrupt() 方法并不像在 for 循环语句中使用 break 语句那样干脆,马上就停止循环。调用 interrupt() 方法仅仅是在当前线程中打一个停止的标记,并不是真的停止线程。

也就是说,线程中断并不会立即终止线程,而是通知目标线程,有人希望你终止。至于目标线程收到通知后会如何处理,则完全由目标线程自行决定。

package wang.test;

public class Demo {
    public static void main(String[] args) {
        MyThread threadA=new MyThread("A");

        threadA.start();
        try{
            threadA.interrupt();//1 打一个中断标记
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}

//实现Thread接口
class MyThread extends Thread {
    private int num=1000;

    public MyThread(String name){
        super();
        this.setName(name);
    }

    @Override
    public void run() {
        while(true){
            System.out.println(num--+" hello " + Thread.currentThread().getName());
            //2 判断是否被中断
            if(this.isInterrupted()){
                //3 处理中断
                break;
            }
        }
    }
}

3.通过异常的方式来停止线程(推荐)

先介绍下这个方法:

  • interrupted() 方法:

测试当前线程是否已中断。此方法清除线程的中断状态。换言之,如果连续两次调用此方法,则第二次调用将返回 false(除非在第一次调用清除其中断状态后,第二次调用检查它之前,当前线程再次中断)。

class MyThread extends Thread{
    @Override
    public void run(){
        try{
            while(true){
                if(this.interrupted()){
                    System.out.println("isInterrupted:" + this.isInterrupted());
                    throw new InterruptedException();
                }
                System.out.println("不会输出");
            }
        } catch(InterruptedException exception){
            System.out.println("进入异常");
        }
    }
}

public class test14{
    public static void main(String[] args){
        Thread myThread = new MyThread();
        myThread.start();
        myThread.interrupt();
    }
}

在这里插入图片描述

4.join() 函数的使用

在很多情况下,主线程创建并启动了子线程,如果子线程中要进行大量的耗时运算,主线程往往将早于子线程结束之前结束,这时如果主线程想等待子线程执行完成之后结束的话,就要用到 join() 方法了。方法 join() 的作用是等待线程销毁。

import java.util.Random;

class ThreadA extends Thread{

    @Override
    public void run() {
        super.run();
        try {
            System.out.println("ThreadA run start");
            Thread.sleep(new Random().nextInt(3000));
            System.out.println("ThreadA run end");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class test36 {
    public static void main(String[] args) {
        Thread a = new ThreadA();
        a.start();
        try {
            a.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("这句话应该等 run end 结束之后再输出");
    }
}

在这里插入图片描述

  • join(long) 函数:最多等待{long}毫秒,使此线程死亡。
  • join(long,int) 函数:最多等待 (long) 毫秒加上{int}纳秒,使此线程死亡。

六、线程的优先级与保护线程

线程可以划分优先级,优先级较高的线程得到的 CPU 资源较多,也就是 CPU 优先执行优先级较高的线程对象中的任务。线程的优先级一共划分为 1 ~ 10 的十个等级。

  • 可使用三个预定义的常量来控制线程的优先级:
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
  • 使用 setPriority() 方法设置优先级:
class MyThread extends Thread{
    @Override
    public void run(){}
}

public class test7{
    public static void main(String[] args){
        MyThread myThread = new MyThread();
        myThread.setPriority(11);
        myThread.start();
    }
}
  • 线程的优先级的继承性:

线程的优先级具备继承的特性,比如 A 线程启动 B 线程,则 B 线程的优先级与 A 线程的是一样的。

class ThreadA extends Thread{
    @Override
    public void run(){
        System.out.println("A priority=" + this.getPriority());
        Thread b = new ThreadB();
        b.start();
    }
}

class ThreadB extends Thread{
    @Override
    public void run(){
        System.out.println("B priority=" + this.getPriority());
    }
}

public class test16{
    public static void main(String[] args){
    	//获取当前线程默认优先级
        System.out.println("test16 priority1=" + Thread.currentThread().getPriority());
        //为当前线程设置优先级6
        Thread.currentThread().setPriority(6);
        System.out.println("test16 priority2=" + Thread.currentThread().getPriority());
        //启动线程A,A将继承当前线程的优先级
        Thread a = new ThreadA();
        a.start();
    }
}

在这里插入图片描述

  • 线程的优先级的规则性:

由于抢占 CPU 的缘故,线程的优先级的规则性只是在一定程度上进行控制,依旧可能出现低优先级的先执行完,这都是正常现象,不过大概率计算机会优先执行优先级较高的线程。

  • 守护线程:

在 java 世界中线程主要分为两种线程,一种是用户线程,另外一种即是守护线程。

守护线程即是一种特殊的线程 ( Daemon ) ,当进程中不存在用户线程后,则守护线程即会自动销毁。java 的垃圾回收线程也是守护线程之一。用户也可以自行设置自身的某一线程为守护线程 Thread.setDeamon(true);

  • yield() 函数:

yield() 函数的作用是放弃当前的 CPU 资源,将当前资源让给其它的任务去占用 CPU 执行时间,但是放弃的时间并不确定,有可能刚刚放弃 CPU 资源,马上又捡起 CPU 资源进行使用。

package wang.test;

public class ThreadA extends Thread{
    @Override
    public void run(){
        long begin = System.currentTimeMillis();
        int count = 0;
        for(int i = 0; i < 1000000; i++){
           // Thread.yield();
            count = count+i;
        }
        long end = System.currentTimeMillis();
        System.out.print("总耗时" + (end - begin));
    }
}

class test{
    public static void main(String[] args){
        Thread a = new ThreadA();
        a.start();
    }
}

执行两次,一次打开Thread.yield();的注释,一次不打开,得到的执行结果如下:

总耗时110
总耗时4

也就是说,在循环中不断地让出cpu使用权又不断地获得使用权会明显延长整个任务执行完的时间,因为它在这个过程中不断地中止又不断地继续。

七、实例对象与并发访问

  • “非实例安全”问题只存在于“实例变量”之中,如果方法内部的私有变量,则不存在“非线程安全”的问题。
class ReturnSomething{
    public int get(String a){
        if(a.equals("abc")){
            System.out.println("a set over");
            return 500;
        }else{
            System.out.println("b set over");
            return 10;
        }
    }
}

class ThreadA extends Thread{
    @Override
    public void run(){
        ReturnSomething returnSomething = new ReturnSomething();
        int a = returnSomething.get("abc");
        System.out.println("ThreadA set = " + a);
    }
}

class ThreadB extends Thread{
    @Override
    public void run(){
        ReturnSomething returnSomething = new ReturnSomething();
        int b = returnSomething.get("def");
        System.out.println("ThreadB set = " + b);
    }
}

public class test20{
    public static void main(String[] args){
        Thread a = new ThreadA();
        a.start();
        Thread b = new ThreadB();
        b.start();
    }
}

在这里插入图片描述

无论如何设置与调用,该返回都将是正确的,方法中的实例变量不存在非线程安全的问题,永远都是线程安全的。这是方法内部的变量是私有的特性所造成的结果。

  • 如果多个线程共同访问 1 个对象中的实例变量,则有可能出现“非线程安全”的问题。运行时可能造成线程交叉、脏读、覆盖,等等相关情况。
class ReturnSomething{
    int i = 0;
    public int get(String a){
        if(a.equals("abc")){
            i = 500;
        }else{
            i = 10;
        }
        return i;
    }
}

class ThreadA extends Thread{
    private ReturnSomething returnSomething;
    public ThreadA(ReturnSomething returnSomething){
        super();
        this.returnSomething = returnSomething;
    }
    @Override
    public void run(){
        int a = returnSomething.get("abc");
        if(a == 10){
            System.out.println("a=10, 线程安全出现问题。");
        }
    }
}

class ThreadB extends Thread{
    private ReturnSomething returnSomething;
    public ThreadB(ReturnSomething returnSomething){
        super();
        this.returnSomething = returnSomething;
    }
    @Override
    public void run(){
        super.run();
        int b = returnSomething.get("def");
        if(b == 500){
            System.out.println("b=500, 线程安全出现问题。");
        }
    }
}

public class test21{
    public static void main(String[] args){
        ReturnSomething returnSomething = new ReturnSomething();
        for(int i=0;i<20;i++){
            Thread a = new ThreadA(returnSomething);
            a.start();
            Thread b = new ThreadB(returnSomething);
            b.start();
        }
    }
}

在这里插入图片描述

对于以上问题,通过给方法上锁就可以解决,这使得方法变得对线程安全:

class ReturnSomething{
    int i = 0;
    synchronized public int get(String a){
        if(a.equals("abc")){
            i = 500;
        }else{
            i = 10;
        }
        return i;
    }
}
  • 当通过创建多个实例获得多把锁时,线程之间将通过并发的方式执行,也就是先启动的线程未必会先执行;而对于上面的一个实例一把锁的情况,始终是同步的,先启动的线程先执行。
class ReturnSomething{
    int i = 0;
    synchronized public int get(String a){
        if(a.equals("abc")){
            System.out.println("a set 500");
            i = 500;
        }else{
            System.out.println("b set 10");
            i = 10;
        }
        return i;
    }
}

class ThreadA extends Thread{
    private ReturnSomething returnSomething;
    public ThreadA(ReturnSomething returnSomething){
        super();
        this.returnSomething = returnSomething;
    }
    @Override
    public void run(){
        int a = returnSomething.get("abc");
        System.out.println("int a=" + a);
        if(a == 10){
            System.out.println("a=10, 线程安全出现问题。");
        }
    }
}

class ThreadB extends Thread{
    private ReturnSomething returnSomething;
    public ThreadB(ReturnSomething returnSomething){
        super();
        this.returnSomething = returnSomething;
    }
    @Override
    public void run(){
        super.run();
        int b = returnSomething.get("def");
        System.out.println("int b=" + b);
        if(b == 500){
            System.out.println("b=500, 线程安全出现问题。");
        }
    }
}

public class test22{
    public static void main(String[] args){
        ReturnSomething returnSomethingA = new ReturnSomething();
        ReturnSomething returnSomethingB = new ReturnSomething();
        Thread a = new ThreadA(returnSomethingA);
        a.start();
        Thread b = new ThreadB(returnSomethingB);
        b.start();
    }
}

在这里插入图片描述

八、synchronized 详解

  • synchronized 锁住的都是对象:
    ①synchronized 锁住都是对象,而非其中的一段代码、一个函数、一个方法。所以上述代码中,那个线程先执行 synchronized 关键字的方法,哪个线程就持有该方法所属对象的锁的 Lock,注意这里持有的是该方法所属对象的 Lock,以对象为基准,其他线程若想调用该对象或该对象内部任何函数的情况下,只能进行等待。
    ②但若多个线程访问多个对象,则 JVM 虚拟机则会创建多个锁,每个锁都由一个线程进行获取,所以互不影响的情况下,两段线程是以异步的方式进行执行的。

  • synchronized 锁住的对象出现异常后将自动释放锁

  • synchronized 无法被继承:
    若此时有类对象被继承了,并且重新实现了上了锁的函数,此函数若没有 synchronized 锁的话,则继承之后的对象依旧处于非线程安全的情况。

  • synchronized 锁的重入:
    ①所谓重入即是,当前线程 A 获得了一个对象的 synchronized 锁后,在没有释放的情况下,线程 A 再次请求该对象时,依旧可以重新进行请求。
    ②当线程 A 获得一个对象的 synchronzied 锁后,在没有释放的情况下,线程 B 是无法再次请求该对象的。
    ③以上情况即是 synchronized 锁的重入,这也说明在一个 synchronized 方法 / 代码块内部调用本类其它 synchronized 方法 / 代码块的情况下,是永远可以得到这个锁的。

  • 区别:

九、线程副本

ThreadLocal.java 线程副本类主要解决的就是,要每个单独的线程,都绑定自己线程内部共享的值,可以想象每个线程都有自己的 ThreadLocal 笔记本,这个笔记本上记载着所有该线程自身的共享变量,即可以理解,该线程的 自身的 static 。

1.ThreadLocal 线程副本的使用

new ThreadLocal 的时候,需要输入相关泛型,自定义一个对象即可,通常内容存放自身线程的日志之内的内容即可,当 ThreadLocal 运用 get() 函数时,将会得到泛型。

class MyThreadLocal{
    private ThreadLocal<String> myThreadLocal = new ThreadLocal<>();
    public void setSomething() {
        myThreadLocal.set("张方兴");
    }
    public String getSomething() {
        return myThreadLocal.get();
    }
}

class ThreadA extends Thread{

    @Override
    public void run() {
        super.run();
        MyThreadLocal myThreadLocal = new MyThreadLocal();
        myThreadLocal.setSomething();
        System.out.println("Thread a get" + myThreadLocal.getSomething());
    }

}
class ThreadB extends Thread{

    @Override
    public void run() {
        super.run();
        System.out.println("Thread b get" + new MyThreadLocal().getSomething());
    }
}

public class test37 {
    public static void main(String[] args) {
        Thread a = new ThreadA();
        Thread b = new ThreadB();
        a.start();
        b.start();
    }
}

在这里插入图片描述

set(),get(),remove(),initialValue()这 4 个函数属于 ThreadLocal 线程副本最为常用的函数,其它没什么值得留意的。

2.InheritableThreadLocal 线程副本的使用

使用 InheritableThreadLocal 线程副本类,可以从子线程中获取父线程继承下来的值。

class MyThreadLocal extends InheritableThreadLocal<String>{
    @Override
    protected String initialValue() {
        return "my initialValue";
    }
}

class MyTools{
    static public MyThreadLocal myThreadLocal = new MyThreadLocal();
}

class ThreadA extends Thread{
    @Override
    public void run() {
        super.run();
        for (int i = 0; i < 10; i++) {
            System.out.println("ThreadA 从 MyThreadLocal 中取到的值:" + MyTools.myThreadLocal.get());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class test38 {

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            System.out.println("Main 从 MyThreadLocal 中取到的值:" + MyTools.myThreadLocal.get());
            Thread.sleep(1000);
        }

        Thread.sleep(5000);

        Thread a = new ThreadA();
        a.start();
    }
}

可以看到,main方法先是获取到了线程副本中的内容,由于线程a是在main方法主线程中启动的,因此它是主线程的子线程,可以继承主线程中的内容。

在这里插入图片描述

十、ReentrantLock类

在上面的内容中我们一直使用synchronized隐式锁实现线程之间同步互斥,但java中的Lock类(显示锁)也可以实现线程间的同步,而且在使用上更加方便。本节研究Lock类的子类ReentrantLock的使用。

  • 公平锁与非公平锁:

公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO顺序。而非公平锁就是一种获取锁的抢占机制,是随机获得锁的,和公平锁不一样的就是先来的不一定先得到锁,这个方式可能造成某些线程一直拿不到锁。synchronized就是一种非公平锁。而ReentrantLock公平锁、非公平锁都支持,默认采用非公平锁,除非在构造方法中传入参数 true。

  • 重入锁:

指在拥有锁的情况下可以调用其它需要本锁的方法或者代码块。ReentrantLock和synchronized都是一种可重入锁。

ReentrantLock也是一种可重入锁,类似于synchronized。lock.getHoldCount()可以获得当前线程拥有锁的层数,可以理解为重入了几层。当为0的时候代表当前线程没有占用锁,每重入一次count就加1。

1.ReentrantLock的基本使用

调用其lock()方法会占用锁,调用unlock()会释放锁,但是需要注意必须手动unlock释放锁,否则其他线程会永远阻塞。而且发生异常不会自动释放锁,所以编写程序的时候需要在finally中手动释放锁。

package wang.test;

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

class MyThread extends Thread{
    private Test test;

    public MyThread(Test test){
        super();
        this.test=test;
    }

    @Override
    public void run() {
        test.show();
    }
}

class Test{
    private Lock lock=new ReentrantLock();

    public void show(){
        try{
            System.out.println(Thread.currentThread().getName()+" 创建");
            lock.lock();
            System.out.println(Thread.currentThread().getName()+" 上锁");
        }catch(Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
            System.out.println(Thread.currentThread().getName()+" 释放锁");
        }
    }
}

public class Demo {
    public static void main(String[] args) {
        Test test=new Test();
        Thread a=new MyThread(test);
        a.setName("thread a");
        a.start();
        Thread b=new MyThread(test);
        b.setName("thread b");
        b.start();
    }
}

在这里插入图片描述

实现了线程之间的互斥同步,thread a释放锁之后thread b才进入Lock,类似于synchronized同步锁的执行效果。

2.使用Condition实现等待/通知

下面,我们来实现一个简单的的生产者消费者(两个生产者、两个消费者)模式:

package wang.test;

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

class Task{
    private LinkedList<String> list=new LinkedList<>();//缓冲区
    private static final int Max_size=5;//最大容量
    private final Lock lock=new ReentrantLock();
    private final Condition prodCondition=lock.newCondition();
    private final Condition consumeCondition= lock.newCondition();

    public void product(String product){
        lock.lock();
        try{
            int count=5;
            while(count--!=0){
                if(list.size()==Max_size)prodCondition.await();//缓冲区满了,等待
                list.addLast(product);
                System.out.println("当前线程为:"+Thread.currentThread().getName()+" 生产了:"+product+" 缓冲区大小:"+list.size());
            }
            System.out.print("缓冲区:");
            for(String i:list){
                System.out.print(i+' ');
            }
            System.out.println();
            System.out.println("=====================================");
            consumeCondition.signalAll();
        }catch(Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public void consume(){
        lock.lock();
        try{
            int count=5;
            while(count--!=0){
                if(list.size()==0)consumeCondition.await();//缓冲区空了,等待
                System.out.println("当前线程为:"+Thread.currentThread().getName()+" 消费了:"+list.removeFirst()+" 缓冲区大小:"+list.size());
            }
            System.out.print("缓冲区:");
            for(String i:list){
                System.out.print(i+' ');
            }
            System.out.println();
            System.out.println("=====================================");
            prodCondition.signalAll();
        }catch(Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

class ProdThread extends Thread{
    private Task task;
    private String product;

    public ProdThread(Task task,String product){
        this.task=task;
        this.product=product;
    }

    @Override
    public void run() {
        task.product(product);
    }
}

class ConsumeThread extends Thread{
    private Task task;

    public ConsumeThread(Task task){
        this.task=task;
    }

    @Override
    public void run() {
        task.consume();
    }
}

public class Demo {
    public static void main(String[] args) {
        Task task=new Task();
        //两个生产者,两个消费者
        Thread appleProductor=new ProdThread(task,"apple");
        appleProductor.setName("apple productor");
        Thread bananaProductor=new ProdThread(task,"banana");
        bananaProductor.setName("banana productor");
        Thread consumer1=new ConsumeThread(task);
        consumer1.setName("consumer1");
        Thread consumer2=new ConsumeThread(task);
        consumer2.setName("consumer2");

        appleProductor.start();
        bananaProductor.start();
        consumer1.start();
        consumer2.start();
    }
}

执行结果:

当前线程为:banana productor 生产了:banana 缓冲区大小:1
当前线程为:banana productor 生产了:banana 缓冲区大小:2
当前线程为:banana productor 生产了:banana 缓冲区大小:3
当前线程为:banana productor 生产了:banana 缓冲区大小:4
当前线程为:banana productor 生产了:banana 缓冲区大小:5
缓冲区:banana banana banana banana banana 
=====================================
当前线程为:consumer1 消费了:banana 缓冲区大小:4
当前线程为:consumer1 消费了:banana 缓冲区大小:3
当前线程为:consumer1 消费了:banana 缓冲区大小:2
当前线程为:consumer1 消费了:banana 缓冲区大小:1
当前线程为:consumer1 消费了:banana 缓冲区大小:0
缓冲区:
=====================================
当前线程为:apple productor 生产了:apple 缓冲区大小:1
当前线程为:apple productor 生产了:apple 缓冲区大小:2
当前线程为:apple productor 生产了:apple 缓冲区大小:3
当前线程为:apple productor 生产了:apple 缓冲区大小:4
当前线程为:apple productor 生产了:apple 缓冲区大小:5
缓冲区:apple apple apple apple apple 
=====================================
当前线程为:consumer2 消费了:apple 缓冲区大小:4
当前线程为:consumer2 消费了:apple 缓冲区大小:3
当前线程为:consumer2 消费了:apple 缓冲区大小:2
当前线程为:consumer2 消费了:apple 缓冲区大小:1
当前线程为:consumer2 消费了:apple 缓冲区大小:0
缓冲区:
=====================================

Process finished with exit code 0

十一、AQS与ReentrantReadWriteLock

1.AQS简介

AQS:AbstractQuenedSynchronizer 抽象的队列式同步器。是除了 java 自带的 synchronized 关键字之外的锁机制。 AQS 的全称为(AbstractQueuedSynchronizer),这个类在 java.util.concurrent.locks 包。在我们前文中讲解的Lock.java、ReentrantLock.java、Condition.java 都属于 AQS 的一部分。

在这里插入图片描述

  • AQS 定义了两种资源共享方式:
  • 获得锁的流程如下所示:

在这里插入图片描述在这里插入图片描述

  • AQS 底层使用了模板方法模式:
    ①使用者继承 AbstractQueuedSynchronizer 并重写指定的对于共享资源 state 的获取和释放的方法。
    ②将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。

自定义同步器在实现的时候只需要实现共享资源 state 的获取和释放方式即可,至于具体线程等待队列的维护,AQS 已经在顶层实现好了。自定义同步器实现的时候主要实现下面几种方法:

ReentrantLock 为例:state 初始化为 0,表示未锁定状态,A 线程 lock() 时,会调用 tryAcquire() 独占锁并将 state+1. 之后其他线程再想 tryAcquire() 的时候就会失败,直到 A 线程unlock() 到 state=0 为止,其他线程才有机会获取该锁。A 释放锁之前,自己也是可以重复获取此锁(state 累加),这就是可重入的概念。 注意:获取多少次锁就要释放多少次锁,保证 state 是能回到零态的。

有兴趣的可自行阅读源码。

2.ReentrantReadWriteLock读写锁

ReentrantLock 类具有完全互斥排他的效果,即同一时间只有一个线程在执行 ReentrantLock.lock() 方法后面的任务,这样做虽然保证了实例变量的线程安全,但效果确实非常低下的,所以在 JDK 中提供了一种读写锁, ReentrantReadWriteLock 锁,使用它可以加快运行效率,在某些不需要操作实例变量的方法中,完全可以使用读写锁 ReentrantReadWriteLock 来提升该方法的代码运行速度。

读写锁表示也有两个锁:
①一个是读操作相关的锁,也称之为共享锁。
②另一个是写操作相关的锁,也称之为排他锁。

也就是多个共享锁之间不互斥,读锁与写锁之间互斥,写锁与写锁之间互斥。

  • 读读共享:
package wang.test;

import java.util.concurrent.locks.ReentrantReadWriteLock;

class Task{
    private ReentrantReadWriteLock lock=new ReentrantReadWriteLock();
    private String str;

    public Task(){
        str="初始化内容";
    }

    public void read(){
        lock.readLock().lock();
        try{
            System.out.println(Thread.currentThread().getName()+" 读:"+str);
            System.out.println("获得锁的时间:"+System.currentTimeMillis());
            Thread.sleep(1000);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.readLock().unlock();
        }
    }
}

class ReadThread extends Thread{
    private Task task;

    public ReadThread(Task task){
        super();
        this.task=task;
    }

    @Override
    public void run() {
        super.run();
        task.read();
    }
}

public class Demo {
    public static void main(String[] args) {
        Task task=new Task();

        Thread read=new ReadThread(task);
        read.setName("read");
        read.start();

        Thread read1=new ReadThread(task);
        read1.setName("read1");
        read1.start();
    }
}


两个线程同时获得锁:

在这里插入图片描述

  • 读写互斥:
package wang.test;

import java.util.concurrent.locks.ReentrantReadWriteLock;

class Task{
    private ReentrantReadWriteLock lock=new ReentrantReadWriteLock();
    private String str;

    public Task(){
        str="初始化内容";
    }

    public void read(){
        lock.readLock().lock();
        try{
            System.out.println(Thread.currentThread().getName()+" 读:"+str);
            System.out.println("获得锁的时间:"+System.currentTimeMillis());
            Thread.sleep(1000);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.readLock().unlock();
        }
    }

    public void write(String str){
        lock.writeLock().lock();
        try{
            this.str=str;
            System.out.println(Thread.currentThread().getName()+" 写:"+str);
            System.out.println("获得锁的时间:"+System.currentTimeMillis());
            Thread.sleep(1000);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.writeLock().unlock();
        }
    }
}

class ReadThread extends Thread{
    private Task task;

    public ReadThread(Task task){
        super();
        this.task=task;
    }

    @Override
    public void run() {
        super.run();
        task.read();
    }
}

class WriteThread extends Thread{
    private Task task;
    private String str;

    public WriteThread(Task task,String str){
        super();
        this.task=task;
        this.str=str;
    }

    @Override
    public void run() {
        super.run();
        task.write(str);
    }
}

public class Demo {
    public static void main(String[] args) {
        Task task=new Task();

        Thread read=new ReadThread(task);
        read.setName("read");
        read.start();

        Thread write=new WriteThread(task,"写入内容");
        write.setName("write");
        write.start();
    }
}

两个线程获取锁的行为是互斥的,从得到锁的时间可以看出来:

在这里插入图片描述

  • 写写互斥:
package wang.test;

import java.util.concurrent.locks.ReentrantReadWriteLock;

class Task{
    private ReentrantReadWriteLock lock=new ReentrantReadWriteLock();
    private String str;

    public Task(){
        str="初始化内容";
    }

    public void write(String str){
        lock.writeLock().lock();
        try{
            this.str=str;
            System.out.println(Thread.currentThread().getName()+" 写:"+str);
            System.out.println("获得锁的时间:"+System.currentTimeMillis());
            Thread.sleep(1000);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.writeLock().unlock();
        }
    }
}

class WriteThread extends Thread{
    private Task task;
    private String str;

    public WriteThread(Task task,String str){
        super();
        this.task=task;
        this.str=str;
    }

    @Override
    public void run() {
        super.run();
        task.write(str);
    }
}

public class Demo {
    public static void main(String[] args) {
        Task task=new Task();

        Thread write=new WriteThread(task,"写入内容");
        write.setName("write");
        write.start();

        Thread write1=new WriteThread(task,"写入内容1");
        write1.setName("write1");
        write1.start();
    }
}

写和写也是互斥的:

在这里插入图片描述

十二、线程池

线程池的概念是初始化线程池时在池中创建空闲的线程,一但有工作任务,可直接使用线程池中的线程进行执行工作任务,任务执行完成后又返回线程池中成为空闲线程。使用线程池可以减少线程的创建和销毁,提高性能。

  • 优点:
    ①减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
    ②可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约 1MB 内存,线程开的越多,消耗的内存也就越大,最后死机)。

  • Java中的线程池:
    ①Java 里面线程池的顶级接口是 Executors,不过真正的线程池接口是 ExecutorService。
    ②Executors 接口是 ExecutorService 的父接口,基于生产者 —— 消费者模式,提交任务的操作相当于生产者,执行任务的线程则相当于消费者,如果要在程序中实现一个生产者 —— 消费者的设计,那么最简单的方式通常是使用 Executors。
    ③ExecutorService 接口是对 Executors 接口的扩展,提供了对生命周期的支持,以及统计信息收集、应用程序管理机制和性能监视等机制。

1.创建连接池的方法及线程池分类

  • java中的线程池有以下几类:

以上几种线程池实际上都是调用java.util.concurrent.ThreadPoolExecutor的构造函数来创建线程池:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);

参数说明如下:

  • 工作队列排队策略:
  • 拒绝策略:

2.四种线程池的代码示例

  • newFixedThreadPool
package wang.test;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Demo {
    public static void main(String[] args) {
        ExecutorService executor= Executors.newFixedThreadPool(3);//1 创建一个固定大小为3的线程池
        for(int i=1;i<=5;++i){
            //2 用匿名内部类的方式创建实现Runnable接口的实现类
            Runnable runnable=new Runnable(){
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+"执行中...");
                }
            };
            executor.execute(runnable);//3 运行
        }
        executor.shutdown();//4 关闭线程池
    }
}

线程池中运行的线程最多只有三个:

在这里插入图片描述

  • newCachedThreadPool
package wang.test;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Demo {
    public static void main(String[] args) {
        ExecutorService executor= Executors.newCachedThreadPool();//1 创建一个大小不被限制的线程池
        for(int i=1;i<=5;++i){
            //2 用匿名内部类的方式创建实现Runnable接口的实现类
            Runnable runnable=new Runnable(){
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+"执行中...");
                }
            };
            executor.execute(runnable);//3 运行
        }
        executor.shutdown();//4 关闭线程池
    }
}

大小是不受限制的:

在这里插入图片描述

  • newSingleThreadExecutor
package wang.test;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Demo {
    public static void main(String[] args) {
        ExecutorService executor= Executors.newSingleThreadExecutor();//1 创建一个大小始终为1的线程池
        for(int i=1;i<=5;++i){
            //2 用匿名内部类的方式创建实现Runnable接口的实现类
            Runnable runnable=new Runnable(){
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+"执行中...");
                }
            };
            executor.execute(runnable);//3 运行
        }
        executor.shutdown();//4 关闭线程池
    }
}

大小始终为1:

在这里插入图片描述

  • newScheduledThreadPool
package wang.test;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class Demo {
    public static void main(String[] args) {
        //1 创建一个大小为3且定时执行的线程池
        ScheduledExecutorService executor= Executors.newScheduledThreadPool(3);
        for(int i=1;i<=5;++i){
            //2 用匿名内部类的方式创建实现Runnable接口的实现类
            Runnable runnable=new Runnable(){
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+"执行中...");
                }
            };
            //3 运行:延时1秒,每3秒执行一次
            executor.scheduleAtFixedRate(runnable,1,3, TimeUnit.SECONDS);
        }
        //4 等待线程池自己关闭
        if (executor.isShutdown()){
            executor.shutdown();
        }

    }
}

会定时执行,且永无止境地执行下去:

在这里插入图片描述

附:参考

教程:https://www.lanqiao.cn/courses/1519/learning/?id=16850
博客:https://www.cnblogs.com/liyutian/p/10196044.html
博客:https://www.cnblogs.com/qlqwjy/p/10130454.html
博客:https://blog.csdn.net/m0_37955444/article/details/79594807
博客:https://blog.csdn.net/weixin_36759405/article/details/82825634

举报

相关推荐

0 条评论