说明:本篇文章是在阅读《Java 并发编程艺术》过程中的一些笔记和分析,由于本人能力有限,如果有书写错误的地方,欢迎各位大佬批评指正!我们互相交流,学习,共同进步!
该项目的地址:https://github.com/xiaoheng1/concurrent-programming
1.现代操作系统调度的最小单位是线程(轻量级进程)
2.为什么要使用多线程了?肯定是为了快. 目前电脑的 CPU 核心数越来越多,如果将没有数据依赖的程序拆分成多个线程,然后放到多个核心上跑,是不是
比程序在单核上跑耗时更短了?
但是值得注意的是:**一个线程在一个时刻,只能运行在一个处理器核心上**.
3.现代操作系统一般采用时分的形式调度,操作系统会分出一个一个的时间片,线程会分配到若干个时间片,当线程的时间片用完了就会发生线程调度,
并且等着下次分配.
换句话说,一个线程能分配到的时间片越多,那么就能占用更长的 CPU 资源,就能干跟多的活. 那什么决定一个线程能够分配多少时间片了?
答案是线程的优先级. 但是我在网上看到有人说,线程的优先级在更大的可能上让优先级高的线程先执行,低优先级的线程在大概率上后执行. 也有人
说线程的优先级高的获得更多的 CPU 资源(获得更多的时间片). 其实这两种说法是不矛盾的. 获得很多的时间片,也就意味着在大概率上先执行.
在 java 线程中,通过一个 priority 变量来控制优先级,优先级的范围是从 1~10. 可以在线程构造时设置,也可以通过 setPriority() 方法
设置线程优先级. 默认的线程优先级是 5.
在设置线程优先级时,针对平凡阻塞(休眠或者I/O操作)的线程需要设置较高优先级,而偏重计算(需要较多CPU时间或者偏运算)的线程则设置较低的
优先级,确保处理器不会被独占. 如果理解了?可以这么理解,I/O操作一般执行时间较短,而偏重计算型的任务一般需要大量的时间. 如果将本来就
耗时的操作还分配跟多的时间片,那么 CPU 将会被偏重计算的线程占有,其他很快能够执行完的线程一直等待(不能及时响应). 现在反过来,让耗时
短的任务先执行,那么偏重计算型的任务则不需要等待那么长的时间(能够更快的响应).
但是需要注意的是:**在不同的 JVM 以及操作系统上,有些系统会忽略对线程优先级的设定**.
4.线程的状态
关于这点可能有的小伙伴有疑问了?线程状态不是只有 5 种状态吗?为啥会有 6 种状态了?其实 5 种状态是早期进程的状态.
1.创建状态:创建进程过程非常复杂,首先需要申请一个空白的 PCB,并向线程中写入用于控制和管理进程的信息. 然后为其分配必要的资源,
最后把线程加入到就绪队列中
2.就绪状态:就绪状态就是说该线程已经准备好运行,只要给我时间片,我就能运行.
3.运行状态:指的是已经获取 CPU 的进程,目前处于执行状态.
4.阻塞状态:指的是正在执行的进程由于发生某件事,例如 I/O 操作等,暂时无法执行,这个状态称为阻塞状态.
5.终止状态:终止状态指的是线程的最终状态,要么线程执行完,要么由于无法解决的错误,被操作系统终止,或者被有权限终止的线程终止.
转换规则:
1.创建状态 -> 就绪状态
获得时间片
2.就绪状态 -> 运行状态
时间片用完
3.运行状态 <- 就绪状态
I/O操作
4.运行状态 -> 阻塞状态
I/O操作完成
5.阻塞状态 -> 就绪状态
线程的 6 大状态
1.初始状态 —— NEW(线程刚被创建,但是还没有调用 start 方法)
2.运行状态 —— RUNNABLE(Java 线程将就绪和运行两种状态统称为运行中)
3.阻塞状态 —— BLOCKED(阻塞状态,表示线程阻塞于锁)
4.等待状态 —— WAITING(表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定的动作,通知或者中断)
5.超时等待状态 —— TIME_WAITING(该状态不同于 WAITING,它可以在指定的时间自行返回)
6.终止状态 —— TERMINATED(终止状态,表示当前线程已经执行完毕)
转换规则:
调用 start 方法
1.创建状态 -> 运行状态(就绪)
获得时间片
2.运行状态(就绪) -> 运行状态(运行中)
等待进入 sync
3.运行状态 -> 阻塞
获取到锁
4.阻塞 -> 运行状态
sleep(100)/Object.wait(100)等
5.运行状态 -> 超时等待状态
notify/unpark
6.超时等待状态 -> 运行状态
wait/join/park
7.运行状态 -> 等待状态
notify/unpark
8.等待状态 -> 运行状态
参考:https://www.zhihu.com/question/56494969
注意的是:只有进入 synchronized 关键字修饰的方法或代码块时,才会是阻塞状态,其他状态都是等待状态. 这样很好理解,Java 中 Lock 的
实现在没有获取到锁的情况下,调用的是 park 方法进行阻塞,这个就是等待状态.
5.守护线程
(1)当不存在非守护线程的时候,java 虚拟机将退出.
(2)可以使用 setDaemon(true) 来设置守护线程,但是要在线程启动前设置,如果线程启动后设置会报错.
(3)Daemon 线程中的 finally 块在 Java 虚拟机退出的时候并不一定会执行. 换句话说,当不存在守护线程的时候,所有的守护线程
**立即终止**,所以,守护线程中的 finally 块可能来不及执行.
6.新线程的创建
(1)新线程是有其 parent 线程来构造的,子线程继承父线程的 daemon, 优先级,contextClassLoader 以及可继承的 ThreadLocal,同时
还会分配一个唯一的 ID 来标识此线程.
7.中断
(1)中断可以理解为线程的一个标识位属性,它标识一个运行中的线程是否被其他线程进行了中断操作. 中断线程好比其他线程对这个线程打了个招呼,
其他线程调用该线程的 interrupt() 方法对其进行中断操作.
(2)被中断线程通过检查自己是否被中断来进行响应. 可以通过 isInterrupted() 方法来进行检查.
(3)可以通过调用 Thread.isInterrupted() 来对当前线程的中断标志进行复位.
注意:**如果该线程已经处于终结状态,即使该线程被中断过,调用 isInterrupted() 方法依旧会返回 false**.
但是我们经常会看到如下写法:就是一个方法在抛出异常前,会将该线程的中断标志位清除,然后抛出异常. 例如 Thread.sleep() 方法. 为什么
要这么设计了?可以这么想,如果说只要中断了,Thread.sleep() 方法就直接抛出异常(不进行复位操作), 那么下一次其他线程在进行中断,该
方法是不是就不能响应中断了?所以,你会发现,JDK 在实现的时候,会有好多的小细节值得我们思考.
8.suspend & resume & stop
(1)suspend 挂起
(2)resume 恢复
(3)stop 终止
这三个 API 已经过期,不建议使用. 为什么了?suspend 调用后,线程不会释放已经占有的资源. 如果先执行了 resume,然后再调用 suspend,
那么将会造成线程无限挂起. 为什么弃用 stop 了?因为 stop() 方法在终结一个线程时不会保证线程的资源正常释放,通常没有给线程释放资源
的聚会,因此导致程序可能工作在不确定的状态下.
上面的方法可以通过 wait/notify 或 lock/conditon 来代替.
9.安全的终止线程
中断只是一个标志,如果程序不响应中断的话,那么你调用 interrupt() 方法是不起作用的. 中断一般用来取消任务,同时也可以使用一个标志位
来停止或者终止任务.
例如:
class A {
private static class Count implements Runnable {
private volatile boolean on = true;
private int i;
public void run(){
while(on && !Thread.currentThread().isInterrupted()){
i++;
}
System.out.println("i= " + i);
}
public void cancle(){
if(on){
on = false;
}
}
}
}
注意:**前提是程序要响应中断,如果程序中没有使用 Thread.isInterrupted 来判断,则中断将会不起作用,一定要理解中断只是一个标志**.
同时这种方式将会更加的优雅(相比较 stop 而言)
10.线程间的通信
(1) 使用 volatile
(2) 使用 synchronized
(3) wait/notify
(4) lock/condition
在使用 wait/notify 时有一些注意事项:
(1) wait/notify/notifyAll 必须位于 synchronized 中
(2) 调用 wait 方法后,线程状态由 RUNNING -> WAITING
(3) notify/notifyAll 方法调用后,等待线程依旧不会从 wait() 方法返回,需要等待调用 notify/notifyAll 的线程释放锁后,等待线程
才有机会从 wait 返回.
(4) notify 方法将一个等待线程从等待队列中移到同步队列中,被移动的线程状态由 WAITING -> BLOCKED.
(5) 从 wait 返回的条件是获得锁.
具体步骤:
A: synchronized(obj){
obj.wait();
}
B: synchronized(obj){
obj.notify();
}
(1)首先线程 A 获得锁,在同步代码块中调用 obj.wait 方法,释放锁,进入到等待队列,线程状态由 RUNNING -> WAITING.
(2)线程B获得锁,在同步代码块中调用 obj.notify 方法,唤醒 A. A 从等待队列移动到同步队列,且线程状态由 WAITING -> BLOCKED.
(3)线程A获得锁,并从 wait 方法返回.
管道输入/输出流
PipedOutputStream/PipedInputStream/PipedReader/PipedWriter 用于线程间的数据传递. 方式:内存为媒介.
如何实现的了?
(1) PipedWriter 和 PipedReader 连接.
(2) 一个线程在 PipedWriter 写入,一个线程从 PipedReader 读取,这样就可以实现通讯了.
注意:**Piped 类型的流,必须先进行绑定(调用 connect) 方法,如果没有进行绑定,则会在使用的过程中抛出异常**.
这样很好理解,比如说我们在读写文件的时候,input 和 output 是如何进行关联了?答案是不是文件?所以此处进行绑定的道理是一样的.
11.Thread.join
Thread.join 的含义是:当前线程等待 thread 线程终止后,才从 join 方法返回. 同时 thread 中的共享变量对当前线程可见.
12.ThreadLocal
每个线程内部持有一个 ThreadLocalMap 的东西,而我发现 ThreadLocal 内中 nextHashCode 为静态变量,这就意味着该变量为所有 ThreadLocal 锁共有.
现在考虑一种极端情况,有两个 ThreadLocal 实例:ThreadLocalA 和 ThreadLocalB. 两个线程:ThreadA 和 ThreadB.
ThreadLocalA 存有线程A和线程B的数据. 反应到底层的数据结构是:
ThreadA.ThreadLocalMap<ThreadLocalA, Value>
ThreadB.ThreadLocalMap<ThreadLocalA, Value>
现在 ThreadLocalB 也存有线程A和线程B的数据. 反应到底层的数据结构是:
ThreadA.ThreadLocalMap<ThreadLocalB, Value>
ThreadB.ThreadLocalMap<ThreadLocalB, Value>
这时候,nextHashCode 就起作用了,每个 threadLocal 的 threadLocalHashCode 不同(存在一个神奇的 hash 值:0x61c88647),具体情况自行百度.
所以ThreadA 和 ThreadB 中存放这两个值的时候很大概率不会出现冲突. 这也是为啥 threadLocalHashCode 是 final 修饰,而 nextHashCode 是 static 修饰的原因.
13.线程应用实例
ps:
public void synchronized get(long timeout) {
long future = Thread.currentTimeMills() + timeout;
long remaining = timeout;
while(remaining > 0){
wait(timeout);
remaining = future - Thread.currentTimeMills();
}
}
**这进行系统设计的时候,特别是针对稀缺资源的获取,例如数据库连接,应该使用超时获取这样的设计,这样在获取不到的时候,线程不会一直挂在
连接获取的操作上,而是按时返回,并告知客户端连接获取出现问题,这是一种系统的自我保护机制**.
线程不是创建的越多越好,线程越多,那么系统在进行上下文切换的时候,已经线程的创建和销毁,都会耗费大量的时间,所以需要使用到线程池
技术.
那么如何设计线程池了?
1.创建的线程数量应当有限
2.应当有一个任务队列,用于存放提交的任务.
3.应当有一个工人队列,用于执行提交的任务.
所以线程池的设计很清晰了,1创建工人(Worker & Thread),启动,并从任务队列中读取任务,执行. 2用户将任务提交到线程池.当任务队列中
没有任务的时候,该如何处理了?答案是 Worker 休眠
分享到:
相关推荐
在本资源中,我们有两个主要的学习材料:一个关于“Java并发编程基础”的PPT和一个包含DEMO示例,另一个是“操作系统概述”的PPT。这些资料对于理解Java多线程编程以及操作系统的基础原理至关重要。 首先,让我们...
### Java并发编程基础 - **并发与并行**:并发是指多个任务在同一时间段内被执行(但不一定同一时刻),而并行则是指多个任务同时执行。理解这两者的区别对于深入学习并发编程至关重要。 - **Java并发机制**:Java...
JAVA并发编程艺术 高清pdf : 1.并发变成的挑战 2. java并发机制的底层实现原理 3. java 内存模型 4. java并发编程基础 5.java中的锁。。。。。。。
《Java并发编程的艺术》内容涵盖Java并发编程机制的底层实现原理、Java内存模型、Java并发编程基础、Java中的锁、并发容器和框架、原子类、并发工具类、线程池、Executor框架等主题,每个主题都做了深入的讲解,同时...
### 一、Java并发编程基础 #### 1.1 并发与并行的概念 - **并发**:指多个事件在同一时间段内发生。 - **并行**:指多个事件在同一时刻发生。 在Java并发编程中,通常会涉及多个线程共享资源并同时运行的情况,...
### 一、Java并发编程基础 #### 1.1 并发与并行 - **并发**:指在同一时间段内处理多个任务的能力。 - **并行**:指同一时刻处理多个任务的能力。并行依赖于多处理器或多核处理器的支持。 #### 1.2 Java并发工具类...
### Java并发编程基础 #### 1. 并发与并行 - **并发(Concurrency)**:指一个程序中存在多个执行序列(如线程)在逻辑上同时执行。 - **并行(Parallelism)**:指多个处理器同时执行不同的任务或指令,通常涉及硬件...
总之,《Java并发编程实战》是一本全面介绍Java并发编程的书籍,适合有一定Java基础并希望提升并发编程能力的开发者阅读。通过学习,你可以掌握解决并发问题的策略和技巧,编写出更加健壮和高效的多线程应用。
, 《Java并发编程的艺术》内容涵盖Java并发编程机制的底层实现原理、Java内存模型、Java并发编程基础、Java中的锁、并发容器和框架、原子类、并发工具类、线程池、Executor框架等主题,每个主题都做了深入的讲解,...
#### 一、Java并发编程基础 ##### 1.1 并发与并行概念区分 在Java并发编程实践中,首先需要理解“并发”与“并行”的区别。“并发”指的是多个任务同时进行,但实际上可能是在多线程环境下通过交替执行的方式实现的...
### Java并发编程基础 #### 1. 并发与并行 - **并发**:指的是多个任务在同一时间段内被执行(可能不是同一时刻)。 - **并行**:指的是多个任务在同一时刻被执行。 在Java中,并发主要是通过多线程来实现的,而...
本书不仅适合已经有一定Java编程经验的开发人员阅读,也适用于希望深入了解并发编程基础的新手。它从最基本的并发概念讲起,如进程与线程的区别、线程生命周期、线程调度策略等,并逐步深入到锁机制、死锁避免、原子...
此外,Java并发编程还包括对并发容器的使用,如ArrayList、LinkedList、HashSet、HashMap等基础容器在并发环境下可能存在问题,Java提供了一些线程安全的容器,如Vector、HashTable以及java.util.concurrent包下的...
《Java并发编程实战》提供了深入浅出的指导,涵盖了从基础理论到高级技术的广泛内容。 第一部分介绍了并发编程的基础,包括线程安全性的概念,如何构建线程安全的类,以及Java平台提供的并发工具如线程、同步机制等...
通过阅读《Java并发编程的艺术》这本书,开发者不仅可以掌握Java并发编程的基础知识,还能了解到一些高级特性和技巧,从而在实际开发中游刃有余。同时,附带的源码将有助于加深理解,提供实际操作的机会。
Java并发编程是软件开发中的一个关键领域,尤其是在大型企业级应用和分布式系统中。通过学习相关的书籍,开发者可以深入理解如何有效地设计和实现高效的多线程应用程序,避免并发问题,如竞态条件、死锁、活锁等。...
《JAVA并发编程实践》这本书是Java开发者深入理解并发编程的重要参考资料。它涵盖了Java并发的核心概念、工具和最佳实践,旨在帮助读者在多线程环境下编写高效、安全的代码。 并发编程是现代软件开发中的关键技能,...