多线程万字源码解析之并发编程

温故知新

在之前我发布的多线程文章中有写过几个小案例并且还有一篇理论篇但是我还是想在把这个技术点在更深入的写一篇总结,会更注重于源码的解析

实现线程的两种方式(Thread类与Runnable接口)

(1) 继承Thread类,重写run方法

package demo2;

/**
 * @Date 2021/7/15 10:58
 * @Version SpringBoot 2.2.2
 * @packageName 继承Thread类
 */
public class ThreadTest {
    public static class MyThread extends Thread{

        @Override
        public void run(){
            System.out.println("测试线程");
        }
    }

    public static void main(String[]args){
        MyThread myThread = new MyThread();
        myThread.start();
    }
}
这里继承Thread类之后重新run方法,重点是在mian方法中的start()方法,只有执行start()方法后
虚拟机才会创建一个线程,等到这个线程第⼀次得到时间⽚时再调⽤run()⽅法。

Thread类是Runnable接口的实现类
在这里插入图片描述
进去查看 Thread 类源码的构造⽅法,发现其实是简单调⽤⼀个私有的 init ⽅法来实现初 始化。 init 的⽅法签名

//  init⽅法
private void init(ThreadGroup g, Runnable target, String name,
 long stackSize, AccessControlContext acc,
 boolean inheritThreadLocals)
 
//  构造函数调⽤init⽅法
public Thread(Runnable target) {
 init(null, target, "Thread-" + nextThreadNum(), 0);
}

//  使⽤在init⽅法⾥初始化AccessControlContext类型的私有属性
this.inheritedAccessControlContext = 
 acc != null ? acc : AccessController.getContext();
// ⽚段4 - 两个对⽤于⽀持ThreadLocal的私有属性
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

inti参数说明:

  1. g
    线程组,指定线程的分组
  2. target
    指定要执行的线程
  3. name
    线程名称,如果创建多个线程,名称可重复
  4. stackSize
    堆栈大小
  5. acc
    用于初始化inheritedAccessControlContext
  6. inheritThreadLocals
    可继承的 ThreadLocal

Thread的常用方法
currentThread()

静态⽅法,返回对当前正在执⾏的线程对象的引⽤;

start()

开始执⾏线程的⽅法,java虚拟机会调⽤线程内的run()⽅法;

yield()

yield在英语⾥有放弃的意思,同样,这⾥的yield()指的是当前线程愿意让出对当前处理器的占⽤。
这⾥需要注意的是,就算当前线程调⽤了yield()⽅法,程序在调度的时候,也还有可能继续运⾏这个线程的;

sleep()

静态⽅法,使当前线程睡眠⼀段时间; 进程与线程基本概念

join()

使当前线程等待另⼀个线程执⾏完毕之后再继续执⾏,内部调⽤的是Object类的wait⽅法实现的;

(2) 实现Runnable接口实现run方法

package demo2;

/**
 * @Date 2021/7/15 11:04
 * @Version SpringBoot 2.2.2
 * @packageName 实现Runnable接口
 */
public class ThreadTest2 {
    public static class MyThread implements Runnable {
        @Override
        public void run() {
            System.out.println("MyThread");
        }
    }
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        Thread thread = new Thread(myThread);
        thread.start();
        
        // Java 8 函数式编程,可以省略MyThread类
//        new Thread(() -> {
//            System.out.println("Java 8 匿名内部类");
//        }).start();
    }
}

Thread与Runnable之间区别

(1)由于Java是"单继承,多实现"的特性,Runnable接⼝使⽤起来⽐Thread更灵活。

(2)Runnable接⼝出现更符合⾯向对象,将线程单独进⾏对象的封装。

(3)Runnable接⼝出现,降低了线程对象和线程任务的耦合性。 如果使⽤线程时不需要使⽤Thread类的诸多⽅法,显然使⽤Runnable接⼝更 为轻量
所以,我们通常优先使⽤“实现 Runnable 接⼝”这种⽅式来⾃定义线程类。

