java琐事

本文详细介绍了Java并发编程,从并发的意义、进程与线程的区别、进程调度算法到Java中的并发实现,包括线程状态、Executor服务、线程安全和锁优化。还探讨了Java.util.concurrent包中的并发工具,如ConcurrentHashMap、CountDownLatch和CyclicBarrier。通过对各种同步机制的讨论,展示了Java如何处理多线程环境中的数据访问和协作问题。
摘要由CSDN通过智能技术生成

并发编程

并发的意义

并发通常是提高运行在单处理器上的程序的性能。如果程序中的某个任务因为该程序控制范围之外的某些条件(I/O)而导致不能继续执行,那么这个任务或线程就阻塞了。如果没有并发,整个程序都讲停下来。从性能的角度来看,单处理器上使用并发没有任何好处。

进程与线程

实现并发有两种方式,多进程与多线程,最直接的方式是使用操作系统级别的进程。进程是运行在它自己地址空间内的自包容的程序。多任务操作系统可以周期性地将CPU从一个进程切换到另一个进程,来实现多进程。操作系统会将多进程隔离开,保证互相不会干涉。但进程通常会有数量和开销的限制,避免它们在不同的并发系统之间的可应用性。Java使用的多线程并发会共享内存和I/O这样的资源,编写多线程程序最基本的困难在于协调不同线程之间的资源使用,保证资源不会被同时访问。函数式编程语言做到了每个函数的调用都不能干涉其他函数,可以当做独立的任务来驱动。如果程序中的某个部分必须大量使用并发,可以考虑使用函数式编程实现。

  1. 进程

进程是资源分配的基本单位

进程有独立的空间地址,一个进程崩溃后,不会对其他进程造成影响,例如最常见的chrome浏览器的多标签也就是用的多进程,防止chrome卡死。

进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对 PCB 的操作

  1. 线程

线程是基本调度的单位

一个进程可以有多个线程,共享进程资源
线程是进程中程序执行的不同路径。线程有自己的堆栈空间和局部变量,单线程之间没有单独的地址空间。一个线程挂了会导致整个进程挂掉。

  1. 区别
  • 资源

    进程是资源分配的基本单位,一个进行可以有多个线程,线程可以访问隶属进程的资源。进程间不会相互影响,
    多进程程序比较强健,鲁棒性高。线程间会相互影响,一个线程的崩溃会导致整个线程的崩溃。

  • 调度

    线程是调度的基本单位,线程切换开销小,进程切换开销大

  • 系统开销
    由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,
    所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,
    涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,
    而线程切换时只需保存和设置少量寄存器内容,开销很小。

  • 通信
    线程间可以通过直接读写同一进程中的数据(资源共享)进行通信,但是进程通信需要借助IPC。

进程调度算法

  1. 批处理系统

先来先服务 first-come first-serverd(FCFS)、短作业优先 shortest job first(SJF)、最短剩余时间优先 shortest remaining time next(SRTN)
2. 交互式系统
时间片轮转、优先级调度、多级反馈队列
3. 实时系统
抢占式的优先数高者优先

进程同步

进程同步是一个操作系统级别的概念,是在多道程序的环境下,存在着不同的制约关系,为了协调这种互相制约的关系,实现资源共享和进程协作,从而避免进程之间的冲突,引入了进程同步。

临界资源

在操作系统中,进程是占有资源的最小单位(线程可以访问其所在进程内的所有资源,但线程本身并不占有资源或仅仅占有一点必须资源)。但对于某些资源来说,其在同一时间只能被一个进程所占用。这些一次只能被一个进程所占用的资源就是所谓的临界资源。典型的临界资源比如物理上的打印机,或是存在硬盘或内存中被多个进程所共享的一些变量和数据等(如果这类资源不被看成临界资源加以保护,那么很有可能造成丢数据的问题)。

对于临界资源的访问,必须是互诉进行。也就是当临界资源被占用时,另一个申请临界资源的进程会被阻塞,直到其所申请的临界资源被释放。而进程内访问临界资源的代码被成为临界区。

对于临界区的访问过程分为四个部分:

1.进入区:查看临界区是否可访问,如果可以访问,则转到步骤二,否则进程会被阻塞

2.临界区:在临界区做操作

3.退出区:清除临界区被占用的标志

4.剩余区:进程与临界区不相关部分的代码

