Java 并发编程(Ⅰ)

Posted 364.99°

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 并发编程(Ⅰ)相关的知识,希望对你有一定的参考价值。

目录


1. 概念

1. 基本概念

进程和线程:

  • 图示:
  • 文字:
    • 进程:程序的一个实例,一个程序可以有多个实例,如下图

      现在QQ这一个程序就拥有了两个进程
    • 线程:线程就是进程的执行单元,一个进程中有多个线程
  • 概念:
    • 进程:进程是 分配和管理资源 的基本单位,不同进程会竞争计算机系统资源
    • 线程:线程是 最小调度单位(处理器调度的基本单位)
  • 区别:
    • 独立 | 共享
      • 线程之间共享本进程的地址空间、资源(内存、CPU、IO等)
      • 每个进程都有独立的地址空间,资源独立
    • 健壮 ?
      • 一个线程崩溃之后,整个进程都要崩溃
      • 一个进程崩溃之后,在保护模式下不会对其他进程产生影响
    • 开销?
      • 线程依附于进程运行,显然进程开销大(包括启动开销、切换开销)
    • 通信
      • 同一台计算机的进程通信称为 IPC(Inter-process communication),不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
      • 线程因为共享进程资源,所以通信很简单,比如说共享一个静态资源

并发与并行:
单核CPU同一时间只能执行一个线程,即 串行执行 。操作系统中的 任务调度器 会将 CPU 的时间片(时间很短)分给不同的线程使用。因为 CPU 在线程间切换非常快,会给人一种多线程同时运行的错觉。

  • 并发(concurrent): 同一时间应对(dealing with)多件事的能力
  • 并行(parallel): 同一时间做(doing)多件事的能力

同步与异步:

  • 同步: 即串行执行,需要等前面的代码返回之后,才能继续执行后续的代码。

    @Slf4j
    public class ShowSync 
    
        public static void count(int i) 
            log.info("第" + i + "次执行count方法");
        
    
        public static void main(String[] args) 
            for (int i = 0; i < 3; i++) 
                count(i);
            
            log.info("线程:" + Thread.currentThread().getName());
        
    
    
  • 异步: 不需要等待前面的代码返回,就能执行下面的代码。

    @Slf4j
    public class ShowAsync 
        public static void main(String[] args) 
            for (int i = 0; i < 3; i++) 
                new Thread(() ->
                    log.info("线程:" + Thread.currentThread().getName())
                ).start();
            
            log.info("线程:" + Thread.currentThread().getName());
        
    
    

2. 线程的状态

Thread 类中提供了一个枚举类 State,如下:

public enum State 

        NEW,
        
        RUNNABLE,

        BLOCKED,

        WAITING,

        TIMED_WAITING,

        TERMINATED;
    

可见,一个Java线程有 六大状态 。可实际上,我们会把 RUNNABLE 分为 ReadyRUNNABLE 两大状态,如下图所示,所以我们一般会说:一个Java线程有 七大状态

  • NEW
    • Thread t1 = new Thread() 进入此状态
  • READY
    • t.start() 进入此状态
    • 此时线程已经可以运行,但还未获取到 CPU 时间片,处于就绪状态
  • RUNNING
    • 就绪的线程获取到了 CPU 时间片,进入运行状态
  • BLOCKED
    • 线程进入 synchronized 修饰的方法或代码块
  • TERMINATED
    • 线程的 run() 跑完或者 main() 跑完
  • WAITING
    • 进入此状态的线程不会被分配 CPU时间片,需要被其他线程唤醒,不然会一直等待下去
  • TIMED_WAITING
    • 进入此状态的线程不会被分配 CPU时间片,不需要被其他线程唤醒,到达一定时间之后会自动苏醒

至于线程中的状态转换及相关方法,我将在后文陆续介绍。

2. 线程的初始化

线程的初始化,也就是 new Thread(),我们有三种方式来实现初始化,分别是:

  • 直接 new Thread() 然后重写 run()
  • new Thread(new Runnable()) ,将 Runnablerun() 组装到 Thread
  • new Thread(new FutureTask(new Callable())) ,将 Callable 中的 call() 先组装到 FutureTask 再组装到 Thread

1. new Thread()

直接 new Thread() 初始化线程,是将线程(Thread)和任务(run())绑在一起,实际开发中不建议使用,代码如下:

