Java基础——线程

一、什么是线程

程序、进程、线程区别

程序:程序是一段静态的代码,是应用软件执行的蓝本。

进程:是程序的一次静态执行过程,它对应了从代码加载(从磁盘加载到内存)、执行(到CPU中执行)、结束的完整过程。这也是进程从开始到消失的过程。作为执行蓝本的同一段程序,可以多次加载到系统的不同内存区域执行,形成不同进程。

线程:包含在进程里面,是进程里面的一个能独立执行自身指令的指令流(即一个子任务)。一个进程里面有一条条指令流(线程),以一定顺序加载到CPU中执行。

进程与线程的物理组成:

进程由代码、数据、内核状态和一组寄存器组成。
线程由表示程序运行状态的寄存器(如程序计数器、栈指针)以及堆栈组成。不包含进程地址空间中的代码和数据。

进程是一个内核级别的实体,进程结构的所有成分都在内核空间中,一个用户程序不能直接访问这些数据。
线程是一个用户级实体,线程结构驻留在用户空间中,能够被普通用户级函数直接访问。

不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间
线程更轻量,线程上下文切换成本一般上要比进程上下文切换低。

很经典的一句话——
线程是最小调度单位,进程是资源分配的最小单位

并行与并发

并发——针对单核CPU

线程串行执行(微观串行,宏观并行(因为切换很快,让人以为是并行)),一般会将这种线程轮流使用CPU的做法称为并发。同一时间只能是调度一个线程。

并行——针对多核CPU

多核CPU,每个核(core)都可以调度运行线程,这时候线程可以是并行的。
在这里插入图片描述

Java中的线程模型

多线程是指一个程序中包含多个执行流(即多个子任务),多线程是实现并发的一种有效手段。
一个执行流是CPU运行程序代码并操作程序的数据所形成的。**因此线程又被认为是以CPU为主体的行为。在Java中的线程模型就是一个虚拟的CPU、程序代码和数据的封装体。**其中代码和数据构成了线程体,线程体决定了线程的行为。而虚拟的CPU实在创建线程时由系统自动封装进Thread类的实例中的。

如图:
请添加图片描述
代码与数据是相互独立的,代码和数据可以与其他线程共享,两个线程可以同时访问同一个对象,它们将共享代码、数据。

而Java天生支持多线程编程。
线程模型在java.lang.Thread类中进行定义与描述,程序中的线程都是Thread类的实例。用户可以通过创建Thread的实例或者定义并创建Thread子类的实例建立和控制自己的线程。

二、线程的创建

用户可以通过创建Thread的实例或者定义并创建Thread子类的实例建立和控制自己的线程。
在Java中,创建线程的关键是构建线程体。**线程体要应用程序通过一个对象传递给Thread类的构造函数。**线程体是在线程类中的run()方法中定义的,在其中定义线程的具体行为,线程开始执行时也是由run()方法开始执行的,就像Java applicantion里面的main()函数一样。
一共有两种方法构建线程体:

(一)通过实现Runnable接口创建

Runnable接口定义为:

public interface Runnable{
void run();
}

使用这种方式创建线程有两大步:
(1)定义一个类实现Runnable接口,在该类中提供run()方法的实现。
(2)把Runnable的一个实例作为作为参数传递给Thread类的一个构造方法,该实例对象提供线程体run()。

例子:
在这里插入图片描述

Hello类的两个实例对象分别创建了t1 t2两个线程,并将线程启动。在创建的线程中,Hello类的run方法就是线程体,int i是线程的数据。线程t1、t2启动时是从Hello类的run方法开始执行的。
如图,每个线程分别打印5个字符串——
Hello0
Hello1
Hello2
Hello3
Hello4
Hello0
Hello1
Hello2
Hello3
Hello4

注意:新建的线程不会自动运行,必须调用线程的start()方法。该方法的调用把嵌入在线程中的虚拟CPU置为可运行(Runnable)状态,意味着其可以被调度运行,但不会被立即运行

