多线程

程序-进程-线程

程序:代码

进程:正在运行的程序,程序的一次执行过程

进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。

进程是资源分配的最小单位,线程是程序执行的最小单位(资源调度的最小单位)

线程:进程可进一步细分为线程,程序内部的一条执行路径。

进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。

线程是进程的一个实体,是cpu调度和分派的基本单位,是比程序更小的能独立运行的基本单位。

同一进程中的多个线程之间可以并发执行。

并行,并发,串行区别

串行:在时间上不可能发生重叠,前一个任务没有搞定,下一个任务只能等着

并行:多个任务在时间上是重叠的,同一时刻互不干扰共同执行

并发:允许两个任务相互干扰,统一的时间点,只用一个任务在执行,交替执行

            

           并行                                                                                           并发

并发的三大特性

原子性

原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,即不被中断操作,要不全部执行完成,要不都不执行。就好比转账,从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。2个操作必须全部完成。

那程序中原子性指的是最小的操作单元,比如自增操作,它本身其实并不是原子性操作,分了3步的,包括读取变量的原始值、进行加1操作、写入工作内存。所以在多线程中,有可能一个线程还没自增完, 可能才执行到第二步,另一个线程就已经读取了值,导致结果错误。那如果我们能保证自增操作是一个原子性的操作,那么就能保证其他线程读取到的一定是自增后的数据。

关键字: synchronized

可见性

当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

若两个线程在不同的cpu,那么线程1改变了i的值还没刷新到主存,线程2又使用了i,那么这个i值肯定还是之前的,线程1对变量的修改线程没看到这就是可见性问题。

关键字: volatile、 synchronized、 final

有序性

虚拟机在进行代码编译时,对于那些改变顺序之后不会对最终结果造成影响的代码,虚拟机不一定会按照我们写的代码的顺序来执行,有可能将他们重排序。实际上,对于有些代码进行重排序之后,虽然对变量的值没有造成影响但有可能会出现线程安全问题。

关键字: volatile、 synchronized

volatile本身就包含了禁止指令重新排序的语义,而synchronized关键字是由‘一个变量在同一时间值允许一条线程对其进行lock操作’这条规则是明确的。

volatile

volatile只是保持变量的线程可见性,通常适用于一个线程写,多个线程读的场景。他不能保证线程安全(原子性)本身就包含了禁止指令重新排序的语义(有序性)

可见性

public  class VolatileDemo{
        public static volatile boolean flag = true;


    public static void main(String[] args) throws InterruptedException {
        //创建两个线程 thredA和 main线程
        new Thread(() -> {
            while(flag){

            }
            System.out.println("======end of thredA======");
        },"thredA").start();

        Thread.sleep(100);
        System.out.println("turn flag off");
        flag = false;
    }
}

turn flag off
======end of thredA======

volatile详解 - 钟齐峰 - 博客园 (cnblogs.com)

指令重排

线程的生命周期

创建、就绪、运行、阻塞、死亡

创建

线程实例的创建

当一个Thread类或其他子类的对象被声明并创建时,新生的线程对象处于新建状态

就绪

执行Start()方法之后

当调用了线程对象的start方法之后,将进入线程队列等待cpu时间片,已经具备了运行的条件。

在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。

运行

run方法的代码开始执行

当就绪的线程被调度并获得了处理器资源时,便进入了运行状态,run()方法定义了线程的操作和逻辑

阻塞

类似与堵车,run方法的代码暂停执行,卡住run方法

在某种特殊情况下,被人为挂起或执行输入输出操作时,让出cpu并临时中止自己的执行,进入阻塞状态
阻塞的情况又分为三种:​

  • 等待阻塞:运行的线程执行wait方法,该线程会释放占用的所有资源,JVM把线程放到"等待池"中
    • 进入这个状态后,不能自动唤醒的,必须依靠其他线程调用notify或notifyAll方法才能被唤醒, 
    • wait是object类的方法
  • 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入”锁池”中。
  • 其他阻塞:运行的线程执行sleep或join方法,或者发出了I/O请求时, JVM会把该线程置为阻塞状态。
    • 当sleep状态超时、join等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
    • sleep是 Thread类的方法.

同步异步阻塞
阻塞和非阻塞指的一个是客户端等待消息处理时的本身的状态,是挂起还是继续干别的。
同步和异步指的对于消息结果的获取是客户端主动获取,还是由服务端间接推送。