Thread与Runnable创建线程的弊端

通常来说,我们使⽤ Runnable 和 Thread 来创建⼀个新的线程。但是它们有⼀个弊 端,就是 run ⽅法是没有返回值的。⽽有时候我们希望开启⼀个线程去执⾏⼀个任 务,并且这个任务执⾏完成后有⼀个返回值。JDK提供了 Callable 接⼝与 Future 类为我们解决这个问题,这也是所谓的“异步” 模型。

Callable接口

Callable 与 Runnable 类似,同样是只有⼀个抽象⽅法的函数式接⼝。不同的 是, Callable 提供的⽅法是有返回值的,⽽且⽀持泛型。,一般配合线程池工具ExecutorService来使用。

package java.util.concurrent;

@FunctionalInterface
public interface Callable<V> {

    V call() throws Exception;
}

Callable配合ExecutorService实现案例

package demo2;

import java.util.concurrent.*;

/**
 * @Date 2021/7/15 11:34
 * @Version SpringBoot 2.2.2
 * @packageName Callable示例
 */
public class TaskTest {
    public static class Task implements Callable<String>{

        @Override
        public String call() throws Exception {
            Thread.sleep(5000);
            return "等待5秒出现";
        }
    }

    public static void main(String[]args) throws ExecutionException, InterruptedException {
        ExecutorService ex = Executors.newCachedThreadPool();
        Task task = new Task();
        Future<String> result = ex.submit(task);
        System.out.println(result.get());
    }
}

Future接口

Future源码方法

//试图取消一个线程的执行,非一定成功
boolean cancel(boolean mayInterruptIfRunning);

//判断任务是否被取消,如果任务在结束(正常执行结束或者执行异常结束)前被取消则返回true,否则返回false
boolean isCancelled();

//判断任务是否已经完成,如果完成则返回true,否则返回false。需要注意的是:任务执行过程中发生异常、任务被取消也属于任务已完成,也会返回true。state!=NEW
boolean isDone();

//:获取任务执行结果,如果任务还没完成(state<=COMPLETING)则会阻塞等待直到任务执行完成。如果任务被取消则会抛出CancellationException异常,如果任务执行过程发生异常则会抛出ExecutionException异常,如果阻塞等待过程中被中断则会抛出InterruptedException异常
V get() throws InterruptedException, ExecutionException;

//带超时时间的get()版本,如果阻塞等待过程中超时则会抛出TimeoutException异常
V get(long timeout, TimeUnit unit)throws InterruptedException, ExecutionException, TimeoutException;

FutureTask类

FutureTask是Future实现类,但是源码是下面这样的,为何会这样呢,
在这里插入图片描述
原因是 FutureTask 是 实现的 RunnableFuture 接⼝的,⽽ RunnableFuture 接⼝同时继承了 Runnable 接⼝ 和 Future 接⼝,下面看看RunnableFuture 的源码
在这里插入图片描述
FutureTask 的作用就是Future的实现类,因为Future只是一个接口⽽它⾥⾯的 cancel , get , isDone 等⽅法要⾃⼰实现 起来都是⾮常复杂的,所以JDK友善的直接写了一个实现类FutureTask 供使用,下面使用FutureTask 类来操作一下看看

package demo2;

import java.util.concurrent.*;

/**
 * @Date 2021/7/15 11:34
 * @Version SpringBoot 2.2.2
 * @packageName Callable示例
 */
public class TaskTest {
    public static class Task implements Callable<String>{

        @Override
        public String call() throws Exception {
            Thread.sleep(5000);
            return "等待2秒出现";
        }
    }

    public static void main(String[]args) throws ExecutionException, InterruptedException {
        ExecutorService ex = Executors.newCachedThreadPool();
        Task task = new Task();
        //Future<String> result = ex.submit(task);
        //System.out.println(result.get());
        FutureTask<String> futureTask =new  FutureTask<>(new Task());
        ex.submit(futureTask);
        System.out.println(futureTask.get());
    }
}