(二)通过继承Thread类创建

使用这种方法也有分为两步:
(1)从Thread类派生子类,并重写其中的run()方法定义线程体。
(2)创建该子类的实例对象创建线程。

如下:
在这里插入图片描述
结果与上面的一致。

(三)实现Callable接口

使用这种方式创建线程有两大步:
(1)继承callable接口并实现call方法,并返回一个sum(随意)值
(2)执行Callable方式,需要FutureTask实现类的支持,用于接收运算结果。 FutureTask 是 Future 接口的实现类

package com.test;

import java.util.concurrent.Callable;

public class calldemo implements Callable {
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i =0;i<33;i++){
            System.out.println(i);
            sum+=i;
        }
        return sum;
    }
}
package com.test;

import java.util.concurrent.FutureTask;

public class calltest {

    public static void main(String[] args) {
        calldemo td = new calldemo();
        //需要一个callable的实现
        //1、执行callable 方式,需要futuretask实现类的支持,用于接受运算结果
        FutureTask<Integer> fu = new FutureTask<>(td);
        new Thread(fu).start();
        try {
            Integer sum = fu.get();
            System.out.println(sum);
            System.out.println("****");
        }catch (Exception e){
            e.printStackTrace();
        }

    }
}

(四)线程池

项目中常常使用
在这里插入图片描述

拓展——使用lamabd表达式便捷创建线程——

public class Lambda {
	    public static void main(String[] args) {
	        //匿名内部类创建多线程
	        new Thread(){
	            @Override
	            public void run() {
	                System.out.println(Thread.currentThread().getName()+"创建新线程1");
	            }
	        }.start();
	
	        
	        //使用Lambda表达式,实现多线程
	        new Thread(()->{
	            System.out.println(Thread.currentThread().getName()+"创建新线程2");
	        }).start();
	
	
	        //优化Lambda
	        new Thread(()-> System.out.println(Thread.currentThread().getName()+"创建新线程3")).start();
	    }
	
	}

结果:

Thread-0创建新线程1
Thread-1创建新线程2
Thread-2创建新线程3

lamabd表达式创建的底层原理——

Lambda 表达式 (参数列表) -> { 方法体 } 实际上是一个函数式接口的实例。函数式接口是只有一个抽象方法的接口。在 Java 中,Runnable 接口就是一个函数式接口,它只有一个抽象方法 run(),因此 Lambda 表达式 (参数列表) -> { 方法体 } 可以被赋值给 Runnable 接口的对象。

当你使用 Lambda 表达式 (参数列表) -> { System.out.println("t1"); } 作为 Thread 构造方法的参数时,编译器会将这个 Lambda 表达式转换为一个匿名内部类的实例,该匿名内部类会实现 Runnable 接口的 run() 方法,并在其中定义了方法体 System.out.println("t1"); }
因此,Lambda 表达式实际上就是一个 Runnable 接口的实现,它的方法体就是 run() 方法的具体实现。

(五)四种方法对比——

(1)runnable和callable 有什么区别?
在这里插入图片描述

(2)在启动线程的时候,可以使用run方法吗?
run()和 start()有什么区别?
在这里插入图片描述

(六)

总之记住了三点就好——
①Thread类相当于虚拟CPU
②run()方法相当于线程体

③实例对象作为驱动CPU运行的数据。

而线程由虚拟CPU和线程体组成。故二者缺一不可。现实中使用方案一较多。

三、线程的调度与基本控制

线程的调度

虽然概念上多个线程可以并发执行,但是目前计算机多数是单个CPU的,所以一个时刻只能运行一个线程。(因为切换速度快,常常认为是并发运行的),在单个CPU上以某种顺序运行多个线程称为线程的调度。

基本控制

Thread类里面的两个重要方法:sleep()、join()

t.sleep()=>把CPU让给优先级比t低的线程。自己先休眠。