举个例子,比如我们去照相馆拍照,拍完照片之后,商家说需要30分钟左右才能洗出来照片,
​
这个时候如果我们一直在店里面啥都不干,一直等待商家面前等待它洗完照片,这个过程就叫**同步阻塞**。 
​
(调用者发起IO操作请求,等待IO操作完成再返回。IO操作的过程需要等待,操作执行完成后返回结果。)
​
当然大部分人很少这么干,更多的是大家拿起手机开始看电视,看一会就会问老板洗完没,老板说没洗完, 然后我们接着看,再过一会接着问,直到照片洗完,这个过程就叫**同步非阻塞**。
​
(调用者发起IO操作请求,询问IO操作的状态,如果未完成,则立即返回;如果完成,则返回结果。IO操作的过程需要等待执行完成才返回结果。)
​
因为店里生意太好了,越来越多的人过来拍,店里面快没地方坐了,老板说你把你手机号留下,我一会洗好了就打电话告诉你过来取,然后你去外面找了一个长凳开始躺着睡觉等待老板打电话,
啥不都干,这个过程就叫
**异步阻塞。** ​ ( 调用者发起IO操作请求,等待IO操作完成再返回。IO操作的过程需要等待,操作完成后通过通知或回调获得结果。) ​ 当然实际情况是,大家可能会直接先去逛街或者吃饭做其他的活动,这样以来两不耽误,这个过程就叫**异步非阻塞** ( 调用者发起IO操作请求,询问IO操作的状态,如果未完成,则立即返回;如果完成,则返回结果。IO操作的过程不需要等待,操作完成后通过通知或回调获得结果)

死亡

自然,强制

如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡。

对于已经死亡的线程,无法再使用start方法令其进入就绪  

开启线程

继承Thread类 重写run方法

public class MyThread extends Thread {
  //重写 run 方法 @Override
public void run() { System.out.println(this.getName());
  }
}
//测试类 public class MyTest { public static void main(String[] args) { MyThread mt2 =new MyThread(); mt2.start(); mt2.run(); } }

1) start:
  用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕而直接继续执行下面的代码。通过调用Thread类的start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行, 然后通过此Thread类调用方法run()来完成其运行操作的, 这里方法run()称为线程体,它包含了要执行的这个线程的内容, Run方法运行结束, 此线程终止。然后CPU再调度其它线程。 一旦得到cpu时间片,就开始执行run()方法,这里方法 run()称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。
2) run:
  run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。

总结:调用start方法方可启动线程,而run方法只是thread的一个普通方法调用,还是在主线程里执行。这两个方法应该都比较熟悉,把需要并行处理的代码放在run()方法中,start()方法启动线程将自动调用 run()方法,这是由jvm的内存机制规定的。并且run()方法必须是public访问权限,返回值类型为void。

实现Runnable接口  实现run方法

实现runnable 接口的类并不是一个线程类,而是线程类的一个target ,可以为线程类构造方法提供参数来实现线程的开启

public class SecondThread implements Runnable{
public void run() {
            System.out.println(Thread.currentThread().getName());
    }
}

public class MyTest {
    public static void main(String[] args) {
                SecondThread s1=new SecondThread();
                Thread t1=new Thread(s1,"线程1");
                Thread t2=new Thread(s1,"线程2");
                t1.start();
                t2.start();
    }
}

实现Callable接口 实现call()方法

创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。

创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。

FutureTask<String> task = new FutureTask<String>(new Callable())

使用FutureTask对象作为Thread对象的target创建并启动新线程。

Thread t1 = new Thread(task,"线程名") ;

t1.start();

调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。

task.get();

public class Target implements Callable<Integer> {
    int i=0;
    public Integer call() throws Exception {
        for (; i < 20; i++) {
            System.out.println(Thread.currentThread().getName()+""+i);
        }
        return i;
    }
}

public class ThirdThread {
     public static void main(String[] args) {
            Target t1=new Target();
            FutureTask<Integer> ft=new FutureTask<Integer>(t1);
            Thread t2=new Thread(ft,"新线程");
            t2.start();
            try {
                System.out.println(ft.get());
            } catch (Exception e) {
                // TODO: handle exception
            }
    }
}

线程池

总结

  • 线程类只是实现了Runnable接口与Callable接口,还可以继承其他类。
  • 在这种方式下,多个线程可以共享一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
  • 编程稍稍复杂,如果需要访问当前线程,则必须使用Thread.currentThread()方法。
  • Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;Callable接口中的call()方法是有返回值的,是一个泛型
  • call()方法可以抛出异常;run()方法不可以。

  • 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检查计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,可以获取执行结果。