使⽤上与第⼀个示例有⼀点⼩的区别。⾸先,调⽤ submit ⽅法是没有返回值的。 这⾥实际上是调⽤的 submit(Runnable task) ⽅法,⽽上⾯的Demo,调⽤的 是 submit(Callable task) ⽅法。 然后,这⾥是使⽤ FutureTask 直接取 get 取值,⽽上⾯的Demo是通过 submit ⽅ 法返回的 Future 去取值。 在很多⾼并发的环境下,有可能Callable和FutureTask会创建多次。FutureTask能够在⾼并发环境下确保任务只执⾏⼀次
FutureTask状态

//运行状态
private volatile int state;
//初始化
private static final int NEW          = 0;
private static final int COMPLETING   = 1;
private static final int NORMAL       = 2;
private static final int EXCEPTIONAL  = 3;
private static final int CANCELLED    = 4;
private static final int INTERRUPTING = 5;
private static final int INTERRUPTED  = 6;

ThreadGroup(线程组)

ThreadGroup的作用是可以对线程进⾏批量控制,每个Thread必 然存在于⼀个ThreadGroup中,Thread不能独⽴于ThreadGroup存在。执⾏main()⽅法线程的名字是main,如果在new Thread时没有显式指定,那么默认将⽗线程 (当前执⾏new Thread的线程)线程组设置为⾃⼰的线程组,下面我拿一个示例操作一下

package demo2;

/**
 * @Date 2021/7/15 15:10
 * @Version SpringBoot 2.2.2
 * @packageName ThreadGroupTest
 */
public class ThreadGroupTest {

    public static void main(String[]args){
        Thread thread = new Thread(()->{
            System.out.println("线程组名称为:"+Thread.currentThread().getThreadGroup().getName());
            System.out.println("线程名称"+Thread.currentThread().getName());
        });

        //启动线程
        thread.start();

        System.out.println("执行main方法的线程名称:"+Thread.currentThread().getName());
    }
}

执行结果
在这里插入图片描述
ThreadGroup管理着它下⾯的Thread,ThreadGroup是⼀个标准的向下引⽤的树状 结构,这样设计的原因是防⽌"上级"线程被"下级"线程引⽤⽽⽆法有效地被GC回收

线程的优先级

Java中线程优先级可以指定,范围是1~10。但是并不是所有的操作系统都⽀持10
级优先级的划分(⽐如有些操作系统只⽀持3级划分:低,中,⾼),Java只是给操作系统⼀个优先级的参考值,线程最终在操作系统的优先级是多少还是由操作系统决定,默认的优先级是5,线程的执⾏顺序由调度程序来决定,线程的优先级会 在线程被调⽤之前设定

通常情况下,⾼优先级的线程将会⽐低优先级的线程有更⾼的⼏率得到执⾏。我们 使⽤⽅法 Thread 类的 setPriority() 实例⽅法来设定线程的优先级,下面来试试

package demo2;

/**
 * @Date 2021/7/15 15:22
 * @Version SpringBoot 2.2.2
 * @packageName 优先级
 */
public class SetPriority {

    public static void main(String[]args){
    	//设定前
        Thread thread = new Thread();
        System.out.println("默认优先级:"+thread.getPriority());

		//设定后
        Thread thread1 = new Thread();
        thread1.setPriority(10);
        System.out.println("设定后优先级:"+thread1.getPriority());
    }
}

这个优先级并非可以采取到业务上实现,因为线程的优先级只是给出操作系统一个建议,主要还是由操作系统的线程调度算法来决定的

线程状态

操作系统

创建:线程从创建到被cpu执行之前的这个阶段。
就绪:指线程已具备各种执行条件,一旦获取cpu便可执行。
运行:表示线程正获得cpu在运行。
阻塞:指线程在执行中因某件事而受阻,处于暂停执行的状态,阻塞的线程不会去竞争cpu。
终止:线程执行完毕,接下来会释放线程占用的资源。
在这里插入图片描述