@Slf4j
public class JustThread 
    public static void main(String[] args) 
        Thread thread1 = new Thread("线程1") 
            @Override
            public void run() 
                log.info("线程:" + Thread.currentThread().getName() + " 运行中...");
            
        ;
        thread1.start();

        log.info("线程:" + Thread.currentThread().getName() + " 运行中...");

        /*
            lambda表达式
         */
        Thread thread2 = new Thread(() -> 
            log.info("线程:" + Thread.currentThread().getName() + " 运行中...");
        , "线程2");
        thread2.start();
    

2. new Thread(new Runnable())

将线程(Thread)和任务(Runnable)分开初始化,然后组合在一起。

@Slf4j
public class RunnableAndThread 
    public static void main(String[] args) 
        Runnable runnable = new Runnable() 
            @Override
            public void run() 
                log.info("线程:" + Thread.currentThread().getName() + " 运行中...");
            
        ;
        Thread thread = new Thread(runnable, "线程3");
        thread.start();

        /*
            lambda表达式
         */
        Runnable runnable1 = () -> log.info("线程:" + Thread.currentThread().getName() + " 运行中...");
        Thread thread1 = new Thread(runnable1, "线程4");
        thread1.start();
    

3. Thread 和 Runnable 的关系

先看一下 Runnable 的源码,如下:

@FunctionalInterface
public interface Runnable 
    public abstract void run();

一个函数式接口,只有一个 run()

然后看一下 Thread 的部分源码:

    @Override
    public void run() 
        if (target != null) 
            target.run();
        
    

    public Thread(Runnable target) 
        init(null, target, "Thread-" + nextThreadNum(), 0);
    

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) 
        init(g, target, name, stackSize, null, true);
    

好的,就看这几段方法,就不在这里做深入分析了。从上面的代码,我们可以很清晰地弄懂 ThreadRunnable 之间的关系:

  • new Thread 的时候,如果没有传入 Runnable,就会重写 Threadrun()
  • new Thread 的时候,如果传入了 Runnable,就会使用 Runnable 中的 run()

所以,Runnable 接口就是为了给 Thread 提供一个方法体 run()

在实际开发中,我们通常会自定义一个类来 implements Runnable,然后将此类传入 Thread 中。案例如下:

  • 定义一个每间隔十秒打印一下当前时间的线程
public class TimePrinter implements Runnable
    private SimpleDateFormat dateFormat= new SimpleDateFormat("hh:mm:ss");

    @Override
    public void run() 
        while (true) 
            try 
                System.out.println("当前时间为: " + dateFormat.format(new Date()));
                TimeUnit.SECONDS.sleep(10);
             catch (InterruptedException e) 
                throw new RuntimeException(e);
            
        
    

public class TaskDemo 
    public static void main(String[] args) 
        Thread thread = new Thread(new TimePrinter(), "测试线程");
        thread.start();
    

4. new Thread(new FutureTask(new Callable()))

@Slf4j
public class FutureTaskAndThread 
    public static void main(String[] args) throws ExecutionException, InterruptedException 


        FutureTask<Long> futureTask = new FutureTask<>(new Callable<Long>() 
            @Override
            public Long call() throws Exception 
                long start = System.currentTimeMillis();
                log.info(Thread.currentThread().getName() + " 运行中...");
                TimeUnit.MILLISECONDS.sleep(123);
                long end = System.currentTimeMillis();
                return end - start;
            
        );
        Thread thread = new Thread(futureTask, "线程5");
        thread.start();
        log.info(thread.getName() + "花费了 " + futureTask.get() + " 毫秒");
    

Runnable与Callable的关系如下图:
可见,相比于 Runnablerun()Callablecall() 多了一个返回值。

@FunctionalInterface
public interface Callable<V> 
    V call() throws Exception;

3. 常用方法

线程状态的切换:

TIMED_WAITING 和 WAITING 的区别:

  • TIMED_WAITING 即使没有外部信号,在等待时间超时后,线程也会恢复
  • WAITING 需要等待外部信号来唤醒

1. start

先来看一下源码:

	// 线程组
	private ThreadGroup group;
	// NEW 状态的线程的 threadStatus = 0
	private volatile int threadStatus = 0;

    public synchronized void start() 

        if (threadStatus != 0)
            throw new IllegalThreadStateException();
		// 将要启动(start)的线程装入线程组中
        group.add(this);

        boolean started = false;
        try 
            start0();
            started = true;
         finally 
            try 
                if (!started) 
                	// 告诉组,这个线程启动(start)失败
                    group.threadStartFailed(this);
                
             catch (Throwable ignore) 

            
        
    

	// native方法,调用本地操作系统开启一个新的线程
    private native void start0();

