近期感觉自己在java方面有很多的不足需要补充,主要为两个方面,第一个是多线程,第二个是NIO。借阅了两本书来补充自己的知识,多线程使用的是高洪岩编著的《java多线程编程技术》另外一本是《netty实战》
本blog主要为学习《java多线程编程技术》的笔记,和部分自己的思考总结
Chapter 01
1. Thread主要有两种实现方式
- 继承Thread子类
- 实现Runnable接口
主要区别为Thread也是实现了Runnable接口,如果只是在简单场合下使用,可以使用继承Thread,复写run方法即可,使用时只需要new XXX.start()即可,可以稍微简化代码量。
如果是复杂场景,建议使用第二种方式,毕竟java不支持多继承。2. 多次调用start出现java.lang.IllegalThreadStateException异常
翻看Thread的start方法,我们可以看到如果threadStatus!=0 就会出现该异常,
threadStatus包括RUNNABLE, BLOCKED, WAITING, TIMED_WAITING or TERMINATED。
多次调用,线程状态已经改变,所以抛出异常。
|
|
所以如下的调用方式是错误的
|
|
正确的使用方式为
|
|
在日常项目中多线程的使用中,尽量使用Runnable,而不是继承。使用继承任务和运行任务的机制混在一起了,实现接口Runnable 将任务从线程中分离出来
3. 自定义线程启动时可用添加名称
线程启动时添加名称,这样在实际项目中查询问题时更加方便,在jvisualvm监控中可以监控是哪个线程占用的资源情况,从而进行优化。
|
|
4. 实例变量与线程安全
- 实例变量线程不共享
|
|
|
|
执行结果如下
|
|
由上可以看出count为线程单独所有。
|
|
|
|
结果如下
|
|
出现了两个3,说明此时count不是线程安全的变量
那么如何解决呢?
在run方法上加上synchronized即可,再次执行
|
|
此时count的计算是递减,线程安全
5. isAlive()方法
isAlive()用于判断线程是否处于可用状态
|
|
|
|
6. 停止线程
java中有三个方式停止线程
1、正常退出,run执行完成
2、使用stop方法强制终止线程(不推荐)
3、使用interrupt中断(推荐)
使用interrupt()方法停止线程
|
|
执行结果并没有中断,线程的run方法正常执行完成
- 使用异常停止线程(建议使用,异常具有传播属性,可以往调用方抛出,更加利于程序控制)
|
|
|
|
执行结果如下,线程执行中途被终止
- 线程在sleep()状态下停止
|
|
|
|
执行结果
- 使用return终止线程
|
|
|
|
执行结果
|
|
7. 线程的优先级
线程可以划分优先级,优先级较高的线程得到的CPU资源较多。
在资源较为紧张的机器上,对外暴露接口时可以附上此值。不同的业务执行的权重不一致,较为关键的指令可以获取更多的系统资源,从而取得较快的运行效率。
|
|
ThreadDemo11类名不同其他同上。
|
|
执行结果
|
|
需要注意的是,并不是优先级高就一定先执行完成,具有一定的随机性,同时还与任务执行的时间有关,时间越长,差异越大。
8. 守护线程
在java中有两种线程,1、用户线程,2、守护线程。
我们常用的是用户线程
守护线程是一种特殊的线程,他具有“陪伴”的特征,当进程中不存在非守护进程了,则守护线程自动销毁。最常见的守护线程是GC,如果jvm中不存在非守护进程了,GC线程也就自动关闭。
|
|
|
|
执行结果如下
|
|
当主线程main方法执行结束后,守护线程自动结束了
chapter02
1. synchronized同步方法
“非线程安全”其实会在多个线程对同一个对象中的实例变量进行并发访问时发生,产生的后果就是就是“脏读”,渠道的数据其实是被修改过的。
“线程安全”就是以获得的实例变量的值是经过同步处理的,不会出现脏读现象。
第一章节中已经展示了脏读的实例。使用synchronized时,需要注意一下的内容。
- 当A线程调用anyObject对象加入synchronized关键字的X方法时,A线程就获得了X方法锁,更准确地将,是获得了对象的锁,所以其他线程必须等A线程执行完毕后才可以调用X方法,但是B线程可以随意调用其他的非synchronized同步方法。
- 当A线程调用anyObject对象加入synchronized关键字的X方法时,A线程就获得了X方法所在的对象锁,所以其他线程必须等A线程执行完毕后才可以调用X方法,而B线程如果调用声明了synchronized关键字的非X方法时,必须等A线程将X方法执行完,也就是释放对象锁后才可以调用。
所以,加了synchronized的方法,如果调用非常频繁,性能的损耗还是比较大。2. 加了synchronized的方法一定是线程安全的吗?
定义一个含有成员变量的类
|
|
|
|
|
|
|
|
执行完成结果如下
|
|
最终i的结果为1,所以单synchronized并不能保证是线程安全的。
有上面的结果可以看到,synchronized如果需要线程安全需要两个条件
第一:在方法上(或代码块)加synchronized关键字
第二:保证成员变量不会被非synchronized方法修改
3.synchronized锁重入
1、synchronized方法/块的内部调用本类的其他synchronized方法/块时,永远可以得到锁
2、可重入锁也支持在父子类继承中使用
如果不支持可重入,会造成死锁
4. 同步代码块的使用
1、同步代码块synchronized(this)拥有当前对象的锁。
2、synchronized(this)代码块间具有同步性。
当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对同一个object中所有其他synchronized(this)同步代码块的访问将被阻塞。这说明synchronized使用的“对象监视器”是一个。
3、将任意对象作为对象监视器
如果在一个类中有很多个synchronized方法,这时虽然能实现同步,但是会受到阻塞,所以影响运行效率;但是如果使用同步代码块锁非this对象,则synchronized(非this对象)代码块中的程序与同步方法是异步的,不与其他锁this同步争抢this锁,可以大大提高运行效率。
4、同步代码块放在非同步synchronized方法中进行申明,并不態保证调用方法的线程的执行同步/顺序性,也就是线程调用方法的顺序是无需的,虽然在同步代码块中执行的顺序是同步的,这样极易出现“脏读”问题。
如下所示:
|
|
|
|
|
|
|
|
|
|
得到的结果为
|
|
此处出现了“脏读”现象,这种是在日常工作中最为常见的一种场景。
如何解决?
|
|
增加代码块锁synchronized (myList) 即可解决此问题,保证方法在调用时候,对单例myList做同步化处理。
5、synchronized(非this对象x)格式写法是将x对象本身作为“对象监视器”,这样就可以得出以下三个结论:
1)当多个线程同时执行synchronized(x)同步代码块时呈同步效果。
2)当其他线程执行x对象中synchronized同步方法时呈同步效果。
3)当其他线程执行x对象方法里面的synchronized(this)代码块时也呈现同步效果。
5. 静态同步synchronized方法与synchronized(class)代码块
关键字synchronized还可以应用咋istatic静态方法上,如果这样写,那就是对当前java文件对应的Class类进行持锁。
public synchronized static void test(){}
public synchronized void test(){}
这两者有区别,第一个是对应Class上锁,第二个是对当前对象上锁。两者并不是同一个锁。所以在执行时,会异步,而不是同步。
6. 多线程的死锁
不同的线程都在等待根本不可能被释放的锁,从而导致所有的任务都无法继续完成。死锁是必须要避免的。
如下展示死锁demo
|
|
|
|
执行结果为
如何检测已经死锁,可以使用jdk子代的工具来检测死锁现象。jdk/bin 执行jps命令
|
|
再执行
|
|
结果最后显示Found 1 deadlock.
另外一种查看的方法是使用jvisualvm。
找到正在执行的pid,选择线程tab,如果出现死锁,将会展示
7. volatile关键字
volatile主要作用是使变量在多个线程间可见。
强制从公共堆栈中取得变量的值,而不是从线程私有数据栈取得变量的值。
如下,在部分jvm运行环境,如果设置isContinue 可能不生效,将发生死循环
|
|
如果将isContinue增加关键字“volatile”修饰,可以解决此问题。
volatile与synchronized比较
1)关键字volatile是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好,并且volatitle只能修饰语变量,而synchronized可以修饰方法及代码块
2)多线程访问volatile不会发生阻塞,而synchronized会出现阻塞
3)volatile能保证数据的可见性,但是不能保证数据的原子性;而synchronized可以保证原子性
4)volatile解决的是变量在多线程之间的数据可见性;而synchronized是解决多线程之间资源的同步性
volatile非原子的特性
|
|
执行结果
原因:
i++的操作并不是线程安全的,分为三步,1)取i值。2)计算i值。3)将i写入内存。
怎么才能保证结果正确呢?
方法一:将volatile修改为synchronized
|
|
方法二:使用原子操作提交i++
|
|
8.线程中出现异常处理
1)UncaughtExceptionHandler
可以在线程上附属UncaughtExceptionHandler来进行异常的加工处理
|
|
执行结果为
|
|
2)设置默认处理器
执行结果为
|
|