Java多线程

State源码

 public enum State {
	 NEW,
	 RUNNABLE,
	 BLOCKED,
	 WAITING,
	 TIMED_WAITING,
	 TERMINATED;
 }

这个我应该是在之前的博客中有介绍过,有人说有五种,有人说有六种,但是其实五种应该是操作系统的,java多线程的有6种状态,我这里在说明一下这六种状态

1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
2. 运行(RUNNABLE)Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
3. 阻塞(BLOCKED):表示线程阻塞于锁。
4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
6. 终止(TERMINATED):表示该线程已经执行完毕。

状态之间的转换

BLOCKED与RUNNABLE状态的转换

public synchronized void testMend(){
        try {
            Thread.sleep(2000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Test
    public void test() throws InterruptedException {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                testMend();
            }
        },"test1");

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                testMend();
            }
        },"test2");

        thread1.start();
        Thread.sleep(1000L);
        thread2.start();

        System.out.println(thread1.getName()+":"+thread1.getState());
        System.out.println(thread2.getName()+":"+thread2.getState());
    }

在这个例⼦中,由于main线程休眠,所以线程a的run()⽅法跟着执⾏,线程b再接 着执⾏。 在线程a执⾏run()调⽤testMethod()之后,线程a休眠了2000ms(注意这⾥是没有 释放锁的),main线程休眠完毕,接着b线程执⾏的时候是争夺不到锁的,这⾥输出

test1:TIMED_WAITING
test2:BLOCKED

WAITING状态与RUNNABLE状态的转换

@Test
    public void test() throws InterruptedException {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                testMend();
            }
        },"test1");

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                testMend();
            }
        },"test2");

        thread1.start();
        //与上面变化在这
        thread1.join();
        thread2.start();

        System.out.println(thread1.getName()+":"+thread1.getState());
        System.out.println(thread2.getName()+":"+thread2.getState());
    }

a线程启动之后⻢上调⽤了join⽅法,这⾥main线程就会等到a线程执⾏完毕,所以 这⾥a线程打印的状态固定是TERMIATED,⾄于b线程的状态,有可能打印RUNNABLE(尚未进⼊同步⽅法),也有可能打印TIMED_WAITING(进⼊了同步⽅法)。

Thread.join()方法

调⽤join()⽅法不会释放锁,会⼀直等待当前线程执⾏完毕(转换为TERMINATED状态)

Object.wait()方法

调⽤wait()⽅法前线程必须持有对象的锁。

线程调⽤wait()⽅法时,会释放当前的锁,直到有其他线程调⽤notify()/notifyAll()⽅法唤醒等待锁的线程。

需要注意的是,其他线程调⽤notify()⽅法只会唤醒单个等待锁的线程,如有 有多个线程都在等待这个锁的话不⼀定会唤醒到之前调⽤wait()⽅法的线程。

同样,调⽤notifyAll()⽅法唤醒所有等待锁的线程之后,也不⼀定会⻢上把时 间⽚分给刚才放弃锁的那个线程,具体要看系统的调度。
TIMED_WAITING与RUNNABLE状态转换
TIMED_WAITING与WAITING状态类似,只是TIMED_WAITING状态等待的时间是指定的

public synchronized void testMend(){
        try {
            Thread.sleep(2000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Test
    public void test() throws InterruptedException {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                testMend();
            }
        },"test1");

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                testMend();
            }
        },"test2");

        thread1.start();
        thread1.join(1000L);
        thread2.start();

        System.out.println(thread1.getName()+":"+thread1.getState());
        System.out.println(thread2.getName()+":"+thread2.getState());
    }

这⾥调⽤a.join(1000L),因为是指定了具体a线程执⾏的时间的,并且执⾏时 间是⼩于a线程sleep的时间,所以a线程状态输出TIMED_WAITING。b线程状态仍然不固定(RUNNABLE或BLOCKED)

