原创: Javay Java3y 4月17日
之前花了一个星期回顾了Java集合:
在写文章之前通读了一遍《Java 核心技术 卷一》的并发章节和《Java并发编程实战》前面的部分,回顾了一下以前写过的笔记。从今天开始进入多线程的知识点咯~
之前在学习Java基础的时候学多线程基础还是挺认真的,可是在后面一直没有回顾它,久而久之就把它给忘掉得差不多了..在学习JavaWeb上也一直没用到多线程的地方(我做的东西太水了…)。
由于面试这一部分是占很大比重的,并且学习多线程对我以后的提升也是很有帮助的(自以为)。
我其实也是相当于从零开始学多线程的,如果文章有错的地方还请大家多多包含,不吝在评论区下指正呢~~
讲到线程,又不得不提进程了~
进程我们估计是很了解的了,在windows下打开任务管理器,可以发现我们在操作系统上运行的程序都是进程:
进程的定义:
进程是程序的一次执行,进程是一个程序及其数据在处理机上顺序执行时所发生的活动,进程是具有独立功能的程序在一个数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位
那系统有了进程这么一个概念了,进程已经是可以进行资源分配和调度了,为什么还要线程呢?
为使程序能并发执行,系统必须进行以下的一系列操作:
可以看到进程实现多处理机环境下的进程调度,分派,切换时,都需要花费较大的时间和空间开销
引入线程主要是为了提高系统的执行效率,减少处理机的空转时间和调度切换的时间,以及便于系统管理。使OS具有更好的并发性
那么线程在哪呢??举个例子:
也就是说:在同一个进程内又可以执行多个任务,而这每一个任务我就可以看出是一个线程。
于是我们可以总结出:
线程有3个基本状态:
线程有5种基本操作:
线程的属性:
线程有两个基本类型:
值得注意的是:多线程的存在,不是提高程序的执行速度。其实是为了提高应用程序的使用率,程序的执行其实都是在抢CPU的资源,CPU的执行权。多个进程是在抢这个资源,而其中的某一个进程如果执行路径比较多,就会有更高的几率抢到CPU的执行权
并行:
并发:
由此可见:并行是针对进程的,并发是针对线程的。
上面说了一大堆基础,理解完的话。我们回到Java中,看看Java是如何实现多线程的~
Java实现多线程是使用Thread这个类的,我们来看看Thread类的顶部注释:
通过上面的顶部注释我们就可以发现,创建多线程有两种方法:
创建一个类,继承Thread,重写run方法
public class MyThread extends Thread {
@Override
public void run() {
for (int x = 0; x < 200; x++) {
System.out.println(x);
}
}
}
我们调用一下测试看看:
public class MyThreadDemo {
public static void main(String[] args) {
// 创建两个线程对象
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
my1.start();
my2.start();
}
}
实现Runnable接口,重写run方法
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println(x);
}
}
}
我们调用一下测试看看:
public class MyRunnableDemo {
public static void main(String[] args) {
// 创建MyRunnable类的对象
MyRunnable my = new MyRunnable();
Thread t1 = new Thread(my);
Thread t2 = new Thread(my);
t1.start();
t2.start();
}
}
结果还是跟上面是一样的,这里我就不贴图了~~~
不要将run()
和start()
搞混了~
run()和start()方法区别:
run()
:仅仅是封装被线程执行的代码,直接调用是普通方法
start()
:首先启动了线程,然后再由jvm去调用该线程的run()方法。
jvm虚拟机的启动是单线程的还是多线程的?
那么,既然有两种方式实现多线程,我们使用哪一种???
一般我们使用实现Runnable接口
这篇主要是讲解了线程是什么,理解线程的基础对我们往后的学习是有帮助的。这里主要是简单的入了个门
在阅读顶部注释的时候我们发现有”优先级“、”后台线程“这类的词,这篇是没有讲解他们是什么东西的~所以下一篇主要讲解的是Thread的API~敬请期待哦~
使用线程其实会导致我们数据不安全,甚至程序无法运行的情况的,这些问题都会再后面讲解到的~
之前在学习操作系统的时候根据《计算机操作系统-汤小丹》这本书也做了一点点笔记,都是比较浅显的知识点。或许对大家有帮助~
参考资料:
昨天已经写了:
如果没看的同学建议先去阅读一遍哦~
在写文章之前通读了一遍《Java 核心技术 卷一》的并发章节和《Java并发编程实战》前面的部分,回顾了一下以前写过的笔记。从今天开始进入多线程的知识点咯~
我其实也是相当于从零开始学多线程的,如果文章有错的地方还请大家多多包含,不吝在评论区下指正呢~~
声明本文使用的是JDK1.8
实现多线程从本质上都是由Thread类来进行操作的~我们来看看Thread类一些重要的知识点。Thread这个类很大,不可能整个把它看下来,只能看一些常见的、重要的方法。
顶部注释的我们已经解析过了,如果不知道的同学可前往:多线程三分钟就可以入个门了!
我们在使用多线程的时候,想要查看线程名是很简单的,调用Thread.currentThread().getName()
即可。
如果没有做什么的设置,我们会发现线程的名字是这样子的:主线程叫做main,其他线程是Thread-x
下面我就带着大家来看看它是怎么命名的:
nextThreadNum()
的方法实现是这样的:
基于这么一个变量-->线程初始化的数量
点进去看到init方法就可以确定了:
看到这里,如果我们想要为线程起个名字,那也是很简单的。Thread给我们提供了构造方法!
下面我们来测试一下:
public class MyThread implements Runnable {
@Override
public void run() {
// 打印出当前线程的名字
System.out.println(Thread.currentThread().getName());
}
}
测试:
public class MyThreadDemo {
public static void main(String[] args) {
MyThread myThread = new MyThread();
//带参构造方法给线程起名字
Thread thread1 = new Thread(myThread, "关注公众号Java3y");
Thread thread2 = new Thread(myThread, "qq群:742919422");
thread1.start();
thread2.start();
// 打印当前线程的名字
System.out.println(Thread.currentThread().getName());
}
}
结果:
当然了,我们还可以通过setName(String name)
的方法来改掉线程的名字的。我们来看看方法实现;
检查是否有权限修改:
至于threadStatus这个状态属性,貌似没发现他会在哪里修改:
守护线程是为其他线程服务的
守护线程有一个特点:
使用线程的时候要注意的地方
setDaemon(boolean on)
测试一波:
public class MyThreadDemo {
public static void main(String[] args) {
MyThread myThread = new MyThread();
//带参构造方法给线程起名字
Thread thread1 = new Thread(myThread, "关注公众号Java3y");
Thread thread2 = new Thread(myThread, "qq群:742919422");
// 设置为守护线程
thread2.setDaemon(true);
thread1.start();
thread2.start();
System.out.println(Thread.currentThread().getName());
}
}
上面的代码运行多次可以出现(电脑性能足够好的同学可能测试不出来):线程1和主线程执行完了,我们的守护线程就不执行了~
原理:这也就为什么我们要在启动之前设置守护线程了。
线程优先级高仅仅表示线程获取的CPU时间片的几率高,但这不是一个确定的因素!
线程的优先级是高度依赖于操作系统的,Windows和Linux就有所区别(Linux下优先级可能就被忽略了)~
可以看到的是,Java提供的优先级默认是5,最低是1,最高是10:
实现:
setPriority0
是一个本地(navite)的方法:
private native void setPriority0(int newPriority);
在上一篇介绍的时候其实也提过了线程的线程有3个基本状态:执行、就绪、阻塞
在Java中我们就有了这个图,Thread上很多的方法都是用来切换线程的状态的,这一部分是重点!
其实上面这个图是不够完整的,省略掉了一些东西。后面在讲解的线程状态的时候我会重新画一个~
下面就来讲解与线程生命周期相关的方法~
调用sleep方法会进入计时等待状态,等时间到了,进入的是就绪状态而并非是运行状态!
于是乎,我们的图就可以补充成这样:
调用yield方法会先让别的线程执行,但是不确保真正让出
于是乎,我们的图就可以补充成这样:
调用join方法,会等待该线程执行完毕后才执行别的线程~
我们进去看看具体的实现:
wait方法是在Object上定义的,它是native本地方法,所以就看不了了:
wait方法实际上它也是计时等待(如果带时间参数)的一种!,于是我们可以补充我们的图:
线程中断在之前的版本有stop方法,但是被设置过时了。现在已经没有强制线程终止的方法了!
由于stop方法可以让一个线程A终止掉另一个线程B
总而言之,Stop方法太暴力了,不安全,所以被设置过时了。
我们一般使用的是interrupt来请求终止线程~
Thread t1 = new Thread( new Runnable(){
public void run(){
// 若未发生中断,就正常执行任务
while(!Thread.currentThread.isInterrupted()){
// 正常任务代码……
}
// 中断的处理代码……
doSomething();
}
} ).start();
再次说明:调用interrupt()并不是要真正终止掉当前线程,仅仅是设置了一个中断标志。这个中断标志可以给我们用来判断什么时候该干什么活!什么时候中断由我们自己来决定,这样就可以安全地终止线程了!
我们来看看源码是怎么讲的吧:
再来看看刚才说抛出的异常是什么东东吧:
所以说:interrupt方法压根是不会对线程的状态造成影响的,它仅仅设置一个标志位罢了
interrupt线程中断还有另外两个方法(检查该线程是否被中断):
上面还提到了,如果阻塞线程调用了interrupt()方法,那么会抛出异常,设置标志位为false,同时该线程会退出阻塞的。我们来测试一波:
public class Main {
/**
* @param args
*/
public static void main(String[] args) {
Main main = new Main();
// 创建线程并启动
Thread t = new Thread(main.runnable);
System.out.println("This is main ");
t.start();
try {
// 在 main线程睡个3秒钟
Thread.sleep(3000);
} catch (InterruptedException e) {
System.out.println("In main");
e.printStackTrace();
}
// 设置中断
t.interrupt();
}
Runnable runnable = () -> {
int i = 0;
try {
while (i < 1000) {
// 睡个半秒钟我们再执行
Thread.sleep(500);
System.out.println(i++);
}
} catch (InterruptedException e) {
// 判断该阻塞线程是否还在
System.out.println(Thread.currentThread().isAlive());
// 判断该线程的中断标志位状态
System.out.println(Thread.currentThread().isInterrupted());
System.out.println("In Runnable");
e.printStackTrace();
}
};
}
结果:
接下来我们分析它的执行流程是怎么样的:
2018年4月18日20:32:15(哇,这个方法真的消耗了我非常长的时间)…..感谢@开始de痕迹的指教~
该参考资料:
可以发现我们的图是还没有补全的~后续的文章讲到同步的时候会继续使用上面的图的。在Thread中重要的还是那几个可以切换线程状态的方法,还有理解中断的真正含义。
使用线程会导致我们数据不安全,甚至程序无法运行的情况的,这些问题都会再后面讲解到的~
之前在学习操作系统的时候根据《计算机操作系统-汤小丹》这本书也做了一点点笔记,都是比较浅显的知识点。或许对大家有帮助
参考资料:
不小心就鸽了几天没有更新了,这个星期回家咯。在学校的日子要努力一点才行!
只有光头才能变强
回顾前面:
本文章的知识主要参考《Java并发编程实战》这本书的前4章,这本书的前4章都是讲解并发的基础的。要是能好好理解这些基础,那么我们往后的学习就会事半功倍。
当然了,《Java并发编程实战》可以说是非常经典的一本书。我是未能完全理解的,在这也仅仅是抛砖引玉。想要更加全面地理解我下面所说的知识点,可以去阅读一下这本书,总的来说还是不错的。
首先来预览一下《Java并发编程实战》前4章的目录究竟在讲什么吧:
第1章 简介
ps:这一部分我就不讲了,主要是引出我们接下来的知识点,有兴趣的同学可翻看原书~
第2章 线程安全性
第3章 对象的共享
第4章 对象的组合
那么接下来我们就开始吧~
在前面的文章中已经讲解了线程【多线程三分钟就可以入个门了!】,多线程主要是为了提高我们应用程序的使用率。但同时,这会给我们带来很多安全问题!
如果我们在单线程中以“顺序”(串行-->独占)的方式执行代码是没有任何问题的。但是到了多线程的环境下(并行),如果没有设计和控制得好,就会给我们带来很多意想不到的状况,也就是线程安全性问题
因为在多线程的环境下,线程是交替执行的,一般他们会使用多个线程执行相同的代码。如果在此相同的代码里边有着共享的变量,或者一些组合操作,我们想要的正确结果就很容易出现了问题
简单举个例子:
public class UnsafeCountingServlet extends GenericServlet implements Servlet {
private long count = 0;
public long getCount() {
return count;
}
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
++count;
// To something else...
}
}
但是在多线程环境下跑起来,它的count值计算就不对了!
首先,它共享了count这个变量,其次来说++count;
这是一个组合的操作(注意,它并非是原子性)
++count
实际上的操作是这样子的:
于是多线程执行的时候很可能就会有这样的情况:
如果说:当多个线程访问某个类的时候,这个类始终能表现出正确的行为,那么这个类就是线程安全的!
有个原则:能使用JDK提供的线程安全机制,就使用JDK的。
当然了,此部分其实是我们学习多线程最重要的环节,这里我就不详细说了。这里只是一个总览,这些知识点在后面的学习中都会遇到~~~
使用多线程我们的目的就是为了提高应用程序的使用率,但是如果多线程的代码没有好好设计的话,那未必会提高效率。反而降低了效率,甚至会造成死锁!
就比如说我们的Servlet,一个Servlet对象可以处理多个请求的,Servlet显然是一个天然支持多线程的。
又以下面的例子来说吧:
public class UnsafeCountingServlet extends GenericServlet implements Servlet {
private long count = 0;
public long getCount() {
return count;
}
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
++count;
// To something else...
}
}
从上面我们已经说了,上面这个类是线程不安全的。最简单的方式:如果我们在service方法上加上JDK为我们提供的内置锁synchronized,那么我们就可以实现线程安全了。
public class UnsafeCountingServlet extends GenericServlet implements Servlet {
private long count = 0;
public long getCount() {
return count;
}
public void synchronized service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
++count;
// To something else...
}
}
虽然实现了线程安全了,但是这会带来很严重的性能问题:
这就导致了:我们完成一个小小的功能,使用了多线程的目的是想要提高效率,但现在没有把握得当,却带来严重的性能问题!
在使用多线程的时候:更严重的时候还有死锁(程序就卡住不动了)。
这些都是我们接下来要学习的地方:学习使用哪种同步机制来实现线程安全,并且性能是提高了而不是降低了~
书上是这样定义发布和逸出的:
发布(publish) 使对象能够在当前作用域之外的代码中使用 逸出(escape) 当某个不应该发布的对象被发布了
常见逸出的有下面几种方式:
静态域逸出:
public修饰get方法:
方法参数传递我就不再演示了,因为把对象传递过去给另外的方法,已经是逸出了~
下面来看看该书给出this逸出的例子:
逸出就是本不应该发布对象的地方,把对象发布了。导致我们的数据泄露出去了,这就造成了一个安全隐患!理解起来是不是简单了一丢丢?
上面谈到了好几种逸出的情况,我们接下来来谈谈如何安全发布对象。
安全发布对象有几种常见的方式:
public static Person = new Person()
;
从上面我们就可以看到,使用多线程会把我们的系统搞得挺复杂的。是需要我们去处理很多事情,为了防止多线程给我们带来的安全和性能的问题~
下面就来简单总结一下我们需要哪些知识点来解决多线程遇到的问题。
使用多线程就一定要保证我们的线程是安全的,这是最重要的地方!
在Java中,我们一般会有下面这么几种办法来实现线程安全问题:
count++
操作,可以使用AtomicLong来实现原子性,那么在增加的时候就不会出差错了!)
何为原子性?何为可见性?当初我在ConcurrentHashMap基于JDK1.8源码剖析中已经简单说了一下了。不了解的同学可以进去看看。
在多线程中很多时候都是因为某个操作不是原子性的,使数据混乱出错。如果操作的数据是原子性的,那么就可以很大程度上避免了线程安全问题了!
count++
,先读取,后自增,再赋值。如果该操作是原子性的,那么就可以说线程安全了(因为没有中间的三部环节,一步到位【原子性】~
原子性就是执行某一个操作是不可分割的,
- 比如上面所说的count++
操作,它就不是一个原子性的操作,它是分成了三个步骤的来实现这个操作的~
- JDK中有atomic包提供给我们实现原子性操作~
也有人将其做成了表格来分类,我们来看看:
图片来源:https://blog.csdn.net/eson_15/article/details/51553338
使用这些类相关的操作也可以进他的博客去看看:
对于可见性,Java提供了一个关键字:volatile给我们使用~
volatile经典总结:volatile仅仅用来保证该变量对所有线程的可见性,但不保证原子性
我们将其拆开来解释一下:
使用了volatile修饰的变量保证了三点:
一般来说,volatile大多用于标志位上(判断操作),满足下面的条件才应该使用volatile修饰变量:
参考资料:
在多线程的环境下,只要我们不使用成员变量(不共享数据),那么就不会出现线程安全的问题了。
就用我们熟悉的Servlet来举例子,写了那么多的Servlet,你见过我们说要加锁吗??我们所有的数据都是在方法(栈封闭)上操作的,每个线程都拥有自己的变量,互不干扰!
在方法上操作,只要我们保证不要在栈(方法)上发布对象(每个变量的作用域仅仅停留在当前的方法上),那么我们的线程就是安全的
在线程封闭上还有另一种方法,就是我之前写过的:ThreadLocal就是这么简单
使用这个类的API就可以保证每个线程自己独占一个变量。(详情去读上面的文章即可)~
不可变对象一定线程安全的。
上面我们共享的变量都是可变的,正由于是可变的才会出现线程安全问题。如果该状态是不可变的,那么随便多个线程访问都是没有问题的!
Java提供了final修饰符给我们使用,final的身影我们可能就见得比较多了,但值得说明的是:
就好像下面这个HashMap,用final修饰了。但是它仅仅保证了该对象引用hashMap变量
所指向是不可变的,但是hashMap内部的数据是可变的,也就是说:可以add,remove等等操作到集合中~~~
final HashMap<Person> hashMap = new HashMap<>();
不可变的对象引用在使用的时候还是需要加锁的
要想将对象设计成不可变对象,那么要满足下面三个条件:
String在我们学习的过程中我们就知道它是一个不可变对象,但是它没有遵循第二点(对象所有的域都是final修饰的),因为JVM在内部做了优化的。但是我们如果是要自己设计不可变对象,是需要满足三个条件的。
很多时候我们要实现线程安全未必就需要自己加锁,自己来设计。
我们可以使用JDK给我们提供的对象来完成线程安全的设计:
非常多的"工具类"供我们使用,这些在往后的学习中都会有所介绍的~~这里就不介绍了
正确使用多线程能够提高我们应用程序的效率,同时给我们会带来非常多的问题,这些都是我们在使用多线程之前需要注意的地方。
无论是不变性、可见性、原子性、线程封闭、委托这些都是实现线程安全的一种手段。要合理地使用这些手段,我们的程序才可以更加健壮!
可以发现的是,上面在很多的地方说到了:锁。但我没有介绍它,因为我打算留在下一篇来写,敬请期待~~~
书上前4章花了65页来讲解,而我只用了一篇文章来概括,这是远远不够的,想要继续深入的同学可以去阅读书籍~
之前在学习操作系统的时候根据《计算机操作系统-汤小丹》这本书也做了一点点笔记,都是比较浅显的知识点。或许对大家有帮助