多线程一 入门序言

简介: 多线程一 入门序言

一 、进程&线程&基本的线程机制#


  • 进程是运行在自己的地址空间内的自包容程序(一个程序至少包含一个进程,而一个进程至少包含一条线程),多任务操作系统周期性的将CPU在进程之间切换,来实现同时运行多个程序,尽管进程的运行歇歇停停,但是CPU的运算速度太快了,以至于给人一种进程一直运行而没有停的假象.


  • windows系统中的一个 .exe 的程序,实际上就是一个进程


  • 线程就是进程中的一个单一的顺序执行流,因此单个进程可以拥有多个线程并发执行任务(底层的实现机制是切分CPU的时间片段),CPU给每个任务轮流的分配其执行的时间,以至于每个任务都觉得自己一直占用CPU.


  • 线程一个理解为进程中的一个子任务


QQ就是一个进程,和好友视频聊天可以理解成一个线程

当然,如果程序确实运行在多核的机器上,那么有可能真的是在同时运行


  • 好处: 可以使我们从单个线程这个层次抽身出来,而多任务和多线程也是使用多处理器系统的最合适的方式


尽管JAVASE5在并发中做出了显著的改进,但是仍然没有编译器验证和检查型异常(?)


二、 线程带来的风险#


2.1活跃性问题:#


  1. 死锁


哲学家问题,当哲学家们都不肯把手里的筷子借给其他人,最后的结果就是全部饿死

  1. 饥饿


排队打饭,假设所有人都来这一个窗口排队打饭,打完饭也不走,可能就会导致比较瘦弱的女生吃不上饭而饥饿(反应在线程的优先级问题上)


  • 高优先级吞噬所有低优先级的时间片
  • 设置线程的优先级 setPrioriry(int newPriority)
  • 线程被永久的堵塞在进入同步块的状态
  • 等待的线程永远不会被唤醒


  1. 活锁


独木桥问题,相互谦让,导致最后谁都过不去


2.2安全性问题#


  • 原子性:访问互斥,同一时刻只允许一个线程对它进行操作为线程安全
  • 可见性:一条线程对主内存的修改可以及时的被其他线程看到
  • 有序性:一个线程观察其他线程中指令的执行顺序,由于指令重排序的存在,一般它们看到的结果都是杂乱无序的


非线程安全&线程安全#


  • 多个线程对同一个实例对象中的实例变量进行并发访问,产生的后果就是脏读(读取到了被更改的数据)--存在非线程安全问题
  • 获取到的对象的实例是经过同步处理的,不会出现脏读的现象--线程安全
  • 说到线程的安全性问题,和重排序和happens-before法则是紧密相关的


2.3性能问题#


多线程速度一定会快吗?


关于性能,是具有多面性的,多线程不一定快,单核的处理器也可以实现多线程,就像烤烧饼,CPU分配给各个线程的时间片很短,但是来回的切换是有成本的但是并发通常是提高运行在单核处理器上的程序的性能,表面上看CPU在多个线程上进行切换很浪费时间,但是阻塞是这个问题变得不同,大多数情况下是因为IO或者进行过一项很复杂的计算,如果没有并发,整个程序都将会停止下来


什么是重排序?#


1:重排序的定义#


重排序就是编译器,处理器,在不改变程序执行结果的前提下,重新排序指令的执行顺序,以达到最佳的运行效果


2:分类#


  • 编译器重排序
  • 处理器重排序


3: 什么是数据依赖#


数据依赖指的是,某些指令存在某种先后关系,比如相邻的两行执行都访问通同一个变量,并且其中一个指令执行了写操作,那么,这两行指令就存在数据依赖,换言之,执行的顺序不能改变,否者得出错误的结果

因此,编译器和处理器仅仅对没有数据依赖的指令进行重排序

指令 实例
读后写 a=b ; b=1;
写后读 a=1; b=a;
写后写 a=1; a=2;


4: 什么是as-if-serial?#


在单线程的开发中程序员不需要知道指令是如何进行重排序的,只是简单认为程序是按顺序执行就行,故 意为:貌似是串行的


5: 多线程的中重排序问题#


举个例子,假设多个线程并发访问下面两个方法


boolean flag;
 private int a;
 public void read(){
     a=1;
     flag=true;
 }
 public void write(){
     if(flag){
        int b = a+1;
        System.out.println("b=="+b);
    }
 }