Thread.sleep(long)方法

使当前线程睡眠指定时间。需要注意这⾥的“睡眠”只是暂时使线程停⽌执 ⾏,并不会释放锁。时间到后,线程会重新进⼊RUNNABLE状态

Object.wait(long)方法

wait(long)⽅法使线程进⼊TIMED_WAITING状态。这⾥的wait(long)⽅法与 ⽆参⽅法wait()相同的地⽅是,都可以通过其他线程调⽤notify()或notifyAll()⽅法来唤醒。 不同的地⽅是,有参⽅法wait(long)就算其他线程不来唤醒它,经过指定时间long之后它会⾃动唤醒,拥有去争夺锁的资格

Thread.join(long)

join(long)使当前线程执⾏指定时间,并且使线程进⼊TIMED_WAITING状 态。

线程中断

在某些情况下,我们在线程启动后发现并不需要它继续执⾏下去时,需要中 断线程。⽬前在Java⾥还没有安全直接的⽅法来停⽌线程,但是Java提供了线程中断机制来处理需要中断线程的情况。 线程中断机制是⼀种协作机制。需要注意,通过中断操作并不能直接终⽌⼀ 个线程,⽽是通知需要被中断的线程⾃⾏处理

线程中断:Thread.interrupt()
这⾥的中断线程并不会⽴即停⽌线程,⽽是设
置线程的中断状态为true(默认是flase)
测试当前线程是否被中断:Thread.interrupted()
线程的中断状态受这个⽅法 的影响,意思是调⽤⼀次使线程中断状态设置为true,连续调⽤两次会使得这 个线程的中断状态重新转为false
测试当前线程是否被中断:Thread.isInterrupted()
与上⾯⽅法不同的是调⽤ 这个⽅法并不会影响线程的中断状态
示例

public synchronized void testMend(){
        try {
            Thread.sleep(2000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Test
    public void test() throws InterruptedException {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                testMend();
            }
        },"test1");

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                testMend();
            }
        },"test2");

        thread1.start();
        //重点
        thread1.interrupt();
        thread2.start();

        System.out.println(thread1.getName()+":"+thread1.getState());
        System.out.println(thread2.getName()+":"+thread2.getState());
    }
}

输出结果
在这里插入图片描述
在线程中断机制⾥,当其他线程通知需要被中断的线程后,线程中断的状态被设置为true,但是具体被要求中断的线程要怎么处理,完全由被中断线程 ⾃⼰⽽定,可以在合适的实际处理中断请求,也可以完全不处理继续执⾏下去

锁之间同步

锁的概念都是基于对象的,所以我们⼜经常称它为对象锁。线程和锁的 关系,我们可以⽤婚姻关系来理解。⼀个锁同⼀时间只能被⼀个线程持有,下面我们先不使用锁来执行程序看看

package demo2;

/**
 * @Date 2021/7/15 17:30
 * @Version SpringBoot 2.2.2
 * @packageName 无锁执行
 */
public class Nolock {

    //线程1
    public static class ThreadOne implements Runnable{

        @Override
        public void run() {
            for (int i=0;i<100;i++){
                System.out.println("线程一"+i);
            }
        }
    }

    //线程2
    public static class ThreadTwo implements Runnable{

        @Override
        public void run() {
            for (int i=0;i<100;i++){
                System.out.println("线程二"+i);
            }
        }
    }

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

输出内容
在这里插入图片描述
从上面执⾏这个程序,线程A和线程B各⾃独⽴⼯作,输出⾃⼰的打印值。如下是我的电脑上某⼀次运⾏的结果。每⼀次运⾏结果都会不⼀样,那我想在执行A之后在执行B这个时候就可以使用到锁对象机制(lock)了。

/**
 * @Date 2021/7/15 17:30
 * @Version SpringBoot 2.2.2
 * @packageName 锁对象
 */
public class Nolock {