进程同步方式
  • 临界区

    通过多线程串行化访问公共资源或一段代码,速度快,适合控制数据访问

    **优点:**保证在某一时刻只有一个线程能访问数据

    **缺点:**虽然临界区同步速度很快,但却只能用来同步本进程内的线程,而不可用来同步多个进程中的线程。

  • 信号量

    信号量(Semaphore)是一个整型变量,可以对其执行 down 和 up 操作,也就是常见的 P 和 V 操作。为控制一个具有有限数量用户资源而设计。它允许多个进程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大进程数目。

    • down : 如果信号量大于 0 ,执行 -1 操作;如果信号量等于 0,进程睡眠,等待信号量大于 0;
    • up :对信号量执行 +1 操作,唤醒睡眠的进程让其完成 down 操作。

    down 和 up 操作需要被设计成原语,不可分割,通常的做法是在执行这些操作的时候屏蔽中断。

    如果信号量的取值只能为 0 或者 1,那么就成为了 互斥量(Mutex) ,0 表示临界区已经加锁,1 表示临界区解锁。

    优点:

    适用于对Socket(套接字)程序中线程的同步。(例如,网络上的HTTP服务器要对同一时间内访问同一页面的用户数加以限制,只有不大于设定的最大用户数目的线程能够进行访问,而其他的访问企图则被挂起,只有在有用户退出对此页面的访问后才有可能进入。)

    缺点:

    ①信号量机制必须有公共内存,不能用于分布式操作系统,这是它最大的弱点;

    ②信号量机制功能强大,但使用时对信号量的操作分散, 而且难以控制,读写和维护都很困难,加重了程序员的编码负担;

    ③核心操作P-V分散在各用户程序的代码中,不易控制和管理,一旦错误,后果严重,且不易发现和纠正。

进程通信

每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间的数据交换必须通过内核,在内核中开辟出一块缓冲区域,进程1把数据拷到缓冲区,进程2在把数据从缓冲区拷走。

1. 管道(匿名管道)

管道的实质是一个内核缓冲区,进程以先进先出的方式从缓冲区存取数据,管道一端的进程顺序的将数据写入缓冲区,另一端的进程则顺序的读出数据。

  • 管道是半双工的,实现双方通信需要建立两个管道
  • 只能用于父子或者兄弟进程之间(具有血缘关系的进程)
  • 单独构成一个文件系统,管道对于管道两端的进程而言,就是一个文件,但他不是普通的文件,不属于某种系统文件系统,而是自立门户,单独构成一种文件系统,只存在于内存中。
  • 一个进程向管道写数据,另一个进程从另一端读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。
  • 管道所传送的是无格式字节流,这就要求管道通信双方约定好数据格式
2. 有名管道(FIFO)

匿名管道,由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道(FIFO)。

有名管道不同于匿名管道之处在于它提供了一个路径名与之关联,以有名管道的文件形式存在于文件系统中,这样,即使与有名管道的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过有名管道相互通信,因此,通过有名管道不相关的进程也能交换数据。值的注意的是,有名管道严格遵循先进先出(first in first out),对匿名管道及有名管道的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。有名管道的名字存在于文件系统中,内容存放在内存中。

3. 消息队列
  • 消息队列可以独立于读写进程存在,从而避免了 FIFO 中同步管道的打开和关闭时可能产生的困难;
  • 避免了 FIFO 的同步阻塞问题,不需要进程自己提供同步方法;
  • 读进程可以根据消息类型有选择地接收消息,而不像 FIFO 那样只能默认地接收。
  • 与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显示地删除一个消息队列时,该消息队列才会被真正的删除。
4.信号
  • 信号是Linux系统中用于进程间互相通信或者操作的一种机制,信号可以在任何时候发给某一进程,而无需知道该进程的状态。
  • 如果该进程当前并未处于执行状态,则该信号就有内核保存起来,直到该进程回复执行并传递给它为止。
  • 如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消是才被传递给进程。

信号是软件层次上对中断机制的一种模拟,是一种异步通信方式,,信号可以在用户空间进程和内核之间直接交互,内核可以利用信号来通知用户空间的进程发生了哪些系统事件,信号事件主要有两个来源:

  • 硬件来源:用户按键输入Ctrl+C退出、硬件异常如无效的存储访问等。
  • 软件终止:终止进程信号、其他进程调用kill函数、软件异常产生信号。
5. 信号量

信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。

(1)创建一个信号量:这要求调用者指定初始值,对于二值信号量来说,它通常是1,也可是0。

(2)等待一个信号量:该操作会测试这个信号量的值,如果小于0,就阻塞。也称为P操作。