上面的代码,a=1,flag=ture;显然没有依赖关系,因此可能会被重排序成flag=true; a=1;这时候就会出现问题,当执行到fl,ag=ture,cpu的执行权被另一个线程抢去执行write(),write()里面的输出语句输出的不再是2,而是其他意想不到的值


6: 多线程中重排序问题的解决方法#


  • 同步,给上面那两个方法加上锁,同一时刻,只允许一个持有该锁线程去访问同步方法,等它执行完释放锁后,其他线程才能去访问


什么是happens-before?#


1: 定义:#


  • happens-before 用来指定两个操作之间的执行顺序,提供跨线程的内存可见性
  • 在java的内存模型中,如果一个操作的执行结果需要对另一个操作可见,那么这两个操作之间必然存在happens-before的关系


规则#


  • 程序顺序规则
  • 单个线程中的每个操作,总是前一个操作happens-before于该线程的任意后续操作


int a=1; // 1
    int b =2; //2
    int c =3;   //3


在上面代码中 1happens-before 2 3


  • 监视器规则
  • 对于同一个锁的解锁,总是 happens-before于 随后对这个锁的加锁


private ReentrantLock lock = new ReentrantLock();
    public void read(){
        lock.lock();
        //do something ...
        lock.unlock();  //1 解锁
    }
    public void write(){
        lock.lock();  // 加锁
        //do something ...
        lock.unlock();


如上, 1的解锁,后跟着2的加锁

  • volatile变量规则
  • 对一个volatile域的写,happens-before于任意后续对这个变量的读
  • 传递性
  • A happens-before B ; B happens-before C; 那么 A happens-before C
  • Start 规则(线程启动规则)
  • 如果ThreadA里面 启动了ThreadB,那么ThreadB.start() happens-before于线程B中的任意操作
  • Join 规则(线程终止规则)
  • 和Start相反,线程中的所有操作,都优先发生于 对线程的终止检验, Thread.jion(); Thread.isAlive()
  • 线程的中断规则:
  • interrupt()优先发生于 被中断线程的代码,检测到中断事件的发生
  • 对象终结规则
  • 一个对象的初始化完成,先行于它的finalize()方法


happens-before 和 重排序的区别与联系#


两个操作具有happens-before关系,并不意味着前一个操作一定要在后一个操作之

前执行,(假如两个操作没有数据依赖那么可能会被编译器处理器进行指令的重排序),他只是要求前一个操作的执行结果对后一个操作是可见的(也就是前一个操作不一定先开始,但是它一定要比后一个操作先结束)让其他线程看到结果,前一个操作肯定要把执行的结果,从他自己的缓存中刷回到内存


三. 什么是锁#


  • 锁是工具,是作为并发共享数据,保证数据一致性的工具


前言:锁的内存语义#


  • 锁的获取与释放建立的happens-before关系


锁存在于对象的哪里?#


存在于对象头中

对象头中的信息

Mark Word:存储对象的hashset值,锁信息

Class MetaData Address:存储对象所属类的位置

Array Length : 数组对象特有的标记数组的长度


1 内置锁#


java中每一个对象都可被当作同步的锁,这些锁就叫做内置锁,例如 Synchronized修饰方法,获取到this对象锁,修饰静态方法,获取到Class类锁,同步代码块里面可以设置this对象锁,非this对象锁等等


2 互斥锁(排它锁)和共享锁#


前者指该锁一次性只能被一条线程占有,后者表示该锁一次性可以被多条线程占有synchronized和ReentarntLock都是互斥的,而ReentrantReadWriteLock中的读锁,是共享的,读读共享


3 偏向锁/轻量级锁/重量级锁#


这三种锁是指锁的状态,并且是针对synchronized,java5通过引入锁的升级来实现高效的synchronized,这三种锁的状态,是通过对象监视器在对象头中的字段来区分

  • 偏向锁是指一段同步代码块一直被一条线程锁访问,那么以后该线程就会自动的获取锁,来降低获取锁的代价
  • 轻量级锁是指,在偏向锁的基础上,出现其他线程来访问此代码,偏向锁升级为轻量级锁,其他线程通过自旋,尝试获取锁


运行流程:


当前线程获取到锁,修改锁对象头里面的Mark Word里面的锁标志位,然后去执行同步代码体,这时候,其他的线程也对象头信息复制到虚拟机栈,企图去更改锁标志位,但是上一个线程没有释放,他就不停的尝试去修改,直到对象锁被释放了为止

  • 当锁是轻量级锁的时候,其他线程来访问代码,会自旋,但是当它自旋到一定次数之后还是没有获取到锁,就阻塞.轻量级锁也就转换成重量级锁


4. 重入锁#


可重入锁,有叫递归锁,就是说某一个线程运气比较好,拿到锁之后,在释放之前,再次拿到了这个锁,而且不会被锁阻塞


  • ReentrantLock就是一把可重入的锁


/*
* 经典的验证 锁的重复问题 ,在单一的线程下, a()  想在  未释放锁  的前提下  调用b(),前提就是可冲入锁
* */
public void a() {
    lock.lock();
    System.out.println("a");
    b();
    lock.unlock();
}
public void b() {
    lock.lock();
    System.out.println("b");
    lock.unlock();
}


实例二: synchronized也是一把重入锁


public class safe{
synchronized public void method01(){
    ....
    method02();
}
synchronized public void method02(){
    ....
}
}


  • method01() 和 method02 () 都是线程安全的,假如当前线程拿到对象锁后,在执行method01()时,碰到了method(),他可以重复拿到锁,而不会被阻塞!
  • 容易和Synchronized方法,或者代码块的特性混淆:两条线程分别竞争执行method01()和method02() ,无论哪条线程正在执行 Synchronized方法也好,同步代码块也好,另一条线程都不能执行其他任意Synchronized方法或者代码块
  • 其中Synchronized 和 locked 都是可重入锁!
  • 如过多个线程使用多个锁对象,一定是一部执行,锁不住线程!


5. 自旋锁#


  • 所谓自旋锁,实际上就是在空转cup的时间片,while(true) 抢到cup的执行权,却不做任何事,while(true){},等着其他线程把cup的执行权抢走!


6. 死锁#


  • 当一个线程永远持有一把锁还不释放,其他线程一直在等待...


public class siSuo {
Object a = new Object();
Object b = new Object();
public void method01(){
    synchronized (a){
        try{
            Thread.sleep(1000);
        }catch(InterruptedException e){
            e.printStackTrace();
        }
        synchronized(b){
            System.out.println("method1执行了");
        }
    }
}
public void method02(){
    synchronized (b){
        synchronized(a){
            System.out.println("method2执行了");
        }
    }
}
public static void main(String[] args) {
    siSuo siSuo = new siSuo();
    new Thread(()->{
        siSuo.method01();
    }).start();
    new Thread(()->{
        siSuo.method02();
    }).start();
}
}


7. 公平锁#


Lock锁分为公平锁和非公平锁,所谓公平锁,就是表示线程获取锁的顺序,是按照线程加锁的顺序来实现的,也就是FIFO的顺序先进先出的顺序,而非公平锁描述的则是一种锁的随机抢占机制,还可能会导致一些线程根本抢不着锁而被饿死,结果就是不公平了


8. 乐观锁和悲观锁#


  • 就像生活中乐观的人,什么事都往好处想,因此它每次拿到数据之后呢,都认为别人不会来修改它的值,也就是读写不互斥,但是为了安全,他需要在更新数据之前判断一下,有没有人修改过,java.util.Concurrent.atomic包下的原子类,就是乐观锁的实现方法cas完成的
  • 就像生活中悲观的人,什么事都往坏处想,因此它在每次拿数据的时候,都会加上锁阻塞住其他的线程,因为它总是想其他线程肯定回来修改它拿到的数据,传统的关系型数据库中就大量的使用了悲观锁,如行锁,表锁,读锁,写锁等等,Synchronized 和 ReentrantLock都是悲观锁思想的实现


9. 分段锁#


分段式锁是一种设计理念分段锁的设计目的就是细化锁的颗粒度,当操作不需要操作整个数组的时候,仅仅获取它想要的那一段数据的锁就可以,而其他线程仍然可以获取其他段的锁的数据的内容


  • ConcurrentHashMap就是通过分段式锁来实现的高效并发Map集合,它的分段式锁叫segment(分片),每一段内部有一个Entry数组,数组中的每一个元素即是一个链表,也是一个ReentrantLock,因此当我们往里面put的时候,只需要获取到此段的锁就可以,实现了并行put,同时gut()无锁
相关文章
|
11天前
|
安全 数据处理 开发者
Python中的多线程编程:从入门到精通
本文将深入探讨Python中的多线程编程,包括其基本原理、应用场景、实现方法以及常见问题和解决方案。通过本文的学习,读者将对Python多线程编程有一个全面的认识,能够在实际项目中灵活运用。
|
1天前
|
安全 Java 调度
Java中的多线程编程入门
【10月更文挑战第29天】在Java的世界中,多线程就像是一场精心编排的交响乐。每个线程都是乐团中的一个乐手,他们各自演奏着自己的部分,却又和谐地共同完成整场演出。本文将带你走进Java多线程的世界,让你从零基础到能够编写基本的多线程程序。
8 1
|
30天前
|
算法 NoSQL Java
Springboot3新特性:GraalVM Native Image Support和虚拟线程(从入门到精通)
这篇文章介绍了Spring Boot 3中GraalVM Native Image Support的新特性,提供了将Spring Boot Web项目转换为可执行文件的步骤,并探讨了虚拟线程在Spring Boot中的使用,包括如何配置和启动虚拟线程支持。
63 9
Springboot3新特性:GraalVM Native Image Support和虚拟线程(从入门到精通)
|
8天前
|
Java 数据处理 开发者
Java多线程编程的艺术:从入门到精通####
【10月更文挑战第21天】 本文将深入探讨Java多线程编程的核心概念,通过生动实例和实用技巧,引导读者从基础认知迈向高效并发编程的殿堂。我们将一起揭开线程管理的神秘面纱,掌握同步机制的精髓,并学习如何在实际项目中灵活运用这些知识,以提升应用性能与响应速度。 ####
30 3
|
9天前
|
Java
Java中的多线程编程:从入门到精通
本文将带你深入了解Java中的多线程编程。我们将从基础概念开始,逐步深入探讨线程的创建、启动、同步和通信等关键知识点。通过阅读本文,你将能够掌握Java多线程编程的基本技能,为进一步学习和应用打下坚实的基础。
|
2月前
|
安全 数据库连接 API
C#一分钟浅谈:多线程编程入门
在现代软件开发中,多线程编程对于提升程序响应性和执行效率至关重要。本文从基础概念入手,详细探讨了C#中的多线程技术,包括线程创建、管理及常见问题的解决策略,如线程安全、死锁和资源泄露等,并通过具体示例帮助读者理解和应用这些技巧,适合初学者快速掌握C#多线程编程。
75 0
|
3月前
|
机器学习/深度学习 Java TensorFlow
深度学习中的图像识别:从理论到实践Java中的多线程编程入门指南
【8月更文挑战第29天】本文将深入探讨深度学习在图像识别领域的应用,从基础理论到实际应用案例,带领读者一步步理解如何利用深度学习技术进行图像识别。我们将通过一个简单的代码示例,展示如何使用Python和TensorFlow库实现一个基本的图像识别模型。无论你是初学者还是有一定经验的开发者,都能从中获得启发和学习。 【8月更文挑战第29天】在Java世界里,线程是程序执行的最小单元,而多线程则是提高程序效率和响应性的关键武器。本文将深入浅出地引导你理解Java多线程的核心概念、创建方法以及同步机制,帮助你解锁并发编程的大门。
|
5月前
|
Java 数据处理 调度
Java多线程编程入门指南
Java多线程编程入门指南
|
5月前
|
Java 开发者
告别单线程时代!Java 多线程入门:选继承 Thread 还是 Runnable?
【6月更文挑战第19天】在Java中,面对多任务需求时,开发者可以选择继承`Thread`或实现`Runnable`接口来创建线程。`Thread`继承直接但限制了单继承,而`Runnable`接口提供多实现的灵活性和资源共享。多线程能提升CPU利用率,适用于并发处理和提高响应速度,如在网络服务器中并发处理请求,增强程序性能。不论是选择哪种方式,都是迈向高效编程的重要一步。
41 2
|
5月前
|
监控 程序员 调度
协程实现单线程并发(入门)
协程实现单线程并发(入门)
57 1