• home > java > KeyConcepts >

    java并发编程(1):Java多线程-基本线程类-基础知识复习笔记

    Author:zhoulujun Date:

    多线程只有一个目的,那就是更好的利用cpu的资源,基本线程类指的是Thread类,Runnable接口,Callable接口,传统java多线程编程 需要注意的 线程安全 线程同步 线程通信 synchronized volatile ……一锅乱炖

    复习资料:《同步与异步:并发/并行/进程/线程/多cpu/多核/超线程/管程 》

    基本线程类

    基本线程类

    基本线程类指的是Thread类,Runnable接口,Callable接口

    继承Thread创建线程

    继承java.lang.Thread类创建线程是最简单的一种方法,也最直接。

    public class MyThread1 extends Thread {}

    种创建方式,把线程执行的逻辑代码直接写在了Thread的子类中,这样根据线程的概念模型,虚拟CPU和代码混合在一起了。并且java是单继承机制,线程体继承Thread类后,就不能继承其他类了,线程的扩展受影响

    实现Runnable接口创建线程

    为了构建结构清晰线程程序,可以把代码独立出来形成线程目标对象,然后传给Thread对象。通常,实现Runnable接口的类创建的对象,称作线程的目标对象。

    public class MyThreadTarget implements Runnable{ }

    出线程目标对象和Thread分开了,并传递给了Thread。如果有比较复杂的数据要处理,可以在线程目标对象中引入数据。使用这种方式获得线程的名字就稍微复杂一些,需要使用到Thread中的静态方法,获得当前线程对象,然后再调用getName()方法。这种方式在较复杂的程序中用得比较普遍。

    Callable

    future模式:并发模式的一种,可以有两种形式,即无阻塞和阻塞,分别是isDone和get。其中Future对象用来存放该线程的返回值以及状态

    ExecutorService e = Executors.newFixedThreadPool(3);
     //submit方法有多重参数版本,及支持callable也能够支持runnable接口类型.
    Future future = e.submit(new myCallable());
    future.isDone() //return true,false 无阻塞
    future.get() // return 返回值,阻塞直到该线程运行结束

    以上是基本方法,总结一下

    多线程的基本操作

    创建线程的方式 

    1. 将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法

    2. 声明实现 Runnable 接口的类。该类然后实现 run 方法

    推荐方式二,因为接口方式比继承方式更灵活,也减少程序间的耦合。

    线程API一览

    • start() 使线程处于就绪状态,Java虚拟机会调用该线程的run方法;

    • stop() 停止线程,已过时,存在不安全性:

      • 一是可能请理性的工作得不得完成;

      • 二是可能对锁定的对象进行“解锁”,导致数据不同步不一致的情况。

      推荐 使用 interrupt() +抛异常 中断线程。

    • suspend() 暂停线程,已过时。

    • resume() 恢复线程,已过时。

      suspend 与resume 不建议使用,存在缺陷:

      • 一是可能独占同步对象;

      • 二是导致数据不一致。

    • yield() 放弃当前线程的CPU资源。放弃时间不确认,也有可能刚刚放弃又获得CPU资源。

    • t.join() 等待该线程t 销毁终止。

    获取当前线程信息

    Thread.currentThread()

    线程的分类

    线程分为守护线程、用户线程。线程初始化默认为用户线程。

    setDaemon(true) 将该线程标记为守护线程或用户线程

    特性:设置守护线程,会作为进程的守护者,如果进程内没有其他非守护线程,那么守护线程也会被销毁,即使可能线程内没有运行结束

    某线程a 中启动另外一个线程t,那么我们称线程t是线程a的一个子线程,而 线程a是线程t的父线程。

    最典型的就是我们在main方法中 启动一个线程去执行。其中main方法隐含的main线程为父线程。


    线程安全

    线程安全就是每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的

    线程安全就是说多线程访问同一代码,不会产生不确定的结果

    为什么有线程安全问题

    反过来,线程不安全就意味着线程的调度顺序会影响最终结果,如不加事务的转账代码:

    线程安全问题多是由全局变量和静态变量引起的

    • 当多个线程对共享数据只执行读操作,不执行写操作时,一般是线程安全的;

    • 当多个线程都执行写操作时,需要考虑线程同步来解决线程安全问题

    线程同步

    同步是保证多个线程之间共享同一个全局变量数据安全问题,保证数据原子性

    多个线程操作一个资源的情况下,导致资源数据前后不一致。这样就需要协调线程的调度,即线程同步。 解决多个线程使用共通资源的方法是:线程操作资源时独占资源,其他线程不能访问资源。使用锁可以保证在某一代码段上只有一条线程访问共用资源。

    有两种方式实现线程同步:

    • synchronized,使用同步代码块解决线程安全问题 

    • 同步锁(Lock)

    Java中的同步指的是通过人为的控制和调度,保证共享资源的多线程访问成为线程安全,来保证结果的准确。如上面的代码简单加入@synchronized关键字。在保证结果准确的同时,提高性能,才是优秀的程序。线程安全的优先级高于性能。

    线程状态

    java多线程-线程状态转换


    synchronized关键字用法

    当它用来修饰一个方法或者一个代码块的时候(同步代码块:将可能会发生线程安全问题的代码给包裹起来),能够保证在同一时刻最多只有一个线程执行该段代码

    1. 原子性(互斥性):实现多线程的同步机制,使得锁内代码的运行必需先获得对应的锁,运行完后自动释放对应的锁。

    2. 内存可见性:在同一锁情况下,synchronized锁内代码保证变量的可见性。

    3. 可重入性:当一个线程获取一个对象的锁,再次请求该对象的锁时是可以再次获取该对象的锁的。

    如果在synchronized锁内发生异常,锁会被释放。

    • synchronized方法 与 synchronized(this) 代码块 锁定的都是当前对象,不同的只是同步代码的范围

    • synchronized (非this对象x) 将对象x本身作为“对象监视器”:

      1. 多个线程同时执行 synchronized(x) 代码块,呈现同步效果。

      2. 当其他线程同时执行对象x里面的 synchronized方法时,呈现同步效果。

      3. 当其他线程同时执行对象x里面的 synchronized(this)方法时,呈现同步效果。

    • 静态synchronized方法 与 synchronized(calss)代码块 锁定的都是Class锁。Class 锁与 对象锁 不是同一个锁,两者同时使用情况可能呈异步效果。

    • 尽量不使用 synchronized(string),是因为string的实际锁为string的常量池对象,多个值相同的string对象可能持有同一个锁。

    synchronized原理:

    • 首先有一个线程已经拿到了锁,其他线程已经有cup执行权,一直排队,等待释放锁。

    • 锁是在什么时候释放?代码执行完毕或者程序抛出异常都会被释放掉。

    • 锁已经被释放掉的话,其他线程开始进行抢锁(资源竞争),谁抢到谁进入同步中去,其他线程继续等待。

    效率非常低,多个线程需要判断锁,比较消耗资源,抢锁的资源。——synchronized 只能有一个线程进行执行(就像厕所只有一个坑,好多人等着上厕所,谁进入厕所谁上锁,其他人在外等待),这个线程不释放锁的话,其他线程就一直等,就会产生死锁问题。

    volatile关键字用法

    多线程的内存模型:main memory(主存)、working memory(线程栈),在处理数据时,线程会把值从主存load到本地栈,完成操作后再save回去(volatile关键词的作用:每次针对该变量的操作都激发一次load and save)。

    volatile与多线程

    Volatile 关键字的作用是变量在多个线程之间可见,但不保证原子性。

    1. 内存可见性:保证变量的可见性,线程在每次使用变量的时候,都会读取变量修改后的最的值。

    2. 不保证原子性

    针对多线程使用的变量如果不是volatile或者final修饰的,很有可能产生不可预知的结果(另一个线程修改了这个值,但是之后在某线程看到的是修改之前的值)。其实道理上讲同一实例的同一属性本身只有一个副本。但是多线程是会缓存值的,本质上,volatile就是不去缓存,直接取值。在线程安全的情况下加volatile会牺牲性能

    线程间的通信方式

    线程间通信的方式主要为共享内存、线程同步。

    线程同步除了synchronized互斥同步外,也可以使用wait/notify实现等待、通知的机制。

    1. wait/notify属于Object类的方法,但wait和notify方法调用,必须获取对象的对象级别锁,即synchronized同步方法或同步块中使用。

    2. wait()方法:在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,或者其他某个线程中断当前线程,导致当前线程一直阻塞等待。等同wait方法。

      1. wait(long timeout) 在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量前,导致当前线程等待。 单位为毫秒。

      2. void wait(long timeout, int nanos) 与 wait(long timeout) 不同的是增加了额外的纳秒级别,更精细的等待时间控制。

    3. notfiy方法:唤醒在此对象监视器上等待的单个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。

    4. notifyAll方法:唤醒在此对象监视器上等待的所有线程。

    需要:wait被执行后,会自动释放锁,而notify被执行后,锁没有立刻释放,由synchronized同步块结束时释放。

    应用场景:简单的生产、消费问题。

    synchronized (lock) {//获取到对象锁lock
        try {
            lock.wait();//等待通信信号, 释放对象锁lock
        } catch (InterruptedException e) {
            e.printStackTrace();
        }	
    }//接到通信信号
    synchronized (lock) {
        //获取到对象锁lock
        lock.notify();//通知并唤醒某个正等待的线程
        //其他操作
    }//释放对象锁lock

    synchronized, wait, notify 是任何对象都具有的同步工具。

    synchronized, wait, notify

    他们是应用于同步问题的人工线程调度工具。讲其本质,首先就要明确monitor的概念,Java中的每个对象都有一个监视器,来监测并发代码的重入。在非多线程编码时该监视器不发挥作用,反之如果在synchronized 范围内,监视器发挥作用。

    wait/notify必须存在于synchronized块中。并且,这三个关键字针对的是同一个监视器(某对象的监视器)。这意味着wait之后,其他线程可以进入同步块执行。

    当某代码并不持有监视器的使用权时(如上图的状态,即脱离同步块)去wait或notify,会抛出java.lang.IllegalMonitorStateException。也包括在synchronized块中去调用另一个对象的wait/notify,因为不同对象的监视器不同,同样会抛出此异常。



    参考文章:

    Java多线程并发编程一览笔录 https://www.cnblogs.com/yw0219/p/10597041.html

    Java 中的多线程你只要看这一篇就够了 https://juejin.im/entry/57339fe82e958a0066bf284f

    阿里大牛详细讲解java多线程并发,PDT文档实战篇https://zhuanlan.zhihu.com/p/92958977

    JAVA多线程之间实现同步+多线程并发同步解决方案 https://blog.csdn.net/yz2015/article/details/79436123



    转载本站文章《java并发编程(1):Java多线程-基本线程类-基础知识复习笔记》,
    请注明出处:https://www.zhoulujun.cn/html/java/KeyConcepts/8471.html