(3)挂出一个信号量:该操作将信号量的值加1,也称为V操作。

6.共享内存(shared memory)

允许多个进程共享一个给定的存储区。因为数据不需要在进程之间复制,所以这是最快的一种 IPC。
为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间。
进程就可以直接读写这一块内存而不需要进行数据的拷贝,从而大大提高效率。
由于多个进程共享一段内存,因此需要依靠某种同步机制(如信号量)来达到进程间的同步及互斥

7. 套接字

套接字是一种通信机制,凭借这种机制,客户/服务器(即要进行通信的进程)系统的开发工作既可以在本地单机上进行,也可以跨网络进行。也就是说它可以让不在同一台计算机但通过网络连接计算机上的进程进行通信。

img

socket是应用层和传输层之间的桥梁

java并发

1. 线程的状态

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OUkWNbuW-1596248114336)(https://github.com/CyC2018/CS-Notes/raw/master/docs/notes/pics/96706658-b3f8-4f32-8eb3-dcb7fc8d5381.jpg)]

  • new(新建)

    线程已经创建但未启动

  • Runnable (可运行)

    线程正在运行或者正在等待cpu时间切片

  • Blocked (阻塞)

    等待获取排它锁中

  • Time waiting (限期等待)

    无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。阻塞和等待的区别在于,阻塞是被动的,
    它是在等待获取一个排它锁。而等待是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入。

    进入方法 退出方法
    Thread.sleep() 方法 时间结束
    设置了 Timeout 参数的 Object.wait() 方法 时间结束 / Object.notify() / Object.notifyAll()
    设置了 Timeout 参数的 Thread.join() 方法 时间结束 / 被调用的线程执行完毕
    LockSupport.parkNanos() 方法 LockSupport.unpark(Thread)
    LockSupport.parkUntil() 方法 LockSupport.unpark(Thread)
  • Waiting (无限期等待)

    等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。

    进入方法 退出方法
    没有设置 Timeout 参数的 Object.wait() 方法 Object.notify() / Object.notifyAll()
    没有设置 Timeout 参数的 Thread.join() 方法 被调用的线程执行完毕
    LockSupport.park() 方法 LockSupport.unpark(Thread)
  • Terminated (死亡)

    可以是线程结束任务之后自己结束,或者产生了异常而结束

2. 实现多线程的方式

  • 实现Runnable接口并实现run()方法

错误实例

public class MyRunnable implements Runnable {
   
    public void run() {
   
        System.out.println("Thread is running");
    }
    public static void main(String[] args) {
   
        MyRunnable instance = new MyRunnable();
        instance.run();  //这里并没有使用额外线程,仍在main线程中执行,这个类本身不会产生任何内在的线程能力,要实现线程行为,必须显示地将任务附着到线程上
	}
}

正确实例

public class MyRunnable implements Runnable {
   
    public void run() {
   
        System.out.println("Thread is running");
    }
    public static void main(String[] args) {
   
        MyRunnable instance = new MyRunnable();
        Thread thread = new Thread(instance);
        thread.start(); //调用Thread的start方法为该线程执行必须的初始化操作然后调用Runnable的run()方法
	}
}

思考:

Q1:主线程和子线程结束的顺序?

任何线程都可以启动另一个线程,这两个线程是独立的两个线程会并行执行,结束的顺序完全取决于线程本身的运行时间。通过对调用setDaemon(true)可以设置线程为后台线程,后台线程是程序运行时提供的通用服务线程并且这种线程不属于程序不可或缺的一部分,当所有非后台线程结束时,程序就已经结束了。

Q2: 执行完thread.start()后,thread对象会被垃圾回收吗?

一个线程会创建一个单独的执行线程,在对start()方法调用后,它仍会继续存在。每个Thread都注册了自己,有一个对自己的引用,在run()方法结束并死亡之前,垃圾回收器无法回收它

  • 继承Thread

同样也是需要实现 run() 方法,因为 Thread 类也实现了 Runable 接口。

当调用 start() 方法启动一个线程时,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的 run() 方法。

public class MyThread extends Thread {
   
    public void run() {
   
        System.out.println("Thread is running");
    }
    public static void main(String[] args) {
   
        MyThread mt = new MyThread();
        mt.start();
    }
}
  • 实现callable接口

与 Runnable 相比,Callable 可以有返回值(从call方法获得返回值),返回值通过 FutureTask 进行封装。


                
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值