看完源码之后,可以得出以下结论:

  • start 启动一个线程需要经历以下步骤:
    1. 将调用 start 的线程装入线程组 ThreadGroup
    2. 调用native方法,启动线程

1. 线程组

此处参考链接:https://zhuanlan.zhihu.com/p/61331974

线程组(Thread Group): 一个线程集合,可以很方便地管理一个组中的线程。

线程组树:

  • System线程组: 处理JVM的系统任务的线程组,例如对象的销毁等

  • main线程组:包含至少一个线程——main(用来执行main方法)

  • 面线程组的子线程组: 应用程序创建的线程组

        public static void main(String[] args) 
            // 输出当前线程组——main线程组
            System.out.println(Thread.currentThread().getThreadGroup().getName());
            // 输出当前线程组的父线程组——System
            System.out.println(Thread.currentThread().getThreadGroup().getParent().getName());
        
    

给线程指定线程组:

  • 在初始化线程的时候如果不指定数组,就会默认指定为 main线程组,代码如下:

    private void init(ThreadGroup g, Runnable target, String name,
                          long stackSize, AccessControlContext acc,
                          boolean inheritThreadLocals) 
                          
            /*
    			其余代码省略
    		*/
            Thread parent = currentThread();
            SecurityManager security = System.getSecurityManager();
            if (g == null) 
              
                if (security != null) 
                    g = security.getThreadGroup();
                
    
                if (g == null) 
                    g = parent.getThreadGroup();
                
            
    
            /*
    			其余代码省略
    		*/
    
    
  • 可以利用以下任意一个构造器来给线程指定一个线程组:

        public Thread(ThreadGroup group, Runnable target)
    
        public Thread(ThreadGroup group, Runnable target) 
    
    	public Thread(ThreadGroup group, Runnable target, String name)
    
        public Thread(ThreadGroup group, Runnable target, String name, long stackSize)
    

    演示一下:

        public static void main(String[] args) 
            ThreadGroup group = new ThreadGroup("我的线程组");
            Thread thread = new Thread(group, "我的线程");
            System.out.println(thread.getName() + " 的线程组是 " + group.getName());
        
    

线程组方法解析: https://blog.csdn.net/a1064072510/article/details/87455525

小结一下: 将线程装入线程组就是为了方便批量操作线程,比如说我要通知好几个线程要终止了,就可以事先把这几个线程装入一个线程组,然后直接通过线程组批量通知:

// 通知main线程组中的所有线程要终止了
Thread.currentThread().getThreadGroup().interrupt();

2. start 和 run

问:为什么 start 能启动一个线程,而 run 就不行呢?

答:因为 Java 并不能直接作用于操作系统,所以需要调用native方法(比如C、C++等编写的方法)来告诉操作系统开启新的线程。而在 start 方法中调用了native方法 start0 来启动线程。至于 run, 这就是线程中的一个方法,当线程跑起来的时候,就会去执行 run,当 run 方法执行完毕线程就会结束。

2. sleep

先来看一下 Thread.sleep 的源码:

	/**
	*让当前线程睡眠指定毫秒数,线程不会丢失任何monitors的所有权
	*/
    public static native void sleep(long millis) throws InterruptedException;

好的,现在来使用一下:

@Slf4j
public class Sleep_md
    public static void main(String[] args) throws InterruptedException 
        Thread t1 = new Thread(()->
            long start = System.currentTimeMillis();
            log.info("t1睡眠5s");
            try 
                Thread.sleep(5 * 1000);
             catch (InterruptedException e) 
            
            long end = System.currentTimeMillis();
            log.info("已过去  毫秒", (end - start));
        );
        t1.start();
    

通过上述代码的运行,可发现 Thread.sleep 可以让当前线程进入 TIMED_WAITING 睡眠一段时间,然后进入 RUNNABLE 。在此期间会让出CPU,给其他线程机会。

1. TimeUtil

除了 Thread.sleepTimeUtil 也有 sleep,可以让线程进入睡眠,两者效果相同。

    public void sleep(long timeout) throws InterruptedException 
        if (timeout > 0) 
            long ms = toMillis(timeout);
            int ns = excessNanosJava 多线程知识的简单总结

并发编程多线程

并发编程--线程开启线程守护线程线程互斥锁

Java多线程面试

Java并发编程:守护线程与线程阻塞的四种情况

Java并发编程:守护线程与线程阻塞的四种情况