    private static Object lock = new Object();

    //线程1
    public static class ThreadOne implements Runnable{

        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i < 100; i++) {
                    System.out.println("线程一" + i);
                }
            }
        }
    }

    //线程2
    public static class ThreadTwo implements Runnable{

        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i < 100; i++) {
                    System.out.println("线程二" + i);
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(new ThreadOne()).start();
        Thread.sleep(10);
        new Thread(new ThreadTwo()).start();
    }
}

在这里我们不难发现只是在上面多添加一个一lock对象,在线程一与线程二内都用上了synchronized关键字加上了同一个对象锁,在线程与锁之间的关系是同一时间段只能有一个线程持久一个锁,那么我们线程一拿到锁之后线程二那肯定要等线程一将锁释放之后,才能去获取锁,这里有一个问题有没有发现,就是说我们的线程二它必须是等待线程一释放锁之后才能去持有锁,那如果中途出现了线程获得锁失败的情况,那么是不是要一直等待呢,是的,这个对于服务器来很消耗资源,下面我在说一个另外的方案来解决这个问题

等待/通知机制

在java中等待/通知机制是通过基于Object()的wait()方法与notify()、notifyAll()方法实现的
notify()与notifyAll()的区别

notify:随机叫醒一个正在等待的线程
notify:叫醒所有正在等待的线程
package demo2;

/**
 * @Date 2021/7/15 17:30
 * @Version SpringBoot 2.2.2
 * @packageName 等待/通知机制
 */
public class Nolock {

    private static Object lock = new Object();

    //线程1
    public static class ThreadOne implements Runnable{

        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i < 100; i++) {
                    try {
                        System.out.println("线程一" + i);
                        lock.notify();
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                lock.notify();
            }
        }
    }

    //线程2
    public static class ThreadTwo implements Runnable{

        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i < 100; i++) {
                    try {
                        System.out.println("线程二" + i);
                        lock.notify();
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                lock.notify();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(new ThreadOne()).start();
        Thread.sleep(10);
        new Thread(new ThreadTwo()).start();
    }
}

好,我们再来看看上面,Object()方法还是原封不动的在,在上面两个线程方法中多使用了wait()与notify()方法,其中wait()需要抛出InterruptedException ()异常,下面是源码中的wait方法
在这里插入图片描述
下面看一下执行输出
在这里插入图片描述
这里需要注意一个问题就是说在线程中你所使用的等待/通信机制得是相同的,如果不同那是不同使用的,那我这边在创建一个lock2看看下面的执行输出验证一下吧,这边会直接报错IllegalMonitorStateException异常
在这里插入图片描述
好了,下面我在出一个需求,我想让线程一输出0,然后线程二输出1,以此类推,这个我们应该怎么办呢,有一个volatile关键字可以解决这个问题它可以保证内存的可见性,使用volatile关键字声明一个变量值之后呢,在线程中改变这个变量值,那么这其他线程可立马查到看这个线程改变后的值,下面直接试验一下吧

/**
 * @Date 2021/7/15 17:30
 * @Version SpringBoot 2.2.2
 * @packageName
 */
public class Nolock {

    private static Object lock = new Object();

    private static volatile int signal = 0;

    //线程1
    public static class ThreadOne implements Runnable{

        @Override
        public void run() {
            while (signal < 100) {
                if (signal % 2 == 0) {
                    System.out.println("线程一: " + signal);
                    synchronized (this) {
                        signal++;
                    }
                }
            }
        }
    }

    //线程二
    public static class ThreadTwo implements Runnable{

