Java多线程初步


写在前面
笔者才疏学浅,本文参考网上众多前辈大佬的文章,整理引用了部分观点及内容,加之笔者些许愚见糅合而成。
如有侵权还请联系笔者,必在第一时间处理
若是有写的有误,还请务必指出,感激不敬


为何要用多线程1

  1. 对于多核处理器来说:提高CPU利用率
    如果在多核处理器上运行单线程程序,会使得一个CPU疯狂运行,其余看戏。但是他们完全可以一起干活的嘛,所以多线程就用上了
  2. 对于单核处理器来说:防止阻塞
    首先,单核处理器进行多线程的操作无疑会增加额外开销,运行效率不如单线程。但是在某些情况下,如对数据的远程请求迟迟没有响应,那么单线程下后续的操作全都不用执行了,但是多线程就可以避免这个问题,各线程独立执行,一条线程GG可以不影响其他线程运行

虽然多线程有以上优点,但是其并非完美无缺

多线程带来的问题:线程安全

首先先来说明一下什么是线程安全:如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的
线程安全的本质是 主内存中变量共享,而各线程不能及时同步。关于这一问题的详细解释,我们就不得不提及并发三大问题:原子性 有序性 可见性 对程序的影响,以及它们如何影响线程的安全。具体将在JMM中对该问题进行说明 如果我忘了挂链接记得提醒我

如何使得线程安全:线程通信及同步

在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递。
在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信。在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信。
同步是指程序用于控制不同线程之间操作发生相对顺序的机制。在共享内存并发模型里,同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。2

Java属于共享内存模型,所以在使用多线程的时候不用发送消息给其他线程,而只需要刷新缓存行。
在Java中,解决线程安全最重要的控制同步:在合适的时间执行合适操作并及时刷新数据。关于这一点则是依靠volatile synchronized关键字 和lock体系实现,具体在Java同步控制说明~~ 假装这是个链接~~

单个线程的说明

线程的生命周期

图源及参考3
问题参考4
在这里插入图片描述

  1. New
    线程创建而未启动

    run() 和 start() 方法的区别
    只有调用了start()才是启动了一个线程。如果在一个线程a中调用线程b的run()方法并不是启动多线程,这种调用方法无异于调用任何类的public方法

  2. Runnable
    包含os中Running 和 Ready,可能处于运行或等待时间片的量子叠加态

  3. Blocked
    该状态下线程放弃CPU使用权,具体分为三种情况
    a.等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。需要其他线程的显示唤醒notif()y/notifyAll()

    wait()和notify()/notifyAll()方法要在同步块中被调用
    JDK强制的,wait()方法和notify()/notifyAll()方法在调用前都必须先获得对象的锁。wait和notify都是对竞争同一锁对象的线程进行调度的,要是连锁都没有我挂谁啊。

    b. 同步阻塞:线程在获取 synchronized 同步锁失败。在其他线程释放锁之后,该阻塞线程又会加入到锁竞争中。得到锁回到Runnable,没抢到继续Blocked。
    c.其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。

    sleep() 和 wait() 方法的异同
    sleep 和 wait 都可以使线程放弃对CPU的使用权,使线程进入Block状态
    不同在于,sleep不会放弃线程所持有monitor,而wait方法则放弃monitor

  4. Terminated
    正常结束/异常退出

    如果这个线程持有某个某个对象的监视器,那么这个对象监视器会被立即释放

如何切换线程状态:Thread类部分常用函数

此部分参考资料5

在这里插入图片描述

  1. start()
    启动线程,进入Runnable状态,在此过程中为线程分配资源
  2. run()
    线程执行的代码,在获得CPU时间片之后开始运行,即进入Running状态
  3. sleep(long millis)
    使线程进入睡眠等待,从Running进入Blocked中的Time Waiting状态。交出CPU使用权,但不交出持有锁
  4. yield()
    令线程交出CPU使用权,但我们无法控制具体交出时间,并不像sleep方法一样立即交出。此时线程从Runing重回Runnable状态
  5. join()
    使当前线程挂起,在其他线程执行完毕之后在运行。线程从Running转为Blocked中的Waiting状态。
    除无参方法外,还有join(long millis)可以设定线程挂起的最大时间
    例如:假如在main线程中,调用thread.join方法,则main方法会等待thread线程执行完毕或者等待一定的时间。
  6. wait()
    交出CPU使用权以及持有的锁。线程从Running变为Blocked中的Waiting状态
  7. notify()/notiflyAll()
    唤醒竞争该锁对象的线程。线程从Waiting回到Runnable状态
    其中notify()在全部对该对象竞争的线程中唤醒随机的一个;notifyAll()唤醒全部6