继承Thread类

  • 编写简单,如果需要访问当前线程,则无须使用Thread.currentThread()方法,直接使用this即可获得当前线程。
  • 因为线程类已经继承了Thread类,所以不能再继承其他父类。

保证线程安全 

1.使用jvm提供的锁,synchronized关键字

2.使用JDK提供的各种锁 

锁状态-----专门针对synchronized的

java的锁就是在对象的Markword中记录的一个锁状态。无状态锁,偏向锁,轻量级锁,重量级锁对应不同的锁状态

java的锁机制就是根据资源竞争的激烈程度不断进行锁升级的过程

偏向锁:在锁对象的对象头中记录一下当前获取该锁的线程ID,这个线程下次来获取该锁就可以直接获取锁了

轻量级锁:一个线程获取到锁后,这个锁是偏向锁,如果有其他线程来竞争这把锁,偏向锁升级为轻量级锁。轻量级锁底层是由自旋锁来实现的,不会造成线程阻塞

重量级锁:自旋次数过多仍然没有获取到锁,就会升级为重量级锁,重量级锁会导致线程阻塞

自旋锁:就是在线程获取锁的过程中,不会去阻塞线程,也就是无所谓唤醒线程,阻塞和唤醒都需要操作系统去进行,比较消耗时间,自旋锁是线程通过CAS获取预期的一个标记,如果没有获取到,就继续循环获取,如果获取到了则表示获取到了锁,这个过程线程是一直在运行的,相对而言没有使用太多的操作系统资源,比较轻量。

 

附加信息

查看对象的内存布局情况 --- 锁情况

我们用 Java Object Layout 工具来查看对象的内存布局情况

<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>

在 main 方法中编写代码:

 public static void main(String[] args) {

        Object obj = new Object();
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());

        synchronized (obj){
             //打印加锁之后的内存布局
            System.out.println(ClassLayout.parseInstance(obj).toPrintable());
        }
    }

运行并查看结果:

 2.判断偏向锁是否开启

public static void main(String[] args) {

        Object obj = new Object();
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
        // -XX:+PrintFlagsInitial | grep -i biased  查看是否开启了偏向锁还有偏向锁的延迟时间  默认是开启偏向锁的,然后延迟时间为0
        // -XX:-UseBiasedLocking  关闭偏向锁 一旦出现锁竞争就直接从无锁变成轻量级锁了
        //-XX:BiasedLockingStartupDelay=4000 设置延迟时间,在延迟时间之内创建对象并打印对象头的话,你可以发现出来的结果是无锁的
        try {
            Thread.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }

守护线程

守护线程:为所有非守护线程提供服务的线程;任何一个守护线程都是整个JVM中所有非守护线程的保姆;

守护线程类似于整个进程的一个默默无闻的小喽喽;它的生死无关重要,它却依赖整个进程而运行;哪天其他线程结束了,没有要执行的了,程序就结束了,理都没理守护线程,就把它中断了;

注意:由于守护线程的终止是自身无法控制的,因此千万不要把I0、File等重要操作逻辑分配给它;因为它不靠谱;

作用

GC垃圾回收线程:就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。

应用场景:

(1) 来为其它线程提供服务支持的情况;

(2) 在任何情况下, 程序结束时,这个线程必须正常且立刻关闭,就可以作为守护线程来使用;

反之,如果一个正在执行某个操作的线程必须要正确地关闭掉否则就会出现不好的后果的话,那么这

个线程就不能是守护线程,而是用户线程。通常都是些关键的事务,比方说,数据库录入或者更新,

这些操作都是不能中断的。

设置

thread.setDaemon(true)必须在thread.start()之前设置,否则会报llegalThreadStateException异常。

注意

不能把正在运行的常规线程设置为守护线程。

在Daemon线程中产生的新线程也是Daemon的。

守护线程不能用于去访问固有资源,比如读写操作或者计算逻辑。因为它会在任何时候甚至在一个操作的中间发生中断。

Java自带的多线程框架,比如ExecutorService, 会将守护线程转换为用户线程,所以如果要使用后台线程就不能用java的线程池

 

posted @ 2023-02-13 14:28  早日月薪五万  阅读(32)  评论(0编辑  收藏  举报