link-JAVA多线程与高并发系列[前言,大纲]
线程
程序,进程,线程,协成/纤程(fiber)
举个例子:
程序:QQ.exe是一个程序,存放在硬盘上,是一个静态的概念;
进程:当你双击它,QQ程序运行起来了,这就是一个进程.相对程序来说,进程是一个动态的概念;
线程:进程中最小的执行单元,一个进程内可以有多个线程同时执行命令
在JAVA中创建一个线程:
如果调用T1的run()方法,那就是简单的方法调用,代码还是在main线程中从上到下依次执行;
如果调用T1的start()方法,则会开启一个新的线程(记为thread1)执行,那么thread1和main线程同时执行,表现为控制台"T1"和"main"交替输出.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31 1public class T01_WhatIsThread {
2 private static class T1 extends Thread {
3 @Override
4 public void run() {
5 for(int i=0; i<10; i++) {
6 try {
7 TimeUnit.MICROSECONDS.sleep(1);
8 } catch (InterruptedException e) {
9 e.printStackTrace();
10 }
11 System.out.println("T1");
12 }
13 }
14 }
15
16 public static void main(String[] args) {
17 //new T1().run();
18 new T1().start();
19 for(int i=0; i<10; i++) {
20 try {
21 TimeUnit.MICROSECONDS.sleep(1);
22 } catch (InterruptedException e) {
23 e.printStackTrace();
24 }
25 System.out.println("main");
26 }
27
28 }
29}
30
31
创建线程的3种方式?
1:Thread 2: Runnable 3:Executors.newCachedThrad
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 1public class T02_HowToCreateThread {
2 static class MyThread extends Thread {
3 @Override
4 public void run() {
5 System.out.println("Hello MyThread!");
6 }
7 }
8
9 static class MyRun implements Runnable {
10 @Override
11 public void run() {
12 System.out.println("Hello MyRun!");
13 }
14 }
15
16 public static void main(String[] args) {
17 // 第一种,对象继承Thread,然后直接new该对象
18 new MyThread().start();
19 // 第二种,对象实现Runnable,该对象作为参数创建一个Thread对象
20 new Thread(new MyRun()).start();
21 // 第二种的变式,其实是匿名内部类的实例作为实现Runnable的对象,又加了lambda表达式简化代码
22 new Thread(()->{
23 System.out.println("Hello Lambda!");
24 }).start();
25 // 第三种线程池Executors.newCachedThrad等,其实最终实现也是前两种之一
26 }
27
28}
29
30
线程的常见操作
前言
CPU只管执行命令,对于CPU来说,没有线程的概念,它只是不断的从内存中去拿取指令去执行.
多线程呢,就是有好多个线程竞争着去给CPU发送命令,每个线程上的命令在CPU上执行一小会,多个线程的命令快速交替执行,这样看起来像是同时执行的.
各个线程和CPU之间,相当于有一个"等待队列",线程们在队列中排队,等着CPU从队列中随机找一个线程去执行;CPU执行某个线程的一小段命令后,不等执行完,就把它扔回等待队列,然后重新找一个线程执行,这样快速的切换感觉像是多个线程同时执行.如果线程执行完了,那就终结了自己的一生,不会再进入等待队列了.
线程操作:
- sleep(long millis):当前线程休息一定毫秒数,把执行命令的机会让给其他线程,睡够一定时间后进入等待队列,继续竞争CPU的执行机会
- yield():当前线程正在CPU上运行时,先退出一下,进去线程等待队列,然后大家再一起公平竞争CPU的执行机会.这个几乎用不到.
- join():假设俩个线程t1,t2,在t1中调用t2.join(),则此时执行t2的命令,t2执行完后t1继续执行,用于保证线程的执行顺序
- stop(): 在工程中尽量不要用,容易出现状态不一致问题,略复杂,就当没有这个方法吧
- interrupt():interrupt后会抛出一个异常,需要在上层catch改异常,然后做一些逻辑处理,控制程序流程.业务逻辑中也几乎没有必须interrupt的,也尽量不要用.
假想的一个interrupt的场景:线程t1调用了sleep(两天)的方法,但是在一天后,需要让它醒来,那就调用t1.interrupt();前提是t1sleep时做好被interrupt的准备,即catchinterrupted异常,然后继续执行或者干点别的事.
线程的常见状态
线程状态迁移图:
线程在"等待队列"中等着被CPU执行时,就是Ready状态;线程正在被CPU执行时,是Running状态;Ready和Running合称为Runnable状态.
线程执行完后(或者被操作系统kill掉),进入terminated状态,结束了自己的一生,啥都不能干了(等待被GC回收).
获取线程状态的方法:new MyThead().getState();
同步
synchronize关键字(常见问题)
多个线程去访问同一个资源的时候,需要上锁,目的是保证状态的一致,就像数据库的事务一样.
类比场景:多个同学去厕所蹲坑.
锁的是什么?
是一个对象(包括class对象),拿到锁之后才能去执行某段代码.而不是锁的代码.
注意不要用String常量和Integer等基础数据类型作为锁的对象
不能用String的原因:所有的字符串常量都是同一个对象,假如引用的依赖包中用了String常量作为锁的对象,自己的程序也刚好用了相同String常量作为锁的对象,那么肯定会引起奇怪的问题.
也尽量不用String对象.
不能用Integer等基础数据类型的原因:Integer内部做了一些特殊处理,Integer的对象的值一旦改变,就会变成一个新对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 1public class T {
2
3 private int count = 10;
4 private Object o = new Object();
5
6 public void m() {
7 synchronized(o) { //任何线程要执行线面的代码,必须先拿到对象o的锁
8 count--;
9 System.out.println(Thread.currentThread().getName() + " count = " + count);
10 }
11 }
12
13}
14
15
synchronize底层怎么实现?什么是锁升级?synchronize一定比原子类慢?
JVM规范中没有任何要求,只要能保证功能完整就行.
HotSpot是这样的:对象头(64位)中,拿出2位来记录这个对象是不是被锁定了,mark word.
- JDK早期,synchronize是重量级的,每次都去找操作系统申请锁,效率很低;
- 后来改进了,引入了锁升级(可以看没错,我就是厕所所长一文)
2.1. 偏向锁:当第一个线程去执行带synchronize方法时,先在对象头的markword上记录这个线程的线程号,不加锁;如果下次又是该线程执行那个方法,就直接访问不需获取锁.(偏向第一个线程)
2.2. 自旋锁:如果有线程争用,则升级为自旋锁,比如线程t1正在访问带锁资源r,这时t2也要访问r,那么t2先不加锁,while(true)空执行一会,看t1是不是会马上释放锁.默认自旋10次,如果10后还得不到锁,则升级为重量级锁
2.3. 重量级锁:去操作系统申请资源加锁
Hotspot目前的实现,锁只能升级,不能降级.
所以现在的synchronize并不一定比那些原子类慢,因为有锁升级
自旋锁占用CPU,但是不去跟操作系统申请资源加锁,只是在用户态,不经过内核态.
当加锁方法执行时间很长,或者线程数很多时,用操作系统锁比较好;
当执行时间很短,且线程不太多时,用自旋锁合适.
synchronize的特点
- 可以在方法上加synchronize关键字,锁定当前对象(非静态方法),或者当前类的class对象(静态方法)
- synchronize(this)锁定当前对象;
- 对于非静态方法,synchronize(this)如果锁住了方法中的所有代码,那就和直接在方法上加synchronize是一样的;对于静态方法,方法上的synchronize相当于synchronize(T.class)
(每一个.class文件,load到内存以后,会生成一个对应的Class对象)
- synchronize既保证可见性,又保证原子性
- 可重入.假如两个方法m1,m2都对同一个对象加了锁;如果m1中调用了m2,是可以再次获得锁的.(如果不允许重入,那就死锁了).可重入的概念是在同一个线程的基础上的.
class对象是单例的吗?
同一个ClassLoader内是单例的;多个ClassLoader间,不是单例;但是不同加载器之间不能互相访问.所以,可以认为是单例.
父类中有一个synchronize方法,在子类中调用,那么锁的是谁?
是子类对象,打印一下this即可证明:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47 1public class T {
2 synchronized void m() {
3 System.out.println("super m start");
4 System.out.println(this);
5 try {
6 TimeUnit.SECONDS.sleep(1);
7 } catch (InterruptedException e) {
8 e.printStackTrace();
9 }
10 System.out.println("super m end");
11 }
12
13 public static void main(String[] args) {
14 new TT().m();
15 System.out.println("---");
16 new TT().m2();
17 System.out.println("---");
18 new TT().m3();
19 }
20
21}
22
23class TT extends T {
24 @Override
25 synchronized void m() {
26 System.out.println("child m start");
27 System.out.println(this);
28 super.m();
29 System.out.println("child m end");
30 }
31
32 synchronized void m2() {
33 System.out.println("child m2 start");
34 System.out.println(this);
35 super.m();
36 System.out.println("child m2 end");
37 }
38
39 void m3() {
40 System.out.println("child m2 start");
41 System.out.println(this);
42 super.m();
43 System.out.println("child m2 end");
44 }
45}
46
47
带锁的方法和不带锁的方法可以同时执行吗?
可以.
程序中如果抛出了异常,锁会被释放吗?
抛出异常会释放锁,所以一定要处理好异常,防止出现异常后被其他线程访问资源,从而导致各种状态不一致的问题.
锁优化
锁细化:锁住(synchronize包括)的代码,在能保证业务逻辑OK下,越少越好.
锁粗化:假如一段业务逻辑,中间有很多个细化的小锁,这些小锁的方法别的业务又不会调用,那就把这些小锁合并成一个大锁.
锁的属性(field)发生变化,会影响锁的使用效果吗?
锁的属性变化不会影响锁的功能;
但是如果锁的变量(或者说"引用")指向了别的对象,那就不是同一把锁了,会出问题.可以通过给变量加上final关键字避免这个问题.