        @Override
        public void run() {
            while (signal < 100) {
                if (signal % 2 == 1) {
                    System.out.println("线程二: " + signal);
                    synchronized (this) {
                        signal = signal + 1;
                    }
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(new ThreadOne()).start();
        Thread.sleep(1000);
        new Thread(new ThreadTwo()).start();
    }
}

执行输出如下,这里说明一下,因为volatile变量需要原子操作,而声明的变量值signal 并不是一个原子操作,这个时候我们就需要synchronized 给它加上锁
在这里插入图片描述

管道

管道是基于“管道流”的通信⽅式。JDK提供了 PipedWriter 、 PipedReader 、PipedOutputStream 、 PipedInputStream 。其中,前⾯两个是基于字符的,后⾯两个是基于字节流的,这边基于字符流来做一个试验,具体实现如下:

package demo;

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

/**
 * @Date 2021/7/19 10:45
 * @Version SpringBoot 2.2.2
 * @packageName 管道流
 */
public class Pipe {
    static class ReaderThread implements Runnable {
        private PipedReader reader;
        public ReaderThread(PipedReader reader) {
            this.reader = reader;
        }
        @Override
        public void run() {
            System.out.println("this is reader");
            int receive = 0;
            try {
                while ((receive = reader.read()) != -1) {
                    System.out.print((char)receive);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    static class WriterThread implements Runnable {
        private PipedWriter writer;
        public WriterThread(PipedWriter writer) {
            this.writer = writer;
        }
        @Override
        public void run() {
            System.out.println("this is writer");
            int receive = 0;
            try {
                writer.write("test");
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public static void main(String[] args) throws IOException, InterruptedException {
        PipedWriter writer = new PipedWriter();
        PipedReader reader = new PipedReader();
        writer.connect(reader); // 这⾥注意⼀定要连接,才能通信
        new Thread(new ReaderThread(reader)).start();
        Thread.sleep(1000);
        new Thread(new WriterThread(writer)).start();
    }
}

执行结果如下,可以看到方法中使用到io流,所以说管道流多半是与io有关的,比如说我们想先发送邮件在发送短信这个时候我们就可以使用到管道通信了
在这里插入图片描述
下面在描述一下上个示例的整个执行流程:

  1. 线程ReaderThread开始执⾏,
  2. 线程ReaderThread使⽤管道reader.read()进⼊”阻塞“,
  3. 线程WriterThread开始执⾏,
  4. 线程WriterThread⽤writer.write(“test”)往管道写⼊字符串,
  5. 线程WriterThread使⽤writer.close()结束管道写⼊,并执⾏完毕,
  6. 线程ReaderThread接受到管道输出的字符串并打印,
  7. 线程ReaderThread执⾏完毕。

这里使用了一个小案例讲述了管道流的基本使用与原理,下面我们在看看其他方法的使用

join()方法

join在上面也描述过,这里在详细的说明一下join()⽅法是Thread类的⼀个实例⽅法。它的作⽤是让当前线程陷⼊“等待”状态,等join的这个线程执⾏完成后,再继续执⾏当前线程。有时候,主线程创建并启动了⼦线程,如果⼦线程中需要进⾏⼤量的耗时运算,主 线程往往将早于⼦线程结束之前结束。 如果主线程想等待⼦线程执⾏完毕后,获得⼦线程中的处理完的某个数据,就要⽤到join⽅法了,下面我们使用一个小案例来说明一下

package demo;

/**
 * @Date 2021/7/19 11:21
 * @Version SpringBoot 2.2.2
 * @packageName
 */
public class JoinTest {

    static class ThreadOne implements Runnable{

        @Override
        public void run() {
            try {
                System.out.println("开启");
                Thread.sleep(1000);
                System.out.println("关闭,是在一秒后执行的");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new ThreadOne());
        thread.start();
        thread.join();
        System.out.println("如果不加join⽅法,我会先被打出来,加了就不⼀样了");
    }
}

下面来看看执行结果
在这里插入图片描述
注意join()⽅法有两个重载⽅法,⼀个是join(long), ⼀个是join(long, int)。 实际上,通过源码你会发现,join()⽅法及其重载⽅法底层都是利⽤了wait(long)这个⽅法

sleep方法

sleep⽅法是Thread类的⼀个静态⽅法。它的作⽤是让当前线程睡眠⼀段时间。它有这样两个⽅法(Thread.sleep(long)与Thread.sleep(long, int)),第⼆个⽅法貌似只对第⼆个参数做了简单的 处理,实际上还是调⽤的第⼀个⽅法

sleep与wait的区别(面试经典)

  1. sleep不会释放当前的锁,而wait会
  2. wait可以指定时间,也可以不指定时间,但是sleep必须指定
  3. wait在释放cpu时,同时也会释放锁,但是sleep不会,所以容易死锁情况发生
  4. wait必须在同步方法或者同步代码块中使用,而sleep可以在任意位置使用

ThreadLocal类

ThreadLocal是⼀个本地线程副本变量⼯具类。内部是⼀个弱引⽤的Map来维护。ThreadLocal类并不属于多线程间的通信,⽽是让每个线程有⾃⼰”独⽴“的变量,线程之间互不 影响。它为每个线程都创建⼀个副本,每个线程可以访问⾃⼰内部的副本变量。ThreadLocal类最常⽤的就是set⽅法和get⽅法,下面我们来做一个试验看看

package demo;

/**
 * @Date 2021/7/19 11:43
 * @Version SpringBoot 2.2.2
 * @packageName
 */
public class ThreadLocalDemo {

    public static class ThreadOne implements Runnable{

        private ThreadLocal<String>threadLocal;

        public ThreadOne(ThreadLocal<String>threadLocal){
            this.threadLocal = threadLocal;
        }

        @Override
        public void run() {
            threadLocal.set("一");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("One:"+threadLocal.get());
        }
    }

    public static class ThreadTwo implements Runnable{

        private ThreadLocal<String>threadLocal;

        public ThreadTwo(ThreadLocal<String>threadLocal){
            this.threadLocal = threadLocal;
        }

        @Override
        public void run() {
            threadLocal.set("二");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Two:"+threadLocal.get());
        }
    }

    public static void main(String[] args) {
        ThreadLocal<String>threadLocal = new ThreadLocal<>();
        new Thread(new ThreadOne(threadLocal)).start();
        new Thread(new ThreadTwo(threadLocal)).start();
    }
}

可以看到,虽然两个线程使⽤的同⼀个ThreadLocal实例(通过构造⽅法传⼊), 但是它们各⾃可以存取⾃⼰当前线程的⼀个值,如果开发者希望将类的某个静态变量(user ID或者transaction ID)与线程状态关 联,则可以考虑使⽤ThreadLocal。最常⻅的ThreadLocal使⽤场景为⽤来解决数据库连接、Session管理等。数据库连 接和Session管理涉及多个复杂对象的初始化和关闭。如果在每个线程中声明⼀些私有变量来进⾏操作,那这个线程就变得不那么“轻量”了,需要频繁的创建和关闭连接

总结

本章主要是结合源码部分讲述了一些小案例,如Thread类中的一些常用方法以及线程分组与优先级,还对锁的描述一些内容,到时候有时间会写一篇专门并发编程的文章,可以说是这篇文章的后续,尽请期待吧

下面还有我写的其他的好文,有兴趣的也可以去了解了解

  1. 《使用SpringBoot+jpa连接MySQL实现页面增删改查》
  2. 《SpringBoot+MyBatis+vue-cli+ElementUI分享篇》
  3. 《ES搜索(ElasticSearch)入门理论篇》
  4. 《ES搜索、Head、Kibana安装使用篇》
  5. 《使用RestFul风格操作ElasticSearch 看这篇够了》
  6. 《SpringBoot集成Swagger实现api接口文档》
  7. 《Spring Security(二)设置登陆页权限》
  8. 《SpringBoot整合OSS实现图片存储》
  9. 《Redis理论实战篇》
  10. 《SpringBoot整合MongoDB实现ResultFul风格接口》

若要前行,就要离开你现在停留的地方!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

藤井大叔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值