t.join()=>使当前的线程等待直到线程t结束为止,该线程才恢复到Runnable状态。

四、线程同步

(一)问题引出

在多线程的程序中,当多个线程并发执行时,虽然各个线程的代码执行顺序是确定的,但是线程的相对执行顺序是不确定的。
在有些情况下如多线程对共享数据操作时,这种线程运行顺序的不确定性将会产生执行结果的不确定性,使共享数据的一致性被破坏,因此在某些应用程序中必须对线程的并发操作进行控制。
解决方案是以下的对象锁。

(二) 对象锁

临界区

一个程序的的各个并发线程中对同一个对象访问的代码段,称为临界区。
在Java中,一个临界区可以是一个语句块或一个方法,并用synchronized关键字标识。临界区的控制是通过对象锁进行的。
Java将每个由synchronized(someObjects){ }语句指定的对象someObjects设置一个锁,称为对象锁。对象锁是一种独占的排它锁,含义是——当一个线程获得了一个对象的锁后,便拥有该对象的操作权,其他任何线程不能该对象进行任何操作。
线程在要进入临界区时,首先通过synchronized(someObjects){ }语句测试并获得对象的锁,只有获得对象锁后才能继续执行临界区的代码,否则将进入等待状态。

例如:

使用对象锁注意事项:

(1)关于对象锁的返还。
对象的锁在如下几种情况下由持有线程返还。

• 当 synchronized()语句块执行完后。
• 当在synchronized()语句块中出现异常(Exception)。
•当持有锁的线程调用该对象的wait()方法。此时该线程将释放对象的锁,而被放人对象的 wait pool 中,等待某种事件的发生。

(2)共享数据的所有访问都必须作为临界区,使用synchronized 进行加锁控制。
对共享数据所有访问的代码,都应该作为临界区使用 synchronized 进行标识。
这样保证所有的操作都能够通过对象锁的机制进行控制。如果有一种访问操作未标记为synchronized,则这种操作将绕过对象锁,很可能破坏共享数据的一致性。

(3)用 synchronized保护的共享数据必须是私有的。
将共享数据定义为私有的,使线程不能直接访问这些数据,必须通过对象的方法。而对象的方法中带有由synchronized 标记的临界区,实现对并发操作多个线程的控制。
(4) 如果一个方法的整个方法体都包含在synchronized 语句块中,则可以把该关键字放在方法的声明中。
如在例10-5(c)的程序中,Push()方法也可定义为:
public synchronized void push( char c) l
data[ idx] = c;
data [ idx]
= с;
idx ++ ;
这种方式程序的可读性好,便于理解,因此比较常用。但控制对象锁的时间稍长,因此并发执行的效率会受到一定的影响,但影响不是很大。

(5) Java 中对象锁具有可重入性。
Java 运行系统中,一个线程在持有某个对象的锁的情况下,可以再次请求并获得该对象的锁,这就是对象锁具有可重入性的含义,锁的可重人性是很重要的,因为这可以避免单个线程因自己已经持有的锁而产生死锁。

(三)死锁的防治

死锁——

如果程序中多个线程互相等待对方持有的锁,而在得到对方锁之前都不会释放自己的锁,由此导致这些线程不能继续运行,这就是死锁。(循环之感)
Java 中没有检测与避免死锁的专门机制。因此完全由程序进行控制,防止死锁的发生。
应用程序可以采用的一般做法是:如果程序要访问多个共享数据,则要首先从全局考虑定义个获得锁的顺序,并且在整个程序中都遵守这个顺序。释放锁时,要按加锁的反序释放。

五、线程间的交互

线程间的交互 wait()和 notify()