中断机制7

给目标线程发送一个中断信号,由线程自行处理该中断。如果目标线程没有接收线程中断的信号并结束线程,线程则不会终止;如果有则执行到接受处理位置时执行中断逻辑。所以中断可以执行也可以不执行
1、java.lang.Thread#interrupt()
中断目标线程,给目标线程发一个中断信号,线程被打上中断标记。
2、java.lang.Thread#isInterrupted()
判断目标线程是否被中断,不会清除中断标记。
3、java.lang.Thread#interrupted()
判断目标线程是否被中断,会清除中断标记。

在实际中,中断常用于唤醒处于Blocked状态的线程

public static void main(String[] args) throws InterruptedException {
     Thread t = new Thread(()->{
         while(true){
             if(Thread.currentThread().isInterrupted()){
                 System.out.println("is interrupted!");
                 return;
             }

             try {
                 Thread.sleep(1000);
             } catch (InterruptedException e) {
                 System.out.println("interrupt in Thread sleep");
                 Thread.currentThread().interrupt();
             }
         }
     });
     t.start();
     Thread.sleep(2000);
     t.interrupt();
}

注意: sleep() 方法被中断后会清除中断标记。
详细内容见知乎原作,写的非常详细

作者:Java技术栈
链接:https://zhuanlan.zhihu.com/p/45667127
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

如何实现一个线程

  1. 继承Thread类

  2. 实现Runnable接口

    public class main {
        private class myThread implements Runnable{
            private String s;
    
            public myThread(String s) {
                this.s = s;
            }
    
            @Override
            public void run() {
                System.out.println(s);
            }
        }
        public static void main(String[] args){
            main tmp = new main();
            Thread t = new Thread(tmp.new myThread("using Runnable interface!"));
            t.start();
        }
    }
    

    Runnable 较 Thread 优势8
    由于Java的单继承,所以实现Runnable接口更为灵活,且对于较为简单的执行代码。如果只是为了实现创建继承Thread类开销过大
    在JDK 8 之后引入lambda表达式能进一步简化线程的实现流程

  3. 线程池实现
    太懒了,留个链接跑路了9

  4. 实现Callable接口10
    首先我们来用lambda表达式实现接口

        Callable<String> callable = ()->{return "I can return back!";};
    

    提交给线程池,并使用Future<V>来接受返回值

        ExecutorService executorService = Executors.newSingleThreadExecutor();
        Future<String> future = executorService.submit(callable);
        System.out.println(future.get());
    

    Runnable 和 Callable 不同
    Callable接口可用于实现传递返回值,而Runnable不可以。
    其次,二者对异常处理方式不同。Runnable接口必须使用try-catch捕获异常;而Callable可以throws给调用者

守护线程11

为其他线程服务的线程。例如GC。
在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。所以JVM退出时,不必关心守护线程是否已结束。

创建守护线程

在调用start()方法前,调用setDaemon(true)把该线程标记为守护线程

Thread t = new Thread();
t.setDaemon(true);
t.start();

注意:守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失。

线程优先级12

每个线程都有一个优先级,分布在Thread.MIN_PRIORITY和Thread.MAX_PRIORITY之间(分别为1和10)
默认情况下,新创建的线程都拥有和创建它的线程相同的优先级。main方法所关联的初始化线程拥有一个默认的优先级,这个优先级是Thread.NORM_PRIORITY (5).

线程的当前优先级可以通过getPriority方法获得。
线程的优先级可以通过setPriority方法来动态的修改,一个线程的最高优先级由其所在的线程组限定。


参考文章:


  1. 史上最全Java多线程面试题及答案 ↩︎

  2. 深入理解Java内存模型(一)——基础 ↩︎

  3. 第十章 多线程 ↩︎

  4. 史上最全Java多线程面试题及答案 ↩︎

  5. Java并发编程:Thread类的使用 ↩︎

  6. Java多线程学习之wait、notify/notifyAll 详解 ↩︎

  7. 一文搞懂 Java 线程中断 - Java技术栈的文章 - 知乎 ↩︎

  8. Java Thread and Runnable Tutorial ↩︎

  9. Java ExecutorService and Thread Pools Tutorial ↩︎

  10. Java Callable and Future Tutorial ↩︎

  11. 守护线程 - 廖雪峰的官方网站 ↩︎

  12. 17、多线程 ↩︎

猜你喜欢

转载自blog.csdn.net/white_156/article/details/107601295