有时,当某个线程进入 synchronized 块后,共享数据的状态并不满足它的需要,它要等待其他线程将共享数据改变为它需要的状态后才能继续执行。
但由于此时它占有了该对象的锁,其他线程无法对共享数据进行操作。(为此Java 引人wait()和 notity()。这两个方法是Java.lang. Object类的方法,是实现线程通信的两个方法

wait()
如果线程调用了某个对象X的wait()方法X.wait(),则该线程将放人X的wait pool,并且该线程将释放×的锁。

notify()
当线程调用X的notify()方法使对象X的wait pool 中的一个线程移入lock pool,在lock pool
中等待X的锁,一旦获得便可运行。

notifyAll()
当线程调用X的notifyAll()方法把对象 wait pool中的所有线程都移人 lock pool。

因此用 wait(和 notify()可以实现线程的同步。

当某线程需要在 synchronized 块中等待共享数据状态改变时,可以调用wait()方法,这样该线程等待并暂时释放共享数据对象的锁,其他线程可以获得该对象的锁并进入 synchronized 块对共享数据进行操作。
当其操作完后,只要调用notify()方法就可以通知正在等待的线程重新占有锁并运行。

不建议使用的方法stop()

stop ()
stop()强行终止线程的运行,容易造成数据的不一致。如在堆栈的例子中,一个线程在压入值但末修改指针时被调用stop()方法终止,就将造成堆栈数据不一致。建议使用标志flag 终止其他线程。

六、线程状态与生命周期

六种线程状态

线程创建后,就开始了它的生命周期。在不同的生命周期阶段线程有不同的状态。对线程调用各种控制方法,就使线程从一种状态转换为另一种状态。
线程的生命周期主要分为如下6个状态:——
NEW(新建状态)、
RUNNABLE.(可运行状态)、
BLOCKED(线程阻塞等待监视器锁的线程状态)、
WAITING(等待线程的线程状态)、
TIMED WAITING(具有指定等待时间的等待线程的线程状态)、
TERMINATED(已终止线程的线程状态。线程已完成执行)

(线程的状态可以参考JDK中的Thread类中的枚举State)

线程状态相互转化

在这里插入图片描述
(记住上图便可记住所有状态与转化。————横着3态,竖着3态

解释上文——

在这里插入图片描述

七、线程中的其他常见问题

新建 T1、T2、T3 三个线程,如何保证它们按顺序执行?

可以使用线程中的join方法解决
t. join(),阻塞调用此方法的线程进入timed_waiting直到线程t执行完成后,此线程再继续执行
在这里插入图片描述
注意上文使用的是lamabd表达式,详见点击这里。

notify()和 notifyALL()有什么区别?

notifyAll():唤醒所有wait的线程
notify():只随机唤醒一个 wait 线程

java中wait和sleep方法的异同?

共同点

wait()、wait(long)和sleep(long)的效果都是让当前线程暂时放弃CPU的使用权,进入阻塞状态(BLOCKED)

不同点

1.方法归属不同

sleep(long)是Thread的静态方法
而wait(),wait(long)都是0bject的成员方法,每个对象都有

2.醒来时机不同

执行sleep(long)和wait(long)的线程都会在等待相应毫秒后醒来
wait(long)和 wait()还可以被 notify 唤醒,wait()如果不唤醒就一直等下去
它们都可以被打断唤醒

3.锁特性不同(重点)

wait 方法的调用必须先获取wait对象的锁,而sleep 则无此限制
wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃cpu,但你们还可以用)
而sleep如果在synchronized 代码块中执行,并不会释放对象锁(我放弃cpu,你们也用不了)

如何停止一个正在运行的线程(4种)?

有四种方式可以停止线程

(0)原始方法

——程序运行完就自然结束。

(1)使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。

package com.mozq.thread.interrupt;
/**
 *     结束线程方法1:使用结束标志
 * @author jie
 *
 */
class StopThread implements Runnable{
    private boolean exit = false;
    public void setExit(boolean exit) {
        this.exit = exit;
    }
    
    @Override
    public synchronized void run() {
        while(!exit) {
            System.out.println(Thread.currentThread().getName() + "run...");
        }
        System.out.println(Thread.currentThread().getName() + "结束了...");
    }
}
public class MyInterrupt2 {
    public static void main(String[] args) {
        StopThread stopThread = new StopThread();
        Thread t = new Thread(stopThread);
        t.start();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //业务逻辑。。。
        //此处想结束线程
        stopThread.setExit(true);
    }
}

(2)使用stop方法强行终止(不推荐,方法已作废)

(3)使用interrupt方法中断线程

打断阻塞的线程(sleep,wait,join)的线程,线程会抛出InterruptedException异常
打断正常的线程,可以根据打断状态来标记是否退出线程

八 、线程中并发安全问题

1、synchronized关键字的底层原理

synchronized关键字的底层原理——基础篇

一、synchronized为对象上锁

先来看一下实际中的线程安全问题——
抢票实现——
在这里插入图片描述

未上锁——导致线程竞争,超卖。
在这里插入图片描述

synchronized为对象上锁后——线程井然有序
在这里插入图片描述

二、synchronized底层分析——monitor

汇编分析——
javap-v xx.class 查看class字节码信息
在这里插入图片描述

深入monitor——

Monitor 被翻译为锁监视器是由jvm提供,c++语言实现
当一个线程进入synchronized代码块之后,会经历以下3步——
在这里插入图片描述
详解monitor三个属性——

  1. Owner:存储当前获取锁的线程的,只能有一个线程可以获取
  2. EntryList:关联没有抢到锁的线程,处于Blocked状态的线程
  3. WaitSet:关联调用了wait方法的线程,处于Waiting状态的线程

在这里插入图片描述

monitor总结——

当一个线程进入对象的synchronized代码块后,会让对象锁与monitor关联,检查monitor中的属性Owner是否为NULL,如果是,就让该线程持有对象锁,如果不为,则进入EntryLIst中,进入阻塞状态(BLOCKED),等待其他线程释放锁。如果线程调用了wait()方法,则进入waitSet中,进入等待状态(WAITING)等待被唤醒

总结synchronized基础篇

在这里插入图片描述

synchronized关键字的底层原理——进阶篇

锁升级(重量锁——>偏向锁/轻量级锁)

monitor是JVM提供的,而JVM属于系统级别的(也就是内核态的)。C++实现的而Java对象要使用moniter就需要完成内核态与用户态的转化。(内核态与用户态详情介绍请点击这里)同时进程的上下文切换,成本较高,性能低。

在这里插入图片描述

二、Java对象的内存结构

Java对象都是在堆中创建的

hotspot将对象分为3部分——对象头、实例数据、对齐填充

其中对象头中的Markword详细描述了对象是如何关联到monitor监视器的。
在这里插入图片描述

深入MarkWord

MarkWord对象头占4字节32位,里面各种状态——
对象如何关联到monitor的——
该对象对象头的Mark Word 中记录了指向 Monitor 对象的指针

在这里插入图片描述

再次回到monitor中,每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针
在这里插入图片描述
在这里插入图片描述

轻量级锁及流程——

在这里插入图片描述

假如此时来临了一个线程——
要去执行method1()方法,则该线程执行时就会创建一个锁记录——Local Record每个线程的栈帧都包含了一个锁记录的结构,内部就可以存储锁定对象的MarkWord。
在这里插入图片描述

交换成功——
在这里插入图片描述

如果CAS失败,交换失败,则(两种情况)——

情况一:多线程竞争导致交换失败

==>直接升级为重量级锁。一旦发生竞争就会升级为重量级锁。

情况二:锁重入导致失败

==>此时会在栈帧上再加一层Lock Record记录重入的锁
在这里插入图片描述
执行完了之后,会释放method2的锁,直接删除最上面的Lock Record即可
在这里插入图片描述

再次释放method1的锁(注意交换回数据)
在这里插入图片描述

偏向锁

引出——

注意上文说的轻量级锁在重入时,每重入一次就会执行CAS交换一次。性能还是不算太好。此时可以用偏向锁再次提升——
偏向锁只会第一次持有锁时CAS连接,并记录线程ID重入时不会再CAS,只会判断锁的线程ID是否只自己的就行

在这里插入图片描述

总结——

在这里插入图片描述
强调一下——
一旦那发生竞争就升级为重量锁(monitor)。

2、JMM

定义——

JMM(Java Memory Model)——Java内存模型,定义了共享内存中多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性。

内存分为两块——共享内存、工作内存

使用————

在这里插入图片描述

总结——

在这里插入图片描述

3、CAS 你知道吗?

介绍与使用CAS

在这里插入图片描述
在这里插入图片描述

自旋锁优劣——

  1. 因为没有加锁,所以线程不会陷入阻塞,效率较高
  2. 如果竞争激烈,重试频繁发生,效率会受影响

CAS 底层——

依赖于一个 Unsafe 类来直接调用操作系统底层的 CAS 指令
在这里插入图片描述

总结——

乐观锁与悲观锁——

在这里插入图片描述

在这里插入图片描述

4、请谈谈你对 volatile 的理解

在这里插入图片描述

(1)volatile 性质一——保证线程间的可见性

在这里插入图片描述

运行上述代码,结果——
线程t1、t2可以运行,但是线程3还是出错
在这里插入图片描述
原因分析——
在这里插入图片描述

解决方案一——添加VM参数

在这里插入图片描述

解决方案二——使用volatile修饰stop

在这里插入图片描述
两者都能使线程t3运行——
则说明线程1的修改在线程3中生效了。
即volatile能够让一个线程对共享变量的修改对另一个线程可见
在这里插入图片描述

(2)volatile 性质二——禁止进行指令重排序

在这里插入图片描述

注意——指令在cpu读取,顺序不固定的,计组里面的知识

尤其是在高并发的情况下,情况4发生得更加多。为了阻止其发生,可用接下来的volatile的功能。——

解决方案——volatile修饰变量,禁止指令重排序

指令重排:用[volatile 修饰共享变量会在读、写共享变量时加入不同的屏障,
阻止其他读写操作越过屏障,从而达到阻正重排序的效果

在这里插入图片描述

注意,变量修饰不能更改——
也就是volatile加在X上不行
在这里插入图片描述

总结volatile

在这里插入图片描述

5、什么是AQS?——抽象队列同步器AbstractQueuedSynchronizer

在这里插入图片描述

AQS基本工作方式——

在这里插入图片描述

(1)AQS-多个线程共同去抢这个资源是如何保证原子性的呢?

在这里插入图片描述

(2)AQS是公平锁吗,还是非公平锁?–>都可以实现

在这里插入图片描述

而在排队的锁就是公平锁

公平锁、非公平锁——

公平锁则体现在按照先后顺序获取锁,非公平体现在不在排队的线程也可以抢锁
在这里插入图片描述

总结AQS——

在这里插入图片描述

6、ReentrantLock的实现原理

相比于synchronized锁,ReentrantLock的特点——

ReentrantLock

使用ReentrantLock——

在这里插入图片描述

ReentrantLock的实现原理

在这里插入图片描述

在这里插入图片描述

总结——ReentrantLock

ReentrantLock

7、synchronized和Lock有什么区别 ?

总结——synchronized和Lock的区别

语法层面

功能层面

性能层面

在这里插入图片描述

在这里插入图片描述

8、死锁产生的条件是什么?

九、线程池

十、ThreadLocal

(一)ThreadLocal概述——

在这里插入图片描述

(二)ThreadLocal基本使用——

在这里插入图片描述

ThreadLocal的代码演示——

在这里插入图片描述

在这里插入图片描述

set方法——

在这里插入图片描述

get/remove方法——

在这里插入图片描述

(三)ThreadLocal的内存泄露问题——

1、Java对象中的四种引用类型——

在这里插入图片描述

2、泄漏原因——key是弱引用,值为强引用

在这里插入图片描述

(四)总结ThreadLocal——

在这里插入图片描述

  • 6
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值